|
| 1 | +// SPDX-License-Identifier: UNLICENSED |
| 2 | +// Gearbox Protocol. Generalized leverage for DeFi protocols |
| 3 | +// (c) Gearbox Foundation, 2023. |
| 4 | +pragma solidity ^0.8.17; |
| 5 | + |
| 6 | +import {Test} from "forge-std/Test.sol"; |
| 7 | + |
| 8 | +import {WAD} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; |
| 9 | +import {ZeroAddressException} from "@gearbox-protocol/core-v3/contracts/interfaces/IExceptions.sol"; |
| 10 | + |
| 11 | +import {WrappedATokenV2} from "../../tokens/aave/WrappedATokenV2.sol"; |
| 12 | +import {IWrappedATokenV2Events} from "../../interfaces/aave/IWrappedATokenV2.sol"; |
| 13 | + |
| 14 | +import {LendingPoolMock} from "../mocks/integrations/aave/LendingPoolMock.sol"; |
| 15 | +import {ERC20Mock} from "@gearbox-protocol/core-v3/contracts/test/mocks/token/ERC20Mock.sol"; |
| 16 | +import {FRIEND, USER} from "@gearbox-protocol/core-v3/contracts/test/lib/constants.sol"; |
| 17 | +import {BalanceHelper} from "@gearbox-protocol/core-v3/contracts/test/helpers/BalanceHelper.sol"; |
| 18 | +import {TokensTestSuite} from "@gearbox-protocol/core-v3/contracts/test/suites/TokensTestSuite.sol"; |
| 19 | + |
| 20 | +/// @title Wrapped aToken V2 unit test |
| 21 | +/// @notice U:[WAT]: Unit tests for Wrapped aToken V2 |
| 22 | +contract WrappedATokenV2UnitTest is Test, BalanceHelper, IWrappedATokenV2Events { |
| 23 | + WrappedATokenV2 public waToken; |
| 24 | + |
| 25 | + LendingPoolMock lendingPool; |
| 26 | + address token; |
| 27 | + address aToken; |
| 28 | + |
| 29 | + uint256 constant TOKEN_AMOUNT = 1e10; |
| 30 | + |
| 31 | + function setUp() public { |
| 32 | + tokenTestSuite = new TokensTestSuite(); |
| 33 | + lendingPool = new LendingPoolMock(); |
| 34 | + |
| 35 | + token = address(new ERC20Mock("Test Token", "TEST", 6)); |
| 36 | + aToken = lendingPool.addReserve(token, 0.02e27); // 2% |
| 37 | + deal(token, aToken, 1e12); // add some liquidity |
| 38 | + waToken = new WrappedATokenV2(aToken); |
| 39 | + |
| 40 | + vm.label(token, "TOKEN"); |
| 41 | + vm.label(aToken, "aTOKEN"); |
| 42 | + vm.label(address(waToken), "waTOKEN"); |
| 43 | + vm.label(address(lendingPool), "LENDING_POOL"); |
| 44 | + |
| 45 | + vm.warp(block.timestamp + 365 days); |
| 46 | + } |
| 47 | + |
| 48 | + /// @notice U:[WAT-1]: Constructor reverts on zero address |
| 49 | + function test_U_WAT_01_constructor_reverts_on_zero_address() public { |
| 50 | + vm.expectRevert(ZeroAddressException.selector); |
| 51 | + new WrappedATokenV2(address(0)); |
| 52 | + } |
| 53 | + |
| 54 | + /// @notice U:[WAT-2]: Constructor sets correct values |
| 55 | + function test_U_WAT_02_constructor_sets_correct_values() public { |
| 56 | + assertEq(waToken.aToken(), aToken, "Incorrect aUSDC address"); |
| 57 | + assertEq(waToken.underlying(), token, "Incorrect USDC address"); |
| 58 | + assertEq(waToken.lendingPool(), address(lendingPool), "Incorrect lending pool address"); |
| 59 | + assertEq(waToken.name(), "Wrapped Aave interest bearing Test Token", "Incorrect name"); |
| 60 | + assertEq(waToken.symbol(), "waTEST", "Incorrect symbol"); |
| 61 | + assertEq(waToken.decimals(), 6, "Incorrect decimals"); |
| 62 | + } |
| 63 | + |
| 64 | + /// @notice U:[WAT-3]: `balanceOfUnderlying` works correctly |
| 65 | + /// @dev Fuzzing times before measuring balances |
| 66 | + /// @dev Small deviations in expected and actual balances are allowed due to rounding errors |
| 67 | + /// Generally, dust size grows with time and number of operations on the wrapper |
| 68 | + /// Nevertheless, the test shows that wrapper stays solvent and doesn't lose deposited funds |
| 69 | + function test_U_WAT_03_balanceOfUnderlying_works_correctly(uint256 timedelta1, uint256 timedelta2) public { |
| 70 | + vm.assume(timedelta1 < 5 * 365 days && timedelta2 < 5 * 365 days); |
| 71 | + uint256 balance1; |
| 72 | + uint256 balance2; |
| 73 | + |
| 74 | + // mint equivalent amounts of aTokens and waTokens to first user and wait for some time |
| 75 | + _mintAToken(USER); |
| 76 | + _mintWAToken(USER); |
| 77 | + vm.warp(block.timestamp + timedelta1); |
| 78 | + |
| 79 | + // balances must stay equivalent (up to some dust) |
| 80 | + balance1 = waToken.balanceOfUnderlying(USER); |
| 81 | + expectBalanceGe(aToken, USER, balance1, "user 1 after t1"); |
| 82 | + expectBalanceLe(aToken, USER, balance1 + 2, "user 1 after t1"); |
| 83 | + |
| 84 | + // also, wrapper's total balance of aToken must be equal to user's balances of underlying |
| 85 | + expectBalanceGe(aToken, address(waToken), balance1, "wrapper after t1"); |
| 86 | + expectBalanceLe(aToken, address(waToken), balance1 + 2, "wrapper after t1"); |
| 87 | + |
| 88 | + // now mint equivalent amounts of aTokens and waTokens to second user and wait for more time |
| 89 | + _mintAToken(FRIEND); |
| 90 | + _mintWAToken(FRIEND); |
| 91 | + vm.warp(block.timestamp + timedelta2); |
| 92 | + |
| 93 | + // balances must stay equivalent for both users |
| 94 | + balance1 = waToken.balanceOfUnderlying(USER); |
| 95 | + expectBalanceGe(aToken, USER, balance1, "user 1 after t2"); |
| 96 | + expectBalanceLe(aToken, USER, balance1 + 2, "user 1 after t2"); |
| 97 | + |
| 98 | + balance2 = waToken.balanceOfUnderlying(FRIEND); |
| 99 | + expectBalanceGe(aToken, FRIEND, balance2, "user 2 after t2"); |
| 100 | + expectBalanceLe(aToken, FRIEND, balance2 + 2, "user 2 after t2"); |
| 101 | + |
| 102 | + // finally, wrapper's total balance of aToken must be equal to sum of users' balances of underlying |
| 103 | + expectBalanceGe(aToken, address(waToken), balance1 + balance2 - 1, "wrapper after t2"); |
| 104 | + expectBalanceLe(aToken, address(waToken), balance1 + balance2 + 4, "wrapper after t2"); |
| 105 | + } |
| 106 | + |
| 107 | + /// @notice U:[WAT-4]: `exchangeRate` can not be manipulated |
| 108 | + function test_U_WAT_04_exchangeRate_can_not_be_manipulated() public { |
| 109 | + uint256 exchangeRateBefore = waToken.exchangeRate(); |
| 110 | + |
| 111 | + deal(token, address(this), TOKEN_AMOUNT); |
| 112 | + tokenTestSuite.approve(token, address(this), address(lendingPool), TOKEN_AMOUNT); |
| 113 | + lendingPool.deposit(token, TOKEN_AMOUNT, address(waToken), 0); |
| 114 | + |
| 115 | + assertEq(waToken.exchangeRate(), exchangeRateBefore, "exchangeRate changed"); |
| 116 | + } |
| 117 | + |
| 118 | + /// @notice U:[WAT-5]: `deposit` works correctly |
| 119 | + /// @dev Fuzzing time before deposit to see if wrapper handles interest properly |
| 120 | + /// @dev Final aToken balances are allowed to deviate by 1 from expected values due to rounding |
| 121 | + function test_U_WAT_05_deposit_works_correctly(uint256 timedelta) public { |
| 122 | + vm.assume(timedelta < 3 * 365 days); |
| 123 | + vm.warp(block.timestamp + timedelta); |
| 124 | + uint256 amount = _mintAToken(USER); |
| 125 | + |
| 126 | + uint256 assets = amount / 2; |
| 127 | + uint256 expectedShares = assets * WAD / waToken.exchangeRate(); |
| 128 | + |
| 129 | + tokenTestSuite.approve(aToken, USER, address(waToken), assets); |
| 130 | + |
| 131 | + vm.expectEmit(true, false, false, true); |
| 132 | + emit Deposit(USER, assets, expectedShares); |
| 133 | + |
| 134 | + vm.prank(USER); |
| 135 | + uint256 shares = waToken.deposit(assets); |
| 136 | + |
| 137 | + assertEq(shares, expectedShares); |
| 138 | + |
| 139 | + expectBalanceGe(aToken, USER, amount - assets - 1, ""); |
| 140 | + expectBalanceLe(aToken, USER, amount - assets + 1, ""); |
| 141 | + expectBalance(address(waToken), USER, shares); |
| 142 | + |
| 143 | + assertEq(waToken.totalSupply(), shares); |
| 144 | + expectBalanceGe(aToken, address(waToken), assets - 1, ""); |
| 145 | + expectBalanceLe(aToken, address(waToken), assets + 1, ""); |
| 146 | + } |
| 147 | + |
| 148 | + /// @notice U:[WAT-6]: `depositUnderlying` works correctly |
| 149 | + /// @dev Fuzzing time before deposit to see if wrapper handles interest properly |
| 150 | + /// @dev Final aToken balances are allowed to deviate by 1 from expected values due to rounding |
| 151 | + function test_U_WAT_06_depositUnderlying_works_correctly(uint256 timedelta) public { |
| 152 | + vm.assume(timedelta < 3 * 365 days); |
| 153 | + vm.warp(block.timestamp + timedelta); |
| 154 | + uint256 amount = _mintUnderlying(USER); |
| 155 | + |
| 156 | + uint256 assets = amount / 2; |
| 157 | + uint256 expectedShares = assets * WAD / waToken.exchangeRate(); |
| 158 | + |
| 159 | + tokenTestSuite.approve(token, USER, address(waToken), assets); |
| 160 | + |
| 161 | + vm.expectCall(address(lendingPool), abi.encodeCall(lendingPool.deposit, (token, assets, address(waToken), 0))); |
| 162 | + |
| 163 | + vm.expectEmit(true, false, false, true); |
| 164 | + emit Deposit(USER, assets, expectedShares); |
| 165 | + |
| 166 | + vm.prank(USER); |
| 167 | + uint256 shares = waToken.depositUnderlying(assets); |
| 168 | + |
| 169 | + assertEq(shares, expectedShares); |
| 170 | + |
| 171 | + expectBalance(token, USER, amount - assets); |
| 172 | + expectBalance(address(waToken), USER, shares); |
| 173 | + |
| 174 | + assertEq(waToken.totalSupply(), shares); |
| 175 | + expectBalance(token, address(waToken), 0); |
| 176 | + expectBalanceGe(aToken, address(waToken), assets - 1, ""); |
| 177 | + expectBalanceLe(aToken, address(waToken), assets + 1, ""); |
| 178 | + } |
| 179 | + |
| 180 | + /// @notice U:[WAT-7]: `withdraw` works correctly |
| 181 | + /// @dev Fuzzing time before deposit to see if wrapper handles interest properly |
| 182 | + /// @dev Final aToken balances are allowed to deviate by 1 from expected values due to rounding |
| 183 | + function test_U_WAT_07_withdraw_works_correctly(uint256 timedelta) public { |
| 184 | + vm.assume(timedelta < 3 * 365 days); |
| 185 | + uint256 amount = _mintWAToken(USER); |
| 186 | + vm.warp(block.timestamp + timedelta); |
| 187 | + |
| 188 | + uint256 shares = amount / 2; |
| 189 | + uint256 expectedAssets = shares * waToken.exchangeRate() / WAD; |
| 190 | + uint256 wrapperBalance = tokenTestSuite.balanceOf(aToken, address(waToken)); |
| 191 | + |
| 192 | + vm.expectEmit(true, false, false, true); |
| 193 | + emit Withdraw(USER, expectedAssets, shares); |
| 194 | + |
| 195 | + vm.prank(USER); |
| 196 | + uint256 assets = waToken.withdraw(shares); |
| 197 | + |
| 198 | + assertEq(assets, expectedAssets); |
| 199 | + |
| 200 | + expectBalanceGe(aToken, USER, assets - 1, ""); |
| 201 | + expectBalanceLe(aToken, USER, assets + 1, ""); |
| 202 | + expectBalance(address(waToken), USER, amount - shares); |
| 203 | + |
| 204 | + assertEq(waToken.totalSupply(), amount - shares); |
| 205 | + expectBalanceGe(aToken, address(waToken), wrapperBalance - assets - 1, ""); |
| 206 | + expectBalanceLe(aToken, address(waToken), wrapperBalance - assets + 1, ""); |
| 207 | + } |
| 208 | + |
| 209 | + /// @notice U:[WAT-8]: `withdrawUnderlying` works correctly |
| 210 | + /// @dev Fuzzing time before deposit to see if wrapper handles interest properly |
| 211 | + /// @dev Final aToken balances are allowed to deviate by 1 from expected values due to rounding |
| 212 | + function test_U_WAT_08_withdrawUnderlying_works_correctly(uint256 timedelta) public { |
| 213 | + vm.assume(timedelta < 3 * 365 days); |
| 214 | + uint256 amount = _mintWAToken(USER); |
| 215 | + vm.warp(block.timestamp + timedelta); |
| 216 | + |
| 217 | + uint256 shares = amount / 2; |
| 218 | + uint256 expectedAssets = shares * waToken.exchangeRate() / WAD; |
| 219 | + uint256 wrapperBalance = tokenTestSuite.balanceOf(aToken, address(waToken)); |
| 220 | + |
| 221 | + vm.expectEmit(true, false, false, true); |
| 222 | + emit Withdraw(USER, expectedAssets, shares); |
| 223 | + |
| 224 | + vm.expectCall(address(lendingPool), abi.encodeCall(lendingPool.withdraw, (token, expectedAssets, USER))); |
| 225 | + |
| 226 | + vm.prank(USER); |
| 227 | + uint256 assets = waToken.withdrawUnderlying(shares); |
| 228 | + |
| 229 | + assertEq(assets, expectedAssets); |
| 230 | + |
| 231 | + expectBalance(token, USER, assets); |
| 232 | + expectBalance(address(waToken), USER, amount - shares); |
| 233 | + |
| 234 | + assertEq(waToken.totalSupply(), amount - shares); |
| 235 | + expectBalance(token, address(waToken), 0); |
| 236 | + expectBalanceGe(aToken, address(waToken), wrapperBalance - assets - 1, ""); |
| 237 | + expectBalanceLe(aToken, address(waToken), wrapperBalance - assets + 1, ""); |
| 238 | + } |
| 239 | + |
| 240 | + /// @notice U:[WAT-9]: waToken resets lendingPool allowance if it falls too low |
| 241 | + function test_U_WAT_09_waToken_resets_lendingPool_allowance_if_it_falls_too_low() public { |
| 242 | + uint256 amount = _mintUnderlying(USER); |
| 243 | + tokenTestSuite.approve(token, USER, address(waToken), amount); |
| 244 | + |
| 245 | + // simulate the situation when lendingPool runs out of approval for underlying from waToken |
| 246 | + tokenTestSuite.approve(token, address(waToken), address(lendingPool), amount - 1); |
| 247 | + |
| 248 | + // waToken then should reset it back to max |
| 249 | + vm.expectCall( |
| 250 | + token, abi.encodeWithSignature("approve(address,uint256)", address(lendingPool), type(uint256).max) |
| 251 | + ); |
| 252 | + |
| 253 | + vm.prank(USER); |
| 254 | + waToken.depositUnderlying(amount); |
| 255 | + } |
| 256 | + |
| 257 | + /// @dev Mints token to user |
| 258 | + function _mintUnderlying(address user) internal returns (uint256 amount) { |
| 259 | + amount = TOKEN_AMOUNT; |
| 260 | + deal(token, user, amount); |
| 261 | + } |
| 262 | + |
| 263 | + /// @dev Mints aToken to user |
| 264 | + function _mintAToken(address user) internal returns (uint256 amount) { |
| 265 | + amount = _mintUnderlying(user); |
| 266 | + tokenTestSuite.approve(token, user, address(lendingPool), amount); |
| 267 | + vm.prank(user); |
| 268 | + lendingPool.deposit(token, amount, address(user), 0); |
| 269 | + } |
| 270 | + |
| 271 | + /// @dev Mints waToken to user |
| 272 | + function _mintWAToken(address user) internal returns (uint256 amount) { |
| 273 | + uint256 assets = _mintUnderlying(user); |
| 274 | + tokenTestSuite.approve(token, user, address(waToken), assets); |
| 275 | + vm.prank(user); |
| 276 | + amount = waToken.depositUnderlying(assets); |
| 277 | + } |
| 278 | +} |
0 commit comments