How Doge Stake works
Written against the contracts in this repository — SDoge, MemeToken, MemeTokenFactory and StakingVault. Where a number or a formula appears below, it is the one the deployed bytecode uses.
Demo mode — contracts not deployed. Every pool, balance and reward below is simulated in your browser and saved per wallet address. Nothing is signed and no chain state changes.
Overview
Doge Stake is two contracts and a token. The MemeTokenFactory deploys fixed-supply meme coins and lists them for staking; the StakingVault holds every pool, streams sDoge emissions, distributes protocol fees in native ETH, and tracks airdrop points. There is no router, no oracle, no proxy and no off-chain component.
A staked position earns three things simultaneously, all settled by the same internal _settle call on every interaction:
- sDoge emissions — a fixed protocol-wide rate per second, divided between pools by allocation point and inside a pool by your share of its stake.
- Protocol fees in ETH — every wei that enters the vault is indexed per allocation point, then per staked share. Claimed as native ETH, not a wrapper.
- Airdrop points — token-seconds staked, recorded on chain per pool.
Why Dogecoin can't be staked
Dogecoin uses proof-of-work. Blocks are produced by miners running Scrypt hashing — merge-mined with Litecoin — and security is paid for with electricity and hardware. There is no bonding step in the protocol, no validator registry, no delegation and no staking function to call. A holder can hold, spend or trade DOGE; there is no native mechanism through which holding it earns more of it.
Proof-of-stake chains work the other way around. Capital is locked, the locked capital proposes and attests blocks, and the protocol pays issuance for that service, with slashing as the penalty for misbehaviour. That is where “staking rewards” on ETH or SOL come from: the stake performs work that the chain needs.
This is the point Robinhood CEO Vlad Tenev has made publicly when asked about staking DOGE: proof-of-work coins cannot be staked, so no amount of product work makes a DOGE staking yield appear. The demand is real — it is one of the most held retail assets in the world — but the supply side does not exist at the consensus layer.
Doge Stake takes that constraint seriously instead of papering over it. It does not claim to stake Dogecoin. It provides the missing layer where it can genuinely exist: smart contracts on Robinhood Chain, paying emissions and real fee revenue to people who commit Doge-culture tokens for a while. The reward token is sDoge, the revenue is ETH, and both are enforced by code you can read in contracts/src.
Robinhood Chain
Robinhood Chain is an Arbitrum-stack L2 on Ethereum. It posts data availability to Ethereum blobs and uses ETH as its gas token, so there is no separate gas asset to acquire. Doge Stake targets Robinhood Chain by default.
| Parameter | Mainnet | Testnet |
|---|---|---|
| Chain id | 4663 | 46630 |
| RPC | https://rpc.mainnet.chain.robinhood.com | https://rpc.testnet.chain.robinhood.com |
| Explorer | robinhoodchain.blockscout.com | explorer.testnet.chain.robinhood.com |
| Gas token | ETH (18) | ETH (18) |
| Faucet | — | faucet.testnet.chain.robinhood.com |
| Data availability | Ethereum blobs (EIP-4844) | |
Adding the network is handled for you: connect a wallet on the wrong chain and the banner on /stake offers a one-click switch, which prompts your wallet to add Robinhood Chain if it does not know it yet. For production traffic, Alchemy is the recommended RPC provider; the public endpoints above are the defaults in src/lib/chains.ts.
sDoge tokenomics
sDoge (name “Doge Stake”) is a plain ERC20 with one unusual property: its supply cap is immutable and set in the constructor, and the only address that may mint is the vault.
- Name / symbol
- Doge Stake / sDoge
- Decimals
- 18
- Supply cap
- Immutable maxSupply, set at deployment (default 100,000,000)
- Premint
- Constructor parameter, default 0 in the deploy script
- Minter
- Set once via setMinter — one-way, and the vault is the only minter
- Emission rate
- rewardPerSecond on the vault, owner-settable, default 0.05 sDoge/s
At the default rate the protocol emits 0.05 sDoge per second — 4,320 per day, about 1,576,800 per year — so a 100,000,000 cap lasts on the order of 63 years. The rate is a single number on the vault and the owner can change it, but never retroactively:setRewardPerSecond settles the global index at the old rate first. To compute the schedule yourself:
emissions per day = rewardPerSecond × 86_400
years until cap = (maxSupply − totalSupply) / (rewardPerSecond × 31_536_000)
your pool's rate = rewardPerSecond × pool.allocPoint / totalAllocPointEmissions are minted at accrual time, into the vault, inside _updatePool. That means the vault always holds the sDoge it owes: a claim is a transfer of tokens that already exist, not a mint at claim time. When the cap is reached the mint is clamped to mintable() and emissions stop without a revert.
Staking mechanics
Both emissions and fees use the same two-level lazy index, which is what keeps every action O(1) no matter how many pools exist. Level one is global and measured per allocation point. Level two is per pool and measured per staked share. A pool pulls from the global index using its own weight whenever it is touched.
Emissions
// global, sDoge per allocation point (PRECISION = 1e18)
rewardIndex += Δt × rewardPerSecond × 1e18 / totalAllocPoint
// pool pulls its slice, minted into the vault, clamped by sDoge.mintable()
reward = (rewardIndex − pool.rewardIndexCheckpoint) × pool.allocPoint / 1e18
pool.accRewardPerShare += reward × 1e18 / pool.totalStaked
// position, settled into rewardsOwed on every touch
earned = amount × pool.accRewardPerShare / 1e18 − position.rewardDebtBecause a pool's entitlement is always (globalIndex − checkpoint) × allocPoint, a change to totalAllocPoint — listing a new token, retuning a weight — only changes how fast the global index grows from that block on. It cannot reprice emissions other pools already earned. Adding a pool therefore does not have to iterate over existing pools, and listing from the launchpad stays cheap.
A pool with zero stake accrues nothing: its slice is skipped rather than minted, so no sDoge is wasted on an empty pool. Emissions allocated to it while empty are simply not minted at all.
Positions and settlement
stake, unstake, claimRewards, claimFees and harvest all begin with _settle, which updates the pool, syncs fees, accrues the position and only then recomputes debt checkpoints. Anything earned is moved into rewardsOwed / feesOwed before those checkpoints move, so no action can silently forfeit rewards — including staking more mid-stream.
Deposits are measured by balance delta rather than by the requested amount, so a staking token that deducts on transfer cannot desync a pool's accounting from its actual holdings.
ETH fee share
The vault treats incoming ETH as protocol revenue. It exposes donateFees() and also accepts plain transfers through receive(). The launchpad forwards every launch fee this way, which is why launching a coin pays the people who are already staking.
// global, wei per allocation point
feeIndex += amount × 1e18 / totalAllocPoint
// pool pulls its slice on its next touch
accrued = (feeIndex − pool.feeIndexCheckpoint) × pool.allocPoint / 1e18
pool.accFeePerShare += accrued × 1e18 / pool.totalStaked
// position
feesOwed += amount × pool.accFeePerShare / 1e18 − position.feeDebtTwo consequences are worth being explicit about. First, fee shares are earned at the moment revenue arrives: stake after a fee has landed and you receive none of that fee, which is checked by the test suite. Second, if a pool has no stake when its slice is pulled, the ETH is added to orphanedFees instead of being distributed — along with any rounding dust that could not be represented in an index. Anyone can call recycleOrphanedFees() to push that balance back into the live fee stream for pools that do have stakers.
Claims are per pool and pay native ETH via a low-level call. totalFeesReceived and totalFeesClaimed are both tracked, and the test suite asserts the conservation invariant: claims never exceed receipts, and the vault's ETH balance always covers what it still owes.
Exit fees and auto-compounding
Unstaking charges a fee in the staked token, set per pool and hard-capped at 5% by MAX_EXIT_FEE_BPS = 500. The fee never leaves the pool: it is spread across the stakers who remain and compounds into their positions.
exitFee = amount × pool.exitFeeBps / 10_000
// spread over the remaining stake, after the leaver's amount is removed
pool.accBonusPerShare += exitFee × 1e18 / pool.totalStaked
// compounded into a remaining staker's position on their next touch
bonus = amount × pool.accBonusPerShare / 1e18 − position.bonusDebt
position.amount += bonus // and pool.totalStaked += bonusIf the person leaving is the last staker there is nobody to pay, so the fee is waived and they withdraw in full rather than stranding tokens in the vault. Compounded amounts are visible before they are realised through pendingBonus(pid, user), and stakedBalance already includes them, so the interface shows your real position rather than a stale one.
Airdrop points
Points measure conviction rather than size alone: they are token-seconds, accrued per pool whenever a position is touched.
position.airdropPoints += amount × (now − position.lastAccrual) / 1e18Because the divisor is 1e18, one whole token staked for one second is one point. Points are additive and are never reset — claiming sDoge or ETH does not touch them, and unstaking keeps the points you already have while stopping accrual on the withdrawn amount. Read them with airdropPoints(pid, user) or totalAirdropPoints(user).
Launchpad
MemeTokenFactory.createToken does four things in one transaction: deploys a MemeToken with the full supply minted to you, forwards the launch fee to the vault as staker revenue, lists the token as a staking pool through addPoolFromLaunchpad, and refunds any ETH you sent above the fee.
- Token type
- Fixed-supply ERC20 — no mint function, no owner, 18 decimals
- Supply recipient
- msg.sender receives 100% at deployment
- Launch fee
- Owner-settable, hard-capped at 0.05 ETH (MAX_LAUNCH_FEE)
- Fee destination
- vault.donateFees() — becomes claimable ETH for stakers
- Pool defaults
- Owner-settable allocPoint (≤ 1,000) and exit fee (≤ 5%)
- Name / symbol caps
- 48 bytes / 12 bytes, both non-empty
- Supply band
- 1,000 … 1,000,000,000,000 tokens
- Metadata caps
- imageURI ≤ 512 bytes, description ≤ 600 bytes
On-chain metadata is deliberately minimal: the token stores imageURI, description, creator and launchedAt, and the factory keeps an enumerable registry so the interface can render every launched coin from tokenInfoPage without an indexer. Validation failures revert with custom errors rather than strings, so the interface can explain them precisely.
Contracts
Nothing is deployed yet, and this page will not pretend otherwise. The addresses below are unset, so the interface runs in demo mode: pools, balances and rewards are simulated in your browser. Deploy the contracts, set the environment variables listed here, and the same interface switches to live reads and writes with no code changes.
Deployment is a single forge script: npm run contracts:deploy -- --network testnet runs a dry run, and adding --broadcast sends it. The script deploys sDoge, the vault and the factory, then wires sDoge.setMinter(vault) and vault.setLaunchpad(factory) and prints every address.
Security model and trust assumptions
What the owner can do
- Change the global emission rate (
setRewardPerSecond) — settled at the old rate first, so it is never retroactive. - List pools (
addPool) and retune weights and exit fees (setPool), within the 5% exit-fee ceiling. - Authorise the launchpad (
setLaunchpad) that may auto-list tokens. - Set the sDoge minter exactly once (
setMinter), and change the launch fee and pool defaults on the factory within their caps.
What the owner cannot do
- Withdraw or move staked tokens. There is no such function.
- Withdraw ETH fees. Fees can only be claimed by stakers, pro rata.
- Mint sDoge. The minter is the vault, set once and never changeable, and every mint is clamped by the immutable supply cap.
- Raise the exit fee above 5% or the launch fee above 0.05 ETH.
- Upgrade or pause anything. There are no proxies, no upgrade paths and no pause switches — the deployed bytecode is final.
- Change a launched token. Meme tokens have no owner and no mint function.
Implementation notes
ReentrancyGuardon every user entry point, andSafeERC20for all token movements.- Emissions are minted to the vault as they accrue, so payouts are always fully backed.
- Deposits use balance deltas, so fee-on-transfer tokens cannot desync a pool.
- Rounding dust is never silently dropped: it accumulates in
orphanedFeesand can be recycled. - The Foundry suite covers emission math, fee conservation, exit-fee compounding, the nothing-forfeited invariant, access control, factory behaviour, and fuzz tests on stake / unstake amounts and fee distribution.
FAQ
Risk disclaimer
Plainly, so there is no misunderstanding:
- Meme coins go to zero. Most tokens launched through any launchpad, including this one, end up worthless. A high emissions yield on a token that loses all its value is still a loss.
- sDoge has no guaranteed value. It is emitted by a contract, not backed by anything, and there is no buyback, no revenue claim and no promise of liquidity.
- Emissions dilute. New pools and rate changes affect future emissions. Your share of a fixed rate falls as more capital stakes.
- The code is unaudited. A bug could lock or lose funds. You are interacting with contracts directly, at your own risk.
- Owner keys matter. The owner cannot take your stake, but can change emission rates and weights, which changes what you earn.
- L2 risk. Robinhood Chain is an Arbitrum-stack rollup. Sequencer downtime, bridge risk and upgrade decisions are outside this protocol's control.
- Points are not a promise. No airdrop has been scheduled or allocated.
- Not financial advice. No part of this interface is a recommendation, and nothing here is a security offering.
