From 0126db59f880432d938fd04fac8541464401e96e Mon Sep 17 00:00:00 2001 From: Dmitry Lekhovitsky Date: Thu, 31 Aug 2023 15:01:57 +0300 Subject: [PATCH 1/2] feat: return `WrappedAToken` It is now placed within `helpers/`, contains minor fixes after ABDK audit, interface removed. --- .../aave/AaveV2_WrappedATokenAdapter.sol | 18 +- .../helpers/aave/AaveV2_WrappedAToken.sol | 137 +++++++++ .../unit/adapters/aave/AaveTestHelper.sol | 10 +- .../aave/AaveV2_WrappedATokenAdapter.t.sol | 26 +- .../aave/AaveV2_WrappedAToken.unit.t.sol | 277 ++++++++++++++++++ contracts/zappers/WATokenZapper.sol | 12 +- 6 files changed, 447 insertions(+), 33 deletions(-) create mode 100644 contracts/helpers/aave/AaveV2_WrappedAToken.sol create mode 100644 contracts/test/unit/helpers/aave/AaveV2_WrappedAToken.unit.t.sol diff --git a/contracts/adapters/aave/AaveV2_WrappedATokenAdapter.sol b/contracts/adapters/aave/AaveV2_WrappedATokenAdapter.sol index e41c5679..08f0c20f 100644 --- a/contracts/adapters/aave/AaveV2_WrappedATokenAdapter.sol +++ b/contracts/adapters/aave/AaveV2_WrappedATokenAdapter.sol @@ -8,8 +8,8 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {AbstractAdapter} from "../AbstractAdapter.sol"; import {AdapterType} from "@gearbox-protocol/sdk-gov/contracts/AdapterType.sol"; +import {WrappedAToken} from "../../helpers/aave/AaveV2_WrappedAToken.sol"; import {IAaveV2_WrappedATokenAdapter} from "../../interfaces/aave/IAaveV2_WrappedATokenAdapter.sol"; -import {IWrappedATokenV2} from "@gearbox-protocol/oracles-v3/contracts/interfaces/aave/IWrappedATokenV2.sol"; /// @title Aave V2 Wrapped aToken adapter /// @notice Implements logic allowing CAs to convert between waTokens, aTokens and underlying tokens @@ -38,10 +38,10 @@ contract AaveV2_WrappedATokenAdapter is AbstractAdapter, IAaveV2_WrappedATokenAd constructor(address _creditManager, address _waToken) AbstractAdapter(_creditManager, _waToken) { waTokenMask = _getMaskOrRevert(targetContract); // F: [AAV2W-1, AAV2W-2] - aToken = IWrappedATokenV2(targetContract).aToken(); // F: [AAV2W-2] + aToken = WrappedAToken(targetContract).aToken(); // F: [AAV2W-2] aTokenMask = _getMaskOrRevert(aToken); // F: [AAV2W-2] - underlying = IWrappedATokenV2(targetContract).underlying(); // F: [AAV2W-2] + underlying = WrappedAToken(targetContract).underlying(); // F: [AAV2W-2] tokenMask = _getMaskOrRevert(underlying); // F: [AAV2W-2] } @@ -127,11 +127,11 @@ contract AaveV2_WrappedATokenAdapter is AbstractAdapter, IAaveV2_WrappedATokenAd (tokensToEnable, tokensToDisable) = (waTokenMask, fromUnderlying ? tokenMask : aTokenMask); } - /// @dev Returns data for `IWrappedATokenV2`'s `deposit` or `depositUnderlying` call + /// @dev Returns data for `WrappedAToken`'s `deposit` or `depositUnderlying` call function _encodeDeposit(uint256 assets, bool fromUnderlying) internal pure returns (bytes memory callData) { callData = fromUnderlying - ? abi.encodeCall(IWrappedATokenV2.depositUnderlying, (assets)) - : abi.encodeCall(IWrappedATokenV2.deposit, (assets)); + ? abi.encodeCall(WrappedAToken.depositUnderlying, (assets)) + : abi.encodeCall(WrappedAToken.deposit, (assets)); } // ----------- // @@ -209,10 +209,10 @@ contract AaveV2_WrappedATokenAdapter is AbstractAdapter, IAaveV2_WrappedATokenAd (tokensToEnable, tokensToDisable) = (toUnderlying ? tokenMask : aTokenMask, waTokenMask); } - /// @dev Returns data for `IWrappedATokenV2`'s `withdraw` or `withdrawUnderlying` call + /// @dev Returns data for `WrappedAToken`'s `withdraw` or `withdrawUnderlying` call function _encodeWithdraw(uint256 shares, bool toUnderlying) internal pure returns (bytes memory callData) { callData = toUnderlying - ? abi.encodeCall(IWrappedATokenV2.withdrawUnderlying, (shares)) - : abi.encodeCall(IWrappedATokenV2.withdraw, (shares)); + ? abi.encodeCall(WrappedAToken.withdrawUnderlying, (shares)) + : abi.encodeCall(WrappedAToken.withdraw, (shares)); } } diff --git a/contracts/helpers/aave/AaveV2_WrappedAToken.sol b/contracts/helpers/aave/AaveV2_WrappedAToken.sol new file mode 100644 index 00000000..6498a267 --- /dev/null +++ b/contracts/helpers/aave/AaveV2_WrappedAToken.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2023. +pragma solidity ^0.8.17; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +import {WAD} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; +import {SanityCheckTrait} from "@gearbox-protocol/core-v3/contracts/traits/SanityCheckTrait.sol"; + +import {IAToken} from "../../integrations/aave/IAToken.sol"; +import {ILendingPool} from "../../integrations/aave/ILendingPool.sol"; + +interface IWrappedATokenEvents { + /// @notice Emitted on deposit + /// @param account Account that performed deposit + /// @param assets Amount of deposited aTokens + /// @param shares Amount of waTokens minted to account + event Deposit(address indexed account, uint256 assets, uint256 shares); + + /// @notice Emitted on withdrawal + /// @param account Account that performed withdrawal + /// @param assets Amount of withdrawn aTokens + /// @param shares Amount of waTokens burnt from account + event Withdraw(address indexed account, uint256 assets, uint256 shares); +} + +/// @title Wrapped aToken +/// @notice Non-rebasing wrapper of Aave V2 aToken +/// @dev Ignores any Aave incentives +contract WrappedAToken is ERC20, SanityCheckTrait, IWrappedATokenEvents { + using SafeERC20 for ERC20; + + /// @notice Underlying aToken + address public immutable aToken; + + /// @notice Underlying token + address public immutable underlying; + + /// @notice Aave lending pool + address public immutable lendingPool; + + /// @dev aToken's normalized income (aka interest accumulator) at the moment of waToken creation + uint256 private immutable _normalizedIncome; + + /// @dev waToken decimals + uint8 private immutable _decimals; + + /// @notice Constructor + /// @param _aToken Underlying aToken address + constructor(address _aToken) + ERC20( + address(_aToken) != address(0) ? string(abi.encodePacked("Wrapped ", ERC20(_aToken).name())) : "", + address(_aToken) != address(0) ? string(abi.encodePacked("w", ERC20(_aToken).symbol())) : "" + ) + nonZeroAddress(_aToken) // U:[WAT-1] + { + aToken = _aToken; // U:[WAT-2] + underlying = IAToken(aToken).UNDERLYING_ASSET_ADDRESS(); // U:[WAT-2] + lendingPool = address(IAToken(aToken).POOL()); // U:[WAT-2] + _normalizedIncome = ILendingPool(lendingPool).getReserveNormalizedIncome(underlying); + _decimals = IAToken(aToken).decimals(); // U:[WAT-2] + ERC20(underlying).approve(lendingPool, type(uint256).max); + } + + /// @notice waToken decimals, same as underlying and aToken + function decimals() public view override returns (uint8) { + return _decimals; + } + + /// @notice Returns amount of aTokens belonging to given account (increases as interest is accrued) + function balanceOfUnderlying(address account) external view returns (uint256) { + return balanceOf(account) * exchangeRate() / WAD; // U:[WAT-3] + } + + /// @notice Returns amount of aTokens per waToken, scaled by 1e18 + function exchangeRate() public view returns (uint256) { + return WAD * ILendingPool(lendingPool).getReserveNormalizedIncome(underlying) / _normalizedIncome; // U:[WAT-4] + } + + /// @notice Deposit given amount of aTokens (aToken must be approved before the call) + /// @param assets Amount of aTokens to deposit in exchange for waTokens + /// @return shares Amount of waTokens minted to the caller + function deposit(uint256 assets) external returns (uint256 shares) { + ERC20(aToken).transferFrom(msg.sender, address(this), assets); + shares = _deposit(assets); // U:[WAT-5] + } + + /// @notice Deposit given amount underlying tokens (underlying must be approved before the call) + /// @param assets Amount of underlying tokens to deposit in exchange for waTokens + /// @return shares Amount of waTokens minted to the caller + function depositUnderlying(uint256 assets) external returns (uint256 shares) { + ERC20(underlying).safeTransferFrom(msg.sender, address(this), assets); + _ensureAllowance(assets); + ILendingPool(lendingPool).deposit(underlying, assets, address(this), 0); // U:[WAT-6] + shares = _deposit(assets); // U:[WAT-6] + } + + /// @notice Withdraw given amount of waTokens for aTokens + /// @param shares Amount of waTokens to burn in exchange for aTokens + /// @return assets Amount of aTokens sent to the caller + function withdraw(uint256 shares) external returns (uint256 assets) { + assets = _withdraw(shares); // U:[WAT-7] + ERC20(aToken).transfer(msg.sender, assets); + } + + /// @notice Withdraw given amount of waTokens for underlying tokens + /// @param shares Amount of waTokens to burn in exchange for underlying tokens + /// @return assets Amount of underlying tokens sent to the caller + function withdrawUnderlying(uint256 shares) external returns (uint256 assets) { + assets = _withdraw(shares); // U:[WAT-8] + ILendingPool(lendingPool).withdraw(underlying, assets, msg.sender); // U:[WAT-8] + } + + /// @dev Internal implementation of deposit + function _deposit(uint256 assets) internal returns (uint256 shares) { + shares = assets * WAD / exchangeRate(); + _mint(msg.sender, shares); // U:[WAT-5,6] + emit Deposit(msg.sender, assets, shares); // U:[WAT-5,6] + } + + /// @dev Internal implementation of withdraw + function _withdraw(uint256 shares) internal returns (uint256 assets) { + assets = shares * exchangeRate() / WAD; + _burn(msg.sender, shares); // U:[WAT-7,8] + emit Withdraw(msg.sender, assets, shares); // U:[WAT-7,8] + } + + /// @dev Gives lending pool max approval for underlying if it falls below `amount` + function _ensureAllowance(uint256 amount) internal { + if (ERC20(underlying).allowance(address(this), lendingPool) < amount) { + ERC20(underlying).approve(lendingPool, type(uint256).max); // [WAT-9] + } + } +} diff --git a/contracts/test/unit/adapters/aave/AaveTestHelper.sol b/contracts/test/unit/adapters/aave/AaveTestHelper.sol index 29762803..698d6a06 100644 --- a/contracts/test/unit/adapters/aave/AaveTestHelper.sol +++ b/contracts/test/unit/adapters/aave/AaveTestHelper.sol @@ -13,7 +13,7 @@ import { WETH_EXCHANGE_AMOUNT } from "@gearbox-protocol/core-v3/contracts/test/lib/constants.sol"; -import {WrappedATokenV2} from "@gearbox-protocol/oracles-v3/contracts/tokens/aave/WrappedATokenV2.sol"; +import {WrappedAToken} from "../../../../helpers/aave/AaveV2_WrappedAToken.sol"; import {IAToken} from "../../../../integrations/aave/IAToken.sol"; import {Tokens} from "@gearbox-protocol/sdk-gov/contracts/Tokens.sol"; @@ -81,9 +81,9 @@ contract AaveTestHelper is AdapterTestHelper { } function _setupWrappers() internal { - waDai = address(new WrappedATokenV2(aDai)); - waUsdc = address(new WrappedATokenV2(aUsdc)); - waWeth = address(new WrappedATokenV2(aWeth)); + waDai = address(new WrappedAToken(aDai)); + waUsdc = address(new WrappedAToken(aUsdc)); + waWeth = address(new WrappedAToken(aWeth)); vm.label(waDai, "waDAI"); vm.label(waUsdc, "waUSDC"); vm.label(waWeth, "waWETH"); @@ -149,7 +149,7 @@ contract AaveTestHelper is AdapterTestHelper { tokenTestSuite.approve(underlying, USER, waToken, amount); vm.prank(USER); - balance = WrappedATokenV2(waToken).depositUnderlying(amount); + balance = WrappedAToken(waToken).depositUnderlying(amount); tokenTestSuite.approve(waToken, USER, address(creditManager), balance); vm.prank(USER); diff --git a/contracts/test/unit/adapters/aave/AaveV2_WrappedATokenAdapter.t.sol b/contracts/test/unit/adapters/aave/AaveV2_WrappedATokenAdapter.t.sol index b7ab1170..0bbc536a 100644 --- a/contracts/test/unit/adapters/aave/AaveV2_WrappedATokenAdapter.t.sol +++ b/contracts/test/unit/adapters/aave/AaveV2_WrappedATokenAdapter.t.sol @@ -7,7 +7,7 @@ import {WAD} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; import {USER, CONFIGURATOR} from "@gearbox-protocol/core-v3/contracts/test/lib/constants.sol"; import {AaveV2_WrappedATokenAdapter} from "../../../../adapters/aave/AaveV2_WrappedATokenAdapter.sol"; -import {IWrappedATokenV2} from "@gearbox-protocol/oracles-v3/contracts/interfaces/aave/IWrappedATokenV2.sol"; +import {WrappedAToken} from "../../../../helpers/aave/AaveV2_WrappedAToken.sol"; import {Tokens} from "@gearbox-protocol/sdk-gov/contracts/Tokens.sol"; import {AaveTestHelper} from "./AaveTestHelper.sol"; @@ -91,13 +91,13 @@ contract AaveV2_WrappedATokenAdapter_Test is AaveTestHelper { uint256 depositAmount = initialBalance / 2; bytes memory callData = fromUnderlying == 1 - ? abi.encodeCall(IWrappedATokenV2.depositUnderlying, (depositAmount)) - : abi.encodeCall(IWrappedATokenV2.deposit, (depositAmount)); + ? abi.encodeCall(WrappedAToken.depositUnderlying, (depositAmount)) + : abi.encodeCall(WrappedAToken.deposit, (depositAmount)); expectMulticallStackCalls(address(adapter), waUsdc, USER, callData, tokenIn, waUsdc, true); executeOneLineMulticall(creditAccount, address(adapter), callData); expectBalance(tokenIn, creditAccount, initialBalance - depositAmount); - expectBalance(waUsdc, creditAccount, depositAmount * WAD / IWrappedATokenV2(waUsdc).exchangeRate()); + expectBalance(waUsdc, creditAccount, depositAmount * WAD / WrappedAToken(waUsdc).exchangeRate()); expectAllowance(tokenIn, creditAccount, waUsdc, 1); @@ -125,8 +125,8 @@ contract AaveV2_WrappedATokenAdapter_Test is AaveTestHelper { if (fromUnderlying == 0) initialBalance = tokenTestSuite.balanceOf(aUsdc, creditAccount); bytes memory expectedCallData = fromUnderlying == 1 - ? abi.encodeCall(IWrappedATokenV2.depositUnderlying, (initialBalance - 1)) - : abi.encodeCall(IWrappedATokenV2.deposit, (initialBalance - 1)); + ? abi.encodeCall(WrappedAToken.depositUnderlying, (initialBalance - 1)) + : abi.encodeCall(WrappedAToken.deposit, (initialBalance - 1)); expectMulticallStackCalls(address(adapter), waUsdc, USER, expectedCallData, tokenIn, waUsdc, true); bytes memory callData = fromUnderlying == 1 @@ -135,7 +135,7 @@ contract AaveV2_WrappedATokenAdapter_Test is AaveTestHelper { executeOneLineMulticall(creditAccount, address(adapter), callData); expectBalance(tokenIn, creditAccount, 1); - expectBalance(waUsdc, creditAccount, (initialBalance - 1) * WAD / IWrappedATokenV2(waUsdc).exchangeRate()); + expectBalance(waUsdc, creditAccount, (initialBalance - 1) * WAD / WrappedAToken(waUsdc).exchangeRate()); expectAllowance(tokenIn, creditAccount, waUsdc, 1); @@ -160,13 +160,13 @@ contract AaveV2_WrappedATokenAdapter_Test is AaveTestHelper { uint256 withdrawAmount = initialBalance / 2; bytes memory callData = toUnderlying == 1 - ? abi.encodeCall(IWrappedATokenV2.withdrawUnderlying, (withdrawAmount)) - : abi.encodeCall(IWrappedATokenV2.withdraw, (withdrawAmount)); + ? abi.encodeCall(WrappedAToken.withdrawUnderlying, (withdrawAmount)) + : abi.encodeCall(WrappedAToken.withdraw, (withdrawAmount)); expectMulticallStackCalls(address(adapter), waUsdc, USER, callData, waUsdc, tokenOut, false); executeOneLineMulticall(creditAccount, address(adapter), callData); expectBalance(waUsdc, creditAccount, initialBalance - withdrawAmount); - expectBalance(tokenOut, creditAccount, withdrawAmount * IWrappedATokenV2(waUsdc).exchangeRate() / WAD); + expectBalance(tokenOut, creditAccount, withdrawAmount * WrappedAToken(waUsdc).exchangeRate() / WAD); expectTokenIsEnabled(creditAccount, waUsdc, true); expectTokenIsEnabled(creditAccount, tokenOut, true); @@ -188,8 +188,8 @@ contract AaveV2_WrappedATokenAdapter_Test is AaveTestHelper { vm.warp(block.timestamp + timedelta); bytes memory expectedCallData = toUnderlying == 1 - ? abi.encodeCall(IWrappedATokenV2.withdrawUnderlying, (initialBalance - 1)) - : abi.encodeCall(IWrappedATokenV2.withdraw, (initialBalance - 1)); + ? abi.encodeCall(WrappedAToken.withdrawUnderlying, (initialBalance - 1)) + : abi.encodeCall(WrappedAToken.withdraw, (initialBalance - 1)); expectMulticallStackCalls(address(adapter), waUsdc, USER, expectedCallData, waUsdc, tokenOut, false); bytes memory callData = toUnderlying == 1 @@ -198,7 +198,7 @@ contract AaveV2_WrappedATokenAdapter_Test is AaveTestHelper { executeOneLineMulticall(creditAccount, address(adapter), callData); expectBalance(waUsdc, creditAccount, 1); - expectBalance(tokenOut, creditAccount, (initialBalance - 1) * IWrappedATokenV2(waUsdc).exchangeRate() / WAD); + expectBalance(tokenOut, creditAccount, (initialBalance - 1) * WrappedAToken(waUsdc).exchangeRate() / WAD); expectTokenIsEnabled(creditAccount, waUsdc, false); expectTokenIsEnabled(creditAccount, tokenOut, true); diff --git a/contracts/test/unit/helpers/aave/AaveV2_WrappedAToken.unit.t.sol b/contracts/test/unit/helpers/aave/AaveV2_WrappedAToken.unit.t.sol new file mode 100644 index 00000000..7b542bdf --- /dev/null +++ b/contracts/test/unit/helpers/aave/AaveV2_WrappedAToken.unit.t.sol @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: UNLICENSED +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2023. +pragma solidity ^0.8.17; + +import {Test} from "forge-std/Test.sol"; + +import {WAD} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; +import {ZeroAddressException} from "@gearbox-protocol/core-v3/contracts/interfaces/IExceptions.sol"; + +import {IWrappedATokenEvents, WrappedAToken} from "../../../../helpers/aave/AaveV2_WrappedAToken.sol"; + +import {LendingPoolMock} from "../../../mocks/integrations/aave/LendingPoolMock.sol"; +import {ERC20Mock} from "@gearbox-protocol/core-v3/contracts/test/mocks/token/ERC20Mock.sol"; +import {FRIEND, USER} from "@gearbox-protocol/core-v3/contracts/test/lib/constants.sol"; +import {BalanceHelper} from "@gearbox-protocol/core-v3/contracts/test/helpers/BalanceHelper.sol"; +import {TokensTestSuite} from "@gearbox-protocol/core-v3/contracts/test/suites/TokensTestSuite.sol"; + +/// @title Wrapped aToken unit test +/// @notice U:[WAT]: Unit tests for Wrapped aToken +contract WrappedATokenUnitTest is Test, BalanceHelper, IWrappedATokenEvents { + WrappedAToken public waToken; + + LendingPoolMock lendingPool; + address token; + address aToken; + + uint256 constant TOKEN_AMOUNT = 1e10; + + function setUp() public { + tokenTestSuite = new TokensTestSuite(); + lendingPool = new LendingPoolMock(); + + token = address(new ERC20Mock("Test Token", "TEST", 6)); + aToken = lendingPool.addReserve(token, 0.02e27); // 2% + deal(token, aToken, 1e12); // add some liquidity + waToken = new WrappedAToken(aToken); + + vm.label(token, "TOKEN"); + vm.label(aToken, "aTOKEN"); + vm.label(address(waToken), "waTOKEN"); + vm.label(address(lendingPool), "LENDING_POOL"); + + vm.warp(block.timestamp + 365 days); + } + + /// @notice U:[WAT-1]: Constructor reverts on zero address + function test_U_WAT_01_constructor_reverts_on_zero_address() public { + vm.expectRevert(ZeroAddressException.selector); + new WrappedAToken(address(0)); + } + + /// @notice U:[WAT-2]: Constructor sets correct values + function test_U_WAT_02_constructor_sets_correct_values() public { + assertEq(waToken.aToken(), aToken, "Incorrect aUSDC address"); + assertEq(waToken.underlying(), token, "Incorrect USDC address"); + assertEq(waToken.lendingPool(), address(lendingPool), "Incorrect lending pool address"); + assertEq(waToken.name(), "Wrapped Aave interest bearing Test Token", "Incorrect name"); + assertEq(waToken.symbol(), "waTEST", "Incorrect symbol"); + assertEq(waToken.decimals(), 6, "Incorrect decimals"); + } + + /// @notice U:[WAT-3]: `balanceOfUnderlying` works correctly + /// @dev Fuzzing times before measuring balances + /// @dev Small deviations in expected and actual balances are allowed due to rounding errors + /// Generally, dust size grows with time and number of operations on the wrapper + /// Nevertheless, the test shows that wrapper stays solvent and doesn't lose deposited funds + function test_U_WAT_03_balanceOfUnderlying_works_correctly(uint256 timedelta1, uint256 timedelta2) public { + vm.assume(timedelta1 < 5 * 365 days && timedelta2 < 5 * 365 days); + uint256 balance1; + uint256 balance2; + + // mint equivalent amounts of aTokens and waTokens to first user and wait for some time + _mintAToken(USER); + _mintWAToken(USER); + vm.warp(block.timestamp + timedelta1); + + // balances must stay equivalent (up to some dust) + balance1 = waToken.balanceOfUnderlying(USER); + expectBalanceGe(aToken, USER, balance1, "user 1 after t1"); + expectBalanceLe(aToken, USER, balance1 + 2, "user 1 after t1"); + + // also, wrapper's total balance of aToken must be equal to user's balances of underlying + expectBalanceGe(aToken, address(waToken), balance1, "wrapper after t1"); + expectBalanceLe(aToken, address(waToken), balance1 + 2, "wrapper after t1"); + + // now mint equivalent amounts of aTokens and waTokens to second user and wait for more time + _mintAToken(FRIEND); + _mintWAToken(FRIEND); + vm.warp(block.timestamp + timedelta2); + + // balances must stay equivalent for both users + balance1 = waToken.balanceOfUnderlying(USER); + expectBalanceGe(aToken, USER, balance1, "user 1 after t2"); + expectBalanceLe(aToken, USER, balance1 + 2, "user 1 after t2"); + + balance2 = waToken.balanceOfUnderlying(FRIEND); + expectBalanceGe(aToken, FRIEND, balance2, "user 2 after t2"); + expectBalanceLe(aToken, FRIEND, balance2 + 2, "user 2 after t2"); + + // finally, wrapper's total balance of aToken must be equal to sum of users' balances of underlying + expectBalanceGe(aToken, address(waToken), balance1 + balance2 - 1, "wrapper after t2"); + expectBalanceLe(aToken, address(waToken), balance1 + balance2 + 4, "wrapper after t2"); + } + + /// @notice U:[WAT-4]: `exchangeRate` can not be manipulated + function test_U_WAT_04_exchangeRate_can_not_be_manipulated() public { + uint256 exchangeRateBefore = waToken.exchangeRate(); + + deal(token, address(this), TOKEN_AMOUNT); + tokenTestSuite.approve(token, address(this), address(lendingPool), TOKEN_AMOUNT); + lendingPool.deposit(token, TOKEN_AMOUNT, address(waToken), 0); + + assertEq(waToken.exchangeRate(), exchangeRateBefore, "exchangeRate changed"); + } + + /// @notice U:[WAT-5]: `deposit` works correctly + /// @dev Fuzzing time before deposit to see if wrapper handles interest properly + /// @dev Final aToken balances are allowed to deviate by 1 from expected values due to rounding + function test_U_WAT_05_deposit_works_correctly(uint256 timedelta) public { + vm.assume(timedelta < 3 * 365 days); + vm.warp(block.timestamp + timedelta); + uint256 amount = _mintAToken(USER); + + uint256 assets = amount / 2; + uint256 expectedShares = assets * WAD / waToken.exchangeRate(); + + tokenTestSuite.approve(aToken, USER, address(waToken), assets); + + vm.expectEmit(true, false, false, true); + emit Deposit(USER, assets, expectedShares); + + vm.prank(USER); + uint256 shares = waToken.deposit(assets); + + assertEq(shares, expectedShares); + + expectBalanceGe(aToken, USER, amount - assets - 1, ""); + expectBalanceLe(aToken, USER, amount - assets + 1, ""); + expectBalance(address(waToken), USER, shares); + + assertEq(waToken.totalSupply(), shares); + expectBalanceGe(aToken, address(waToken), assets - 1, ""); + expectBalanceLe(aToken, address(waToken), assets + 1, ""); + } + + /// @notice U:[WAT-6]: `depositUnderlying` works correctly + /// @dev Fuzzing time before deposit to see if wrapper handles interest properly + /// @dev Final aToken balances are allowed to deviate by 1 from expected values due to rounding + function test_U_WAT_06_depositUnderlying_works_correctly(uint256 timedelta) public { + vm.assume(timedelta < 3 * 365 days); + vm.warp(block.timestamp + timedelta); + uint256 amount = _mintUnderlying(USER); + + uint256 assets = amount / 2; + uint256 expectedShares = assets * WAD / waToken.exchangeRate(); + + tokenTestSuite.approve(token, USER, address(waToken), assets); + + vm.expectCall(address(lendingPool), abi.encodeCall(lendingPool.deposit, (token, assets, address(waToken), 0))); + + vm.expectEmit(true, false, false, true); + emit Deposit(USER, assets, expectedShares); + + vm.prank(USER); + uint256 shares = waToken.depositUnderlying(assets); + + assertEq(shares, expectedShares); + + expectBalance(token, USER, amount - assets); + expectBalance(address(waToken), USER, shares); + + assertEq(waToken.totalSupply(), shares); + expectBalance(token, address(waToken), 0); + expectBalanceGe(aToken, address(waToken), assets - 1, ""); + expectBalanceLe(aToken, address(waToken), assets + 1, ""); + } + + /// @notice U:[WAT-7]: `withdraw` works correctly + /// @dev Fuzzing time before deposit to see if wrapper handles interest properly + /// @dev Final aToken balances are allowed to deviate by 1 from expected values due to rounding + function test_U_WAT_07_withdraw_works_correctly(uint256 timedelta) public { + vm.assume(timedelta < 3 * 365 days); + uint256 amount = _mintWAToken(USER); + vm.warp(block.timestamp + timedelta); + + uint256 shares = amount / 2; + uint256 expectedAssets = shares * waToken.exchangeRate() / WAD; + uint256 wrapperBalance = tokenTestSuite.balanceOf(aToken, address(waToken)); + + vm.expectEmit(true, false, false, true); + emit Withdraw(USER, expectedAssets, shares); + + vm.prank(USER); + uint256 assets = waToken.withdraw(shares); + + assertEq(assets, expectedAssets); + + expectBalanceGe(aToken, USER, assets - 1, ""); + expectBalanceLe(aToken, USER, assets + 1, ""); + expectBalance(address(waToken), USER, amount - shares); + + assertEq(waToken.totalSupply(), amount - shares); + expectBalanceGe(aToken, address(waToken), wrapperBalance - assets - 1, ""); + expectBalanceLe(aToken, address(waToken), wrapperBalance - assets + 1, ""); + } + + /// @notice U:[WAT-8]: `withdrawUnderlying` works correctly + /// @dev Fuzzing time before deposit to see if wrapper handles interest properly + /// @dev Final aToken balances are allowed to deviate by 1 from expected values due to rounding + function test_U_WAT_08_withdrawUnderlying_works_correctly(uint256 timedelta) public { + vm.assume(timedelta < 3 * 365 days); + uint256 amount = _mintWAToken(USER); + vm.warp(block.timestamp + timedelta); + + uint256 shares = amount / 2; + uint256 expectedAssets = shares * waToken.exchangeRate() / WAD; + uint256 wrapperBalance = tokenTestSuite.balanceOf(aToken, address(waToken)); + + vm.expectEmit(true, false, false, true); + emit Withdraw(USER, expectedAssets, shares); + + vm.expectCall(address(lendingPool), abi.encodeCall(lendingPool.withdraw, (token, expectedAssets, USER))); + + vm.prank(USER); + uint256 assets = waToken.withdrawUnderlying(shares); + + assertEq(assets, expectedAssets); + + expectBalance(token, USER, assets); + expectBalance(address(waToken), USER, amount - shares); + + assertEq(waToken.totalSupply(), amount - shares); + expectBalance(token, address(waToken), 0); + expectBalanceGe(aToken, address(waToken), wrapperBalance - assets - 1, ""); + expectBalanceLe(aToken, address(waToken), wrapperBalance - assets + 1, ""); + } + + /// @notice U:[WAT-9]: waToken resets lendingPool allowance if it falls too low + function test_U_WAT_09_waToken_resets_lendingPool_allowance_if_it_falls_too_low() public { + uint256 amount = _mintUnderlying(USER); + tokenTestSuite.approve(token, USER, address(waToken), amount); + + // simulate the situation when lendingPool runs out of approval for underlying from waToken + tokenTestSuite.approve(token, address(waToken), address(lendingPool), amount - 1); + + // waToken then should reset it back to max + vm.expectCall( + token, abi.encodeWithSignature("approve(address,uint256)", address(lendingPool), type(uint256).max) + ); + + vm.prank(USER); + waToken.depositUnderlying(amount); + } + + /// @dev Mints token to user + function _mintUnderlying(address user) internal returns (uint256 amount) { + amount = TOKEN_AMOUNT; + deal(token, user, amount); + } + + /// @dev Mints aToken to user + function _mintAToken(address user) internal returns (uint256 amount) { + amount = _mintUnderlying(user); + tokenTestSuite.approve(token, user, address(lendingPool), amount); + vm.prank(user); + lendingPool.deposit(token, amount, address(user), 0); + } + + /// @dev Mints waToken to user + function _mintWAToken(address user) internal returns (uint256 amount) { + uint256 assets = _mintUnderlying(user); + tokenTestSuite.approve(token, user, address(waToken), assets); + vm.prank(user); + amount = waToken.depositUnderlying(assets); + } +} diff --git a/contracts/zappers/WATokenZapper.sol b/contracts/zappers/WATokenZapper.sol index 6579c36f..149b220a 100644 --- a/contracts/zappers/WATokenZapper.sol +++ b/contracts/zappers/WATokenZapper.sol @@ -7,7 +7,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {WAD} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol"; -import {IWrappedATokenV2} from "@gearbox-protocol/oracles-v3/contracts/interfaces/aave/IWrappedATokenV2.sol"; +import {WrappedAToken} from "../helpers/aave/AaveV2_WrappedAToken.sol"; import {WERC20ZapperBase} from "./WERC20ZapperBase.sol"; /// @title waToken zapper @@ -19,7 +19,7 @@ contract WATokenZapper is WERC20ZapperBase { /// @notice Constructor /// @param pool_ Pool to connect this zapper to constructor(address pool_) WERC20ZapperBase(pool_) { - _aToken = IWrappedATokenV2(wrappedToken).aToken(); + _aToken = WrappedAToken(wrappedToken).aToken(); IERC20(_aToken).approve(wrappedToken, type(uint256).max); } @@ -30,22 +30,22 @@ contract WATokenZapper is WERC20ZapperBase { /// @dev Wraps aToken function _wrap(uint256 amount) internal override returns (uint256 assets) { - return IWrappedATokenV2(wrappedToken).deposit(amount); + return WrappedAToken(wrappedToken).deposit(amount); } /// @dev Unwraps waToken function _unwrap(uint256 assets) internal override returns (uint256 amount) { - return IWrappedATokenV2(wrappedToken).withdraw(assets); + return WrappedAToken(wrappedToken).withdraw(assets); } /// @dev Returns amount of waToken one would receive for wrapping `amount` of aToken function _previewWrap(uint256 amount) internal view override returns (uint256 wrappedAmount) { - return amount * WAD / IWrappedATokenV2(wrappedToken).exchangeRate(); + return amount * WAD / WrappedAToken(wrappedToken).exchangeRate(); } /// @dev Returns amount of aToken one would receive for unwrapping `amount` of waToken function _previewUnwrap(uint256 amount) internal view override returns (uint256 unwrappedAmount) { - return amount * IWrappedATokenV2(wrappedToken).exchangeRate() / WAD; + return amount * WrappedAToken(wrappedToken).exchangeRate() / WAD; } /// @dev Pool has infinite waToken allowance so this step can be skipped From 5fb127b834f2ca5a6d90a39bc50092865752f99c Mon Sep 17 00:00:00 2001 From: Dmitry Lekhovitsky Date: Sun, 3 Sep 2023 13:12:12 +0300 Subject: [PATCH 2/2] fix: allowance is always set to max after deposit --- .../helpers/aave/AaveV2_WrappedAToken.sol | 12 ++++------ .../aave/AaveV2_WrappedAToken.unit.t.sol | 23 +++++-------------- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/contracts/helpers/aave/AaveV2_WrappedAToken.sol b/contracts/helpers/aave/AaveV2_WrappedAToken.sol index 6498a267..bbbcd8c5 100644 --- a/contracts/helpers/aave/AaveV2_WrappedAToken.sol +++ b/contracts/helpers/aave/AaveV2_WrappedAToken.sol @@ -62,7 +62,7 @@ contract WrappedAToken is ERC20, SanityCheckTrait, IWrappedATokenEvents { lendingPool = address(IAToken(aToken).POOL()); // U:[WAT-2] _normalizedIncome = ILendingPool(lendingPool).getReserveNormalizedIncome(underlying); _decimals = IAToken(aToken).decimals(); // U:[WAT-2] - ERC20(underlying).approve(lendingPool, type(uint256).max); + _resetAllowance(); // U:[WAT-2] } /// @notice waToken decimals, same as underlying and aToken @@ -93,8 +93,8 @@ contract WrappedAToken is ERC20, SanityCheckTrait, IWrappedATokenEvents { /// @return shares Amount of waTokens minted to the caller function depositUnderlying(uint256 assets) external returns (uint256 shares) { ERC20(underlying).safeTransferFrom(msg.sender, address(this), assets); - _ensureAllowance(assets); ILendingPool(lendingPool).deposit(underlying, assets, address(this), 0); // U:[WAT-6] + _resetAllowance(); // U:[WAT-6] shares = _deposit(assets); // U:[WAT-6] } @@ -128,10 +128,8 @@ contract WrappedAToken is ERC20, SanityCheckTrait, IWrappedATokenEvents { emit Withdraw(msg.sender, assets, shares); // U:[WAT-7,8] } - /// @dev Gives lending pool max approval for underlying if it falls below `amount` - function _ensureAllowance(uint256 amount) internal { - if (ERC20(underlying).allowance(address(this), lendingPool) < amount) { - ERC20(underlying).approve(lendingPool, type(uint256).max); // [WAT-9] - } + /// @dev Gives lending pool max approval for underlying + function _resetAllowance() internal { + ERC20(underlying).approve(lendingPool, type(uint256).max); } } diff --git a/contracts/test/unit/helpers/aave/AaveV2_WrappedAToken.unit.t.sol b/contracts/test/unit/helpers/aave/AaveV2_WrappedAToken.unit.t.sol index 7b542bdf..ff6f9fcb 100644 --- a/contracts/test/unit/helpers/aave/AaveV2_WrappedAToken.unit.t.sol +++ b/contracts/test/unit/helpers/aave/AaveV2_WrappedAToken.unit.t.sol @@ -58,6 +58,9 @@ contract WrappedATokenUnitTest is Test, BalanceHelper, IWrappedATokenEvents { assertEq(waToken.name(), "Wrapped Aave interest bearing Test Token", "Incorrect name"); assertEq(waToken.symbol(), "waTEST", "Incorrect symbol"); assertEq(waToken.decimals(), 6, "Incorrect decimals"); + assertEq( + ERC20Mock(token).allowance(address(waToken), address(lendingPool)), type(uint256).max, "Incorrect allowance" + ); } /// @notice U:[WAT-3]: `balanceOfUnderlying` works correctly @@ -174,6 +177,9 @@ contract WrappedATokenUnitTest is Test, BalanceHelper, IWrappedATokenEvents { expectBalance(token, address(waToken), 0); expectBalanceGe(aToken, address(waToken), assets - 1, ""); expectBalanceLe(aToken, address(waToken), assets + 1, ""); + assertEq( + ERC20Mock(token).allowance(address(waToken), address(lendingPool)), type(uint256).max, "Incorrect allowance" + ); } /// @notice U:[WAT-7]: `withdraw` works correctly @@ -236,23 +242,6 @@ contract WrappedATokenUnitTest is Test, BalanceHelper, IWrappedATokenEvents { expectBalanceLe(aToken, address(waToken), wrapperBalance - assets + 1, ""); } - /// @notice U:[WAT-9]: waToken resets lendingPool allowance if it falls too low - function test_U_WAT_09_waToken_resets_lendingPool_allowance_if_it_falls_too_low() public { - uint256 amount = _mintUnderlying(USER); - tokenTestSuite.approve(token, USER, address(waToken), amount); - - // simulate the situation when lendingPool runs out of approval for underlying from waToken - tokenTestSuite.approve(token, address(waToken), address(lendingPool), amount - 1); - - // waToken then should reset it back to max - vm.expectCall( - token, abi.encodeWithSignature("approve(address,uint256)", address(lendingPool), type(uint256).max) - ); - - vm.prank(USER); - waToken.depositUnderlying(amount); - } - /// @dev Mints token to user function _mintUnderlying(address user) internal returns (uint256 amount) { amount = TOKEN_AMOUNT;