Skip to content

Commit be4031f

Browse files
committed
Merge remote-tracking branch 'origin/main' into pricefeed-optimisation
2 parents 225e695 + 72d4aaf commit be4031f

File tree

6 files changed

+408
-128
lines changed

6 files changed

+408
-128
lines changed

contracts/interfaces/aave/IWrappedAToken.sol renamed to contracts/interfaces/aave/IWrappedATokenV2.sol

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,9 @@
33
// (c) Gearbox Foundation, 2023.
44
pragma solidity ^0.8.17;
55

6-
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
76
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
87

9-
import {IAToken} from "./IAToken.sol";
10-
import {ILendingPool} from "./ILendingPool.sol";
11-
12-
/// @title Wrapped aToken interface
13-
interface IWrappedAToken is IERC20Metadata {
8+
interface IWrappedATokenV2Events {
149
/// @notice Emitted on deposit
1510
/// @param account Account that performed deposit
1611
/// @param assets Amount of deposited aTokens
@@ -22,15 +17,21 @@ interface IWrappedAToken is IERC20Metadata {
2217
/// @param assets Amount of withdrawn aTokens
2318
/// @param shares Amount of waTokens burnt from account
2419
event Withdraw(address indexed account, uint256 assets, uint256 shares);
20+
}
21+
22+
/// @title Wrapped aToken V2 interface
23+
interface IWrappedATokenV2 is IERC20Metadata, IWrappedATokenV2Events {
24+
/// @notice waToken decimals, same as underlying and aToken
25+
function decimals() external view override returns (uint8);
2526

2627
/// @notice Underlying aToken
27-
function aToken() external view returns (IAToken);
28+
function aToken() external view returns (address);
2829

2930
/// @notice Underlying token
30-
function underlying() external view returns (IERC20);
31+
function underlying() external view returns (address);
3132

3233
/// @notice Aave lending pool
33-
function lendingPool() external view returns (ILendingPool);
34+
function lendingPool() external view returns (address);
3435

3536
/// @notice Returns amount of aTokens belonging to given account (increases as interest is accrued)
3637
function balanceOfUnderlying(address account) external view returns (uint256);

contracts/oracles/aave/WrappedAaveV2PriceFeed.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ pragma solidity ^0.8.17;
55

66
import {WAD} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol";
77
import {PriceFeedType} from "@gearbox-protocol/sdk/contracts/PriceFeedType.sol";
8-
import {IWrappedAToken} from "../../interfaces/aave/IWrappedAToken.sol";
8+
import {IWrappedATokenV2} from "../../interfaces/aave/IWrappedATokenV2.sol";
99
import {SingleAssetLPPriceFeed} from "../SingleAssetLPPriceFeed.sol";
1010

1111
/// @title Aave V2 waToken price feed
@@ -21,7 +21,7 @@ contract WrappedAaveV2PriceFeed is SingleAssetLPPriceFeed {
2121
}
2222

2323
function getLPExchangeRate() public view override returns (uint256) {
24-
return IWrappedAToken(lpToken).exchangeRate();
24+
return IWrappedATokenV2(lpToken).exchangeRate();
2525
}
2626

2727
function getScale() public pure override returns (uint256) {

contracts/test/unit/AavePriceFeed.unit.t.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {PriceFeedMock} from "@gearbox-protocol/core-v3/contracts/test/mocks/orac
1111

1212
import "@gearbox-protocol/core-v3/contracts/interfaces/IExceptions.sol";
1313

14-
import {WrappedAToken} from "../../tokens/aave/WrappedAToken.sol";
14+
import {WrappedATokenV2} from "../../tokens/aave/WrappedATokenV2.sol";
1515

1616
import {WrappedAaveV2PriceFeed} from "../../oracles/aave/WrappedAaveV2PriceFeed.sol";
1717

@@ -28,7 +28,7 @@ contract WrappedAaveV2PriceFeedTest is Test {
2828

2929
// address dai;
3030
// ATokenMock aDai;
31-
// WrappedAToken waDai;
31+
// WrappedATokenV2 waDai;
3232
// LendingPoolMock lendingPool;
3333

3434
// PriceFeedMock daiPriceFeed;
@@ -46,7 +46,7 @@ contract WrappedAaveV2PriceFeedTest is Test {
4646
// // set yearly interest equal to the range width
4747
// aDai = ATokenMock(lendingPool.addReserve(dai, RANGE_WIDTH * RAY / PERCENTAGE_FACTOR));
4848
// tokensTestSuite.mint(Tokens.DAI, address(aDai), 1_000_000e18);
49-
// waDai = new WrappedAToken(aDai);
49+
// waDai = new WrappedATokenV2(aDai);
5050

5151
// daiPriceFeed = new PriceFeedMock(DAI_PRICE, 8);
5252
// daiPriceFeed.setParams(11, 1111, 1112, 11);
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
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

Comments
 (0)