├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── foundry.toml ├── script └── CurveVault.s.sol ├── src ├── ConvexVault.sol ├── CurveVault.sol └── interfaces │ ├── IConvexBooster.sol │ ├── IConvexRewardPool.sol │ ├── ICurve.sol │ ├── IPool.sol │ ├── IUniswap.sol │ └── IWETH.sol └── test ├── BaseSetup.sol ├── ConvexVault.t.sol ├── CurveVault.t.sol └── utils └── Util.sol /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | remappings = ["@openzeppelin/=lib/openzeppelin-contracts"] 7 | 8 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config -------------------------------------------------------------------------------- /script/CurveVault.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | 6 | contract CurveVaultScript is Script { 7 | function setUp() public {} 8 | 9 | function run() public { 10 | vm.broadcast(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ConvexVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.13; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7 | import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 8 | import "@openzeppelin/contracts/utils/math/SafeMath.sol"; 9 | import "@openzeppelin/contracts/access/Ownable.sol"; 10 | import "./interfaces/IConvexBooster.sol"; 11 | import "./interfaces/IConvexRewardPool.sol"; 12 | import "./interfaces/IPool.sol"; 13 | 14 | contract ConvexVault is Ownable { 15 | using SafeMath for uint256; 16 | using SafeERC20 for IERC20; 17 | 18 | address private constant CONVEX_BOOSTER_ADDRESS = 19 | 0xF403C135812408BFbE8713b5A23a04b3D48AAE31; 20 | 21 | uint256 private constant CVX_REWARD_POOL_INDEX = 0; 22 | uint256 private constant CRV_REWARD_POOL_INDEX = 1; 23 | 24 | // Info of each user. 25 | struct UserInfo { 26 | uint256 amount; // How many LP tokens the user has provided. 27 | uint256[] rewardDebts; // Reward debt. See explanation below. 28 | } 29 | 30 | struct RewardPoolInfo { 31 | IERC20 rewardToken; 32 | IConvexRewardPool pool; 33 | } 34 | 35 | uint256[] public lastRewardTimestamps; 36 | uint256[] public accRewardPerShares; // Accumulated Rewards, times 1e18. See below. 37 | RewardPoolInfo[] public rewardPools; 38 | mapping(address => UserInfo) public userInfo; 39 | 40 | IERC20 public lpToken; 41 | IConvexBooster public booster; 42 | 43 | uint256 public pid; 44 | uint256 public totalDepositAmount; 45 | 46 | uint256 private lastCVXBalance; 47 | bool private needCVXWithdraw; 48 | 49 | event Deposit(address indexed user, uint256 amount); 50 | event Withdraw(address indexed user, uint256 amount); 51 | event ClaimReward(address indexed user, address rewardToken); 52 | 53 | error InvalidPoolId(); 54 | error InsufficientBalance(uint256 available, uint256 requested); 55 | 56 | constructor(address _lpToken, uint256 _pid) { 57 | lpToken = IERC20(_lpToken); 58 | booster = IConvexBooster(CONVEX_BOOSTER_ADDRESS); 59 | pid = _pid; 60 | 61 | if (pid > IConvexBooster(booster).poolLength()) revert InvalidPoolId(); 62 | 63 | (, , , address _crvRewards, , ) = IConvexBooster(booster).poolInfo(pid); 64 | 65 | IConvexRewardPool baseRewardPool = IConvexRewardPool(_crvRewards); 66 | IConvexRewardPool cvxRewardPool = IConvexRewardPool( 67 | booster.stakerRewards() 68 | ); 69 | 70 | uint256 extraRewardLength = baseRewardPool.extraRewardsLength(); 71 | uint256 rewardTokenCount = extraRewardLength.add(2); 72 | 73 | rewardPools.push( 74 | RewardPoolInfo({ 75 | rewardToken: IERC20(cvxRewardPool.stakingToken()), 76 | pool: cvxRewardPool 77 | }) 78 | ); 79 | rewardPools.push( 80 | RewardPoolInfo({ 81 | rewardToken: IERC20(baseRewardPool.rewardToken()), 82 | pool: baseRewardPool 83 | }) 84 | ); 85 | 86 | for (uint256 i; i != rewardTokenCount; ++i) { 87 | lastRewardTimestamps.push(0); 88 | accRewardPerShares.push(0); 89 | 90 | if (i > 1) { 91 | IConvexRewardPool extraRewardPool = IConvexRewardPool( 92 | baseRewardPool.extraRewards(i - 2) 93 | ); 94 | rewardPools.push( 95 | RewardPoolInfo({ 96 | rewardToken: IERC20(extraRewardPool.rewardToken()), 97 | pool: extraRewardPool 98 | }) 99 | ); 100 | } 101 | } 102 | } 103 | 104 | /** 105 | @dev Allows a user to deposit LP tokens into the farming pool and earn rewards. 106 | @param _amount The amount of LP tokens to deposit 107 | */ 108 | function deposit(uint256 _amount) public { 109 | UserInfo storage user = userInfo[msg.sender]; 110 | 111 | updateReward(); 112 | 113 | uint256 pending; 114 | uint256 amountAfterDeposit = user.amount.add(_amount); 115 | uint256 rewardTokenCount = rewardPools.length; 116 | 117 | for (uint256 i; i < rewardTokenCount; ++i) { 118 | if (user.amount > 0) { 119 | pending = accRewardPerShares[i].mul(user.amount).div(1e18).sub( 120 | user.rewardDebts[i] 121 | ); 122 | 123 | if (pending > 0) { 124 | safeRewardTransfer( 125 | rewardPools[i].rewardToken, 126 | msg.sender, 127 | pending 128 | ); 129 | 130 | // CVX RewardPool's index is 0 131 | if (i == CVX_REWARD_POOL_INDEX) { 132 | lastCVXBalance = rewardPools[i].rewardToken.balanceOf( 133 | address(this) 134 | ); 135 | needCVXWithdraw = false; 136 | } 137 | 138 | user.rewardDebts[i] = accRewardPerShares[i] 139 | .mul(amountAfterDeposit) 140 | .div(1e18); 141 | } 142 | } else { 143 | user.rewardDebts = new uint256[](rewardTokenCount); 144 | break; 145 | } 146 | } 147 | 148 | if (_amount > 0) { 149 | lpToken.safeTransferFrom(msg.sender, address(this), _amount); 150 | lpToken.safeApprove(address(booster), _amount); 151 | booster.deposit(pid, _amount, true); 152 | user.amount = user.amount.add(_amount); 153 | totalDepositAmount = totalDepositAmount.add(_amount); 154 | } 155 | 156 | emit Deposit(msg.sender, _amount); 157 | } 158 | 159 | /** 160 | @dev Allows a user to withdraw their deposited LP tokens from the pool along with any earned rewards. Updates the accumulated rewards 161 | - for all reward tokens and stores them in the accRewardPerShares array. The function first checks if the user has sufficient balance 162 | - before updating the rewards and withdrawing the LP tokens from the external staking contract and the MasterChef booster. It then calculates 163 | - the pending rewards for each token and transfers them to the user before updating their reward debts based on the new deposit amount. 164 | - Finally, it updates the user's deposit amount and the total deposit amount before emitting an event indicating that the withdrawal 165 | has been processed successfully. 166 | @param _amount The amount of LP tokens to be withdrawn by the user. 167 | */ 168 | function withdraw(uint256 _amount) public { 169 | UserInfo storage user = userInfo[msg.sender]; 170 | 171 | if (user.amount < _amount) 172 | revert InsufficientBalance(user.amount, _amount); 173 | 174 | updateReward(); 175 | 176 | rewardPools[CRV_REWARD_POOL_INDEX].pool.withdraw(_amount, true); 177 | booster.withdraw(pid, _amount); 178 | 179 | uint256 pending; 180 | uint256 amountAfterWithdraw = user.amount.sub(_amount); 181 | uint256 rewardTokenCount = rewardPools.length; 182 | 183 | for (uint256 i; i != rewardTokenCount; ++i) { 184 | if (user.amount > 0) { 185 | pending = accRewardPerShares[i].mul(user.amount).div(1e18).sub( 186 | user.rewardDebts[i] 187 | ); 188 | 189 | if (pending > 0) { 190 | safeRewardTransfer( 191 | rewardPools[i].rewardToken, 192 | msg.sender, 193 | pending 194 | ); 195 | 196 | if (i == CVX_REWARD_POOL_INDEX) { 197 | lastCVXBalance = rewardPools[i].rewardToken.balanceOf( 198 | address(this) 199 | ); 200 | needCVXWithdraw = false; 201 | } 202 | } 203 | } 204 | 205 | user.rewardDebts[i] = accRewardPerShares[i] 206 | .mul(amountAfterWithdraw) 207 | .div(1e18); 208 | } 209 | 210 | user.amount = user.amount.sub(_amount); 211 | lpToken.safeTransfer(address(msg.sender), _amount); 212 | totalDepositAmount = totalDepositAmount.sub(_amount); 213 | 214 | emit Withdraw(msg.sender, _amount); 215 | } 216 | 217 | /** 218 | @dev Allows a user to claim their pending rewards for a specific reward token. 219 | @param _rewardToken The address of the reward token to be claimed. 220 | Emits a {ClaimReward} event indicating that the reward has been claimed by the user. 221 | */ 222 | function claim(address _rewardToken) public { 223 | UserInfo storage user = userInfo[msg.sender]; 224 | 225 | updateReward(); 226 | 227 | uint256 pending; 228 | uint256 rewardTokenCount = rewardPools.length; 229 | 230 | for (uint256 i; i != rewardTokenCount; ++i) { 231 | if (address(rewardPools[i].rewardToken) == _rewardToken) { 232 | pending = accRewardPerShares[i].mul(user.amount).div(1e18).sub( 233 | user.rewardDebts[i] 234 | ); 235 | 236 | if (pending > 0) { 237 | if (i == CVX_REWARD_POOL_INDEX) { 238 | lastCVXBalance = rewardPools[i].rewardToken.balanceOf( 239 | address(this) 240 | ); 241 | needCVXWithdraw = false; 242 | } 243 | 244 | safeRewardTransfer( 245 | rewardPools[i].rewardToken, 246 | msg.sender, 247 | pending 248 | ); 249 | 250 | user.rewardDebts[i] = accRewardPerShares[i] 251 | .mul(user.amount) 252 | .div(1e18); 253 | } 254 | 255 | break; 256 | } 257 | } 258 | 259 | emit ClaimReward(msg.sender, _rewardToken); 260 | } 261 | 262 | /** 263 | @dev Updates the accumulated rewards for all reward tokens and stores them in the accRewardPerShares array. 264 | This function is internal and can only be called by other functions within the contract. It calculates the rewards earned 265 | since the last update based on the deposit amount and time elapsed. If the reward token is CVX, it also checks if any CVX 266 | has been withdrawn from the MasterChef pool since the last update and calculates the earned rewards accordingly. 267 | */ 268 | function updateReward() internal { 269 | uint256 rewardTokenCount = rewardPools.length; 270 | 271 | for (uint256 i; i != rewardTokenCount; ++i) { 272 | if (block.timestamp <= lastRewardTimestamps[i]) { 273 | continue; 274 | } 275 | 276 | if (totalDepositAmount == 0) { 277 | lastRewardTimestamps[i] = block.timestamp; 278 | continue; 279 | } 280 | 281 | uint256 earned; 282 | 283 | if (i == CVX_REWARD_POOL_INDEX) { 284 | uint256 cvxBalance = rewardPools[i].rewardToken.balanceOf( 285 | address(this) 286 | ); 287 | 288 | if (needCVXWithdraw == true && lastCVXBalance == cvxBalance) { 289 | earned = 0; 290 | } else { 291 | earned = cvxBalance - lastCVXBalance; 292 | needCVXWithdraw = true; 293 | } 294 | } else { 295 | earned = rewardPools[i].pool.earned(address(this)); 296 | rewardPools[i].pool.getReward(); 297 | } 298 | 299 | accRewardPerShares[i] = accRewardPerShares[i].add( 300 | earned.mul(1e18).div(totalDepositAmount) 301 | ); 302 | 303 | lastRewardTimestamps[i] = block.timestamp; 304 | } 305 | } 306 | 307 | // Safe rewardToken transfer function, just in case if rounding error causes pool to not have enough SUSHIs. 308 | function safeRewardTransfer( 309 | IERC20 _token, 310 | address _to, 311 | uint256 _amount 312 | ) internal { 313 | uint256 balance = _token.balanceOf(address(this)); 314 | if (_amount > balance) { 315 | _token.transfer(_to, balance); 316 | } else { 317 | _token.transfer(_to, _amount); 318 | } 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/CurveVault.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 4 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 5 | import "@openzeppelin/contracts/utils/math/SafeMath.sol"; 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | import "./interfaces/ICurve.sol"; 8 | import "forge-std/Test.sol"; 9 | 10 | /** 11 | * @title CurveVault Contract 12 | * @dev The CurveVault contract allows users to deposit LP tokens and receive rewards proportionally based on their share of the total deposited tokens. 13 | */ 14 | contract CurveVault is Ownable { 15 | using SafeMath for uint256; 16 | using SafeERC20 for IERC20; 17 | 18 | // const uint256 CRV_TOKEN_ADDRESS = "0xD533a949740bb3306d119CC777fa900bA034cd52"; 19 | 20 | // Info of each user. 21 | struct UserInfo { 22 | uint256 amount; // How many LP tokens the user has deposited. 23 | uint256 rewardDebt; // Reward debt. 24 | } 25 | 26 | address constant CRV_TOKEN_ADDRESS = 27 | 0xD533a949740bb3306d119CC777fa900bA034cd52; 28 | address constant CRV_TOKEN_MINTER_ADDRESS = 29 | 0xd061D61a4d941c39E5453435B6345Dc261C2fcE0; 30 | 31 | // Address of LP token contract. 32 | IERC20 public lpToken; 33 | 34 | // Last block number that CRV distribution occurs. 35 | uint256 public lastRewardTimestamp; 36 | 37 | uint256 private lastRewardBalance; 38 | bool private needRewardWithdraw; 39 | 40 | // The Curve gauge pool. 41 | ILiquidityGauge public crvLiquidityGauge; 42 | 43 | // CRV token address. 44 | ICurveToken public crvToken; 45 | 46 | // CRV token minter; 47 | ICurveMinter public crvMinter; 48 | 49 | // Accumulated CRV per share, times 1e18. 50 | uint256 public accRewardPerShare; 51 | 52 | // The total amount of LP tokens deposited in the vault. 53 | uint256 public totalDepositAmount; 54 | 55 | // Info of each user that stakes LP tokens. 56 | mapping(address => UserInfo) public userInfo; 57 | 58 | // Events 59 | event Deposit(address indexed user, uint256 amount); 60 | event Withdraw(address indexed user, uint256 amount); 61 | event HarvestRewards(address indexed user, uint256 amount); 62 | 63 | error InsufficientBalance(); 64 | 65 | constructor(address _lpToken, address _curveGauge) { 66 | lpToken = IERC20(_lpToken); 67 | crvLiquidityGauge = ILiquidityGauge(_curveGauge); 68 | crvMinter = ICurveMinter(CRV_TOKEN_MINTER_ADDRESS); 69 | crvToken = ICurveToken(CRV_TOKEN_ADDRESS); 70 | } 71 | 72 | /** 73 | * @dev Harvest user rewards from a given pool id 74 | */ 75 | function harvestRewards() public { 76 | updateReward(); 77 | 78 | UserInfo storage user = userInfo[msg.sender]; 79 | 80 | uint256 rewardsToHarvest = accRewardPerShare 81 | .mul(user.amount) 82 | .div(1e18) 83 | .sub(user.rewardDebt); 84 | 85 | if (rewardsToHarvest == 0) { 86 | user.rewardDebt = accRewardPerShare.mul(user.amount).div(1e18); 87 | return; 88 | } 89 | 90 | safeCrvTransfer(msg.sender, rewardsToHarvest); 91 | 92 | lastRewardBalance = getReward(); 93 | 94 | needRewardWithdraw = false; 95 | 96 | emit HarvestRewards(msg.sender, rewardsToHarvest); 97 | } 98 | 99 | /** 100 | * @dev Deposits LP tokens into the vault and updates the user's share of the rewards. 101 | * @param _amount The amount of LP tokens to deposit. 102 | */ 103 | function deposit(uint256 _amount) public { 104 | UserInfo storage user = userInfo[msg.sender]; 105 | 106 | harvestRewards(); 107 | 108 | if (_amount > 0) { 109 | // Transfer LP tokens to the vault and update the total deposited amount. 110 | lpToken.safeTransferFrom(msg.sender, address(this), _amount); 111 | 112 | // Deposit LP tokens into the Curve gauge pool. 113 | lpToken.approve(address(crvLiquidityGauge), _amount); 114 | crvLiquidityGauge.deposit(_amount); 115 | 116 | user.amount = user.amount.add(_amount); 117 | user.rewardDebt = accRewardPerShare.mul(user.amount).div(1e18); 118 | totalDepositAmount = totalDepositAmount.add(_amount); 119 | } 120 | 121 | emit Deposit(msg.sender, _amount); 122 | } 123 | 124 | /** 125 | * @dev Withdraws LP tokens from the vault and updates the user's share of the rewards. 126 | * @param _amount The amount of LP tokens to withdraw. 127 | */ 128 | function withdraw(uint256 _amount) public { 129 | UserInfo storage user = userInfo[msg.sender]; 130 | 131 | if (user.amount < _amount) revert InsufficientBalance(); 132 | 133 | harvestRewards(); 134 | 135 | user.amount = user.amount.sub(_amount); 136 | user.rewardDebt = accRewardPerShare.mul(user.amount).div(1e18); 137 | totalDepositAmount = totalDepositAmount.sub(_amount); 138 | 139 | if (_amount > 0) { 140 | // Withdraw LP tokens from the Curve gauge pool and transfer them to the user. 141 | lpToken.approve(address(this), _amount); 142 | crvLiquidityGauge.withdraw(_amount); 143 | lpToken.transferFrom(address(this), address(msg.sender), _amount); 144 | } 145 | 146 | emit Withdraw(msg.sender, _amount); 147 | } 148 | 149 | /** 150 | * @dev Withdraws all LP tokens from the vault and updates the user's share of the rewards. 151 | */ 152 | function withdrawAll() public { 153 | UserInfo storage user = userInfo[msg.sender]; 154 | withdraw(user.amount); 155 | } 156 | 157 | /** 158 | * @dev Updates the CRV reward variables for the vault. 159 | */ 160 | function updateReward() internal { 161 | if (block.timestamp <= lastRewardTimestamp) return; 162 | 163 | if (totalDepositAmount == 0) { 164 | lastRewardTimestamp = block.timestamp; 165 | return; 166 | } 167 | 168 | crvMinter.mint(address(crvLiquidityGauge)); 169 | 170 | uint256 earned; 171 | uint256 currentBalance = getReward(); 172 | 173 | if (needRewardWithdraw == true && lastRewardBalance == currentBalance) { 174 | earned = 0; 175 | } else { 176 | earned = currentBalance - lastRewardBalance; 177 | needRewardWithdraw = true; 178 | } 179 | 180 | accRewardPerShare = accRewardPerShare.add( 181 | earned.mul(1e18).div(totalDepositAmount) 182 | ); 183 | 184 | lastRewardTimestamp = block.timestamp; 185 | } 186 | 187 | function getReward() internal returns (uint256) { 188 | return crvToken.balanceOf(address(this)); 189 | } 190 | 191 | /** 192 | * @dev Transfers CRV tokens from the contract to the recipient. 193 | * @param _to The address to transfer the CRV tokens to. 194 | * @param _amount The amount of CRV tokens to transfer. 195 | */ 196 | function safeCrvTransfer(address _to, uint256 _amount) internal { 197 | uint256 crvBalance = crvToken.balanceOf(address(this)); 198 | if (_amount > crvBalance) { 199 | crvToken.transfer(_to, crvBalance); 200 | } else { 201 | crvToken.transfer(_to, _amount); 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/interfaces/IConvexBooster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | //main Convex contract(booster.sol) basic interface 5 | interface IConvexBooster { 6 | //deposit into convex, receive a tokenized deposit. parameter to stake immediately 7 | function deposit( 8 | uint256 _pid, 9 | uint256 _amount, 10 | bool _stake 11 | ) external returns (bool); 12 | 13 | //burn a tokenized deposit to receive curve lp tokens back 14 | function withdraw(uint256 _pid, uint256 _amount) external returns (bool); 15 | 16 | function withdrawAll(uint256 _pid) external returns (bool); 17 | 18 | function poolLength() external view returns (uint256); 19 | 20 | function stakerRewards() external view returns (address); 21 | 22 | function poolInfo( 23 | uint256 _pid 24 | ) 25 | external 26 | view 27 | returns ( 28 | address _lptoken, 29 | address _token, 30 | address _gauge, 31 | address _crvRewards, 32 | address _stash, 33 | bool _shutdown 34 | ); 35 | 36 | function earmarkRewards(uint256 _pid) external returns (bool); 37 | } 38 | -------------------------------------------------------------------------------- /src/interfaces/IConvexRewardPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IConvexRewardPool { 5 | function stakeFor(address, uint256) external; 6 | 7 | function stake(uint256) external; 8 | 9 | function withdraw(uint256 amount, bool claim) external; 10 | 11 | function withdrawAndUnwrap(uint256 amount, bool claim) external; 12 | 13 | function earned(address account) external view returns (uint256); 14 | 15 | function getReward() external; 16 | 17 | function getReward(address _account, bool _claimExtras) external; 18 | 19 | function getReward(bool stake) external; 20 | 21 | function extraRewardsLength() external view returns (uint256); 22 | 23 | function extraRewards(uint256) external view returns (address); 24 | 25 | function rewardToken() external view returns (address); 26 | 27 | function stakingToken() external view returns (address); 28 | 29 | function balanceOf(address _account) external view returns (uint256); 30 | 31 | function rewardRate() external view returns (uint256); 32 | 33 | function totalSupply() external view returns (uint256); 34 | } 35 | -------------------------------------------------------------------------------- /src/interfaces/ICurve.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface ICurveMinter { 5 | function mint_for(address, address) external; 6 | 7 | function mint(address) external; 8 | } 9 | 10 | interface ICurveToken { 11 | function approve(address, uint256) external returns (bool); 12 | 13 | function balanceOf(address) external returns (uint256); 14 | 15 | function transfer(address, uint256) external returns (bool); 16 | 17 | function transferFrom(address, address, uint256) external returns (bool); 18 | } 19 | 20 | interface ICurvePool { 21 | function add_liquidity( 22 | uint256[3] memory, 23 | uint256, 24 | bool 25 | ) external returns (uint256); 26 | 27 | function add_liquidity( 28 | uint256[3] memory, 29 | uint256 30 | ) external returns (uint256); 31 | 32 | function lp_token() external returns (address); 33 | } 34 | 35 | interface ILiquidityGauge { 36 | function deposit(uint256) external; 37 | 38 | function withdraw(uint256) external; 39 | 40 | function balanceOf(address account) external view returns (uint256); 41 | 42 | function claimable_tokens(address) external returns (uint256); 43 | 44 | function claimable_reward(address) external returns (uint256); 45 | 46 | function claim_rewards(address) external; 47 | 48 | function integrate_fraction( 49 | address _account 50 | ) external view returns (uint256); 51 | } 52 | -------------------------------------------------------------------------------- /src/interfaces/IPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IPool { 5 | function addPool( 6 | address _lptoken, 7 | address _gauge, 8 | uint256 _stashVersion 9 | ) external returns (bool); 10 | 11 | function forceAddPool( 12 | address _lptoken, 13 | address _gauge, 14 | uint256 _stashVersion 15 | ) external returns (bool); 16 | 17 | function shutdownPool(uint256 _pid) external returns (bool); 18 | 19 | function poolInfo( 20 | uint256 _pid 21 | ) 22 | external 23 | view 24 | returns ( 25 | address _lptoken, 26 | address _token, 27 | address _gauge, 28 | address _crvRewards, 29 | address _stash, 30 | bool _shutdown 31 | ); 32 | 33 | function poolLength() external view returns (uint256); 34 | 35 | function gaugeMap(address) external view returns (bool); 36 | 37 | function setPoolManager(address _poolM) external; 38 | } 39 | -------------------------------------------------------------------------------- /src/interfaces/IUniswap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IUniswapRouter { 5 | function swapExactETHForTokens( 6 | uint amountOutMin, 7 | address[] calldata path, 8 | address to, 9 | uint deadline 10 | ) external payable returns (uint[] memory amounts); 11 | } 12 | -------------------------------------------------------------------------------- /src/interfaces/IWETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | interface IWETH { 5 | function deposit() external payable; 6 | 7 | function transfer(address to, uint value) external returns (bool); 8 | 9 | function withdraw(uint) external; 10 | 11 | function approve(address, uint) external; 12 | } 13 | -------------------------------------------------------------------------------- /test/BaseSetup.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.0; 3 | 4 | import {console} from "forge-std/console.sol"; 5 | import {stdStorage, StdStorage, Test} from "forge-std/Test.sol"; 6 | 7 | import {Utils} from "./utils/Util.sol"; 8 | import {IUniswapRouter} from "../src/interfaces/IUniswap.sol"; 9 | import {IWETH} from "../src/interfaces/IWETH.sol"; 10 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 11 | 12 | contract BaseSetup is Test { 13 | address internal constant USDC_ADDRESS = 14 | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; 15 | address internal constant USDT_ADDRESS = 16 | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; 17 | address internal constant UNISWAP_ROUTER_ADDRESS = 18 | 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; 19 | address internal constant WETH_ADDRESS = 20 | 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; 21 | address internal constant DAI_ADDRESS = 22 | 0x6B175474E89094C44Da98b954EedeAC495271d0F; 23 | address internal constant WBTC_ADDRESS = 24 | 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; 25 | address internal constant CRV_TOKEN_ADDRESS = 26 | 0xD533a949740bb3306d119CC777fa900bA034cd52; 27 | address internal constant CVX_TOKEN_ADDRESS = 28 | 0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B; 29 | // Skip forward block.timestamp for 3 days. 30 | uint256 internal constant SKIP_FORWARD_PERIOD = 3600 * 24 * 3; 31 | uint256 internal constant ALICE_DEPOSIT_AMOUNT_PER_ONCE = 3000000; 32 | uint256 internal constant ALICE_WITHDRAW_AMOUNT_PER_ONCE = 100000; 33 | uint256 internal constant BOB_DEPOSIT_AMOUNT_PER_ONCE = 2000000; 34 | uint256 internal constant BOB_WITHDRAW_AMOUNT_PER_ONCE = 1000000; 35 | 36 | address[] internal pathDAI; 37 | address[] internal pathUSDT; 38 | address[] internal pathUSDC; 39 | address[] internal pathWBTC; 40 | 41 | Utils internal utils; 42 | 43 | address payable[] internal users; 44 | address internal alice; 45 | address internal bob; 46 | 47 | IWETH internal weth; 48 | IERC20 internal dai; 49 | IERC20 internal usdc; 50 | IERC20 internal usdt; 51 | IERC20 internal wbtc; 52 | 53 | IUniswapRouter internal uniswapRouter; 54 | 55 | function setUp() public virtual { 56 | console.log("address = %s", address(this)); 57 | utils = new Utils(); 58 | users = utils.createUsers(5); 59 | 60 | alice = users[0]; 61 | vm.label(alice, "Alice"); 62 | bob = users[1]; 63 | vm.label(bob, "Bob"); 64 | 65 | initPathForSwap(); 66 | getStableCoinBalanceForTesting(); 67 | } 68 | 69 | function initPathForSwap() internal { 70 | weth = IWETH(WETH_ADDRESS); 71 | dai = IERC20(DAI_ADDRESS); 72 | usdc = IERC20(USDC_ADDRESS); 73 | usdt = IERC20(USDT_ADDRESS); 74 | wbtc = IERC20(WBTC_ADDRESS); 75 | 76 | pathDAI = new address[](2); 77 | pathDAI[0] = WETH_ADDRESS; 78 | pathDAI[1] = DAI_ADDRESS; 79 | 80 | pathUSDC = new address[](2); 81 | pathUSDC[0] = WETH_ADDRESS; 82 | pathUSDC[1] = USDC_ADDRESS; 83 | 84 | pathUSDT = new address[](2); 85 | pathUSDT[0] = WETH_ADDRESS; 86 | pathUSDT[1] = USDT_ADDRESS; 87 | 88 | pathWBTC = new address[](2); 89 | pathWBTC[0] = WETH_ADDRESS; 90 | pathWBTC[1] = WBTC_ADDRESS; 91 | } 92 | 93 | function swapETHToToken( 94 | address[] memory _path, 95 | address _to, 96 | uint256 _amount 97 | ) internal { 98 | uint256 deadline = block.timestamp + 3600000; 99 | 100 | uniswapRouter.swapExactETHForTokens{value: _amount}( 101 | 0, 102 | _path, 103 | _to, 104 | deadline 105 | ); 106 | } 107 | 108 | function getStableCoinBalanceForTesting() internal { 109 | uint wethAmount = 10 * 1e18; 110 | 111 | weth.approve(address(uniswapRouter), wethAmount * 10); 112 | 113 | uniswapRouter = IUniswapRouter(UNISWAP_ROUTER_ADDRESS); 114 | 115 | swapETHToToken(pathDAI, address(alice), wethAmount); 116 | swapETHToToken(pathUSDC, address(alice), wethAmount); 117 | swapETHToToken(pathUSDT, address(alice), wethAmount); 118 | swapETHToToken(pathWBTC, address(alice), wethAmount); 119 | 120 | swapETHToToken(pathDAI, address(bob), wethAmount); 121 | swapETHToToken(pathUSDC, address(bob), wethAmount); 122 | swapETHToToken(pathUSDT, address(bob), wethAmount); 123 | swapETHToToken(pathWBTC, address(bob), wethAmount); 124 | 125 | console.log("Alice's dai balance = %d", dai.balanceOf(address(alice))); 126 | console.log( 127 | "Alice's usdc balance = %d", 128 | usdc.balanceOf(address(alice)) 129 | ); 130 | console.log( 131 | "Alice's usdt balance = %d", 132 | usdt.balanceOf(address(alice)) 133 | ); 134 | console.log( 135 | "Alice's wbtc balance = %d", 136 | wbtc.balanceOf(address(alice)) 137 | ); 138 | 139 | console.log("Bob's dai balance = %d", dai.balanceOf(address(bob))); 140 | console.log("Bob's usdc balance = %d", usdc.balanceOf(address(bob))); 141 | console.log("Bob's usdt balance = %d", usdt.balanceOf(address(bob))); 142 | console.log("Bob's wbtc balance = %d", wbtc.balanceOf(address(bob))); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /test/ConvexVault.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/ConvexVault.sol"; 6 | import {BaseSetup} from "./BaseSetup.sol"; 7 | import {ICurveToken, ICurvePool} from "../src/interfaces/ICurve.sol"; 8 | 9 | /** 10 | 11 | @title ConvexVaultTest 12 | @dev This is a contract for testing the functions in the ConvexVault smart contract. 13 | The tests include depositing LP tokens, withdrawing LP tokens, and claiming rewards. 14 | The test cases are performed using mocked contracts and functions, using the Forge test library. */ 15 | 16 | contract ConvexVaultTest is BaseSetup { 17 | ConvexVault public vault; 18 | 19 | address private constant WBTC_LP_TOKEN_ADDRESS = 20 | 0xc4AD29ba4B3c580e6D59105FFf484999997675Ff; 21 | address private constant WBTC_POOL_ADDRESS = 22 | 0xD51a44d3FaE010294C616388b506AcdA1bfAAE46; 23 | address private constant CONVEX_BOOSTER_ADDRESS = 24 | 0xF403C135812408BFbE8713b5A23a04b3D48AAE31; 25 | 26 | function setUp() public virtual override { 27 | BaseSetup.setUp(); 28 | vault = new ConvexVault(WBTC_LP_TOKEN_ADDRESS, 38); 29 | } 30 | 31 | /** 32 | @dev This function tests the deposit and withdrawal of LP tokens, as well as claiming rewards, in a simulated environment using mocked contracts and functions. The function initializes variables and sets up the test environment before performing the following steps: 33 | - Alice and Bob add liquidity to the WBTC pool and receive LP tokens. 34 | - Alice and Bob deposit some of their LP tokens into the ConvexVault. 35 | - Alice and Bob wait for a specified period of time. 36 | - Bob deposits more LP tokens into the ConvexVault. 37 | - Alice and Bob withdraw some of their LP tokens from the ConvexVault. 38 | - Alice and Bob claim their CRV and CVX rewards. 39 | 40 | The function uses the Forge test library to simulate these interactions with the smart contract and to assert that certain conditions are met during each step of the process. Overall, this function provides a comprehensive test of the functionality of the ConvexVault smart contract and its integration with other contracts in the ecosystem. 41 | */ 42 | function test_depositLpTokenAndWithdraw() public { 43 | IERC20 wbtcLpToken = IERC20(WBTC_LP_TOKEN_ADDRESS); 44 | ICurvePool wbtcPool = ICurvePool(WBTC_POOL_ADDRESS); 45 | 46 | uint256 wbtcBalance = wbtc.balanceOf(address(alice)); 47 | 48 | vm.startPrank(alice); 49 | wbtc.approve(address(wbtcPool), wbtcBalance); 50 | (bool success, ) = address(wbtcPool).call( 51 | abi.encodeWithSignature( 52 | "add_liquidity(uint256[3],uint256)", 53 | [0, wbtcBalance, 0], 54 | 0 55 | ) 56 | ); 57 | assertEq(success, true); 58 | vm.stopPrank(); 59 | // Once finished adding liquidity to pool. Alice's LP Token balance should be bigger than ZERO. 60 | assertGe(wbtcLpToken.balanceOf(address(alice)), 0); 61 | 62 | console.log( 63 | "Alice's WBTC LP Token = %d", 64 | wbtcLpToken.balanceOf(address(alice)) 65 | ); 66 | 67 | wbtcBalance = wbtc.balanceOf(address(bob)); 68 | 69 | vm.startPrank(bob); 70 | wbtc.approve(address(wbtcPool), wbtcBalance); 71 | (success, ) = address(wbtcPool).call( 72 | abi.encodeWithSignature( 73 | "add_liquidity(uint256[3],uint256)", 74 | [0, wbtcBalance, 0], 75 | 0 76 | ) 77 | ); 78 | assertEq(success, true); 79 | vm.stopPrank(); 80 | // Once finished adding liquidity to pool. Alice's LP Token balance should be bigger than ZERO. 81 | assertGe(wbtcLpToken.balanceOf(address(bob)), 0); 82 | 83 | console.log( 84 | "Bob's WBTC LP Token = %d", 85 | wbtcLpToken.balanceOf(address(bob)) 86 | ); 87 | 88 | uint256 lpTokenBalance = wbtcLpToken.balanceOf(address(alice)); 89 | 90 | vm.prank(alice); 91 | wbtcLpToken.approve(address(vault), lpTokenBalance); 92 | 93 | // 1st Alice's Deposit ------ Alice deposit some LP Tokens (amount = ALICE_DEPOSIT_AMOUNT_PER_ONCE) to CurveVault 94 | vm.prank(alice); 95 | vault.deposit(ALICE_DEPOSIT_AMOUNT_PER_ONCE); 96 | assertEq( 97 | wbtcLpToken.balanceOf(address(alice)), 98 | lpTokenBalance - ALICE_DEPOSIT_AMOUNT_PER_ONCE 99 | ); 100 | // Advance block.timestamp to current timestamp + SKIP_FORWARD_PERIOD 101 | skip(SKIP_FORWARD_PERIOD); 102 | 103 | vm.prank(bob); 104 | lpTokenBalance = wbtcLpToken.balanceOf(address(bob)); 105 | vm.prank(bob); 106 | wbtcLpToken.approve(address(vault), lpTokenBalance); 107 | 108 | // 1st Bob's Deposit ------ Bob deposit some LP Tokens (amount = ALICE_DEPOSIT_AMOUNT_PER_ONCE) to CurveVault 109 | vm.prank(bob); 110 | vault.deposit(ALICE_DEPOSIT_AMOUNT_PER_ONCE); 111 | // Once finished depositing, Bob's LP Token amount = Before LP TokenAmount - ALICE_DEPOSIT_AMOUNT_PER_ONCE 112 | assertEq( 113 | wbtcLpToken.balanceOf(address(bob)), 114 | lpTokenBalance - ALICE_DEPOSIT_AMOUNT_PER_ONCE 115 | ); 116 | 117 | // Once finished depositing LP Token, WBTC's Liquidity gauge's balance should be bigger than ZERO. 118 | assertGe(wbtcLpToken.balanceOf(CONVEX_BOOSTER_ADDRESS), 0); 119 | // Advance block.timestamp to current timestamp + SKIP_FORWARD_PERIOD 120 | skip(SKIP_FORWARD_PERIOD); 121 | 122 | // 2nd Bob's Deposit ------ Bob deposit some LP Tokens (amount = ALICE_DEPOSIT_AMOUNT_PER_ONCE) to CurveVault 123 | vm.prank(bob); 124 | vault.deposit(BOB_DEPOSIT_AMOUNT_PER_ONCE); 125 | 126 | // Once finished depositing LP Token, WBTC's Liquidity gauge's balance should be bigger than ZERO. 127 | assertGe(wbtcLpToken.balanceOf(CONVEX_BOOSTER_ADDRESS), 0); 128 | skip(SKIP_FORWARD_PERIOD); 129 | 130 | // 1st Alice's withdraw ----- Alice withdraw some LP Tokens (amount = ALICE_WITHDRAW_AMOUNT_PER_ONCE) from CurveVault 131 | vm.startPrank(alice); 132 | vault.withdraw(ALICE_WITHDRAW_AMOUNT_PER_ONCE); 133 | skip(SKIP_FORWARD_PERIOD); 134 | 135 | // 1st Bob's withdraw ----- Bob withdraw some LP Tokens (amount = ALICE_WITHDRAW_AMOUNT_PER_ONCE) from CurveVault 136 | vm.prank(bob); 137 | vault.withdraw(BOB_WITHDRAW_AMOUNT_PER_ONCE); 138 | skip(SKIP_FORWARD_PERIOD); 139 | 140 | // 2nd Alice's withdraw ----- Alice withdraw some LP Tokens (amount = ALICE_WITHDRAW_AMOUNT_PER_ONCE) from CurveVault 141 | vm.prank(alice); 142 | vault.withdraw(ALICE_WITHDRAW_AMOUNT_PER_ONCE); 143 | // Advance block.timestamp to current timestamp + SKIP_FORWARD_PERIOD 144 | skip(SKIP_FORWARD_PERIOD); 145 | 146 | // 2nd Bob's withdraw ----- Bob withdraw some LP Tokens (amount = ALICE_WITHDRAW_AMOUNT_PER_ONCE) from CurveVault 147 | vm.prank(bob); 148 | vault.withdraw(BOB_WITHDRAW_AMOUNT_PER_ONCE); 149 | skip(SKIP_FORWARD_PERIOD); 150 | 151 | // Alice and Bob's CRV Token amount should be bigger than ZERO 152 | IERC20 crvToken = IERC20(CRV_TOKEN_ADDRESS); 153 | IERC20 cvxToken = IERC20(CVX_TOKEN_ADDRESS); 154 | assertGe(crvToken.balanceOf(address(alice)), 0); 155 | assertGe(crvToken.balanceOf(address(bob)), 0); 156 | assertGe(cvxToken.balanceOf(address(alice)), 0); 157 | assertGe(cvxToken.balanceOf(address(bob)), 0); 158 | 159 | console.log( 160 | "Alice's CRV Token balance = %d", 161 | crvToken.balanceOf(address(alice)) 162 | ); 163 | console.log( 164 | "Alice's CVX Token balance = %d", 165 | cvxToken.balanceOf(address(alice)) 166 | ); 167 | 168 | console.log( 169 | "Bob's CRV Token balance = %d", 170 | crvToken.balanceOf(address(bob)) 171 | ); 172 | console.log( 173 | "Bob's CVX Token balance = %d", 174 | cvxToken.balanceOf(address(bob)) 175 | ); 176 | } 177 | 178 | /** 179 | @dev This function tests the deposit of LP tokens and claiming rewards in a simulated environment using mocked contracts and functions. The function initializes variables and sets up the test environment before performing the following steps: 180 | - Alice and Bob add liquidity to the WBTC pool and receive LP tokens. 181 | - Alice and Bob deposit some of their LP tokens into the ConvexVault. 182 | - Alice and Bob wait for a specified period of time. 183 | - Bob deposits more LP tokens into the ConvexVault. 184 | - Alice and Bob wait for a specified period of time. 185 | - Alice claims her CRV reward from the ConvexVault. 186 | - Bob claims his CVX reward from the ConvexVault. 187 | 188 | The function uses the Forge test library to simulate these interactions with the smart contract and to assert that certain conditions are met during each step of the process. Overall, this function provides a comprehensive test of the functionality of the ConvexVault smart contract and its integration with other contracts in the ecosystem. 189 | */ 190 | function test_depositLpTokenAndClaimReward() public { 191 | IERC20 wbtcLpToken = IERC20(WBTC_LP_TOKEN_ADDRESS); 192 | ICurvePool wbtcPool = ICurvePool(WBTC_POOL_ADDRESS); 193 | 194 | uint256 wbtcBalance = wbtc.balanceOf(address(alice)); 195 | 196 | vm.startPrank(alice); 197 | wbtc.approve(address(wbtcPool), wbtcBalance); 198 | (bool success, ) = address(wbtcPool).call( 199 | abi.encodeWithSignature( 200 | "add_liquidity(uint256[3],uint256)", 201 | [0, wbtcBalance, 0], 202 | 0 203 | ) 204 | ); 205 | assertEq(success, true); 206 | vm.stopPrank(); 207 | // Once finished adding liquidity to pool. Alice's LP Token balance should be bigger than ZERO. 208 | assertGe(wbtcLpToken.balanceOf(address(alice)), 0); 209 | 210 | console.log( 211 | "Alice's WBTC LP Token = %d", 212 | wbtcLpToken.balanceOf(address(alice)) 213 | ); 214 | 215 | wbtcBalance = wbtc.balanceOf(address(bob)); 216 | 217 | vm.startPrank(bob); 218 | wbtc.approve(address(wbtcPool), wbtcBalance); 219 | (success, ) = address(wbtcPool).call( 220 | abi.encodeWithSignature( 221 | "add_liquidity(uint256[3],uint256)", 222 | [0, wbtcBalance, 0], 223 | 0 224 | ) 225 | ); 226 | assertEq(success, true); 227 | vm.stopPrank(); 228 | // Once finished adding liquidity to pool. Alice's LP Token balance should be bigger than ZERO. 229 | assertGe(wbtcLpToken.balanceOf(address(bob)), 0); 230 | 231 | console.log( 232 | "Bob's WBTC LP Token = %d", 233 | wbtcLpToken.balanceOf(address(bob)) 234 | ); 235 | 236 | uint256 lpTokenBalance = wbtcLpToken.balanceOf(address(alice)); 237 | 238 | vm.prank(alice); 239 | wbtcLpToken.approve(address(vault), lpTokenBalance); 240 | 241 | // 1st Alice's Deposit ------ Alice deposit some LP Tokens (amount = ALICE_DEPOSIT_AMOUNT_PER_ONCE) to CurveVault 242 | vm.prank(alice); 243 | vault.deposit(ALICE_DEPOSIT_AMOUNT_PER_ONCE); 244 | assertEq( 245 | wbtcLpToken.balanceOf(address(alice)), 246 | lpTokenBalance - ALICE_DEPOSIT_AMOUNT_PER_ONCE 247 | ); 248 | // Advance block.timestamp to current timestamp + SKIP_FORWARD_PERIOD 249 | skip(SKIP_FORWARD_PERIOD); 250 | 251 | lpTokenBalance = wbtcLpToken.balanceOf(address(bob)); 252 | 253 | vm.prank(bob); 254 | wbtcLpToken.approve(address(vault), lpTokenBalance); 255 | 256 | // 1st Bob's Deposit ------ Bob deposit some LP Tokens (amount = ALICE_DEPOSIT_AMOUNT_PER_ONCE) to CurveVault 257 | vm.prank(bob); 258 | vault.deposit(BOB_DEPOSIT_AMOUNT_PER_ONCE); 259 | // Once finished depositing, Bob's LP Token amount = Before LP TokenAmount - ALICE_DEPOSIT_AMOUNT_PER_ONCE 260 | assertEq( 261 | wbtcLpToken.balanceOf(address(bob)), 262 | lpTokenBalance - BOB_DEPOSIT_AMOUNT_PER_ONCE 263 | ); 264 | 265 | // Once finished depositing LP Token, WBTC's Liquidity gauge's balance should be bigger than ZERO. 266 | assertGe(wbtcLpToken.balanceOf(CONVEX_BOOSTER_ADDRESS), 0); 267 | // Advance block.timestamp to current timestamp + SKIP_FORWARD_PERIOD 268 | skip(SKIP_FORWARD_PERIOD); 269 | 270 | // 2nd Bob's Deposit ------ Bob deposit some LP Tokens (amount = ALICE_DEPOSIT_AMOUNT_PER_ONCE) to CurveVault 271 | vm.prank(bob); 272 | vault.deposit(BOB_DEPOSIT_AMOUNT_PER_ONCE); 273 | 274 | // 2st Alice's Deposit ------ Alice deposit some LP Tokens (amount = ALICE_DEPOSIT_AMOUNT_PER_ONCE) to CurveVault 275 | vm.prank(alice); 276 | vault.deposit(ALICE_DEPOSIT_AMOUNT_PER_ONCE); 277 | 278 | skip(SKIP_FORWARD_PERIOD); 279 | skip(SKIP_FORWARD_PERIOD); 280 | skip(SKIP_FORWARD_PERIOD); 281 | 282 | // Alice claim rewards 283 | 284 | vm.prank(bob); 285 | vault.claim(CVX_TOKEN_ADDRESS); 286 | 287 | vm.prank(alice); 288 | vault.claim(CRV_TOKEN_ADDRESS); 289 | 290 | // Alice and Bob's CRV Token amount should be bigger than ZERO 291 | IERC20 crvToken = IERC20(CRV_TOKEN_ADDRESS); 292 | IERC20 cvxToken = IERC20(CVX_TOKEN_ADDRESS); 293 | assertGe(crvToken.balanceOf(address(alice)), 0); 294 | assertGe(crvToken.balanceOf(address(bob)), 0); 295 | assertGe(cvxToken.balanceOf(address(alice)), 0); 296 | assertGe(cvxToken.balanceOf(address(bob)), 0); 297 | 298 | console.log( 299 | "Alice's CRV Token balance = %d", 300 | crvToken.balanceOf(address(alice)) 301 | ); 302 | console.log( 303 | "Alice's CVX Token balance = %d", 304 | cvxToken.balanceOf(address(alice)) 305 | ); 306 | 307 | console.log( 308 | "Bob's CRV Token balance = %d", 309 | crvToken.balanceOf(address(bob)) 310 | ); 311 | console.log( 312 | "Bob's CVX Token balance = %d", 313 | cvxToken.balanceOf(address(bob)) 314 | ); 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /test/CurveVault.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/CurveVault.sol"; 6 | import {BaseSetup} from "./BaseSetup.sol"; 7 | import {ICurveToken, ICurvePool} from "../src/interfaces/ICurve.sol"; 8 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 9 | 10 | contract CurveVaultTest is BaseSetup { 11 | CurveVault public vault; 12 | 13 | address internal constant AAVE_POOL_LP_TOKEN_ADDRESS = 14 | 0xFd2a8fA60Abd58Efe3EeE34dd494cD491dC14900; 15 | address internal constant AAVE_POOL_ADDRESS = 16 | 0xDeBF20617708857ebe4F679508E7b7863a8A8EeE; 17 | address public constant AAVE_LIQUIDITY_GAUGE_ADDRESS = 18 | 0xd662908ADA2Ea1916B3318327A97eB18aD588b5d; 19 | 20 | function setUp() public virtual override { 21 | BaseSetup.setUp(); 22 | 23 | vault = new CurveVault( 24 | AAVE_POOL_LP_TOKEN_ADDRESS, 25 | AAVE_LIQUIDITY_GAUGE_ADDRESS 26 | ); 27 | } 28 | 29 | function test_depositLpTokenAndWithdraw() public { 30 | IERC20 aaveLpToken = IERC20(AAVE_POOL_LP_TOKEN_ADDRESS); 31 | ICurvePool aavePool = ICurvePool(AAVE_POOL_ADDRESS); 32 | 33 | uint256 daiBalance = dai.balanceOf(address(alice)); 34 | uint256 usdcBalance = usdc.balanceOf(address(alice)); 35 | uint256 usdtBalance = usdt.balanceOf(address(alice)); 36 | 37 | vm.startPrank(alice); 38 | dai.approve(address(aavePool), daiBalance); 39 | usdt.approve(address(aavePool), usdtBalance); 40 | usdc.approve(address(aavePool), usdcBalance); 41 | 42 | uint256 mint_amount = aavePool.add_liquidity( 43 | [daiBalance, usdcBalance, 0], 44 | 100, 45 | true 46 | ); 47 | 48 | vm.stopPrank(); 49 | 50 | // The minted amount should be always bigger than 100 51 | assertGe(mint_amount, 100); 52 | // Once finished adding liquidity to pool. Alice's LP Token balance should be bigger than ZERO. 53 | assertGe(aaveLpToken.balanceOf(address(alice)), 0); 54 | 55 | daiBalance = dai.balanceOf(address(bob)); 56 | usdcBalance = usdc.balanceOf(address(bob)); 57 | usdtBalance = usdt.balanceOf(address(bob)); 58 | 59 | vm.startPrank(bob); 60 | dai.approve(address(aavePool), daiBalance); 61 | usdt.approve(address(aavePool), usdtBalance); 62 | usdc.approve(address(aavePool), usdcBalance); 63 | 64 | mint_amount = aavePool.add_liquidity( 65 | [daiBalance, usdcBalance, 0], 66 | 0, 67 | true 68 | ); 69 | vm.stopPrank(); 70 | 71 | // The minted amount should be always bigger than 100 72 | assertGe(mint_amount, 100); 73 | // Once finished adding liquidity to pool. Alice's LP Token balance should be bigger than ZERO. 74 | assertGe(aaveLpToken.balanceOf(address(bob)), 0); 75 | 76 | uint256 lpTokenBalance = aaveLpToken.balanceOf(address(alice)); 77 | 78 | vm.prank(alice); 79 | aaveLpToken.approve(address(vault), lpTokenBalance); 80 | 81 | // 1st Alice's Deposit ------ Alice deposit some LP Tokens (amount = ALICE_DEPOSIT_AMOUNT_PER_ONCE) to CurveVault 82 | vm.prank(alice); 83 | vault.deposit(ALICE_DEPOSIT_AMOUNT_PER_ONCE); 84 | assertEq( 85 | aaveLpToken.balanceOf(address(alice)), 86 | lpTokenBalance - ALICE_DEPOSIT_AMOUNT_PER_ONCE 87 | ); 88 | // Advance block.timestamp to current timestamp + SKIP_FORWARD_PERIOD 89 | skip(SKIP_FORWARD_PERIOD); 90 | 91 | vm.prank(bob); 92 | lpTokenBalance = aaveLpToken.balanceOf(address(bob)); 93 | vm.prank(bob); 94 | aaveLpToken.approve(address(vault), lpTokenBalance); 95 | 96 | // 1st Bob's Deposit ------ Bob deposit some LP Tokens (amount = ALICE_DEPOSIT_AMOUNT_PER_ONCE) to CurveVault 97 | vm.prank(bob); 98 | vault.deposit(ALICE_DEPOSIT_AMOUNT_PER_ONCE); 99 | // Once finished depositing, Bob's LP Token amount = Before LP TokenAmount - ALICE_DEPOSIT_AMOUNT_PER_ONCE 100 | assertEq( 101 | aaveLpToken.balanceOf(address(bob)), 102 | lpTokenBalance - ALICE_DEPOSIT_AMOUNT_PER_ONCE 103 | ); 104 | 105 | // Once finished depositing LP Token, Aave's Liquidity gauge's balance should be bigger than ZERO. 106 | assertGe(aaveLpToken.balanceOf(AAVE_LIQUIDITY_GAUGE_ADDRESS), 0); 107 | // Advance block.timestamp to current timestamp + SKIP_FORWARD_PERIOD 108 | skip(SKIP_FORWARD_PERIOD); 109 | 110 | // 2nd Bob's Deposit ------ Bob deposit some LP Tokens (amount = ALICE_DEPOSIT_AMOUNT_PER_ONCE) to CurveVault 111 | vm.prank(bob); 112 | vault.deposit(BOB_DEPOSIT_AMOUNT_PER_ONCE); 113 | 114 | // Once finished depositing LP Token, Aave's Liquidity gauge's balance should be bigger than ZERO. 115 | assertGe(aaveLpToken.balanceOf(AAVE_LIQUIDITY_GAUGE_ADDRESS), 0); 116 | skip(SKIP_FORWARD_PERIOD); 117 | 118 | IERC20 crvToken = IERC20(CRV_TOKEN_ADDRESS); 119 | 120 | vm.prank(alice); 121 | vault.harvestRewards(); 122 | 123 | vm.prank(bob); 124 | vault.harvestRewards(); 125 | 126 | assertGe(crvToken.balanceOf(address(alice)), 0); 127 | assertGe(crvToken.balanceOf(address(bob)), 0); 128 | 129 | console.log( 130 | "Alice's CRV Token balance = %d", 131 | crvToken.balanceOf(address(alice)) 132 | ); 133 | 134 | console.log( 135 | "Bob's CRV Token balance = %d", 136 | crvToken.balanceOf(address(bob)) 137 | ); 138 | } 139 | 140 | function test_depositLpTokenAndHarvestCrvReward() public { 141 | IERC20 aaveLpToken = IERC20(AAVE_POOL_LP_TOKEN_ADDRESS); 142 | ICurvePool aavePool = ICurvePool(AAVE_POOL_ADDRESS); 143 | 144 | uint256 daiBalance = dai.balanceOf(address(alice)); 145 | uint256 usdcBalance = usdc.balanceOf(address(alice)); 146 | uint256 usdtBalance = usdt.balanceOf(address(alice)); 147 | 148 | vm.startPrank(alice); 149 | dai.approve(address(aavePool), daiBalance); 150 | usdt.approve(address(aavePool), usdtBalance); 151 | usdc.approve(address(aavePool), usdcBalance); 152 | 153 | uint256 mint_amount = aavePool.add_liquidity( 154 | [daiBalance, usdcBalance, 0], 155 | 100, 156 | true 157 | ); 158 | 159 | vm.stopPrank(); 160 | 161 | // The minted amount should be always bigger than 100 162 | assertGe(mint_amount, 100); 163 | // Once finished adding liquidity to pool. Alice's LP Token balance should be bigger than ZERO. 164 | assertGe(aaveLpToken.balanceOf(address(alice)), 0); 165 | 166 | daiBalance = dai.balanceOf(address(bob)); 167 | usdcBalance = usdc.balanceOf(address(bob)); 168 | usdtBalance = usdt.balanceOf(address(bob)); 169 | 170 | vm.startPrank(bob); 171 | dai.approve(address(aavePool), daiBalance); 172 | usdt.approve(address(aavePool), usdtBalance); 173 | usdc.approve(address(aavePool), usdcBalance); 174 | 175 | mint_amount = aavePool.add_liquidity( 176 | [daiBalance, usdcBalance, 0], 177 | 0, 178 | true 179 | ); 180 | vm.stopPrank(); 181 | 182 | // The minted amount should be always bigger than 100 183 | assertGe(mint_amount, 100); 184 | // Once finished adding liquidity to pool. Alice's LP Token balance should be bigger than ZERO. 185 | assertGe(aaveLpToken.balanceOf(address(bob)), 0); 186 | 187 | uint256 lpTokenBalance = aaveLpToken.balanceOf(address(alice)); 188 | 189 | vm.prank(alice); 190 | aaveLpToken.approve(address(vault), lpTokenBalance); 191 | 192 | // 1st Alice's Deposit ------ Alice deposit some LP Tokens (amount = ALICE_DEPOSIT_AMOUNT_PER_ONCE) to CurveVault 193 | vm.prank(alice); 194 | vault.deposit(ALICE_DEPOSIT_AMOUNT_PER_ONCE); 195 | assertEq( 196 | aaveLpToken.balanceOf(address(alice)), 197 | lpTokenBalance - ALICE_DEPOSIT_AMOUNT_PER_ONCE 198 | ); 199 | // Advance block.timestamp to current timestamp + SKIP_FORWARD_PERIOD 200 | skip(SKIP_FORWARD_PERIOD); 201 | 202 | vm.prank(bob); 203 | lpTokenBalance = aaveLpToken.balanceOf(address(bob)); 204 | vm.prank(bob); 205 | aaveLpToken.approve(address(vault), lpTokenBalance); 206 | 207 | // 1st Bob's Deposit ------ Bob deposit some LP Tokens (amount = ALICE_DEPOSIT_AMOUNT_PER_ONCE) to CurveVault 208 | vm.prank(bob); 209 | vault.deposit(ALICE_DEPOSIT_AMOUNT_PER_ONCE); 210 | // Once finished depositing, Bob's LP Token amount = Before LP TokenAmount - ALICE_DEPOSIT_AMOUNT_PER_ONCE 211 | assertEq( 212 | aaveLpToken.balanceOf(address(bob)), 213 | lpTokenBalance - ALICE_DEPOSIT_AMOUNT_PER_ONCE 214 | ); 215 | 216 | // Once finished depositing LP Token, Aave's Liquidity gauge's balance should be bigger than ZERO. 217 | assertGe(aaveLpToken.balanceOf(AAVE_LIQUIDITY_GAUGE_ADDRESS), 0); 218 | // Advance block.timestamp to current timestamp + SKIP_FORWARD_PERIOD 219 | skip(SKIP_FORWARD_PERIOD); 220 | 221 | // 2nd Bob's Deposit ------ Bob deposit some LP Tokens (amount = ALICE_DEPOSIT_AMOUNT_PER_ONCE) to CurveVault 222 | vm.prank(bob); 223 | vault.deposit(BOB_DEPOSIT_AMOUNT_PER_ONCE); 224 | 225 | // Once finished depositing LP Token, Aave's Liquidity gauge's balance should be bigger than ZERO. 226 | assertGe(aaveLpToken.balanceOf(AAVE_LIQUIDITY_GAUGE_ADDRESS), 0); 227 | skip(SKIP_FORWARD_PERIOD); 228 | 229 | // 1st Alice's withdraw ----- Alice withdraw some LP Tokens (amount = ALICE_WITHDRAW_AMOUNT_PER_ONCE) from CurveVault 230 | vm.startPrank(alice); 231 | vault.withdraw(ALICE_WITHDRAW_AMOUNT_PER_ONCE); 232 | skip(SKIP_FORWARD_PERIOD); 233 | 234 | // 1st Bob's withdraw ----- Bob withdraw some LP Tokens (amount = ALICE_WITHDRAW_AMOUNT_PER_ONCE) from CurveVault 235 | vm.prank(bob); 236 | vault.withdraw(BOB_WITHDRAW_AMOUNT_PER_ONCE); 237 | skip(SKIP_FORWARD_PERIOD); 238 | 239 | // 2nd Alice's withdraw ----- Alice withdraw some LP Tokens (amount = ALICE_WITHDRAW_AMOUNT_PER_ONCE) from CurveVault 240 | vm.prank(alice); 241 | vault.withdraw(ALICE_WITHDRAW_AMOUNT_PER_ONCE); 242 | // Advance block.timestamp to current timestamp + SKIP_FORWARD_PERIOD 243 | skip(SKIP_FORWARD_PERIOD); 244 | 245 | // 2nd Bob's withdraw ----- Bob withdraw some LP Tokens (amount = ALICE_WITHDRAW_AMOUNT_PER_ONCE) from CurveVault 246 | vm.prank(bob); 247 | vault.withdraw(BOB_WITHDRAW_AMOUNT_PER_ONCE); 248 | skip(SKIP_FORWARD_PERIOD); 249 | 250 | // Alice and Bob's CRV Token amount should be bigger than ZERO 251 | IERC20 crvToken = IERC20(CRV_TOKEN_ADDRESS); 252 | assertGe(crvToken.balanceOf(address(alice)), 0); 253 | assertGe(crvToken.balanceOf(address(bob)), 0); 254 | 255 | console.log( 256 | "Alice's CRV Token balance = %d", 257 | crvToken.balanceOf(address(alice)) 258 | ); 259 | 260 | console.log( 261 | "Bob's CRV Token balance = %d", 262 | crvToken.balanceOf(address(bob)) 263 | ); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /test/utils/Util.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | 6 | contract Utils is Test { 7 | bytes32 internal nextUser = keccak256(abi.encodePacked("user address")); 8 | 9 | function getNextUserAddress() external returns (address payable) { 10 | address payable user = payable(address(uint160(uint256(nextUser)))); 11 | nextUser = keccak256(abi.encodePacked(nextUser)); 12 | return user; 13 | } 14 | 15 | // create users with 100 ETH balance each 16 | function createUsers( 17 | uint256 userNum 18 | ) external returns (address payable[] memory) { 19 | address payable[] memory users = new address payable[](userNum); 20 | for (uint256 i = 0; i < userNum; i++) { 21 | address payable user = this.getNextUserAddress(); 22 | vm.deal(user, 100 ether); 23 | users[i] = user; 24 | } 25 | 26 | return users; 27 | } 28 | 29 | // move block.number forward by a given number of blocks 30 | function mineBlocks(uint256 numBlocks) external { 31 | uint256 targetBlock = block.number + numBlocks; 32 | vm.roll(targetBlock); 33 | } 34 | } 35 | --------------------------------------------------------------------------------