├── foundry.toml ├── .gitmodules ├── .gitignore ├── .github └── workflows │ └── test.yml ├── README.md ├── src └── MicroStaking.sol └── test └── MicroStaking.t.sol /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/solmate"] 5 | path = lib/solmate 6 | url = https://github.com/transmissions11/solmate 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | env: 9 | FOUNDRY_PROFILE: ci 10 | 11 | jobs: 12 | check: 13 | strategy: 14 | fail-fast: true 15 | 16 | name: Foundry project 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | submodules: recursive 22 | 23 | - name: Install Foundry 24 | uses: foundry-rs/foundry-toolchain@v1 25 | with: 26 | version: nightly 27 | 28 | - name: Show Forge version 29 | run: | 30 | forge --version 31 | 32 | - name: Run Forge fmt 33 | run: | 34 | forge fmt --check 35 | id: fmt 36 | 37 | - name: Run Forge build 38 | run: | 39 | forge build --sizes 40 | id: build 41 | 42 | - name: Run Forge tests 43 | run: | 44 | forge test -vvv 45 | id: test 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### MicroStaking 2 | 3 | MicroStaking is a minimalistic staking contract that allows users to stake a custom ERC20 token, earn rewards over time, and then unstake their tokens. It uses the Solmate library for streamlined ERC20 and ownership functionality. 4 | 5 | ## Overview 6 | 7 | - **Stake** tokens to earn yield over time. 8 | - **Mint** new reward tokens automatically, based on a fixed rate set at contract deployment. 9 | - **Unstake** at any time (in this version, fully unstaking all tokens). 10 | 11 | ## How It Works 12 | 13 | 1. **Reward Rate** 14 | A fixed `ratePerSecond` determines how many tokens are minted per second in total. 15 | 16 | 2. **rewardPerShare Tracking** 17 | 18 | - Every time someone calls `stake()`, `unstake()`, or triggers the `update` modifier, the contract calculates how much time has passed since the last update. 19 | - It then mints `minted = timeElapsed * ratePerSecond` tokens. 20 | - `rewardPerShare` is updated as `rewardPerShare += (minted * 1e18) / totalStaked`. 21 | - Each user’s pending rewards are `staked[msg.sender] * rewardPerShare / 1e18 - debt[msg.sender]`. 22 | - Those rewards are minted directly to the user, and `debt[msg.sender]` is updated accordingly. 23 | 24 | 3. **Staking Flow** 25 | 26 | - User calls `stake(amount)` to deposit tokens in the contract. 27 | - The user’s `staked[msg.sender]` increases by `amount`. 28 | - The user’s `debt[msg.sender]` is set to the new checkpoint so that future reward calculations are accurate. 29 | 30 | 4. **Unstaking Flow** 31 | - User calls `unstake()`, which currently withdraws **all** staked tokens. 32 | - The contract transfers staked tokens back to the user, and updates their `debt` to reflect a zero stake. 33 | -------------------------------------------------------------------------------- /src/MicroStaking.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {ERC20} from "lib/solmate/src/tokens/ERC20.sol"; 5 | import {Owned} from "lib/solmate/src/auth/Owned.sol"; 6 | 7 | contract Token is ERC20("shafu Token", "ST", 18), Owned(msg.sender) { 8 | function mint(address to, uint amount) external onlyOwner { _mint(to, amount); } 9 | function burn(address from, uint amount) external onlyOwner { _burn(from, amount); } 10 | } 11 | 12 | contract MicroStaking { 13 | Token public token; 14 | 15 | uint public lastUpdate; 16 | uint public rewardPerShare; 17 | uint public ratePerSecond; 18 | 19 | mapping(address => uint) public debt; 20 | mapping(address => uint) public staked; 21 | 22 | modifier update() { 23 | uint timeElapsed = block.timestamp - lastUpdate; 24 | lastUpdate = block.timestamp; 25 | uint minted = timeElapsed * ratePerSecond; 26 | uint totalStaked = token.balanceOf(address(this)); 27 | if (totalStaked > 0) rewardPerShare += (minted * 1e18) / totalStaked; 28 | uint rewards = staked[msg.sender] * rewardPerShare / 1e18 - debt[msg.sender]; 29 | token.mint(msg.sender, rewards); 30 | _; 31 | } 32 | 33 | constructor(Token _token) { 34 | token = _token; 35 | lastUpdate = block.timestamp; 36 | ratePerSecond = 10e18; 37 | } 38 | 39 | function stake(uint amount) external update { 40 | token.transferFrom(msg.sender, address(this), amount); 41 | staked[msg.sender] += amount; 42 | debt [msg.sender] = staked[msg.sender] * rewardPerShare / 1e18; 43 | } 44 | 45 | function unstake() external update { 46 | token.transfer(msg.sender, staked[msg.sender]); 47 | debt [msg.sender] = staked[msg.sender] * rewardPerShare / 1e18; 48 | staked[msg.sender] = 0; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/MicroStaking.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity =0.8.26; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import {Token, MicroStaking} from "../src/MicroStaking.sol"; 7 | 8 | contract MicroStaking_Test is Test { 9 | Token token; 10 | MicroStaking staking; 11 | 12 | address bob; 13 | address alice; 14 | 15 | function setUp() public { 16 | token = new Token(); 17 | staking = new MicroStaking(token); 18 | token.transferOwnership(address(staking)); 19 | 20 | bob = makeAddr("bob"); 21 | alice = makeAddr("alice"); 22 | } 23 | 24 | function test_staking() public { 25 | uint bobAmount = 100e18; 26 | uint aliceAmount = 100e18; 27 | 28 | deal(address(token), bob, bobAmount); 29 | deal(address(token), alice, aliceAmount); 30 | 31 | vm.startPrank(bob); 32 | token.approve(address(staking), bobAmount); 33 | staking.stake(bobAmount); 34 | vm.stopPrank(); 35 | 36 | vm.startPrank(alice); 37 | token.approve(address(staking), aliceAmount); 38 | staking.stake(aliceAmount); 39 | vm.stopPrank(); 40 | 41 | vm.warp(block.timestamp + 1 days); 42 | 43 | vm.prank(bob); 44 | staking.unstake(); 45 | assertTrue(token.balanceOf(bob) > bobAmount); 46 | 47 | vm.prank(alice); 48 | staking.unstake(); 49 | assertTrue(token.balanceOf(alice) > aliceAmount); 50 | 51 | console.log(token.balanceOf(bob)); 52 | console.log(token.balanceOf(alice)); 53 | } 54 | 55 | function test_staking_differentTimes() public { 56 | uint bobAmount = 100e18; 57 | uint aliceAmount = 100e18; 58 | 59 | deal(address(token), bob, bobAmount); 60 | deal(address(token), alice, aliceAmount); 61 | 62 | vm.startPrank(bob); 63 | token.approve(address(staking), bobAmount); 64 | staking.stake(bobAmount); 65 | vm.stopPrank(); 66 | 67 | vm.warp(block.timestamp + 1 days); 68 | 69 | vm.startPrank(alice); 70 | token.approve(address(staking), aliceAmount); 71 | staking.stake(aliceAmount); 72 | vm.stopPrank(); 73 | 74 | vm.warp(block.timestamp + 1 days); 75 | 76 | vm.prank(bob); 77 | staking.unstake(); 78 | 79 | vm.prank(alice); 80 | staking.unstake(); 81 | 82 | console.log(token.balanceOf(bob)); 83 | console.log(token.balanceOf(alice)); 84 | } 85 | } --------------------------------------------------------------------------------