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 - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| Function Name |
+
+ Hit count |
+
+
+
| SimpleRewarderPerSec.updatePool | + +0 | + + +
| SimpleRewarderPerSec.emergencyWithdraw | + +1 | + + +
| SimpleRewarderPerSec.pendingTokens | + +1 | + + +
| SimpleRewarderPerSec.rewardToken | + +1 | + + +
| SimpleRewarderPerSec.setRewardRate | + +1 | + + +
| SimpleRewarderPerSec.balance | + +2 | + + +
| SimpleRewarderPerSec.constructor | + +10 | + + +
| SimpleRewarderPerSec.onVoltReward | + +19 | + + +
| SimpleRewarderPerSec.onlyMCJ | + +19 | + + +
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| Function Name |
+
+ Hit count |
+
+
+
| SimpleRewarderPerSec.balance | + +2 | + + +
| SimpleRewarderPerSec.constructor | + +10 | + + +
| SimpleRewarderPerSec.emergencyWithdraw | + +1 | + + +
| SimpleRewarderPerSec.onVoltReward | + +19 | + + +
| SimpleRewarderPerSec.onlyMCJ | + +19 | + + +
| SimpleRewarderPerSec.pendingTokens | + +1 | + + +
| SimpleRewarderPerSec.rewardToken | + +1 | + + +
| SimpleRewarderPerSec.setRewardRate | + +1 | + + +
| SimpleRewarderPerSec.updatePool | + +0 | + + +
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | + | + | + | ||
| Filename |
+ Line Coverage |
+ Function Coverage |
+ ||||||
| Rate | +Total | +Hit | +Rate | +Total | +Hit | +|||
| SimpleRewarderPerSec.sol | +
+ |
+ 79.5 % | +73 | +58 | +88.9 % | +9 | +8 | +|
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | + | + | + | ||
| Filename |
+ Line Coverage |
+ Function Coverage |
+ ||||||
| Rate | +Total | +Hit | +Rate | +Total | +Hit | +|||
| SimpleRewarderPerSec.sol | +
+ |
+ 79.5 % | +73 | +58 | +88.9 % | +9 | +8 | +|
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | + | + | + | ||
| Filename |
+ Line Coverage |
+ Function Coverage |
+ ||||||
| Rate | +Total | +Hit | +Rate | +Total | +Hit | +|||
| SimpleRewarderPerSec.sol | +
+ |
+ 79.5 % | +73 | +58 | +88.9 % | +9 | +8 | +|
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| Function Name |
+
+ Hit count |
+
+
+
| SimpleStakingChef.emergencyWithdraw | + +1 | + + +
| SimpleStakingChef.pendingTokens | + +1 | + + +
| SimpleStakingChef.poolLength | + +1 | + + +
| SimpleStakingChef.set | + +1 | + + +
| SimpleStakingChef.updatePool | + +1 | + + +
| SimpleStakingChef.withdraw | + +2 | + + +
| SimpleStakingChef.deposit | + +5 | + + +
| SimpleStakingChef.add | + +10 | + + +
| SimpleStakingChef.initialize | + +10 | + + +
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| Function Name |
+
+ Hit count |
+
+
+
| SimpleStakingChef.add | + +10 | + + +
| SimpleStakingChef.deposit | + +5 | + + +
| SimpleStakingChef.emergencyWithdraw | + +1 | + + +
| SimpleStakingChef.initialize | + +10 | + + +
| SimpleStakingChef.pendingTokens | + +1 | + + +
| SimpleStakingChef.poolLength | + +1 | + + +
| SimpleStakingChef.set | + +1 | + + +
| SimpleStakingChef.updatePool | + +1 | + + +
| SimpleStakingChef.withdraw | + +2 | + + +
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| Function Name |
+
+ Hit count |
+
+
+
| MockERC20.constructor | + +20 | + + +
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| Function Name |
+
+ Hit count |
+
+
+
| MockERC20.constructor | + +20 | + + +
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
+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 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | + | + | + | ||
| Filename |
+ Line Coverage |
+ Function Coverage |
+ ||||||
| Rate | +Total | +Hit | +Rate | +Total | +Hit | +|||
| MockERC20.sol | +
+ |
+ 100.0 % | +2 | +2 | +100.0 % | +1 | +1 | +|
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | + | + | + | ||
| Filename |
+ Line Coverage |
+ Function Coverage |
+ ||||||
| Rate | +Total | +Hit | +Rate | +Total | +Hit | +|||
| MockERC20.sol | +
+ |
+ 100.0 % | +2 | +2 | +100.0 % | +1 | +1 | +|
| Generated by: LCOV version 2.0-1 |
| LCOV - code coverage report | ||||||||||||||||||||||
+
|
+ ||||||||||||||||||||||
| + | + | + | + | + | + | + | ||
| Filename |
+ Line Coverage |
+ Function Coverage |
+ ||||||
| Rate | +Total | +Hit | +Rate | +Total | +Hit | +|||
| MockERC20.sol | +
+ |
+ 100.0 % | +2 | +2 | +100.0 % | +1 | +1 | +|
| Generated by: LCOV version 2.0-1 |