A Solidity staking-rewards contract implementing the Synthetix StakingRewards
algorithm — time-based reward distribution with O(1) accounting via
rewardPerToken / earned, so per-user rewards are tracked without ever looping
over stakers.
Contracts-only project (no frontend). Written to understand the canonical staking reward math from scratch, tested with Hardhat 3 +
viemand deployable to Sepolia.
| Contract | What it does |
|---|---|
staking.sol |
Users stake one ERC20 and earn a second ERC20 as rewards. Reward accrual is handled by the updateReward modifier with rewardPerToken() / earned(); the owner funds rewards through notifyRewardAmount over a configurable rewardsDuration. Reentrancy-guarded. |
tokenA.sol / tokenB.sol |
Minimal OpenZeppelin ERC20 test tokens used as the staking / reward assets. |
Instead of updating every staker whenever rewards accrue, the contract maintains a
single global rewardPerTokenStored accumulator. Each account snapshots it in
userRewardPerTokenPaid, so a user's earnings are:
earned = balance * (rewardPerToken() - userRewardPerTokenPaid[account]) + rewards[account]
This is the standard gas-efficient staking pattern used by Synthetix and most DeFi staking pools — reward accounting stays O(1) regardless of how many users stake.
Key functions: stake · withdraw · getReward · notifyRewardAmount · setRewardsDuration
Events: Staked · Withdraw · RewardPaid · RewardAdded · RewardsDurationUpdated
npx hardhat test # run all tests
npx hardhat test solidity # Solidity (Foundry-compatible) tests
npx hardhat test nodejs # TypeScript integration tests (node:test + viem)Set the deployer key (kept out of the repo) via the keystore, then deploy with Ignition:
npx hardhat keystore set SEPOLIA_PRIVATE_KEY
npx hardhat ignition deploy --network sepolia ignition/modules/DeployStaking.tsA multi-pool variant (MultiPool.sol — multiple staking tokens sharing one reward
token, registered via addPool) is in progress on the feat/multi-pool branch.