# Smart Contracts (/docs/cookbook/rise-slots/smart-contracts)

import { Steps, Step } from 'fumadocs-ui/components/steps';
import { Callout } from 'fumadocs-ui/components/callout';

## Contract Architecture

We'll build two contracts:

1. **RCT (RISE Casino Token)**: ERC20 token that powers the game economy
   * Pre-mints 10M tokens to the slot machine (house bankroll)
   * One-time 1000 token claim for new players
   * Faucet for broke players (only works at 0 balance)
   * Auto-approves SlotMachine for seamless UX

2. **SlotMachine**: VRF-powered gaming contract
   * Accepts 10 RCT per spin
   * Requests 3 random numbers from RISE VRF
   * Maps numbers to symbols with weighted probabilities
   * Distributes tiered payouts for matching symbols
   * Emits events for real-time UI updates

## VRF Integration Overview

RISE VRF uses a standard consumer pattern:

1. Your contract implements `IVRFConsumer` interface
2. Call `coordinator.requestRandomNumbers()` to request randomness
3. Coordinator calls back your `rawFulfillRandomNumbers()` with results (3-5ms!)
4. Process the random numbers and emit events

## Writing the Contracts

<Steps>
  <Step>
    ### Create RCT Token Contract

    Navigate to your Foundry project:

    ```bash
    cd foundry-app
    ```

    Create `src/RCT.sol`:

    ```solidity
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.30;

    import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

    contract RCT is ERC20 {
        mapping(address => bool) public hasMinted;
        address public immutable slotMachine;

        constructor(address _slotMachine) ERC20("RISE Casino Token", "RCT") {
            slotMachine = _slotMachine;
            // Mint 10,000,000 RCT to SlotMachine contract as house bankroll
            _mint(_slotMachine, 10_000_000 * 10**decimals());
        }

        function mintInitial() external {
            require(!hasMinted[msg.sender], "Already minted initial tokens");
            hasMinted[msg.sender] = true;
            _mint(msg.sender, 1000 * 10**decimals());

            // Auto-approve SlotMachine to spend user's RCT tokens
            _approve(msg.sender, slotMachine, type(uint256).max);
        }

        function faucet() external {
            require(balanceOf(msg.sender) == 0, "Balance must be 0 to use faucet");
            _mint(msg.sender, 1000 * 10**decimals());
        }
    }
    ```

    **Key Features**:

    * `constructor`: Mints 10M RCT to the SlotMachine address (house bankroll)
    * `mintInitial()`: One-time claim of 1000 RCT + auto-approves SlotMachine for unlimited spending
    * `faucet()`: Refills broke players with 1000 RCT (only works at 0 balance)
    * `hasMinted` mapping: Tracks which users have claimed their initial tokens

    The auto-approve in `mintInitial()` is crucial for UX - users never need to approve spending!
  </Step>

  <Step>
    ### Create SlotMachine Contract

    Create `src/SlotMachine.sol`:

    ```solidity
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.30;

    import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
    import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

    // RISE VRF Interfaces
    interface IVRFCoordinator {
        function requestRandomNumbers(
            uint32 numNumbers,
            uint256 seed
        ) external returns (uint256 requestId);
    }

    interface IVRFConsumer {
        function rawFulfillRandomNumbers(
            uint256 requestId,
            uint256[] memory randomNumbers
        ) external;
    }

    contract SlotMachine is IVRFConsumer {
        using SafeERC20 for IERC20;

        error OnlyCoordinator();
        error InsufficientTokenBalance();

        event SpinRequested(uint256 indexed requestId, address indexed player);
        event SpinResult(uint256 indexed requestId, uint8[3] result, uint256 payout);

        uint256 constant COST_PER_SPIN = 10 * 10**18; // 10 RCT

        // Payout structure based on symbol rarity
        uint256 constant CHERRY_PAYOUT = 15 * 10**18;  // 15 RCT (0-3)
        uint256 constant LEMON_PAYOUT = 30 * 10**18;   // 30 RCT (4-6)
        uint256 constant DIAMOND_PAYOUT = 100 * 10**18; // 100 RCT (7-8)
        uint256 constant LUCKY9_PAYOUT = 500 * 10**18;  // 500 RCT (9)

        IVRFCoordinator public coordinator;
        IERC20 public rctToken;
        address public owner;
        mapping(uint256 => address) public requestToPlayer;

        modifier onlyCoordinator() {
            if (msg.sender != address(coordinator)) revert OnlyCoordinator();
            _;
        }

        modifier onlyOwner() {
            require(msg.sender == owner, "Not owner");
            _;
        }

        constructor(address _coordinator, address _rctToken) {
            coordinator = IVRFCoordinator(_coordinator);
            rctToken = IERC20(_rctToken);
            owner = msg.sender;
        }

        function setRCTToken(address _rctToken) external onlyOwner {
            rctToken = IERC20(_rctToken);
        }

        // Helper function to map number to symbol ID
        function getSymbolId(uint8 num) internal pure returns (uint8) {
            if (num <= 3) return 0;  // Cherry
            if (num <= 6) return 1;  // Lemon
            if (num <= 8) return 2;  // Diamond
            return 3;                 // Lucky 9
        }

        function spin() external returns (uint256) {
            // Check player has enough RCT tokens
            if (rctToken.balanceOf(msg.sender) < COST_PER_SPIN) {
                revert InsufficientTokenBalance();
            }

            // Transfer 10 RCT from player to contract
            rctToken.safeTransferFrom(msg.sender, address(this), COST_PER_SPIN);

            // Request 3 random numbers from RISE VRF (no fees!)
            uint256 requestId = coordinator.requestRandomNumbers(
                3,
                uint256(keccak256(abi.encode(msg.sender, block.timestamp, block.number)))
            );

            requestToPlayer[requestId] = msg.sender;
            emit SpinRequested(requestId, msg.sender);

            return requestId;
        }

        function rawFulfillRandomNumbers(
            uint256 requestId,
            uint256[] memory randomNumbers
        ) external override onlyCoordinator {
            require(randomNumbers.length == 3, "Invalid random numbers");
            require(requestToPlayer[requestId] != address(0), "Invalid request");

            address player = requestToPlayer[requestId];

            // Map to 0-9 range (10 possible values)
            uint8[3] memory result = [
                uint8(randomNumbers[0] % 10),
                uint8(randomNumbers[1] % 10),
                uint8(randomNumbers[2] % 10)
            ];

            // Map to symbol IDs
            uint8[3] memory symbolIds = [
                getSymbolId(result[0]),
                getSymbolId(result[1]),
                getSymbolId(result[2])
            ];

            // Check if all three SYMBOLS match
            bool isWin = symbolIds[0] == symbolIds[1] && symbolIds[1] == symbolIds[2];
            uint256 payout = 0;

            if (isWin) {
                // Determine payout based on which symbol won
                if (symbolIds[0] == 0) {
                    payout = CHERRY_PAYOUT;
                } else if (symbolIds[0] == 1) {
                    payout = LEMON_PAYOUT;
                } else if (symbolIds[0] == 2) {
                    payout = DIAMOND_PAYOUT;
                } else {
                    payout = LUCKY9_PAYOUT;
                }

                rctToken.safeTransfer(player, payout);
            }

            emit SpinResult(requestId, result, payout);
            delete requestToPlayer[requestId];
        }
    }
    ```
  </Step>

  <Step>
    ### Understanding the SlotMachine Contract

    **Symbol Mapping System**:

    The contract uses a two-step mapping system:

    1. VRF returns numbers → modulo 10 → gives 0-9
    2. Numbers map to symbol IDs:
       * 0-3 → Symbol ID 0 (Cherry) = 40% probability
       * 4-6 → Symbol ID 1 (Lemon) = 30% probability
       * 7-8 → Symbol ID 2 (Diamond) = 20% probability
       * 9 → Symbol ID 3 (Lucky 9) = 10% probability

    **Why Symbol IDs?**

    Without symbol IDs, \[0, 1, 2] would be a loss because the numbers don't match. But all three are cherries! The `getSymbolId()` function maps them all to 0, making it a valid win.

    **Key Functions**:

    1. **spin()**: Player initiates a spin
       * Validates player has 10 RCT balance
       * Transfers 10 RCT to contract
       * Requests 3 random numbers from VRF coordinator
       * Uses unique seed: `keccak256(abi.encode(msg.sender, block.timestamp, block.number))`
       * Stores request mapping
       * Emits `SpinRequested` event
       * Returns `requestId` for tracking

    2. **rawFulfillRandomNumbers()**: VRF callback (called by coordinator only)
       * Receives array of 3 random numbers
       * Maps each to 0-9 range via modulo
       * Converts numbers to symbol IDs
       * Checks if all 3 symbol IDs match
       * Calculates payout based on winning symbol
       * Transfers payout to player if win
       * Emits `SpinResult` event with result array and payout
       * Cleans up request mapping

    **Events**:

    * `SpinRequested(requestId, player)`: Emitted when spin is initiated
    * `SpinResult(requestId, result, payout)`: Emitted when VRF delivers result
  </Step>

  <Step>
    ### Compile the Contracts

    ```bash
    forge build
    ```

    This generates ABIs in:

    * `out/RCT.sol/RCT.json`
    * `out/SlotMachine.sol/SlotMachine.json`
  </Step>

  <Step>
    ### Deploy to RISE Testnet

    Create deployment script `script/Deploy.s.sol`:

    ```solidity
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.30;

    import "forge-std/Script.sol";
    import "../src/RCT.sol";
    import "../src/SlotMachine.sol";

    contract DeployScript is Script {
        function run() external {
            uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");

            vm.startBroadcast(deployerPrivateKey);

            // RISE Testnet VRF Coordinator
            address coordinator = 0xc0d49A572cF25aC3e9ae21B939e8B619b39291Ea;

            // Deploy SlotMachine first to get its address
            SlotMachine slotMachine = new SlotMachine(coordinator, address(0));

            // Deploy RCT with SlotMachine address
            RCT rct = new RCT(address(slotMachine));

            // Set RCT token address in SlotMachine
            slotMachine.setRCTToken(address(rct));

            console.log("RCT deployed at:", address(rct));
            console.log("SlotMachine deployed at:", address(slotMachine));

            vm.stopBroadcast();
        }
    }
    ```

    Deploy using your private key:

    ```bash
    forge script script/Deploy.s.sol:DeployScript \
      --rpc-url https://testnet.riselabs.xyz \
      --private-key 0xYOUR_PRIVATE_KEY_HERE \
      --broadcast
    ```

    Alternatively, use a keystore for better security:

    ```bash
    forge script script/Deploy.s.sol:DeployScript \
      --rpc-url https://testnet.riselabs.xyz \
      --account <keystore-name> \
      --broadcast
    ```

    <Callout type="info">
      Learn how to create a keystore: [Foundry Keystore Guide](https://book.getfoundry.sh/reference/cast/cast-wallet-import)
    </Callout>

    Save both deployed contract addresses!
  </Step>

  <Step>
    ### Extract Contract ABIs

    Copy the ABIs to your frontend:

    ```bash
    # From foundry-app directory
    cat out/SlotMachine.sol/SlotMachine.json | jq '.abi' > ../src/constants/slotMachineAbi.json
    cat out/RCT.sol/RCT.json | jq '.abi' > ../src/constants/rctAbi.json
    ```
  </Step>

  <Step>
    ### Update Constants

    Update `src/constants/index.ts` with your deployed addresses:

    ```typescript
    import SLOT_MACHINE_ABI from './slotMachineAbi.json'
    import RCT_ABI from './rctAbi.json'

    export const SLOT_MACHINE_ADDRESS = "0x6d30dE27786Fd46F468Ea16C34243386d2b11153" as const
    export const RCT_TOKEN_ADDRESS = "0xA42a61FB25323923999e747c73dDCCb2C3547B0B" as const

    export { SLOT_MACHINE_ABI, RCT_ABI }
    ```

    Replace with your actual deployed addresses.
  </Step>
</Steps>

## Key Concepts

### Request Seed

The seed ensures each VRF request is unique:

```solidity
uint256(keccak256(abi.encode(msg.sender, block.timestamp, block.number)))
```

This combines player address, timestamp, and block number for uniqueness.

### Symbol vs Number Matching

**Wrong approach (matches numbers)**:

```solidity
bool isWin = result[0] == result[1] && result[1] == result[2];
```

This would fail for \[0, 1, 2] even though they're all cherries!

**Correct approach (matches symbols)**:

```solidity
uint8[3] memory symbolIds = [getSymbolId(result[0]), getSymbolId(result[1]), getSymbolId(result[2])];
bool isWin = symbolIds[0] == symbolIds[1] && symbolIds[1] == symbolIds[2];
```

This correctly identifies \[0, 1, 2] as triple cherries.

### Payout Calculations

```solidity
if (symbolIds[0] == 0) payout = CHERRY_PAYOUT;  // 15 RCT
else if (symbolIds[0] == 1) payout = LEMON_PAYOUT;  // 30 RCT
else if (symbolIds[0] == 2) payout = DIAMOND_PAYOUT;  // 100 RCT
else payout = LUCKY9_PAYOUT;  // 500 RCT
```

Expected value for 10 RCT spin:

* Cherry win (0.4 × 0.4 × 0.4 = 6.4%): 15 RCT → 0.96 RCT expected
* Lemon win (0.3 × 0.3 × 0.3 = 2.7%): 30 RCT → 0.81 RCT expected
* Diamond win (0.2 × 0.2 × 0.2 = 0.8%): 100 RCT → 0.80 RCT expected
* Lucky 9 win (0.1 × 0.1 × 0.1 = 0.1%): 500 RCT → 0.50 RCT expected

Total expected return: \~31% (69% house edge)

### Auto-Approve Pattern

```solidity
_approve(msg.sender, slotMachine, type(uint256).max);
```

When users call `mintInitial()`, they automatically approve the SlotMachine for unlimited RCT spending. This means:

* No separate approval transaction needed
* Users can spin immediately after claiming
* Seamless UX with no extra popups

## Next Steps

Your smart contracts are deployed! Next, we'll implement session keys to enable completely gasless, popup-free gameplay.
