# Smart Contract (/docs/cookbook/vrf-rock-paper-scissors/smart-contract)

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

## Contract Architecture

Our smart contract will:

* Accept ticket purchases from players (0.001 ETH per ticket)
* Request random numbers from RISE VRF Coordinator
* Receive VRF fulfillment with random AI choice
* Emit events for the backend to watch
* Manage ticket balances and request mappings

## 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
4. Process the random numbers and emit events

## Writing the Contract

<Steps>
  <Step>
    ### Create Contract File

    Navigate to your Foundry project and create the contract:

    ```bash
    cd foundry-project
    ```

    Create `src/RockPaperScissors.sol`:

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

    // 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 RockPaperScissors is IVRFConsumer {
        error NotEnoughEtherSent();
        error OnlyCoordinator();

        event ChoiceRequested(uint256 indexed requestId, address indexed player);
        event ChoiceResult(uint256 indexed requestId, uint256 result);

        uint constant COST_PER_GAME = 0.001 ether;

        address payable public sponsor;
        IVRFCoordinator public coordinator;

        mapping(address => uint256) public ticketBalance;
        mapping(uint256 => address) public requestToPlayer;

        modifier onlySponsor() {
            require(msg.sender == sponsor, "not sponsor");
            _;
        }

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

        constructor(address _coordinator, address payable _sponsor) {
            coordinator = IVRFCoordinator(_coordinator);
            sponsor = _sponsor;
        }

        function buyTickets(uint _numTickets) external payable {
            if (msg.value < _numTickets * COST_PER_GAME) {
                revert NotEnoughEtherSent();
            }

            (bool sent, ) = sponsor.call{value: msg.value}("");
            require(sent, "failed");

            ticketBalance[msg.sender] += _numTickets;
        }

        function request(address _player) external onlySponsor {
            require(ticketBalance[_player] > 0, "No tickets");

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

            ticketBalance[_player] -= 1;
            requestToPlayer[requestId] = _player;

            emit ChoiceRequested(requestId, _player);
        }

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

            uint256 result = randomNumbers[0] % 3; // 0=rock, 1=paper, 2=scissors

            emit ChoiceResult(requestId, result);

            delete requestToPlayer[requestId];
        }

        receive() external payable {}
        fallback() external payable {}
    }
    ```
  </Step>

  <Step>
    ### Understanding the Contract

    **Constructor Parameters**:

    * `_coordinator`: RISE VRF Coordinator address (`0x9d57aB4517ba97349551C876a01a7580B1338909` on testnet)
    * `_sponsor`: Backend wallet that pays for VRF requests

    **Key Functions**:

    1. `buyTickets()`: Players purchase game tickets
       * Forwards ETH to sponsor (who pays for VRF)
       * Increments player's ticket balance

    2. `request()`: Sponsor initiates VRF request for a player
       * Only callable by sponsor (backend)
       * Requests 1 random number from coordinator
       * Uses unique seed per request
       * Decrements ticket, stores request mapping
       * Emits `ChoiceRequested` event

    3. `rawFulfillRandomNumbers()`: VRF callback
       * Only callable by coordinator
       * Receives random number array
       * Calculates AI choice: `randomNumbers[0] % 3`
       * Emits `ChoiceResult` event with 0=rock, 1=paper, 2=scissors
       * Cleans up request mapping

    **Events**:

    * `ChoiceRequested`: Emitted when VRF request is made
    * `ChoiceResult`: Emitted when VRF delivers random choice
  </Step>

  <Step>
    ### Compile the Contract

    ```bash
    forge build
    ```

    This generates the ABI in `out/RockPaperScissors.sol/RockPaperScissors.json`.
  </Step>

  <Step>
    ### Deploy to RISE Testnet

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

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

    import "forge-std/Script.sol";
    import "../src/RockPaperScissors.sol";

    contract DeployScript is Script {
        function run() external {
            uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
            address payable sponsor = payable(vm.envAddress("SPONSOR_ADDRESS"));

            vm.startBroadcast(deployerPrivateKey);

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

            RockPaperScissors rps = new RockPaperScissors(coordinator, sponsor);

            console.log("RockPaperScissors deployed to:", address(rps));

            vm.stopBroadcast();
        }
    }
    ```

    Create `.env` in foundry-project directory:

    ```bash
    PRIVATE_KEY=0x...your_deployer_private_key
    SPONSOR_ADDRESS=0x...your_sponsor_wallet_address
    ```

    Deploy using your private key:

    ```bash
    forge script script/Deploy.s.sol:DeployScript \
      --rpc-url https://testnet.riselabs.xyz \
      --private-key $PRIVATE_KEY \
      --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 the deployed contract address!
  </Step>

  <Step>
    ### Extract Contract ABI

    Copy the ABI from the compiled output:

    ```bash
    cat out/RockPaperScissors.sol/RockPaperScissors.json | jq '.abi' > ../src/constants/abi.json
    ```

    Or manually copy the ABI array from `out/RockPaperScissors.sol/RockPaperScissors.json`.
  </Step>

  <Step>
    ### Update Constants

    Update `src/constants/index.ts` with your deployed contract address and ABI:

    ```typescript
    export const RPS_ADDRESS = "0x5723cA8d0E664D8BFE301aA8b0c7DbBaa43E5806" as const

    export const ABI = [
      {
        "type": "constructor",
        "inputs": [
          { "name": "_coordinator", "type": "address", "internalType": "address" },
          { "name": "_sponsor", "type": "address", "internalType": "address payable" }
        ],
        "stateMutability": "nonpayable"
      },
      {
        "type": "function",
        "name": "buyTickets",
        "inputs": [{ "name": "_numTickets", "type": "uint256", "internalType": "uint256" }],
        "outputs": [],
        "stateMutability": "payable"
      },
      {
        "type": "function",
        "name": "request",
        "inputs": [{ "name": "_player", "type": "address", "internalType": "address" }],
        "outputs": [],
        "stateMutability": "nonpayable"
      },
      {
        "type": "function",
        "name": "ticketBalance",
        "inputs": [{ "name": "", "type": "address", "internalType": "address" }],
        "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }],
        "stateMutability": "view"
      },
      {
        "type": "event",
        "name": "ChoiceRequested",
        "inputs": [
          { "name": "requestId", "type": "uint256", "indexed": true, "internalType": "uint256" },
          { "name": "player", "type": "address", "indexed": true, "internalType": "address" }
        ],
        "anonymous": false
      },
      {
        "type": "event",
        "name": "ChoiceResult",
        "inputs": [
          { "name": "requestId", "type": "uint256", "indexed": true, "internalType": "uint256" },
          { "name": "result", "type": "uint256", "indexed": false, "internalType": "uint256" }
        ],
        "anonymous": false
      },
      {
        "type": "error",
        "name": "NotEnoughEtherSent",
        "inputs": []
      },
      {
        "type": "error",
        "name": "OnlyCoordinator",
        "inputs": []
      }
    ] as const
    ```

    Replace with your actual ABI from the compilation output.
  </Step>
</Steps>

## Key VRF Concepts

### Request Seed

The seed ensures each VRF request is unique:

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

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

### Random Number Range

VRF returns a `uint256` random number. We mod it to get our choice:

```solidity
uint256 result = randomNumbers[0] % 3;  // 0, 1, or 2
```

This maps to rock (0), paper (1), scissors (2).

## Next Steps

Your smart contract is deployed and ready! Next, we'll build the backend API that orchestrates VRF requests and watches for events.
