> ## Documentation Index
> Fetch the complete documentation index at: https://docs.herodotus.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Example Smart Contract - Voting

> Example smart contract that uses the Satellite Contract to access Storage Proofs data for voting.

For easier integration, you can add the Satellite contract as a dependency and use its interfaces inside your smart contract.

To do that using Foundry, you should first install it using:

```bash theme={null}
forge install HerodotusDev/satellite
```

and then we recommend configuring a remapping inside your `foundry.toml` file:

```toml theme={null}
remappings = [
  # Your other remappings go here
  "@HerodotusDev/satellite/=lib/satellite/",
]
```

Or if you are using NPM (e.g. with Hardhat), you can just install it using:

```bash theme={null}
npm install https://github.com/HerodotusDev/satellite
```

## How it works

The contract uses historical token balances as voting power. Because the system operates on **timestamps rather than block numbers**, the same historical moment can be proven across multiple chains simultaneously — Satellite resolves the correct block number per chain internally. This makes it possible to aggregate cross-chain balances in a single vote without manually tracking chain-specific block heights.

Proving individual storage slots for every voter via the Storage Proof API would be impractical at scale. Instead, only **two things need to be proven once per vote**: the block header (to anchor the timestamp on-chain) and the token account's storage root (a single 32-byte value that commits to the entire account storage at that block). After that, each voter's balance is verified **on-demand** at vote time using an MPT proof they fetch themselves from any standard RPC node (`eth_getProof`). The contract checks this proof against the already-proven storage root using `verifyOnlyStorage` from Satellite — no Storage Proof API call is needed per voter.

<Note>
  In practice, getting the MPT proof and constructing the transaction would be
  abstracted away from the user behind a frontend (or a frontend + backend). The
  UI would fetch the MPT proof from an RPC node, construct the transaction, and
  submit it on the user's behalf — voters would just click "Vote" without being
  aware of the proof mechanics.
</Note>

Our example flow is:

1. **Operator initializes a vote** at a historical checkpoint timestamp via `initializeVote`.
2. **One-time proof request** is submitted to the Storage Proof API for that timestamp. It only needs to cover the block header (timestamp → block number) and the token account's storage root. Proof generation takes non-trivial time — submit requests as early as possible, well before the voting window opens.
3. **Voters call `vote`** with an MPT proof for their own balance slot, fetched from any RPC node via `eth_getProof` at the checkpoint block. The contract verifies the proof on-chain against the proven storage root and derives voting power without any additional Storage Proof API requests.
4. **Operator closes the vote** via `closeVote`, which tallies results and records the winning outcome on-chain.

<Note>
  To find the correct `balancesSlot` for your token, run `forge inspect <YourToken> storage-layout`. For most OpenZeppelin ERC-20 tokens the `_balances` mapping is at slot `0`.
</Note>

## Example contract

```solidity theme={null}
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.27;

import {ISatellite} from "@HerodotusDev/satellite/solidity/src/interfaces/ISatellite.sol";
import {IEvmFactRegistryModule} from "@HerodotusDev/satellite/solidity/src/interfaces/modules/IEvmFactRegistryModule.sol";

/**
 * @notice Proof generation via the Storage Proof API takes non-trivial time. After initializing
 * a vote at a checkpoint timestamp, submit a proof request for that timestamp before the voting
 * window opens. The same checkpoint timestamp maps to the correct block number on every chain, so
 * cross-chain balances can be aggregated in a single vote without tracking per-chain block numbers.
 *
 * @notice Only the block header and token account storage root need to be proven via the Storage
 * Proof API — once per vote. Each voter's balance is then verified on-demand using an MPT proof
 * they supply themselves (via eth_getProof from any RPC node), checked against the proven storage
 * root with `verifyOnlyStorage`. No per-voter Storage Proof API requests are required.
 */
contract HistoricalBalanceVoting {
    ISatellite public immutable satellite;

    /// @notice Account allowed to initialize and close votes.
    address public immutable operator;

    /// @notice The ERC-20 token contract whose balance determines voting power.
    address public immutable token;

    /// @notice The chain ID on which the token lives.
    uint256 public immutable tokenChainId;

    /**
     * @notice Solidity storage slot index of the token's `_balances` mapping.
     * @dev For most OpenZeppelin ERC-20 tokens this is 0.
     *      Verify with `forge inspect <Token> storage-layout`.
     */
    uint256 public immutable balancesSlot;

    // ─── Vote storage ─────────────────────────────────────────────────────────

    struct VoteInfo {
        uint256 checkpointTimestamp;
        bool active;
        bool closed;
        bytes32 winner;
        bytes32[] outcomes;
    }

    uint256 public voteCount;
    mapping(uint256 voteId => VoteInfo) public voteInfo;
    mapping(uint256 voteId => mapping(bytes32 outcome => uint256 power)) public votesByOutcome;
    mapping(uint256 voteId => mapping(address voter => bool)) public hasVoted;

    // ─── Events ───────────────────────────────────────────────────────────────

    event VoteInitialized(uint256 indexed voteId, uint256 checkpointTimestamp, bytes32[] outcomes);
    event Voted(uint256 indexed voteId, address indexed voter, bytes32 outcome, uint256 power);
    event VoteClosed(uint256 indexed voteId, bytes32 winner);

    modifier onlyOperator() {
        require(msg.sender == operator, "Only operator");
        _;
    }

    constructor(
        address _satellite,
        address _operator,
        address _token,
        uint256 _tokenChainId,
        uint256 _balancesSlot
    ) {
        satellite = ISatellite(_satellite);
        operator = _operator;
        token = _token;
        tokenChainId = _tokenChainId;
        balancesSlot = _balancesSlot;
    }

    // ─── Operator actions ─────────────────────────────────────────────────────

    /**
     * @notice Opens a new vote anchored to a historical checkpoint timestamp.
     * @dev Before the voting window opens, request proofs for `checkpointTimestamp` via the
     *      Storage Proof API. The API needs to prove the block header (anchoring the timestamp
     *      on-chain) and the token account's storage root (one value that commits to all balances
     *      at that block). Because the API works with timestamps, the same request can cover data
     *      from multiple chains at once. Individual balance slots are verified on-demand by voters.
     * @param checkpointTimestamp Unix timestamp of the balance checkpoint (must be in the past).
     * @param outcomes          At least two distinct outcome labels (e.g. keccak256("FOR")).
     * @return voteId           Identifier for this vote.
     */
    function initializeVote(uint256 checkpointTimestamp, bytes32[] calldata outcomes)
        external
        onlyOperator
        returns (uint256 voteId)
    {
        require(checkpointTimestamp < block.timestamp, "Timestamp must be in the past");
        require(outcomes.length >= 2, "At least 2 outcomes required");

        voteId = voteCount++;
        VoteInfo storage v = voteInfo[voteId];
        v.checkpointTimestamp = checkpointTimestamp;
        v.active = true;
        v.outcomes = outcomes;

        emit VoteInitialized(voteId, checkpointTimestamp, outcomes);
    }

    /**
     * @notice Closes an active vote and records the winning outcome on-chain.
     */
    function closeVote(uint256 voteId) external onlyOperator {
        VoteInfo storage v = voteInfo[voteId];
        require(v.active && !v.closed, "Vote not active");

        bytes32 winner;
        uint256 highestVotes;
        for (uint256 i = 0; i < v.outcomes.length; i++) {
            bytes32 outcome = v.outcomes[i];
            if (votesByOutcome[voteId][outcome] > highestVotes) {
                highestVotes = votesByOutcome[voteId][outcome];
                winner = outcome;
            }
        }

        v.active = false;
        v.closed = true;
        v.winner = winner;

        emit VoteClosed(voteId, winner);
    }

    // ─── Voter actions ────────────────────────────────────────────────────────

    /**
     * @notice Cast a vote. Voting power equals the token balance at the checkpoint timestamp,
     *         verified on-demand against the proven storage root using the caller-supplied MPT proof.
     * @dev The block header and token account storage root must already be proven in Satellite via
     *      the Storage Proof API. The `storageSlotMptProof` can be fetched from any RPC node via
     *      `eth_getProof` at the checkpoint block number — no Storage Proof API call is needed.
     * @param storageSlotMptProof MPT proof for the caller's balance slot, from eth_getProof.
     */
    function vote(uint256 voteId, bytes32 outcome, bytes calldata storageSlotMptProof) external {
        VoteInfo storage v = voteInfo[voteId];
        require(v.active && !v.closed, "Vote not active");
        require(!hasVoted[voteId][msg.sender], "Already voted");

        bool valid = false;
        for (uint256 i = 0; i < v.outcomes.length; i++) {
            if (v.outcomes[i] == outcome) {
                valid = true;
                break;
            }
        }
        require(valid, "Invalid outcome");

        uint256 power = getVotingPower(voteId, msg.sender, storageSlotMptProof);
        require(power > 0, "No voting power at this checkpoint");

        hasVoted[voteId][msg.sender] = true;
        votesByOutcome[voteId][outcome] += power;

        emit Voted(voteId, msg.sender, outcome, power);
    }

    // ─── Views ────────────────────────────────────────────────────────────────

    /**
     * @notice Returns the voting power of `account` for a given vote.
     * @dev Resolves the checkpoint timestamp to a block number via Satellite, reads the proven
     *      storage root of the token account, then verifies the caller-supplied MPT proof against
     *      it on-demand. Reverts if the block header or storage root are not yet proven in Satellite.
     * @param storageSlotMptProof MPT proof for `account`'s balance slot, obtained via eth_getProof
     *                            at the checkpoint block number from any standard RPC node.
     */
    function getVotingPower(uint256 voteId, address account, bytes calldata storageSlotMptProof)
        public
        view
        returns (uint256)
    {
        VoteInfo storage v = voteInfo[voteId];
        require(v.checkpointTimestamp != 0, "Vote does not exist");

        (bool tsOk, uint256 blockNumber) = satellite.timestampSafe(tokenChainId, v.checkpointTimestamp);
        require(tsOk, "Checkpoint not yet proven — request proof via Storage Proof API first");

        (bool rootOk, bytes32 storageRoot) = satellite.accountFieldSafe(
            tokenChainId,
            blockNumber,
            token,
            IEvmFactRegistryModule.AccountField.STORAGE_ROOT
        );
        require(rootOk, "Storage root not yet proven — request proof via Storage Proof API first");

        // Storage slot for `_balances[account]` in a standard ERC-20.
        // The MPT proof is verified on-demand against the proven storage root — no Storage Proof
        // API request is needed per voter.
        bytes32 slot = keccak256(abi.encode(account, balancesSlot));
        bytes32 balance = satellite.verifyOnlyStorage(slot, storageRoot, storageSlotMptProof);

        return uint256(balance);
    }
}
```
