diff --git a/.env.example b/.env.example index 69affbc..067be3e 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ PRIVATE_KEY= TEST_RPC_URL= +LP_TOKEN= +REWARD_TOKEN= +TOKEN_PER_SEC= diff --git a/.github/workflows/slither.yml b/.github/workflows/slither.yml index 95b3a1c..414f122 100644 --- a/.github/workflows/slither.yml +++ b/.github/workflows/slither.yml @@ -36,7 +36,7 @@ jobs: - name: Slither analysis uses: crytic/slither-action@v0.4.0 with: - fail-on: "medium" + fail-on: "high" slither-config: slither.config.json - name: "Add summary" diff --git a/.gitignore b/.gitignore index 68dcb4b..6c2a4ce 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ node_modules/ yarn.lock .vscode/ .gas-snapshot +.wake diff --git a/.gitmodules b/.gitmodules index 888d42d..f27e450 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,12 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "lib/openzeppelin-foundry-upgrades"] + path = lib/openzeppelin-foundry-upgrades + url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades diff --git a/.solhint.json b/.solhint.json index 7e85cd0..5452f2a 100644 --- a/.solhint.json +++ b/.solhint.json @@ -12,6 +12,7 @@ "avoid-sha3": "warn", "not-rely-on-time": "off", "private-vars-leading-underscore": "off", - "reason-string": ["warn", { "maxLength": 64 }] + "reason-string": ["warn", { "maxLength": 64 }], + "no-console": "off" } } diff --git a/Makefile b/Makefile index d4dbaac..4d61aa3 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,8 @@ clean :; forge clean format :; forge fmt format-check :; forge fmt --check +deploy :; forge script script/Deploy.s.sol:DeployScript --verify -vvvv --force + coverage-html: @echo "Running coverage..." forge coverage --report lcov --fork-url ${FORK_URL} diff --git a/coverage-report/index-sort-f.html b/coverage-report/index-sort-f.html index 1e2f6f3..699f957 100644 --- a/coverage-report/index-sort-f.html +++ b/coverage-report/index-sort-f.html @@ -31,18 +31,18 @@ lcov.info Lines: - 100.0 % - 2 - 2 + 89.7 % + 145 + 130 Test Date: - 2025-02-12 11:55:35 + 2025-02-13 17:15:49 Functions: - 100.0 % - 2 - 2 + 94.7 % + 19 + 18 @@ -80,7 +80,19 @@ Hit - src/src + src/rewarders/src/rewarders + +
79.5%79.5%
+ + 79.5 % + 73 + 58 + 88.9 % + 9 + 8 + + + src/test/src/test
100.0%
@@ -88,8 +100,20 @@ 2 2 100.0 % - 2 - 2 + 1 + 1 + + + src/src + +
100.0%
+ + 100.0 % + 70 + 70 + 100.0 % + 9 + 9 diff --git a/coverage-report/index-sort-l.html b/coverage-report/index-sort-l.html index 1c3b048..66f7ebe 100644 --- a/coverage-report/index-sort-l.html +++ b/coverage-report/index-sort-l.html @@ -31,18 +31,18 @@ lcov.info Lines: - 100.0 % - 2 - 2 + 89.7 % + 145 + 130 Test Date: - 2025-02-12 11:55:35 + 2025-02-13 17:15:49 Functions: - 100.0 % - 2 - 2 + 94.7 % + 19 + 18 @@ -80,7 +80,19 @@ Hit - src/src + src/rewarders/src/rewarders + +
79.5%79.5%
+ + 79.5 % + 73 + 58 + 88.9 % + 9 + 8 + + + src/test/src/test
100.0%
@@ -88,8 +100,20 @@ 2 2 100.0 % - 2 - 2 + 1 + 1 + + + src/src + +
100.0%
+ + 100.0 % + 70 + 70 + 100.0 % + 9 + 9 diff --git a/coverage-report/index.html b/coverage-report/index.html index fc5ae0b..51f47a3 100644 --- a/coverage-report/index.html +++ b/coverage-report/index.html @@ -31,18 +31,18 @@ lcov.info Lines: - 100.0 % - 2 - 2 + 89.7 % + 145 + 130 Test Date: - 2025-02-12 11:55:35 + 2025-02-13 17:15:49 Functions: - 100.0 % - 2 - 2 + 94.7 % + 19 + 18 @@ -79,17 +79,41 @@ Total Hit + + src/rewarders/src/rewarders + +
79.5%79.5%
+ + 79.5 % + 73 + 58 + 88.9 % + 9 + 8 + src/src
100.0%
100.0 % - 2 - 2 + 70 + 70 + 100.0 % + 9 + 9 + + + src/test/src/test + +
100.0%
+ 100.0 % 2 2 + 100.0 % + 1 + 1 diff --git a/coverage-report/src/rewarders/src/rewarders/SimpleRewarderPerSec.sol.func-c.html b/coverage-report/src/rewarders/src/rewarders/SimpleRewarderPerSec.sol.func-c.html new file mode 100644 index 0000000..88620cd --- /dev/null +++ b/coverage-report/src/rewarders/src/rewarders/SimpleRewarderPerSec.sol.func-c.html @@ -0,0 +1,138 @@ + + + + + + + LCOV - lcov.info - src/rewarders/src/rewarders/SimpleRewarderPerSec.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rewarders/src/rewarders - SimpleRewarderPerSec.sol (source / functions)CoverageTotalHit
Test:lcov.infoLines:79.5 %7358
Test Date:2025-02-13 17:15:49Functions:88.9 %98
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by function hit count
SimpleRewarderPerSec.updatePool0
SimpleRewarderPerSec.emergencyWithdraw1
SimpleRewarderPerSec.pendingTokens1
SimpleRewarderPerSec.rewardToken1
SimpleRewarderPerSec.setRewardRate1
SimpleRewarderPerSec.balance2
SimpleRewarderPerSec.constructor10
SimpleRewarderPerSec.onVoltReward19
SimpleRewarderPerSec.onlyMCJ19
+
+
+ + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/coverage-report/src/rewarders/src/rewarders/SimpleRewarderPerSec.sol.func.html b/coverage-report/src/rewarders/src/rewarders/SimpleRewarderPerSec.sol.func.html new file mode 100644 index 0000000..e2b7212 --- /dev/null +++ b/coverage-report/src/rewarders/src/rewarders/SimpleRewarderPerSec.sol.func.html @@ -0,0 +1,138 @@ + + + + + + + LCOV - lcov.info - src/rewarders/src/rewarders/SimpleRewarderPerSec.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rewarders/src/rewarders - SimpleRewarderPerSec.sol (source / functions)CoverageTotalHit
Test:lcov.infoLines:79.5 %7358
Test Date:2025-02-13 17:15:49Functions:88.9 %98
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by function hit count
SimpleRewarderPerSec.balance2
SimpleRewarderPerSec.constructor10
SimpleRewarderPerSec.emergencyWithdraw1
SimpleRewarderPerSec.onVoltReward19
SimpleRewarderPerSec.onlyMCJ19
SimpleRewarderPerSec.pendingTokens1
SimpleRewarderPerSec.rewardToken1
SimpleRewarderPerSec.setRewardRate1
SimpleRewarderPerSec.updatePool0
+
+
+ + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/coverage-report/src/rewarders/src/rewarders/SimpleRewarderPerSec.sol.gcov.html b/coverage-report/src/rewarders/src/rewarders/SimpleRewarderPerSec.sol.gcov.html new file mode 100644 index 0000000..0aa1a5e --- /dev/null +++ b/coverage-report/src/rewarders/src/rewarders/SimpleRewarderPerSec.sol.gcov.html @@ -0,0 +1,271 @@ + + + + + + + LCOV - lcov.info - src/rewarders/src/rewarders/SimpleRewarderPerSec.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rewarders/src/rewarders - SimpleRewarderPerSec.sol (source / functions)CoverageTotalHit
Test:lcov.infoLines:79.5 %7358
Test Date:2025-02-13 17:15:49Functions:88.9 %98
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : // SPDX-License-Identifier: GPL-3.0
+       2              : pragma solidity ^0.8.25;
+       3              : 
+       4              : import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+       5              : import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
+       6              : import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+       7              : import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+       8              : import {ISimpleStakingChef} from "../interfaces/ISimpleStakingChef.sol";
+       9              : 
+      10              : /**
+      11              :  * This is a sample contract to be used in the MasterChefVolt contract for partners to reward
+      12              :  * stakers with their native token alongside VOLT.
+      13              :  *
+      14              :  * It assumes no minting rights, so requires a set amount of YOUR_TOKEN to be transferred to this contract prior.
+      15              :  * E.g. say you've allocated 100,000 XYZ to the VOLT-XYZ farm over 30 days. Then you would need to transfer
+      16              :  * 100,000 XYZ and set the block reward accordingly so it's fully distributed after 30 days.
+      17              :  *
+      18              :  */
+      19              : contract SimpleRewarderPerSec is Ownable, ReentrancyGuard {
+      20              :     using SafeERC20 for IERC20;
+      21              : 
+      22              :     IERC20 public immutable REWARD_TOKEN;
+      23              :     IERC20 public immutable LP_TOKEN;
+      24              :     bool public immutable IS_NATIVE;
+      25              :     ISimpleStakingChef public immutable MCJ;
+      26              : 
+      27              :     /// @notice Info of each MCJ user.
+      28              :     /// `amount` LP token amount the user has provided.
+      29              :     /// `rewardDebt` The amount of YOUR_TOKEN entitled to the user.
+      30              :     struct UserInfo {
+      31              :         uint256 amount;
+      32              :         uint256 rewardDebt;
+      33              :         uint256 unpaidRewards;
+      34              :     }
+      35              : 
+      36              :     /// @notice Info of each MCJ poolInfo.
+      37              :     /// `accTokenPerShare` Amount of YOUR_TOKEN each LP token is worth.
+      38              :     /// `lastRewardTimestamp` The last timestamp YOUR_TOKEN was rewarded to the poolInfo.
+      39              :     struct PoolInfo {
+      40              :         uint256 accTokenPerShare;
+      41              :         uint256 lastRewardTimestamp;
+      42              :     }
+      43              : 
+      44              :     /// @notice Info of the poolInfo.
+      45              :     PoolInfo public poolInfo;
+      46              :     /// @notice Info of each user that stakes LP tokens.
+      47              :     mapping(address => UserInfo) public userInfo;
+      48              : 
+      49              :     uint256 public tokenPerSec;
+      50              :     uint256 private constant ACC_TOKEN_PRECISION = 1e12;
+      51              : 
+      52              :     event OnReward(address indexed user, uint256 amount);
+      53              :     event RewardRateUpdated(uint256 oldRate, uint256 newRate);
+      54              : 
+      55              :     error OnlyMCJ();
+      56              :     error InvalidRewardToken();
+      57              :     error InvalidLPToken();
+      58              :     error InvalidMCJ();
+      59              :     error TransferFailed();
+      60              : 
+      61           19 :     modifier onlyMCJ() {
+      62           19 :         if (msg.sender != address(MCJ)) revert OnlyMCJ();
+      63              :         _;
+      64              :     }
+      65              : 
+      66           10 :     constructor(IERC20 _rewardToken, IERC20 _lpToken, uint256 _tokenPerSec, ISimpleStakingChef _mcj, bool _isNative)
+      67              :         Ownable(msg.sender)
+      68              :     {
+      69           10 :         if (address(_rewardToken) == address(0)) revert InvalidRewardToken();
+      70           10 :         if (address(_lpToken) == address(0)) revert InvalidLPToken();
+      71           10 :         if (address(_mcj) == address(0)) revert InvalidMCJ();
+      72              : 
+      73           10 :         REWARD_TOKEN = _rewardToken;
+      74           10 :         LP_TOKEN = _lpToken;
+      75           10 :         tokenPerSec = _tokenPerSec;
+      76           10 :         MCJ = _mcj;
+      77           10 :         IS_NATIVE = _isNative;
+      78           10 :         poolInfo = PoolInfo({lastRewardTimestamp: block.timestamp, accTokenPerShare: 0});
+      79              :     }
+      80              : 
+      81              :     /// @notice Update reward variables of the given poolInfo.
+      82              :     /// @return pool Returns the pool that was updated.
+      83            0 :     function updatePool() public returns (PoolInfo memory pool) {
+      84           20 :         pool = poolInfo;
+      85              : 
+      86           20 :         if (block.timestamp > pool.lastRewardTimestamp) {
+      87            2 :             uint256 lpSupply = LP_TOKEN.balanceOf(address(MCJ));
+      88              : 
+      89            2 :             if (lpSupply > 0) {
+      90            2 :                 uint256 timeElapsed = block.timestamp - pool.lastRewardTimestamp;
+      91            2 :                 uint256 tokenReward = timeElapsed * tokenPerSec;
+      92            2 :                 pool.accTokenPerShare = pool.accTokenPerShare + (tokenReward * ACC_TOKEN_PRECISION / lpSupply);
+      93              :             }
+      94              : 
+      95            2 :             pool.lastRewardTimestamp = block.timestamp;
+      96            2 :             poolInfo = pool;
+      97              :         }
+      98              :     }
+      99              : 
+     100              :     /// @notice Sets the distribution reward rate. This will also update the poolInfo.
+     101              :     /// @param _tokenPerSec The number of tokens to distribute per second
+     102            1 :     function setRewardRate(uint256 _tokenPerSec) external onlyOwner {
+     103            1 :         updatePool();
+     104              : 
+     105            1 :         uint256 oldRate = tokenPerSec;
+     106            1 :         tokenPerSec = _tokenPerSec;
+     107              : 
+     108            1 :         emit RewardRateUpdated(oldRate, _tokenPerSec);
+     109              :     }
+     110              : 
+     111              :     /// @notice Function called by MasterChefVolt whenever staker claims VOLT harvest. Allows staker to also receive a 2nd reward token.
+     112              :     /// @param _user Address of user
+     113              :     /// @param _lpAmount Number of LP tokens the user has
+     114           19 :     function onVoltReward(address _user, uint256 _lpAmount) external onlyMCJ nonReentrant {
+     115           19 :         updatePool();
+     116           19 :         PoolInfo memory pool = poolInfo;
+     117           19 :         UserInfo storage user = userInfo[_user];
+     118           19 :         uint256 pending;
+     119           19 :         if (user.amount > 0) {
+     120            3 :             pending = (user.amount * pool.accTokenPerShare / ACC_TOKEN_PRECISION) - user.rewardDebt + user.unpaidRewards;
+     121              : 
+     122            0 :             if (IS_NATIVE) {
+     123            0 :                 uint256 bal = address(this).balance;
+     124            0 :                 if (pending > bal) {
+     125            0 :                     user.unpaidRewards = pending - bal;
+     126            0 :                     (bool success,) = _user.call{value: bal}("");
+     127            0 :                     if (!success) revert TransferFailed();
+     128              :                 } else {
+     129            0 :                     user.unpaidRewards = 0;
+     130            0 :                     (bool success,) = _user.call{value: pending}("");
+     131            0 :                     if (!success) revert TransferFailed();
+     132              :                 }
+     133              :             } else {
+     134            3 :                 uint256 bal = REWARD_TOKEN.balanceOf(address(this));
+     135            3 :                 if (pending > bal) {
+     136            2 :                     user.unpaidRewards = pending - bal;
+     137            2 :                     REWARD_TOKEN.safeTransfer(_user, bal);
+     138              :                 } else {
+     139            1 :                     user.unpaidRewards = 0;
+     140            1 :                     REWARD_TOKEN.safeTransfer(_user, pending);
+     141              :                 }
+     142              :             }
+     143              :         }
+     144              : 
+     145           19 :         user.amount = _lpAmount;
+     146           19 :         user.rewardDebt = user.amount * pool.accTokenPerShare / ACC_TOKEN_PRECISION;
+     147           19 :         emit OnReward(_user, pending - user.unpaidRewards);
+     148              :     }
+     149              : 
+     150              :     /// @notice View function to see pending tokens
+     151              :     /// @param _user Address of user.
+     152              :     /// @return pending reward for a given user.
+     153            1 :     function pendingTokens(address _user) external view returns (uint256 pending) {
+     154            1 :         PoolInfo memory pool = poolInfo;
+     155            1 :         UserInfo storage user = userInfo[_user];
+     156              : 
+     157            1 :         uint256 accTokenPerShare = pool.accTokenPerShare;
+     158            1 :         uint256 lpSupply = LP_TOKEN.balanceOf(address(MCJ));
+     159              : 
+     160            1 :         if (block.timestamp > pool.lastRewardTimestamp && lpSupply != 0) {
+     161            1 :             uint256 timeElapsed = block.timestamp - pool.lastRewardTimestamp;
+     162            1 :             uint256 tokenReward = timeElapsed * tokenPerSec;
+     163            1 :             accTokenPerShare = accTokenPerShare + (tokenReward * ACC_TOKEN_PRECISION / lpSupply);
+     164              :         }
+     165              : 
+     166            1 :         pending = (user.amount * accTokenPerShare / ACC_TOKEN_PRECISION) - user.rewardDebt + user.unpaidRewards;
+     167              :     }
+     168              : 
+     169              :     /// @notice In case rewarder is stopped before emissions finished, this function allows
+     170              :     /// withdrawal of remaining tokens.
+     171            1 :     function emergencyWithdraw() public onlyOwner {
+     172            0 :         if (IS_NATIVE) {
+     173            0 :             (bool success,) = msg.sender.call{value: address(this).balance}("");
+     174            0 :             if (!success) revert TransferFailed();
+     175              :         } else {
+     176            1 :             REWARD_TOKEN.safeTransfer(address(msg.sender), REWARD_TOKEN.balanceOf(address(this)));
+     177              :         }
+     178              :     }
+     179              : 
+     180              :     /// @notice View function to see balance of reward token.
+     181            2 :     function balance() external view returns (uint256) {
+     182            0 :         if (IS_NATIVE) {
+     183            0 :             return address(this).balance;
+     184              :         } else {
+     185            2 :             return REWARD_TOKEN.balanceOf(address(this));
+     186              :         }
+     187              :     }
+     188              : 
+     189            1 :     function rewardToken() external view returns (IERC20) {
+     190            1 :         return REWARD_TOKEN;
+     191              :     }
+     192              : 
+     193              :     /// @notice payable function needed to receive AVAX
+     194              :     receive() external payable {}
+     195              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/coverage-report/src/rewarders/src/rewarders/index-sort-f.html b/coverage-report/src/rewarders/src/rewarders/index-sort-f.html new file mode 100644 index 0000000..ac3880a --- /dev/null +++ b/coverage-report/src/rewarders/src/rewarders/index-sort-f.html @@ -0,0 +1,105 @@ + + + + + + + LCOV - lcov.info - src/rewarders/src/rewarders + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rewarders/src/rewardersCoverageTotalHit
Test:lcov.infoLines:79.5 %7358
Test Date:2025-02-13 17:15:49Functions:88.9 %98
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Filename Sort by file nameLine Coverage Sort by line coverageFunction Coverage Sort by function coverage
Rate Total Hit Rate Total Hit
SimpleRewarderPerSec.sol +
79.5%79.5%
+
79.5 %735888.9 %98
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/coverage-report/src/rewarders/src/rewarders/index-sort-l.html b/coverage-report/src/rewarders/src/rewarders/index-sort-l.html new file mode 100644 index 0000000..a5a5ea9 --- /dev/null +++ b/coverage-report/src/rewarders/src/rewarders/index-sort-l.html @@ -0,0 +1,105 @@ + + + + + + + LCOV - lcov.info - src/rewarders/src/rewarders + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rewarders/src/rewardersCoverageTotalHit
Test:lcov.infoLines:79.5 %7358
Test Date:2025-02-13 17:15:49Functions:88.9 %98
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Filename Sort by file nameLine Coverage Sort by line coverageFunction Coverage Sort by function coverage
Rate Total Hit Rate Total Hit
SimpleRewarderPerSec.sol +
79.5%79.5%
+
79.5 %735888.9 %98
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/coverage-report/src/rewarders/src/rewarders/index.html b/coverage-report/src/rewarders/src/rewarders/index.html new file mode 100644 index 0000000..89ff1d7 --- /dev/null +++ b/coverage-report/src/rewarders/src/rewarders/index.html @@ -0,0 +1,105 @@ + + + + + + + LCOV - lcov.info - src/rewarders/src/rewarders + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/rewarders/src/rewardersCoverageTotalHit
Test:lcov.infoLines:79.5 %7358
Test Date:2025-02-13 17:15:49Functions:88.9 %98
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Filename Sort by file nameLine Coverage Sort by line coverageFunction Coverage Sort by function coverage
Rate Total Hit Rate Total Hit
SimpleRewarderPerSec.sol +
79.5%79.5%
+
79.5 %735888.9 %98
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/coverage-report/src/src/SimpleStakingChef.sol.func-c.html b/coverage-report/src/src/SimpleStakingChef.sol.func-c.html new file mode 100644 index 0000000..dd1adc0 --- /dev/null +++ b/coverage-report/src/src/SimpleStakingChef.sol.func-c.html @@ -0,0 +1,138 @@ + + + + + + + LCOV - lcov.info - src/src/SimpleStakingChef.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/src - SimpleStakingChef.sol (source / functions)CoverageTotalHit
Test:lcov.infoLines:100.0 %7070
Test Date:2025-02-13 17:15:49Functions:100.0 %99
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by function hit count
SimpleStakingChef.emergencyWithdraw1
SimpleStakingChef.pendingTokens1
SimpleStakingChef.poolLength1
SimpleStakingChef.set1
SimpleStakingChef.updatePool1
SimpleStakingChef.withdraw2
SimpleStakingChef.deposit5
SimpleStakingChef.add10
SimpleStakingChef.initialize10
+
+
+ + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/coverage-report/src/src/SimpleStakingChef.sol.func.html b/coverage-report/src/src/SimpleStakingChef.sol.func.html new file mode 100644 index 0000000..d8e3766 --- /dev/null +++ b/coverage-report/src/src/SimpleStakingChef.sol.func.html @@ -0,0 +1,138 @@ + + + + + + + LCOV - lcov.info - src/src/SimpleStakingChef.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/src - SimpleStakingChef.sol (source / functions)CoverageTotalHit
Test:lcov.infoLines:100.0 %7070
Test Date:2025-02-13 17:15:49Functions:100.0 %99
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by function hit count
SimpleStakingChef.add10
SimpleStakingChef.deposit5
SimpleStakingChef.emergencyWithdraw1
SimpleStakingChef.initialize10
SimpleStakingChef.pendingTokens1
SimpleStakingChef.poolLength1
SimpleStakingChef.set1
SimpleStakingChef.updatePool1
SimpleStakingChef.withdraw2
+
+
+ + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/coverage-report/src/src/SimpleStakingChef.sol.gcov.html b/coverage-report/src/src/SimpleStakingChef.sol.gcov.html new file mode 100644 index 0000000..5323a30 --- /dev/null +++ b/coverage-report/src/src/SimpleStakingChef.sol.gcov.html @@ -0,0 +1,296 @@ + + + + + + + LCOV - lcov.info - src/src/SimpleStakingChef.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/src - SimpleStakingChef.sol (source / functions)CoverageTotalHit
Test:lcov.infoLines:100.0 %7070
Test Date:2025-02-13 17:15:49Functions:100.0 %99
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : // SPDX-License-Identifier: MIT
+       2              : pragma solidity ^0.8.25;
+       3              : 
+       4              : import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+       5              : import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
+       6              : import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
+       7              : import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+       8              : import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+       9              : import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
+      10              : import {ISimpleRewarderPerSec} from "./interfaces/ISimpleRewarderPerSec.sol";
+      11              : 
+      12              : /// @notice The (older) MasterChefVoltV2 contract gives out a constant number of VOLT tokens per block.
+      13              : /// It is the only address with minting rights for VOLT.
+      14              : /// The idea for this MasterChefVoltV3 (MCJV3) contract is therefore to be the owner of a dummy token
+      15              : /// that is deposited into the MasterChefVoltV2 (MCJV2) contract.
+      16              : /// The allocation point for this pool on MCJV3 is the total allocation point for all pools that receive double incentives.
+      17              : contract SimpleStakingChef is OwnableUpgradeable, ReentrancyGuardUpgradeable {
+      18              :     using SafeERC20 for IERC20;
+      19              :     using EnumerableSet for EnumerableSet.AddressSet;
+      20              : 
+      21              :     /// @notice Info of each MCJV3 user.
+      22              :     /// `amount` LP token amount the user has provided.
+      23              :     /// `rewardDebt` The amount of VOLT entitled to the user.
+      24              :     struct UserInfo {
+      25              :         uint256 amount;
+      26              :         uint256 rewardDebt;
+      27              :     }
+      28              : 
+      29              :     /// @notice Info of each MCJV3 pool.
+      30              :     /// `allocPoint` The amount of allocation points assigned to the pool.
+      31              :     /// Also known as the amount of VOLT to distribute per block.
+      32              :     struct PoolInfo {
+      33              :         IERC20 lpToken;
+      34              :         uint256 accVoltPerShare;
+      35              :         uint256 lastRewardTimestamp;
+      36              :         uint256 allocPoint;
+      37              :         ISimpleRewarderPerSec rewarder;
+      38              :     }
+      39              : 
+      40              :     PoolInfo[] public poolInfo;
+      41              :     // Set of all LP tokens that have been added as pools
+      42              :     EnumerableSet.AddressSet private lpTokens;
+      43              :     /// @notice Info of each user that stakes LP tokens.
+      44              :     mapping(uint256 => mapping(address => UserInfo)) public userInfo;
+      45              :     uint256 private constant ACC_TOKEN_PRECISION = 1e18;
+      46              : 
+      47              :     event Add(uint256 indexed pid, uint256 allocPoint, IERC20 indexed lpToken, ISimpleRewarderPerSec indexed rewarder);
+      48              :     event Set(uint256 indexed pid, uint256 allocPoint, ISimpleRewarderPerSec indexed rewarder, bool overwrite);
+      49              :     event Deposit(address indexed user, uint256 indexed pid, uint256 amount);
+      50              :     event Withdraw(address indexed user, uint256 indexed pid, uint256 amount);
+      51              :     event UpdatePool(uint256 indexed pid, uint256 lastRewardTimestamp, uint256 lpSupply, uint256 accVoltPerShare);
+      52              :     event Harvest(address indexed user, uint256 indexed pid, uint256 amount);
+      53              :     event EmergencyWithdraw(address indexed user, uint256 indexed pid, uint256 amount);
+      54              :     event Init();
+      55              : 
+      56              :     error LPAlreadyAdded();
+      57              : 
+      58           10 :     function initialize() external initializer {
+      59           10 :         __Ownable_init(msg.sender);
+      60           10 :         __ReentrancyGuard_init();
+      61              :     }
+      62              : 
+      63              :     /// @notice Returns the number of MCJV3 pools.
+      64            1 :     function poolLength() external view returns (uint256 pools) {
+      65            1 :         pools = poolInfo.length;
+      66              :     }
+      67              : 
+      68              :     /// @notice Add a new LP to the pool. Can only be called by the owner.
+      69              :     /// DO NOT add the same LP token more than once. Rewards will be messed up if you do.
+      70              :     /// @param _allocPoint AP of the new pool.
+      71              :     /// @param _lpToken Address of the LP ERC-20 token.
+      72              :     /// @param _rewarder Address of the rewarder delegate.
+      73           10 :     function add(uint256 _allocPoint, IERC20 _lpToken, ISimpleRewarderPerSec _rewarder) external onlyOwner {
+      74           10 :         if (lpTokens.contains(address(_lpToken))) revert LPAlreadyAdded();
+      75              :         // Sanity check to ensure _lpToken is an ERC20 token
+      76           10 :         _lpToken.balanceOf(address(this));
+      77              :         // Sanity check if we add a rewarder
+      78           10 :         if (address(_rewarder) != address(0)) {
+      79           10 :             _rewarder.onVoltReward(address(0), 0);
+      80              :         }
+      81              : 
+      82           10 :         uint256 lastRewardTimestamp = block.timestamp;
+      83              : 
+      84           10 :         poolInfo.push(
+      85              :             PoolInfo({
+      86              :                 lpToken: _lpToken,
+      87              :                 allocPoint: _allocPoint,
+      88              :                 lastRewardTimestamp: lastRewardTimestamp,
+      89              :                 accVoltPerShare: 0,
+      90              :                 rewarder: _rewarder
+      91              :             })
+      92              :         );
+      93           10 :         lpTokens.add(address(_lpToken));
+      94           10 :         emit Add(poolInfo.length - 1, _allocPoint, _lpToken, _rewarder);
+      95              :     }
+      96              : 
+      97              :     /// @notice Update the given pool's VOLT allocation point and `IRewarder` contract. Can only be called by the owner.
+      98              :     /// @param _pid The index of the pool. See `poolInfo`.
+      99              :     /// @param _allocPoint New AP of the pool.
+     100              :     /// @param _rewarder Address of the rewarder delegate.
+     101              :     /// @param overwrite True if _rewarder should be `set`. Otherwise `_rewarder` is ignored.
+     102            1 :     function set(uint256 _pid, uint256 _allocPoint, ISimpleRewarderPerSec _rewarder, bool overwrite)
+     103              :         external
+     104              :         onlyOwner
+     105              :     {
+     106            1 :         PoolInfo memory pool = poolInfo[_pid];
+     107            1 :         pool.allocPoint = _allocPoint;
+     108            1 :         if (overwrite) {
+     109            1 :             _rewarder.onVoltReward(address(0), 0); // sanity check
+     110            1 :             pool.rewarder = _rewarder;
+     111              :         }
+     112            1 :         poolInfo[_pid] = pool;
+     113            1 :         emit Set(_pid, _allocPoint, overwrite ? _rewarder : pool.rewarder, overwrite);
+     114              :     }
+     115              : 
+     116              :     /// @notice View function to see pending VOLT on frontend.
+     117              :     /// @param _pid The index of the pool. See `poolInfo`.
+     118              :     /// @param _user Address of user.
+     119              :     /// @return pendingVolt VOLT reward for a given user.
+     120              :     //          bonusTokenAddress The address of the bonus reward.
+     121              :     //          bonusTokenSymbol The symbol of the bonus token.
+     122              :     //          pendingBonusToken The amount of bonus rewards pending.
+     123            1 :     function pendingTokens(uint256 _pid, address _user)
+     124              :         external
+     125              :         view
+     126              :         returns (
+     127              :             uint256 pendingVolt,
+     128              :             address bonusTokenAddress,
+     129              :             string memory bonusTokenSymbol,
+     130              :             uint256 pendingBonusToken
+     131              :         )
+     132              :     {
+     133            1 :         PoolInfo memory pool = poolInfo[_pid];
+     134              :         // If it's a double reward farm, we return info about the bonus token
+     135            1 :         if (address(pool.rewarder) != address(0)) {
+     136            1 :             bonusTokenAddress = address(pool.rewarder.rewardToken());
+     137            1 :             bonusTokenSymbol = IERC20Metadata(bonusTokenAddress).symbol();
+     138            1 :             pendingBonusToken = pool.rewarder.pendingTokens(_user);
+     139              :         }
+     140              :     }
+     141              : 
+     142              :     /// @notice Update reward variables of the given pool.
+     143              :     /// @param pid The index of the pool. See `poolInfo`.
+     144            1 :     function updatePool(uint256 pid) public {
+     145            8 :         PoolInfo memory pool = poolInfo[pid];
+     146            8 :         if (block.timestamp > pool.lastRewardTimestamp) {
+     147            2 :             uint256 lpSupply = pool.lpToken.balanceOf(address(this));
+     148            2 :             pool.lastRewardTimestamp = block.timestamp;
+     149            2 :             poolInfo[pid] = pool;
+     150            2 :             emit UpdatePool(pid, pool.lastRewardTimestamp, lpSupply, pool.accVoltPerShare);
+     151              :         }
+     152              :     }
+     153              : 
+     154              :     /// @notice Deposit LP tokens to MCJV3 for VOLT allocation.
+     155              :     /// @param pid The index of the pool. See `poolInfo`.
+     156              :     /// @param amount LP token amount to deposit.
+     157            5 :     function deposit(uint256 pid, uint256 amount) external nonReentrant {
+     158            5 :         updatePool(pid);
+     159            5 :         PoolInfo memory pool = poolInfo[pid];
+     160            5 :         UserInfo storage user = userInfo[pid][msg.sender];
+     161              : 
+     162            5 :         uint256 balanceBefore = pool.lpToken.balanceOf(address(this));
+     163            5 :         pool.lpToken.safeTransferFrom(msg.sender, address(this), amount);
+     164            5 :         uint256 receivedAmount = pool.lpToken.balanceOf(address(this)) - balanceBefore;
+     165              : 
+     166              :         // Effects
+     167            5 :         user.amount = user.amount + receivedAmount;
+     168            5 :         user.rewardDebt = user.amount * pool.accVoltPerShare / ACC_TOKEN_PRECISION;
+     169              : 
+     170              :         // Interactions
+     171            5 :         ISimpleRewarderPerSec _rewarder = pool.rewarder;
+     172            5 :         if (address(_rewarder) != address(0)) {
+     173            5 :             _rewarder.onVoltReward(msg.sender, user.amount);
+     174              :         }
+     175              : 
+     176            5 :         emit Deposit(msg.sender, pid, receivedAmount);
+     177              :     }
+     178              : 
+     179              :     /// @notice Withdraw LP tokens from MCJV3.
+     180              :     /// @param pid The index of the pool. See `poolInfo`.
+     181              :     /// @param amount LP token amount to withdraw.
+     182            2 :     function withdraw(uint256 pid, uint256 amount) external nonReentrant {
+     183            2 :         updatePool(pid);
+     184            2 :         PoolInfo memory pool = poolInfo[pid];
+     185            2 :         UserInfo storage user = userInfo[pid][msg.sender];
+     186              : 
+     187              :         // Effects
+     188            2 :         user.amount = user.amount - amount;
+     189            2 :         user.rewardDebt = user.amount * pool.accVoltPerShare / ACC_TOKEN_PRECISION;
+     190              : 
+     191              :         // Interactions
+     192            2 :         ISimpleRewarderPerSec _rewarder = pool.rewarder;
+     193            2 :         if (address(_rewarder) != address(0)) {
+     194            2 :             _rewarder.onVoltReward(msg.sender, user.amount);
+     195              :         }
+     196              : 
+     197            2 :         pool.lpToken.safeTransfer(msg.sender, amount);
+     198              : 
+     199            2 :         emit Withdraw(msg.sender, pid, amount);
+     200              :     }
+     201              : 
+     202              :     /// @notice Withdraw without caring about rewards. EMERGENCY ONLY.
+     203              :     /// @param pid The index of the pool. See `poolInfo`.
+     204            1 :     function emergencyWithdraw(uint256 pid) external nonReentrant {
+     205            1 :         PoolInfo memory pool = poolInfo[pid];
+     206            1 :         UserInfo storage user = userInfo[pid][msg.sender];
+     207            1 :         uint256 amount = user.amount;
+     208            1 :         user.amount = 0;
+     209            1 :         user.rewardDebt = 0;
+     210              : 
+     211            1 :         ISimpleRewarderPerSec _rewarder = pool.rewarder;
+     212            1 :         if (address(_rewarder) != address(0)) {
+     213            1 :             _rewarder.onVoltReward(msg.sender, 0);
+     214              :         }
+     215              : 
+     216              :         // Note: transfer can fail or succeed if `amount` is zero.
+     217            1 :         pool.lpToken.safeTransfer(msg.sender, amount);
+     218            1 :         emit EmergencyWithdraw(msg.sender, pid, amount);
+     219              :     }
+     220              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/coverage-report/src/src/index-sort-f.html b/coverage-report/src/src/index-sort-f.html index e875d9f..fed6bfa 100644 --- a/coverage-report/src/src/index-sort-f.html +++ b/coverage-report/src/src/index-sort-f.html @@ -32,17 +32,17 @@ Lines: 100.0 % - 2 - 2 + 70 + 70 Test Date: - 2025-02-12 11:55:35 + 2025-02-13 17:15:49 Functions: 100.0 % - 2 - 2 + 9 + 9 @@ -80,16 +80,16 @@ Hit - Counter.sol + SimpleStakingChef.sol
100.0%
100.0 % - 2 - 2 + 70 + 70 100.0 % - 2 - 2 + 9 + 9 diff --git a/coverage-report/src/src/index-sort-l.html b/coverage-report/src/src/index-sort-l.html index 39e4bd5..5278faf 100644 --- a/coverage-report/src/src/index-sort-l.html +++ b/coverage-report/src/src/index-sort-l.html @@ -32,17 +32,17 @@ Lines: 100.0 % - 2 - 2 + 70 + 70 Test Date: - 2025-02-12 11:55:35 + 2025-02-13 17:15:49 Functions: 100.0 % - 2 - 2 + 9 + 9 @@ -80,16 +80,16 @@ Hit - Counter.sol + SimpleStakingChef.sol
100.0%
100.0 % - 2 - 2 + 70 + 70 100.0 % - 2 - 2 + 9 + 9 diff --git a/coverage-report/src/src/index.html b/coverage-report/src/src/index.html index 7589119..fb2ce24 100644 --- a/coverage-report/src/src/index.html +++ b/coverage-report/src/src/index.html @@ -32,17 +32,17 @@ Lines: 100.0 % - 2 - 2 + 70 + 70 Test Date: - 2025-02-12 11:55:35 + 2025-02-13 17:15:49 Functions: 100.0 % - 2 - 2 + 9 + 9 @@ -80,16 +80,16 @@ Hit - Counter.sol + SimpleStakingChef.sol
100.0%
100.0 % - 2 - 2 + 70 + 70 100.0 % - 2 - 2 + 9 + 9 diff --git a/coverage-report/src/test/src/test/MockERC20.sol.func-c.html b/coverage-report/src/test/src/test/MockERC20.sol.func-c.html new file mode 100644 index 0000000..7629f35 --- /dev/null +++ b/coverage-report/src/test/src/test/MockERC20.sol.func-c.html @@ -0,0 +1,82 @@ + + + + + + + LCOV - lcov.info - src/test/src/test/MockERC20.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/test/src/test - MockERC20.sol (source / functions)CoverageTotalHit
Test:lcov.infoLines:100.0 %22
Test Date:2025-02-13 17:15:49Functions:100.0 %11
+
+ +
+ + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by function hit count
MockERC20.constructor20
+
+
+ + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/coverage-report/src/test/src/test/MockERC20.sol.func.html b/coverage-report/src/test/src/test/MockERC20.sol.func.html new file mode 100644 index 0000000..65ba3be --- /dev/null +++ b/coverage-report/src/test/src/test/MockERC20.sol.func.html @@ -0,0 +1,82 @@ + + + + + + + LCOV - lcov.info - src/test/src/test/MockERC20.sol - functions + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/test/src/test - MockERC20.sol (source / functions)CoverageTotalHit
Test:lcov.infoLines:100.0 %22
Test Date:2025-02-13 17:15:49Functions:100.0 %11
+
+ +
+ + + + + + + + + + + + + + + + +

Function Name Sort by function nameHit count Sort by function hit count
MockERC20.constructor20
+
+
+ + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/coverage-report/src/test/src/test/MockERC20.sol.gcov.html b/coverage-report/src/test/src/test/MockERC20.sol.gcov.html new file mode 100644 index 0000000..a69b536 --- /dev/null +++ b/coverage-report/src/test/src/test/MockERC20.sol.gcov.html @@ -0,0 +1,87 @@ + + + + + + + LCOV - lcov.info - src/test/src/test/MockERC20.sol + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/test/src/test - MockERC20.sol (source / functions)CoverageTotalHit
Test:lcov.infoLines:100.0 %22
Test Date:2025-02-13 17:15:49Functions:100.0 %11
+
+ + + + + + + + +

+
            Line data    Source code
+
+       1              : // SPDX-License-Identifier: MIT
+       2              : pragma solidity ^0.8.25;
+       3              : 
+       4              : import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
+       5              : 
+       6              : // Mock ERC20 for testing
+       7              : contract MockERC20 is ERC20 {
+       8           20 :     constructor(string memory name, string memory symbol) ERC20(name, symbol) {
+       9           20 :         _mint(msg.sender, 10000 * 10 ** decimals());
+      10              :     }
+      11              : }
+        
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/coverage-report/src/test/src/test/index-sort-f.html b/coverage-report/src/test/src/test/index-sort-f.html new file mode 100644 index 0000000..4ace071 --- /dev/null +++ b/coverage-report/src/test/src/test/index-sort-f.html @@ -0,0 +1,105 @@ + + + + + + + LCOV - lcov.info - src/test/src/test + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/test/src/testCoverageTotalHit
Test:lcov.infoLines:100.0 %22
Test Date:2025-02-13 17:15:49Functions:100.0 %11
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Filename Sort by file nameLine Coverage Sort by line coverageFunction Coverage Sort by function coverage
Rate Total Hit Rate Total Hit
MockERC20.sol +
100.0%
+
100.0 %22100.0 %11
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/coverage-report/src/test/src/test/index-sort-l.html b/coverage-report/src/test/src/test/index-sort-l.html new file mode 100644 index 0000000..0b67bb1 --- /dev/null +++ b/coverage-report/src/test/src/test/index-sort-l.html @@ -0,0 +1,105 @@ + + + + + + + LCOV - lcov.info - src/test/src/test + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/test/src/testCoverageTotalHit
Test:lcov.infoLines:100.0 %22
Test Date:2025-02-13 17:15:49Functions:100.0 %11
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Filename Sort by file nameLine Coverage Sort by line coverageFunction Coverage Sort by function coverage
Rate Total Hit Rate Total Hit
MockERC20.sol +
100.0%
+
100.0 %22100.0 %11
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/coverage-report/src/test/src/test/index.html b/coverage-report/src/test/src/test/index.html new file mode 100644 index 0000000..b12a730 --- /dev/null +++ b/coverage-report/src/test/src/test/index.html @@ -0,0 +1,105 @@ + + + + + + + LCOV - lcov.info - src/test/src/test + + + + + + + + + + + + + + +
LCOV - code coverage report
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Current view:top level - src/test/src/testCoverageTotalHit
Test:lcov.infoLines:100.0 %22
Test Date:2025-02-13 17:15:49Functions:100.0 %11
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Filename Sort by file nameLine Coverage Sort by line coverageFunction Coverage Sort by function coverage
Rate Total Hit Rate Total Hit
MockERC20.sol +
100.0%
+
100.0 %22100.0 %11
+
+
+ + + + +
Generated by: LCOV version 2.0-1
+
+ + + diff --git a/foundry.toml b/foundry.toml index 64c6aae..48eb24d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,11 +2,15 @@ src = "src" out = "out" libs = ["node_modules", "lib"] -evm_version = "cancun" +evm_version = "london" optimizer = true optimizer_runs = 10_000_000 solc = "0.8.25" verbosity = 3 +ffi = true +ast = true +build_info = true +extra_output = ["storageLayout"] [profile.ci] fuzz = { runs = 5000 } diff --git a/lcov.info b/lcov.info index 0e63929..6bc27bd 100644 --- a/lcov.info +++ b/lcov.info @@ -1,13 +1,204 @@ TN: -SF:src/Counter.sol -FN:11,Counter.increment -FN:7,Counter.setNumber -FNDA:1,Counter.increment -FNDA:258,Counter.setNumber -FNF:2 -FNH:2 -DA:8,516 -DA:12,2 +SF:src/SimpleStakingChef.sol +FN:102,SimpleStakingChef.set +FN:123,SimpleStakingChef.pendingTokens +FN:144,SimpleStakingChef.updatePool +FN:157,SimpleStakingChef.deposit +FN:182,SimpleStakingChef.withdraw +FN:204,SimpleStakingChef.emergencyWithdraw +FN:58,SimpleStakingChef.initialize +FN:64,SimpleStakingChef.poolLength +FN:73,SimpleStakingChef.add +FNDA:1,SimpleStakingChef.set +FNDA:1,SimpleStakingChef.pendingTokens +FNDA:1,SimpleStakingChef.updatePool +FNDA:5,SimpleStakingChef.deposit +FNDA:2,SimpleStakingChef.withdraw +FNDA:1,SimpleStakingChef.emergencyWithdraw +FNDA:10,SimpleStakingChef.initialize +FNDA:1,SimpleStakingChef.poolLength +FNDA:10,SimpleStakingChef.add +FNF:9 +FNH:9 +DA:58,10 +DA:59,10 +DA:60,10 +DA:64,1 +DA:65,1 +DA:73,10 +DA:74,10 +DA:76,10 +DA:78,10 +DA:79,10 +DA:82,10 +DA:84,10 +DA:93,10 +DA:94,10 +DA:102,1 +DA:106,1 +DA:107,1 +DA:108,1 +DA:109,1 +DA:110,1 +DA:112,1 +DA:113,1 +DA:123,1 +DA:133,1 +DA:135,1 +DA:136,1 +DA:137,1 +DA:138,1 +DA:144,1 +DA:145,8 +DA:146,8 +DA:147,2 +DA:148,2 +DA:149,2 +DA:150,2 +DA:157,5 +DA:158,5 +DA:159,5 +DA:160,5 +DA:162,5 +DA:163,5 +DA:164,5 +DA:167,5 +DA:168,5 +DA:171,5 +DA:172,5 +DA:173,5 +DA:176,5 +DA:182,2 +DA:183,2 +DA:184,2 +DA:185,2 +DA:188,2 +DA:189,2 +DA:192,2 +DA:193,2 +DA:194,2 +DA:197,2 +DA:199,2 +DA:204,1 +DA:205,1 +DA:206,1 +DA:207,1 +DA:208,1 +DA:209,1 +DA:211,1 +DA:212,1 +DA:213,1 +DA:217,1 +DA:218,1 +LF:70 +LH:70 +end_of_record +TN: +SF:src/rewarders/SimpleRewarderPerSec.sol +FN:102,SimpleRewarderPerSec.setRewardRate +FN:114,SimpleRewarderPerSec.onVoltReward +FN:153,SimpleRewarderPerSec.pendingTokens +FN:171,SimpleRewarderPerSec.emergencyWithdraw +FN:181,SimpleRewarderPerSec.balance +FN:189,SimpleRewarderPerSec.rewardToken +FN:61,SimpleRewarderPerSec.onlyMCJ +FN:66,SimpleRewarderPerSec.constructor +FN:83,SimpleRewarderPerSec.updatePool +FNDA:1,SimpleRewarderPerSec.setRewardRate +FNDA:19,SimpleRewarderPerSec.onVoltReward +FNDA:1,SimpleRewarderPerSec.pendingTokens +FNDA:1,SimpleRewarderPerSec.emergencyWithdraw +FNDA:2,SimpleRewarderPerSec.balance +FNDA:1,SimpleRewarderPerSec.rewardToken +FNDA:19,SimpleRewarderPerSec.onlyMCJ +FNDA:10,SimpleRewarderPerSec.constructor +FNDA:0,SimpleRewarderPerSec.updatePool +FNF:9 +FNH:8 +DA:61,19 +DA:62,19 +DA:66,10 +DA:69,10 +DA:70,10 +DA:71,10 +DA:73,10 +DA:74,10 +DA:75,10 +DA:76,10 +DA:77,10 +DA:78,10 +DA:83,0 +DA:84,20 +DA:86,20 +DA:87,2 +DA:89,2 +DA:90,2 +DA:91,2 +DA:92,2 +DA:95,2 +DA:96,2 +DA:102,1 +DA:103,1 +DA:105,1 +DA:106,1 +DA:108,1 +DA:114,19 +DA:115,19 +DA:116,19 +DA:117,19 +DA:118,19 +DA:119,19 +DA:120,3 +DA:122,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:134,3 +DA:135,3 +DA:136,2 +DA:137,2 +DA:139,1 +DA:140,1 +DA:145,19 +DA:146,19 +DA:147,19 +DA:153,1 +DA:154,1 +DA:155,1 +DA:157,1 +DA:158,1 +DA:160,1 +DA:161,1 +DA:162,1 +DA:163,1 +DA:166,1 +DA:171,1 +DA:172,0 +DA:173,0 +DA:174,0 +DA:176,1 +DA:181,2 +DA:182,0 +DA:183,0 +DA:185,2 +DA:189,1 +DA:190,1 +LF:73 +LH:58 +end_of_record +TN: +SF:src/test/MockERC20.sol +FN:8,MockERC20.constructor +FNDA:20,MockERC20.constructor +FNF:1 +FNH:1 +DA:8,20 +DA:9,20 LF:2 LH:2 end_of_record diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..7ccb79f --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit 7ccb79f6ce7c69b0c0c311235c3ce3bad99dccca diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..5eec6a4 --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 5eec6a4983cad8638bea3199d2ea46470b956e9b diff --git a/lib/openzeppelin-foundry-upgrades b/lib/openzeppelin-foundry-upgrades new file mode 160000 index 0000000..cbce1e0 --- /dev/null +++ b/lib/openzeppelin-foundry-upgrades @@ -0,0 +1 @@ +Subproject commit cbce1e00305e943aa1661d43f41e5ac72c662b07 diff --git a/remappings.txt b/remappings.txt index feaba2d..2039af3 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1 +1,8 @@ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/ forge-std/=lib/forge-std/src/ +halmos-cheatcodes/=lib/openzeppelin-contracts-upgradeable/lib/halmos-cheatcodes/src/ +openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/ +openzeppelin-contracts/=lib/openzeppelin-contracts/ +openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/ diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index df9ee8b..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; - -contract CounterScript is Script { - function setUp() public {} - - function run() public { - vm.broadcast(); - } -} diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol new file mode 100644 index 0000000..5539482 --- /dev/null +++ b/script/Deploy.s.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {Script, console2} from "forge-std/Script.sol"; +import {DeploySimpleStakingChefScript} from "./DeploySimpleStakingChef.s.sol"; +import {DeploySimpleRewarderPerSecScript} from "./DeploySimpleRewarderPerSec.s.sol"; +import {ISimpleStakingChef} from "../src/interfaces/ISimpleStakingChef.sol"; +import {ISimpleRewarderPerSec} from "../src/interfaces/ISimpleRewarderPerSec.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract DeployScript is Script { + string public forkUrl; + uint256 public fork; + + function run() public { + forkUrl = vm.envString("TEST_RPC_URL"); + fork = vm.createFork(forkUrl); + vm.selectFork(fork); + + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + // Get parameters from environment + address lpToken = vm.envAddress("LP_TOKEN"); + address rewardToken = vm.envAddress("REWARD_TOKEN"); + uint256 tokenPerSec = vm.envUint("TOKEN_PER_SEC"); + + // Deploy staking chef first + DeploySimpleStakingChefScript stakingDeployer = new DeploySimpleStakingChefScript(); + (address chefImplementation, address stakingChefProxy) = stakingDeployer.run(); + console2.log("SimpleStakingChef Proxy:", stakingChefProxy); + console2.log("SimpleStakingChef Implementation:", chefImplementation); + + // Deploy rewarder with staking chef address + DeploySimpleRewarderPerSecScript rewarderDeployer = new DeploySimpleRewarderPerSecScript(); + address rewarderImplementation = rewarderDeployer.run(lpToken, rewardToken, stakingChefProxy, tokenPerSec); + console2.log("SimpleRewarderPerSec Implementation:", rewarderImplementation); + + // Set up the pool in staking chef + ISimpleStakingChef(stakingChefProxy).add(0, IERC20(lpToken), ISimpleRewarderPerSec(rewarderImplementation)); + + vm.stopBroadcast(); + } +} diff --git a/script/DeploySimpleRewarderPerSec.s.sol b/script/DeploySimpleRewarderPerSec.s.sol new file mode 100644 index 0000000..f007429 --- /dev/null +++ b/script/DeploySimpleRewarderPerSec.s.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {Script} from "forge-std/Script.sol"; +import {SimpleRewarderPerSec} from "../src/rewarders/SimpleRewarderPerSec.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ISimpleStakingChef} from "../src/interfaces/ISimpleStakingChef.sol"; + +contract DeploySimpleRewarderPerSecScript is Script { + function run(address lpToken, address rewardToken, address mcj, uint256 tokenPerSec) + public + returns (address implementation) + { + implementation = address( + new SimpleRewarderPerSec(IERC20(rewardToken), IERC20(lpToken), tokenPerSec, ISimpleStakingChef(mcj), false) + ); + + return implementation; + } +} diff --git a/script/DeploySimpleStakingChef.s.sol b/script/DeploySimpleStakingChef.s.sol new file mode 100644 index 0000000..53383a3 --- /dev/null +++ b/script/DeploySimpleStakingChef.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {Script} from "forge-std/Script.sol"; +import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; +import {SimpleStakingChef} from "../src/SimpleStakingChef.sol"; + +contract DeploySimpleStakingChefScript is Script { + function run() public returns (address implementation, address proxy) { + // Deploy the upgradeable contract + proxy = Upgrades.deployTransparentProxy( + "SimpleStakingChef.sol", msg.sender, abi.encodeCall(SimpleStakingChef.initialize, ()) + ); + + implementation = Upgrades.getImplementationAddress(proxy); + + return (implementation, proxy); + } +} diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/SimpleStakingChef.sol b/src/SimpleStakingChef.sol new file mode 100644 index 0000000..9aa222b --- /dev/null +++ b/src/SimpleStakingChef.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {ISimpleRewarderPerSec} from "./interfaces/ISimpleRewarderPerSec.sol"; + +/// @notice The (older) MasterChefVoltV2 contract gives out a constant number of VOLT tokens per block. +/// It is the only address with minting rights for VOLT. +/// The idea for this MasterChefVoltV3 (MCJV3) contract is therefore to be the owner of a dummy token +/// that is deposited into the MasterChefVoltV2 (MCJV2) contract. +/// The allocation point for this pool on MCJV3 is the total allocation point for all pools that receive double incentives. +contract SimpleStakingChef is OwnableUpgradeable, ReentrancyGuardUpgradeable { + using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.AddressSet; + + /// @notice Info of each MCJV3 user. + /// `amount` LP token amount the user has provided. + /// `rewardDebt` The amount of VOLT entitled to the user. + struct UserInfo { + uint256 amount; + uint256 rewardDebt; + } + + /// @notice Info of each MCJV3 pool. + /// `allocPoint` The amount of allocation points assigned to the pool. + /// Also known as the amount of VOLT to distribute per block. + struct PoolInfo { + IERC20 lpToken; + uint256 accVoltPerShare; + uint256 lastRewardTimestamp; + uint256 allocPoint; + ISimpleRewarderPerSec rewarder; + } + + PoolInfo[] public poolInfo; + // Set of all LP tokens that have been added as pools + EnumerableSet.AddressSet private lpTokens; + /// @notice Info of each user that stakes LP tokens. + mapping(uint256 => mapping(address => UserInfo)) public userInfo; + uint256 private constant ACC_TOKEN_PRECISION = 1e18; + + event Add(uint256 indexed pid, uint256 allocPoint, IERC20 indexed lpToken, ISimpleRewarderPerSec indexed rewarder); + event Set(uint256 indexed pid, uint256 allocPoint, ISimpleRewarderPerSec indexed rewarder, bool overwrite); + event Deposit(address indexed user, uint256 indexed pid, uint256 amount); + event Withdraw(address indexed user, uint256 indexed pid, uint256 amount); + event UpdatePool(uint256 indexed pid, uint256 lastRewardTimestamp, uint256 lpSupply, uint256 accVoltPerShare); + event Harvest(address indexed user, uint256 indexed pid, uint256 amount); + event EmergencyWithdraw(address indexed user, uint256 indexed pid, uint256 amount); + event Init(); + + error LPAlreadyAdded(); + + function initialize() external initializer { + __Ownable_init(msg.sender); + __ReentrancyGuard_init(); + } + + /// @notice Returns the number of MCJV3 pools. + function poolLength() external view returns (uint256 pools) { + pools = poolInfo.length; + } + + /// @notice Add a new LP to the pool. Can only be called by the owner. + /// DO NOT add the same LP token more than once. Rewards will be messed up if you do. + /// @param _allocPoint AP of the new pool. + /// @param _lpToken Address of the LP ERC-20 token. + /// @param _rewarder Address of the rewarder delegate. + function add(uint256 _allocPoint, IERC20 _lpToken, ISimpleRewarderPerSec _rewarder) external onlyOwner { + if (lpTokens.contains(address(_lpToken))) revert LPAlreadyAdded(); + // Sanity check to ensure _lpToken is an ERC20 token + _lpToken.balanceOf(address(this)); + // Sanity check if we add a rewarder + if (address(_rewarder) != address(0)) { + _rewarder.onVoltReward(address(0), 0); + } + + uint256 lastRewardTimestamp = block.timestamp; + + poolInfo.push( + PoolInfo({ + lpToken: _lpToken, + allocPoint: _allocPoint, + lastRewardTimestamp: lastRewardTimestamp, + accVoltPerShare: 0, + rewarder: _rewarder + }) + ); + lpTokens.add(address(_lpToken)); + emit Add(poolInfo.length - 1, _allocPoint, _lpToken, _rewarder); + } + + /// @notice Update the given pool's VOLT allocation point and `IRewarder` contract. Can only be called by the owner. + /// @param _pid The index of the pool. See `poolInfo`. + /// @param _allocPoint New AP of the pool. + /// @param _rewarder Address of the rewarder delegate. + /// @param overwrite True if _rewarder should be `set`. Otherwise `_rewarder` is ignored. + function set(uint256 _pid, uint256 _allocPoint, ISimpleRewarderPerSec _rewarder, bool overwrite) + external + onlyOwner + { + PoolInfo memory pool = poolInfo[_pid]; + pool.allocPoint = _allocPoint; + if (overwrite) { + _rewarder.onVoltReward(address(0), 0); // sanity check + pool.rewarder = _rewarder; + } + poolInfo[_pid] = pool; + emit Set(_pid, _allocPoint, overwrite ? _rewarder : pool.rewarder, overwrite); + } + + /// @notice View function to see pending VOLT on frontend. + /// @param _pid The index of the pool. See `poolInfo`. + /// @param _user Address of user. + /// @return pendingVolt VOLT reward for a given user. + // bonusTokenAddress The address of the bonus reward. + // bonusTokenSymbol The symbol of the bonus token. + // pendingBonusToken The amount of bonus rewards pending. + function pendingTokens(uint256 _pid, address _user) + external + view + returns ( + uint256 pendingVolt, + address bonusTokenAddress, + string memory bonusTokenSymbol, + uint256 pendingBonusToken + ) + { + PoolInfo memory pool = poolInfo[_pid]; + // If it's a double reward farm, we return info about the bonus token + if (address(pool.rewarder) != address(0)) { + bonusTokenAddress = address(pool.rewarder.rewardToken()); + bonusTokenSymbol = IERC20Metadata(bonusTokenAddress).symbol(); + pendingBonusToken = pool.rewarder.pendingTokens(_user); + } + } + + /// @notice Update reward variables of the given pool. + /// @param pid The index of the pool. See `poolInfo`. + function updatePool(uint256 pid) public { + PoolInfo memory pool = poolInfo[pid]; + if (block.timestamp > pool.lastRewardTimestamp) { + uint256 lpSupply = pool.lpToken.balanceOf(address(this)); + pool.lastRewardTimestamp = block.timestamp; + poolInfo[pid] = pool; + emit UpdatePool(pid, pool.lastRewardTimestamp, lpSupply, pool.accVoltPerShare); + } + } + + /// @notice Deposit LP tokens to MCJV3 for VOLT allocation. + /// @param pid The index of the pool. See `poolInfo`. + /// @param amount LP token amount to deposit. + function deposit(uint256 pid, uint256 amount) external nonReentrant { + updatePool(pid); + PoolInfo memory pool = poolInfo[pid]; + UserInfo storage user = userInfo[pid][msg.sender]; + + uint256 balanceBefore = pool.lpToken.balanceOf(address(this)); + pool.lpToken.safeTransferFrom(msg.sender, address(this), amount); + uint256 receivedAmount = pool.lpToken.balanceOf(address(this)) - balanceBefore; + + // Effects + user.amount = user.amount + receivedAmount; + user.rewardDebt = user.amount * pool.accVoltPerShare / ACC_TOKEN_PRECISION; + + // Interactions + ISimpleRewarderPerSec _rewarder = pool.rewarder; + if (address(_rewarder) != address(0)) { + _rewarder.onVoltReward(msg.sender, user.amount); + } + + emit Deposit(msg.sender, pid, receivedAmount); + } + + /// @notice Withdraw LP tokens from MCJV3. + /// @param pid The index of the pool. See `poolInfo`. + /// @param amount LP token amount to withdraw. + function withdraw(uint256 pid, uint256 amount) external nonReentrant { + updatePool(pid); + PoolInfo memory pool = poolInfo[pid]; + UserInfo storage user = userInfo[pid][msg.sender]; + + // Effects + user.amount = user.amount - amount; + user.rewardDebt = user.amount * pool.accVoltPerShare / ACC_TOKEN_PRECISION; + + // Interactions + ISimpleRewarderPerSec _rewarder = pool.rewarder; + if (address(_rewarder) != address(0)) { + _rewarder.onVoltReward(msg.sender, user.amount); + } + + pool.lpToken.safeTransfer(msg.sender, amount); + + emit Withdraw(msg.sender, pid, amount); + } + + /// @notice Withdraw without caring about rewards. EMERGENCY ONLY. + /// @param pid The index of the pool. See `poolInfo`. + function emergencyWithdraw(uint256 pid) external nonReentrant { + PoolInfo memory pool = poolInfo[pid]; + UserInfo storage user = userInfo[pid][msg.sender]; + uint256 amount = user.amount; + user.amount = 0; + user.rewardDebt = 0; + + ISimpleRewarderPerSec _rewarder = pool.rewarder; + if (address(_rewarder) != address(0)) { + _rewarder.onVoltReward(msg.sender, 0); + } + + // Note: transfer can fail or succeed if `amount` is zero. + pool.lpToken.safeTransfer(msg.sender, amount); + emit EmergencyWithdraw(msg.sender, pid, amount); + } +} diff --git a/src/interfaces/ISimpleRewarderPerSec.sol b/src/interfaces/ISimpleRewarderPerSec.sol new file mode 100644 index 0000000..3af1e10 --- /dev/null +++ b/src/interfaces/ISimpleRewarderPerSec.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface ISimpleRewarderPerSec { + function onVoltReward(address user, uint256 newLpAmount) external; + + function pendingTokens(address user) external view returns (uint256 pending); + + function rewardToken() external view returns (IERC20); +} diff --git a/src/interfaces/ISimpleStakingChef.sol b/src/interfaces/ISimpleStakingChef.sol new file mode 100644 index 0000000..f3ec6bd --- /dev/null +++ b/src/interfaces/ISimpleStakingChef.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ISimpleRewarderPerSec} from "./ISimpleRewarderPerSec.sol"; + +interface ISimpleStakingChef { + struct UserInfo { + uint256 amount; // How many LP tokens the user has provided. + uint256 rewardDebt; // Reward debt. See explanation below. + } + + struct PoolInfo { + IERC20 lpToken; // Address of LP token contract. + uint256 allocPoint; // How many allocation points assigned to this pool. VOLT to distribute per block. + uint256 lastRewardTimestamp; // Last block number that VOLT distribution occurs. + uint256 accVoltPerShare; // Accumulated VOLT per share, times 1e12. See below. + } + + function poolInfo(uint256 pid) external view returns (ISimpleStakingChef.PoolInfo memory); + + function totalAllocPoint() external view returns (uint256); + + function deposit(uint256 _pid, uint256 _amount) external; + + function add(uint256 _allocPoint, IERC20 _lpToken, ISimpleRewarderPerSec _rewarder) external; +} diff --git a/src/rewarders/SimpleRewarderPerSec.sol b/src/rewarders/SimpleRewarderPerSec.sol new file mode 100644 index 0000000..c4b583a --- /dev/null +++ b/src/rewarders/SimpleRewarderPerSec.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.25; + +import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ISimpleStakingChef} from "../interfaces/ISimpleStakingChef.sol"; + +/** + * This is a sample contract to be used in the MasterChefVolt contract for partners to reward + * stakers with their native token alongside VOLT. + * + * It assumes no minting rights, so requires a set amount of YOUR_TOKEN to be transferred to this contract prior. + * E.g. say you've allocated 100,000 XYZ to the VOLT-XYZ farm over 30 days. Then you would need to transfer + * 100,000 XYZ and set the block reward accordingly so it's fully distributed after 30 days. + * + */ +contract SimpleRewarderPerSec is Ownable, ReentrancyGuard { + using SafeERC20 for IERC20; + + IERC20 public immutable REWARD_TOKEN; + IERC20 public immutable LP_TOKEN; + bool public immutable IS_NATIVE; + ISimpleStakingChef public immutable MCJ; + + /// @notice Info of each MCJ user. + /// `amount` LP token amount the user has provided. + /// `rewardDebt` The amount of YOUR_TOKEN entitled to the user. + struct UserInfo { + uint256 amount; + uint256 rewardDebt; + uint256 unpaidRewards; + } + + /// @notice Info of each MCJ poolInfo. + /// `accTokenPerShare` Amount of YOUR_TOKEN each LP token is worth. + /// `lastRewardTimestamp` The last timestamp YOUR_TOKEN was rewarded to the poolInfo. + struct PoolInfo { + uint256 accTokenPerShare; + uint256 lastRewardTimestamp; + } + + /// @notice Info of the poolInfo. + PoolInfo public poolInfo; + /// @notice Info of each user that stakes LP tokens. + mapping(address => UserInfo) public userInfo; + + uint256 public tokenPerSec; + uint256 private constant ACC_TOKEN_PRECISION = 1e12; + + event OnReward(address indexed user, uint256 amount); + event RewardRateUpdated(uint256 oldRate, uint256 newRate); + + error OnlyMCJ(); + error InvalidRewardToken(); + error InvalidLPToken(); + error InvalidMCJ(); + error TransferFailed(); + + modifier onlyMCJ() { + if (msg.sender != address(MCJ)) revert OnlyMCJ(); + _; + } + + constructor(IERC20 _rewardToken, IERC20 _lpToken, uint256 _tokenPerSec, ISimpleStakingChef _mcj, bool _isNative) + Ownable(msg.sender) + { + if (address(_rewardToken) == address(0)) revert InvalidRewardToken(); + if (address(_lpToken) == address(0)) revert InvalidLPToken(); + if (address(_mcj) == address(0)) revert InvalidMCJ(); + + REWARD_TOKEN = _rewardToken; + LP_TOKEN = _lpToken; + tokenPerSec = _tokenPerSec; + MCJ = _mcj; + IS_NATIVE = _isNative; + poolInfo = PoolInfo({lastRewardTimestamp: block.timestamp, accTokenPerShare: 0}); + } + + /// @notice Update reward variables of the given poolInfo. + /// @return pool Returns the pool that was updated. + function updatePool() public returns (PoolInfo memory pool) { + pool = poolInfo; + + if (block.timestamp > pool.lastRewardTimestamp) { + uint256 lpSupply = LP_TOKEN.balanceOf(address(MCJ)); + + if (lpSupply > 0) { + uint256 timeElapsed = block.timestamp - pool.lastRewardTimestamp; + uint256 tokenReward = timeElapsed * tokenPerSec; + pool.accTokenPerShare = pool.accTokenPerShare + (tokenReward * ACC_TOKEN_PRECISION / lpSupply); + } + + pool.lastRewardTimestamp = block.timestamp; + poolInfo = pool; + } + } + + /// @notice Sets the distribution reward rate. This will also update the poolInfo. + /// @param _tokenPerSec The number of tokens to distribute per second + function setRewardRate(uint256 _tokenPerSec) external onlyOwner { + updatePool(); + + uint256 oldRate = tokenPerSec; + tokenPerSec = _tokenPerSec; + + emit RewardRateUpdated(oldRate, _tokenPerSec); + } + + /// @notice Function called by MasterChefVolt whenever staker claims VOLT harvest. Allows staker to also receive a 2nd reward token. + /// @param _user Address of user + /// @param _lpAmount Number of LP tokens the user has + function onVoltReward(address _user, uint256 _lpAmount) external onlyMCJ nonReentrant { + updatePool(); + PoolInfo memory pool = poolInfo; + UserInfo storage user = userInfo[_user]; + uint256 pending; + if (user.amount > 0) { + pending = (user.amount * pool.accTokenPerShare / ACC_TOKEN_PRECISION) - user.rewardDebt + user.unpaidRewards; + + if (IS_NATIVE) { + uint256 bal = address(this).balance; + if (pending > bal) { + user.unpaidRewards = pending - bal; + (bool success,) = _user.call{value: bal}(""); + if (!success) revert TransferFailed(); + } else { + user.unpaidRewards = 0; + (bool success,) = _user.call{value: pending}(""); + if (!success) revert TransferFailed(); + } + } else { + uint256 bal = REWARD_TOKEN.balanceOf(address(this)); + if (pending > bal) { + user.unpaidRewards = pending - bal; + REWARD_TOKEN.safeTransfer(_user, bal); + } else { + user.unpaidRewards = 0; + REWARD_TOKEN.safeTransfer(_user, pending); + } + } + } + + user.amount = _lpAmount; + user.rewardDebt = user.amount * pool.accTokenPerShare / ACC_TOKEN_PRECISION; + emit OnReward(_user, pending - user.unpaidRewards); + } + + /// @notice View function to see pending tokens + /// @param _user Address of user. + /// @return pending reward for a given user. + function pendingTokens(address _user) external view returns (uint256 pending) { + PoolInfo memory pool = poolInfo; + UserInfo storage user = userInfo[_user]; + + uint256 accTokenPerShare = pool.accTokenPerShare; + uint256 lpSupply = LP_TOKEN.balanceOf(address(MCJ)); + + if (block.timestamp > pool.lastRewardTimestamp && lpSupply != 0) { + uint256 timeElapsed = block.timestamp - pool.lastRewardTimestamp; + uint256 tokenReward = timeElapsed * tokenPerSec; + accTokenPerShare = accTokenPerShare + (tokenReward * ACC_TOKEN_PRECISION / lpSupply); + } + + pending = (user.amount * accTokenPerShare / ACC_TOKEN_PRECISION) - user.rewardDebt + user.unpaidRewards; + } + + /// @notice In case rewarder is stopped before emissions finished, this function allows + /// withdrawal of remaining tokens. + function emergencyWithdraw() public onlyOwner { + if (IS_NATIVE) { + (bool success,) = msg.sender.call{value: address(this).balance}(""); + if (!success) revert TransferFailed(); + } else { + REWARD_TOKEN.safeTransfer(address(msg.sender), REWARD_TOKEN.balanceOf(address(this))); + } + } + + /// @notice View function to see balance of reward token. + function balance() external view returns (uint256) { + if (IS_NATIVE) { + return address(this).balance; + } else { + return REWARD_TOKEN.balanceOf(address(this)); + } + } + + function rewardToken() external view returns (IERC20) { + return REWARD_TOKEN; + } + + /// @notice payable function needed to receive AVAX + receive() external payable {} +} diff --git a/src/test/MockERC20.sol b/src/test/MockERC20.sol new file mode 100644 index 0000000..47bdaf6 --- /dev/null +++ b/src/test/MockERC20.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +// Mock ERC20 for testing +contract MockERC20 is ERC20 { + constructor(string memory name, string memory symbol) ERC20(name, symbol) { + _mint(msg.sender, 10000 * 10 ** decimals()); + } +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/test/SimpleStaking.t.sol b/test/SimpleStaking.t.sol new file mode 100644 index 0000000..770fdf8 --- /dev/null +++ b/test/SimpleStaking.t.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {MockERC20} from "../src/test/MockERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SimpleStakingChef} from "../src/SimpleStakingChef.sol"; +import {SimpleRewarderPerSec} from "../src/rewarders/SimpleRewarderPerSec.sol"; +import {ISimpleStakingChef} from "../src/interfaces/ISimpleStakingChef.sol"; +import {ISimpleRewarderPerSec} from "../src/interfaces/ISimpleRewarderPerSec.sol"; + +contract SimpleStakingTest is Test { + SimpleStakingChef public chef; + SimpleRewarderPerSec public rewarder; + IERC20 public lpToken; + IERC20 public rewardToken; + + address public alice = makeAddr("alice"); + address public bob = makeAddr("bob"); + + function setUp() public { + // Deploy mock tokens + lpToken = new MockERC20("LP Token", "LP"); + rewardToken = new MockERC20("Reward Token", "RWD"); + + // Deploy main contracts + chef = new SimpleStakingChef(); + chef.initialize(); + rewarder = new SimpleRewarderPerSec( + rewardToken, + lpToken, + 1 ether, // 1 token per second + ISimpleStakingChef(address(chef)), + false // not native token + ); + + // Setup initial state + chef.add(0, lpToken, ISimpleRewarderPerSec(address(rewarder))); + + // Fund accounts + lpToken.transfer(alice, 1000 ether); + lpToken.transfer(bob, 1000 ether); + rewardToken.transfer(address(rewarder), 10000 ether); + + // Approve spending + vm.startPrank(alice); + lpToken.approve(address(chef), type(uint256).max); + vm.stopPrank(); + + vm.startPrank(bob); + lpToken.approve(address(chef), type(uint256).max); + vm.stopPrank(); + } + + function testPoolLength() public view { + assertEq(chef.poolLength(), 1, "Chef should have 1 pool"); + } + + function testSet() public { + chef.set(0, 100, ISimpleRewarderPerSec(address(rewarder)), true); + (,,, uint256 allocPoint, ISimpleRewarderPerSec _rewarder) = chef.poolInfo(0); + assertEq(allocPoint, 100, "Should set alloc point"); + assertEq(address(_rewarder), address(rewarder), "Should set rewarder"); + } + + function testUpdatePool() public { + chef.updatePool(0); + (,, uint256 lastRewardTimestamp,,) = chef.poolInfo(0); + assertEq(lastRewardTimestamp, block.timestamp, "Should update last reward timestamp"); + } + + function testDeposit() public { + vm.startPrank(alice); + uint256 depositAmount = 100 ether; + chef.deposit(0, depositAmount); + + (uint256 amount,) = chef.userInfo(0, alice); + assertEq(amount, depositAmount, "Deposit amount should match"); + assertEq(lpToken.balanceOf(address(chef)), depositAmount, "Chef should have LP tokens"); + vm.stopPrank(); + } + + function testPendingTokens() public { + vm.startPrank(alice); + uint256 depositAmount = 100 ether; + chef.deposit(0, depositAmount); + + uint256 startTime = block.timestamp; + skip(1 days); // Advance time by 1 day + + (uint256 pendingVolt, address bonusTokenAddress, string memory bonusTokenSymbol, uint256 pendingBonusToken) = + chef.pendingTokens(0, alice); + + assertEq(bonusTokenAddress, address(rewardToken), "Should have reward token as bonus token address"); + assertEq(bonusTokenSymbol, "RWD", "Should have reward token symbol as bonus token symbol"); + assertEq(pendingVolt, 0, "Should have no pending volt tokens"); + assertEq( + pendingBonusToken, + (block.timestamp - startTime) * 1 ether, + "Should have correct amount of pending bonus tokens" + ); + vm.stopPrank(); + } + + function testWithdraw() public { + vm.startPrank(alice); + uint256 depositAmount = 100 ether; + chef.deposit(0, depositAmount); + + skip(1 days); // Advance time by 1 day + + uint256 initialBalance = lpToken.balanceOf(alice); + chef.withdraw(0, depositAmount); + + assertEq(lpToken.balanceOf(alice), initialBalance + depositAmount, "Should receive LP tokens back"); + vm.stopPrank(); + } + + function testEmergencyWithdraw() public { + vm.startPrank(alice); + uint256 depositAmount = 100 ether; + chef.deposit(0, depositAmount); + + uint256 initialBalance = lpToken.balanceOf(alice); + chef.emergencyWithdraw(0); + + assertEq(lpToken.balanceOf(alice), initialBalance + depositAmount, "Should receive LP tokens back"); + vm.stopPrank(); + } + + function testRewarderDistribution() public { + vm.startPrank(alice); + uint256 depositAmount = 100 ether; + chef.deposit(0, depositAmount); + + skip(1 days); // Advance time by 1 day + + uint256 initialRewardBalance = rewardToken.balanceOf(alice); + chef.withdraw(0, depositAmount); + + assertTrue(rewardToken.balanceOf(alice) > initialRewardBalance, "Should receive reward tokens"); + vm.stopPrank(); + } + + function testSetRewardRate() public { + assertEq(rewarder.tokenPerSec(), 1 ether, "Should have initial reward rate"); + rewarder.setRewardRate(0); + assertEq(rewarder.tokenPerSec(), 0, "Should set reward rate"); + } + + function testRewardEmergencyWithdraw() public { + assertEq(rewarder.balance(), 10000 ether, "Should have reward tokens"); + rewarder.emergencyWithdraw(); + assertEq(rewarder.balance(), 0, "Should have no reward tokens"); + assertEq(rewardToken.balanceOf(address(this)), 10000 ether, "Should have reward tokens"); + } +}