Staking & Governance

The SBET staking and governance system lets token holders stake SBET to earn protocol fee rewards, receive SBET emissions, and govern the protocol through on-chain proposals. The system is built on four contracts that integrate with the existing treasury module architecture.

4
New Contracts
~750
Lines of Solidity
92
Tests Passing
20
Handoff Assertions

Overview

The staking system has three pillars:

  • Stake & Vote: Deposit SBET 1:1 to receive stkSBET, an ERC-20 with on-chain voting power (ERC20Votes). Delegation is supported.
  • Hybrid Rewards: Earn protocol trading fees (ETH) and SBET token emissions simultaneously through a multi-token Synthetix-style accumulator.
  • On-Chain Governance: Propose and vote on protocol parameter changes. All proposals execute through a 2-day timelock.

Contract Architecture

ContractPurposeKey Standard
StakedSBET ERC20Votes wrapper — 1:1 SBET deposit, voting power, flash-loan cooldown ERC-20, ERC-6372, EIP-712
MultiRewardStaking Multi-token reward distributor (Treasury module) — ETH fees + SBET emissions Synthetix accumulator
SBETGovernor On-chain governance — proposals, voting, timelock execution OpenZeppelin Governor
SBETTimelock Execution delay for governance proposals OpenZeppelin TimelockController

StakedSBET (stkSBET)

StakedSBET is an ERC-20 token that wraps SBET at a 1:1 ratio. Depositing SBET mints stkSBET; burning stkSBET returns SBET. The token implements ERC20Votes for governance-compatible voting power with historical checkpoint snapshots.

Staking Flow

  1. User approves StakedSBET to spend their SBET tokens.
  2. User calls stake(amount) — SBET is transferred in, stkSBET is minted 1:1.
  3. On first stake, the user is auto-self-delegated (voting power is active immediately).
  4. To unstake: unstake(amount) burns stkSBET and returns SBET, subject to cooldown.

Flash-Loan Protection

Three layers prevent flash-loan governance attacks:

LayerMechanismEffect
Transfer Cooldown Configurable delay (12s–1 day) after staking before stkSBET can be transferred or burned Cannot return flash-loaned SBET in the same block
Voting Snapshot OpenZeppelin Votes records voting power at the proposal’s snapshot timestamp Tokens staked after snapshot have zero voting power for that proposal
Voting Delay 1-day delay between proposal creation and vote start Attacker must commit capital for at least the delay period

Key Functions

// Deposit SBET, receive stkSBET 1:1. Auto-self-delegates on first stake.
function stake(uint256 amount) external;

// Burn stkSBET, receive SBET 1:1. Requires cooldown elapsed.
function unstake(uint256 amount) external;

// Delegate voting power to another address.
function delegate(address delegatee) external;

// Current voting power (includes delegated votes).
function getVotes(address account) external view returns (uint256);

// Historical voting power at a past timestamp.
function getPastVotes(address account, uint256 timepoint) external view returns (uint256);

MultiRewardStaking

MultiRewardStaking distributes up to 10 reward tokens simultaneously to stkSBET holders, proportional to their staked balance. It follows the Synthetix StakingRewards accumulator pattern, extended to multiple tokens. The contract reads balances from StakedSBET (it does not hold staked tokens) and integrates with the Treasury module system.

Reward Accumulator Math

For each reward token r:

rewardPerToken_r = stored_r + (min(now, periodFinish_r) - lastUpdate_r) * rate_r * 1e18 / totalSupply
earned(user, r) = balanceOf(user) * (rewardPerToken_r - paid[user][r]) / 1e18 + owed[user][r]

This is gas-efficient: no per-block iteration. Rewards accrue continuously and are claimed on demand. The accumulator updates automatically via a callback from StakedSBET on every balance change (mint, burn, transfer).

Reward Types

RewardSourceDurationDescription
ETH (Protocol Fees) TreasuryFeeManager 7 days Real yield from protocol trading fees, distributed via FeeManager
SBET (Emissions) Treasury Budget 30 days Token emissions from treasury budget allocation

Funding Patterns

Two methods to activate reward periods:

  • notifyRewardAmount(token, amount) — Distributor transfers tokens to the contract via safeTransferFrom and starts/extends a reward period.
  • notifyRewardFromBalance(token) — For pre-funded balances (e.g., when Treasury’s fundModule() or FeeManager’s distributeFees() pushes tokens directly). Detects surplus between actual balance and tracked balance.

Reward Token Lifecycle

  • Add: Admin calls addRewardToken(token, distributor, duration). Max 10 tokens. stkSBET itself and previously-removed tokens are blocked.
  • Remove: Admin calls removeRewardToken(token) after the reward period ends. Remaining balance is swept back to the distributor. Users must claim before removal.
Reward Confiscation Policy: When a reward token is removed, unclaimed rewards are returned to the distributor. This prevents a reward-theft attack where post-removal stkSBET recipients could claim rewards they never earned. Users have the full reward period plus any additional time until admin removal to claim.

Key Functions

// Start/extend a reward period (transfers tokens from caller).
function notifyRewardAmount(address token, uint256 amount) external payable;

// Start/extend using pre-funded balance (for Treasury/FeeManager push pattern).
function notifyRewardFromBalance(address token) external;

// Claim rewards for a single token.
function claimReward(address token) external;

// Claim all active reward tokens in one transaction.
function claimAllRewards() external;

// View pending rewards.
function earned(address account, address token) external view returns (uint256);

On-Chain Governance

The SBET Governor is an OpenZeppelin Governor composition that enables stkSBET holders to propose, vote on, and execute protocol parameter changes. All execution goes through a 2-day timelock.

Parameters

ParameterDefaultDescription
Voting Delay1 dayTime between proposal creation and vote start
Voting Period5 daysDuration of the voting window
Proposal Threshold100,000 stkSBETMinimum stake to create a proposal
Quorum4% of stkSBET supplyDynamic — scales with total stake
Timelock Delay2 daysExecution delay after a proposal passes
Late Quorum Extension1 dayExtends voting if quorum reached late (anti-sniping)

All parameters are self-governable — governance can change its own rules through proposals.

Proposal Lifecycle

  1. Propose: A stkSBET holder with ≥ 100,000 tokens calls propose(targets, values, calldatas, description).
  2. Voting Delay (1 day): Snapshot of voting power is taken. No voting yet.
  3. Voting Period (5 days): Holders cast votes (For / Against / Abstain).
  4. Succeeded: If quorum is met and For > Against, the proposal succeeds.
  5. Queue: Proposal is queued in the SBETTimelock (2-day delay).
  6. Execute: After the timelock delay, anyone can execute the proposal.

What Governance Controls

After deployment, the Timelock owns or administers every contract in the protocol:

ContractControlExamples
SBETTreasuryOwner + all rolesFee rates, daily limits, token allowlist, module management
SBET DiamondOwnerOracle management, match finalization, emergency pause
PredictionMarketOwnerCreation fee, dispute bond, dispute window
PredictionAMMOwnerProtocol fee, fee recipient
PredictionExchangeAdminAdmin transfer, fee recipient, operators
MultiRewardStakingAdmin rolesReward tokens, distributors, durations
StakedSBETOwnerCooldown period, pause/unpause
All Treasury ModulesDEFAULT_ADMIN_ROLESub-role management on FeeManager, Vesting, Budgets, Yield, MultiSig, NFT modules
TreasuryFeeManagerAll operational rolesQueue/execute fee recipients, distribute fees

Post-handoff deployer privileges: none. The deployer retains only the non-admin distributor role on reward tokens (a narrow operational keeper function that can only activate reward periods, not change configuration). Governance can rotate this to a dedicated keeper bot via setDistributor().

Security Model

ThreatMitigation
Flash-loan governance3-layer defense: cooldown + snapshot + voting delay
ReentrancyReentrancyGuard on all state mutations, CEI pattern for ETH
Donation attackInternal balance tracking (RewardData.balance), not address(this).balance
Reward rate manipulationOnly authorized distributor can call notifyRewardAmount
stkSBET as reward tokenBlocked by CannotRewardStakingToken guard (prevents circular self-accrual)
Removed token re-additionBlocked by wasRemovedToken mapping (prevents stale checkpoint corruption)
Governance captureDynamic quorum, late quorum extension, 2-day timelock, emergency cancel
Deployer privilege leakPhase 12 handoff: 20 assertion tests verify deployer has zero roles/ownership

Governance Handoff (Phase 12)

The deployment script’s Phase 12 transfers ownership and revokes all deployer roles across every contract. A dedicated test suite (GovernanceHandoff.t.sol) with 21 assertions verifies that the deployer ends with zero privileges:

  • Zero roles on Treasury (all 6 roles revoked)
  • Zero roles on all AccessControl modules (FeeManager, Budgets, Vesting, Yield, MultiSig, NFT modules)
  • Zero ownership on all Ownable contracts (Diamond, Treasury, StakedSBET, PredictionMarket/AMM, NFTVault, IntegratorHub, DonationManager, TreasuryFacade)
  • Zero admin on PredictionExchange and Timelock

Operational Flows

Protocol Fee Sharing

After deployment, fee sharing requires a post-deployment governance setup:

  1. Governance proposes feeManager.queueAddFeeRecipient(multiRewardStaking, percentageBps, "Staker Share")
  2. After FeeManager’s internal timelock delay, governance executes feeManager.executeQueuedFeeOp(id)
  3. Governance (Timelock) triggers feeManager.distributeFees(token) to push fees to MultiRewardStaking
  4. Keeper calls multiRewardStaking.notifyRewardFromBalance(token) to activate the reward period

SBET Emissions

  1. Governance proposes treasury.fundModule(multiRewardStaking, sbetToken, amount) to allocate SBET from treasury budget
  2. Keeper calls multiRewardStaking.notifyRewardFromBalance(sbetToken) to start the emission period

Deployed Addresses

ContractSepoliaMainnet
StakedSBET0xEB22630E55Bb3ed925f73e5415374C1ACB7b698b
MultiRewardStaking0x7B1D557d3d82A5303D29deFBBfEd9F608C5e6479
SBETGovernor0x49c23D5794E392522E88950450242274Eb751BF1
SBETTimelock0xCA817bcA55e853653a7B6205470018dDEA7431C3