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.
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
| Contract | Purpose | Key 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
- User approves
StakedSBETto spend their SBET tokens. - User calls
stake(amount)— SBET is transferred in, stkSBET is minted 1:1. - On first stake, the user is auto-self-delegated (voting power is active immediately).
- To unstake:
unstake(amount)burns stkSBET and returns SBET, subject to cooldown.
Flash-Loan Protection
Three layers prevent flash-loan governance attacks:
| Layer | Mechanism | Effect |
|---|---|---|
| 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
| Reward | Source | Duration | Description |
|---|---|---|---|
| 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 viasafeTransferFromand starts/extends a reward period.notifyRewardFromBalance(token)— For pre-funded balances (e.g., when Treasury’sfundModule()or FeeManager’sdistributeFees()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
| Parameter | Default | Description |
|---|---|---|
| Voting Delay | 1 day | Time between proposal creation and vote start |
| Voting Period | 5 days | Duration of the voting window |
| Proposal Threshold | 100,000 stkSBET | Minimum stake to create a proposal |
| Quorum | 4% of stkSBET supply | Dynamic — scales with total stake |
| Timelock Delay | 2 days | Execution delay after a proposal passes |
| Late Quorum Extension | 1 day | Extends voting if quorum reached late (anti-sniping) |
All parameters are self-governable — governance can change its own rules through proposals.
Proposal Lifecycle
- Propose: A stkSBET holder with ≥ 100,000 tokens calls
propose(targets, values, calldatas, description). - Voting Delay (1 day): Snapshot of voting power is taken. No voting yet.
- Voting Period (5 days): Holders cast votes (For / Against / Abstain).
- Succeeded: If quorum is met and For > Against, the proposal succeeds.
- Queue: Proposal is queued in the SBETTimelock (2-day delay).
- 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:
| Contract | Control | Examples |
|---|---|---|
| SBETTreasury | Owner + all roles | Fee rates, daily limits, token allowlist, module management |
| SBET Diamond | Owner | Oracle management, match finalization, emergency pause |
| PredictionMarket | Owner | Creation fee, dispute bond, dispute window |
| PredictionAMM | Owner | Protocol fee, fee recipient |
| PredictionExchange | Admin | Admin transfer, fee recipient, operators |
| MultiRewardStaking | Admin roles | Reward tokens, distributors, durations |
| StakedSBET | Owner | Cooldown period, pause/unpause |
| All Treasury Modules | DEFAULT_ADMIN_ROLE | Sub-role management on FeeManager, Vesting, Budgets, Yield, MultiSig, NFT modules |
| TreasuryFeeManager | All operational roles | Queue/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
| Threat | Mitigation |
|---|---|
| Flash-loan governance | 3-layer defense: cooldown + snapshot + voting delay |
| Reentrancy | ReentrancyGuard on all state mutations, CEI pattern for ETH |
| Donation attack | Internal balance tracking (RewardData.balance), not address(this).balance |
| Reward rate manipulation | Only authorized distributor can call notifyRewardAmount |
| stkSBET as reward token | Blocked by CannotRewardStakingToken guard (prevents circular self-accrual) |
| Removed token re-addition | Blocked by wasRemovedToken mapping (prevents stale checkpoint corruption) |
| Governance capture | Dynamic quorum, late quorum extension, 2-day timelock, emergency cancel |
| Deployer privilege leak | Phase 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:
- Governance proposes
feeManager.queueAddFeeRecipient(multiRewardStaking, percentageBps, "Staker Share") - After FeeManager’s internal timelock delay, governance executes
feeManager.executeQueuedFeeOp(id) - Governance (Timelock) triggers
feeManager.distributeFees(token)to push fees to MultiRewardStaking - Keeper calls
multiRewardStaking.notifyRewardFromBalance(token)to activate the reward period
SBET Emissions
- Governance proposes
treasury.fundModule(multiRewardStaking, sbetToken, amount)to allocate SBET from treasury budget - Keeper calls
multiRewardStaking.notifyRewardFromBalance(sbetToken)to start the emission period
Deployed Addresses
| Contract | Sepolia | Mainnet |
|---|---|---|
| StakedSBET | 0xEB22630E55Bb3ed925f73e5415374C1ACB7b698b | — |
| MultiRewardStaking | 0x7B1D557d3d82A5303D29deFBBfEd9F608C5e6479 | — |
| SBETGovernor | 0x49c23D5794E392522E88950450242274Eb751BF1 | — |
| SBETTimelock | 0xCA817bcA55e853653a7B6205470018dDEA7431C3 | — |