Skip to content

Commit fd3aa4f

Browse files
committed
add re-done integration test for AaveYieldBackend
1 parent 3500962 commit fd3aa4f

File tree

1 file changed

+329
-0
lines changed

1 file changed

+329
-0
lines changed
Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
// SPDX-License-Identifier: AGPLv3
2+
pragma solidity ^0.8.23;
3+
4+
import { Test } from "forge-std/Test.sol";
5+
import { AaveYieldBackend } from "../../../../contracts/superfluid/AaveYieldBackend.sol";
6+
import { IERC20, ISuperfluid } from "../../../../contracts/interfaces/superfluid/ISuperfluid.sol";
7+
import { SuperToken } from "../../../../contracts/superfluid/SuperToken.sol";
8+
import { IPool } from "aave-v3/src/contracts/interfaces/IPool.sol";
9+
10+
/**
11+
* @title AaveYieldBackendIntegrationTest
12+
* @notice Integration tests for AaveYieldBackend with USDC on Base
13+
* @author Superfluid
14+
*/
15+
contract AaveYieldBackendIntegrationTest is Test {
16+
address internal constant ALICE = address(0x420);
17+
address internal constant ADMIN = address(0xAAA);
18+
19+
// Base network constants
20+
uint256 internal constant CHAIN_ID = 8453;
21+
string internal constant RPC_URL = "https://mainnet.base.org";
22+
23+
// Aave V3 Pool on Base (verified address)
24+
address internal constant AAVE_POOL = 0xA238Dd80C259a72e81d7e4664a9801593F98d1c5;
25+
26+
// Common tokens on Base
27+
address internal constant USDCX = 0xD04383398dD2426297da660F9CCA3d439AF9ce1b; // USDCx on Base
28+
address internal constant USDC = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; // USDC on Base
29+
address internal constant A_USDC = 0x4e65fE4DbA92790696d040ac24Aa414708F5c0AB; // aUSDC on Base
30+
address internal constant SURPLUS_RECEIVER = 0xac808840f02c47C05507f48165d2222FF28EF4e1; // dao.superfluid.eth
31+
32+
/// Rounding tolerance for Aave deposit/withdraw operations (in wei)
33+
uint256 internal constant AAVE_ROUNDING_TOLERANCE = 2;
34+
35+
SuperToken public superToken;
36+
AaveYieldBackend public aaveBackend;
37+
IERC20 public underlyingToken;
38+
IPool public aavePool;
39+
/// Initial excess underlying balance (underlyingBalance - normalizedTotalSupply)
40+
uint256 public initialExcessUnderlying;
41+
42+
/// @notice Set up the test environment by forking the chain and deploying AaveYieldBackend
43+
function setUp() public {
44+
vm.createSelectFork(RPC_URL);
45+
46+
// Verify chain id
47+
assertEq(block.chainid, CHAIN_ID, "Chainid mismatch");
48+
49+
// Get Aave Pool
50+
aavePool = IPool(AAVE_POOL);
51+
52+
// Set up USDC
53+
underlyingToken = IERC20(USDC);
54+
superToken = SuperToken(USDCX);
55+
56+
// Deploy AaveBackend
57+
aaveBackend = new AaveYieldBackend(IERC20(USDC), IPool(AAVE_POOL), SURPLUS_RECEIVER);
58+
59+
// upgrade SuperToken to new logic (including the yield backend related code)
60+
SuperToken newSuperTokenLogic = new SuperToken(ISuperfluid(superToken.getHost()), superToken.POOL_ADMIN_NFT());
61+
vm.startPrank(address(superToken.getHost()));
62+
superToken.updateCode(address(newSuperTokenLogic));
63+
vm.stopPrank();
64+
65+
// designate an admin for the SuperToken
66+
vm.startPrank(address(superToken.getHost()));
67+
superToken.changeAdmin(ADMIN);
68+
vm.stopPrank();
69+
70+
// provide ALICE with underlying and let her approve for upgrade
71+
deal(USDC, ALICE, type(uint128).max);
72+
vm.startPrank(ALICE);
73+
IERC20(USDC).approve(address(superToken), type(uint256).max);
74+
vm.stopPrank();
75+
76+
// Calculate and store initial excess underlying balance
77+
// The underlying balance may be greater than totalSupply due to rounding or initial state
78+
uint256 underlyingBalance = IERC20(USDC).balanceOf(address(superToken));
79+
(uint256 normalizedTotalSupply,) = superToken.toUnderlyingAmount(superToken.totalSupply());
80+
81+
// assert that the underlying balance is equal or greater than total supply (aka the SuperToken is solvent)
82+
assertGe(
83+
underlyingBalance,
84+
normalizedTotalSupply,
85+
"underlyingBalance should be >= normalizedTotalSupply"
86+
);
87+
initialExcessUnderlying = underlyingBalance - normalizedTotalSupply;
88+
}
89+
90+
function _enableYieldBackend() public {
91+
vm.startPrank(ADMIN);
92+
superToken.enableYieldBackend(aaveBackend);
93+
vm.stopPrank();
94+
}
95+
96+
/// @notice Verify invariants for the SuperToken yield backend system
97+
/// @param preserveInitialExcess If true, expect initial excess to be preserved (not withdrawn as surplus)
98+
/// @param numAaveOperations Number of Aave deposit/withdraw operations that have occurred
99+
function _verifyInvariants(bool preserveInitialExcess, uint256 numAaveOperations) internal view {
100+
// underlyingBalance + aTokenBalance >= superToken.supply() [+ initialExcessUnderlying if preserved]
101+
// Allow for Aave rounding tolerance (may lose up to 2 wei per operation)
102+
uint256 underlyingBalance = IERC20(USDC).balanceOf(address(superToken));
103+
uint256 aTokenBalance = IERC20(A_USDC).balanceOf(address(superToken));
104+
(uint256 superTokenNormalizedSupply,) = superToken.toUnderlyingAmount(superToken.totalSupply());
105+
106+
uint256 expectedMinTotalAssets = preserveInitialExcess
107+
? superTokenNormalizedSupply + initialExcessUnderlying
108+
: superTokenNormalizedSupply;
109+
uint256 totalAssets = underlyingBalance + aTokenBalance;
110+
111+
// Calculate total tolerance based on number of operations
112+
uint256 totalTolerance = numAaveOperations * AAVE_ROUNDING_TOLERANCE;
113+
114+
// Add tolerance to actual to avoid underflow, equivalent to: actual >= expected - tolerance
115+
assertGe(
116+
totalAssets + totalTolerance,
117+
expectedMinTotalAssets,
118+
preserveInitialExcess
119+
? "invariant failed: total assets should be >= supply + initial excess (accounting for rounding)"
120+
: "invariant failed: total assets should be >= supply (accounting for rounding)"
121+
);
122+
}
123+
124+
/// @notice Test enabling yield backend
125+
function testEnableYieldBackend() public {
126+
// Record state before enabling
127+
uint256 underlyingBalanceBefore = IERC20(USDC).balanceOf(address(superToken));
128+
(uint256 normalizedTotalSupplyBefore,) = superToken.toUnderlyingAmount(superToken.totalSupply());
129+
uint256 expectedUnderlyingBefore = normalizedTotalSupplyBefore + initialExcessUnderlying;
130+
131+
// Verify initial state
132+
assertGe(
133+
underlyingBalanceBefore,
134+
expectedUnderlyingBefore,
135+
"initial underlying should be >= supply + initial excess"
136+
);
137+
138+
_enableYieldBackend();
139+
140+
assertEq(address(superToken.getYieldBackend()), address(aaveBackend), "Yield backend mismatch");
141+
142+
// the SuperToken should now have a zero USDC balance (all deposited)
143+
assertEq(IERC20(USDC).balanceOf(address(superToken)), 0, "USDC balance should be zero");
144+
145+
uint256 aTokenBalanceAfter = IERC20(A_USDC).balanceOf(address(superToken));
146+
assertGe(
147+
aTokenBalanceAfter,
148+
underlyingBalanceBefore - AAVE_ROUNDING_TOLERANCE,
149+
"aUSDC balance should match previous underlying balance"
150+
);
151+
152+
// The aToken balance should approximately match what was deposited
153+
// Account for initial excess and potential rounding in Aave
154+
assertGe(
155+
aTokenBalanceAfter,
156+
expectedUnderlyingBefore - 1000, // Allow some rounding tolerance
157+
"aToken balance should approximately match deposited amount"
158+
);
159+
160+
// 1 operation: enable deposits all existing underlying
161+
_verifyInvariants(true, 1);
162+
}
163+
164+
/// @notice Test disabling yield backend
165+
function testDisableYieldBackend() public {
166+
// verify: underlying >= totalSupply (with initial excess accounted for)
167+
uint256 underlyingBalanceBefore = IERC20(USDC).balanceOf(address(superToken));
168+
uint256 superTokenBalanceBefore = superToken.totalSupply();
169+
(uint256 normalizedTotalSupply,) = superToken.toUnderlyingAmount(superTokenBalanceBefore);
170+
uint256 expectedUnderlying = normalizedTotalSupply + initialExcessUnderlying;
171+
assertGe(
172+
underlyingBalanceBefore,
173+
expectedUnderlying,
174+
"precondition failed: underlyingBalanceBefore should be >= supply + initial excess"
175+
);
176+
177+
_enableYieldBackend();
178+
179+
vm.startPrank(ADMIN);
180+
superToken.disableYieldBackend();
181+
vm.stopPrank();
182+
assertEq(address(superToken.getYieldBackend()), address(0), "Yield backend mismatch");
183+
184+
// the SuperToken should now have a non-zero USDC balance and a zero aUSDC balance
185+
uint256 underlyingBalanceAfter = IERC20(USDC).balanceOf(address(superToken));
186+
assertGt(underlyingBalanceAfter, 0, "USDC balance should be non-zero");
187+
assertEq(IERC20(A_USDC).balanceOf(address(superToken)), 0, "aUSDC balance should be zero");
188+
189+
// After disabling, underlying balance should be at least the amount we had in aTokens + initial excess
190+
// (the aTokens were converted back to underlying)
191+
// Allow for Aave rounding tolerance (may lose up to 2 wei)
192+
// Add tolerance to actual to avoid underflow
193+
assertGe(
194+
underlyingBalanceAfter + AAVE_ROUNDING_TOLERANCE,
195+
expectedUnderlying,
196+
"underlying balance after disable should be >= original underlying + initial excess"
197+
);
198+
199+
// 2 operations: enable deposits + disable withdraws
200+
_verifyInvariants(true, 2);
201+
}
202+
203+
/// @notice Test upgrade and downgrade with fuzzed amount
204+
function testUpgradeDowngrade(uint256 amount) public {
205+
// Bound amount to reasonable range
206+
// USDC has 6 decimals, SuperToken has 18 decimals
207+
// Minimum: 1 USDC (1e6) = 1e18 SuperToken units
208+
// Maximum: 1M USDC (1e6 * 1e6) = 1e24 SuperToken units
209+
amount = bound(amount, 1e18, 1_000_000 * 1e18);
210+
211+
_enableYieldBackend();
212+
213+
vm.startPrank(ALICE);
214+
superToken.upgrade(amount);
215+
vm.stopPrank();
216+
217+
// Downgrade
218+
vm.startPrank(ALICE);
219+
// Note: upgrade may have down-rounded the amount, but doesn't tell us (via return value).
220+
// In that case a consecutive downgrade (of the un-adjusted amount) might revert.
221+
// For fuzzing, we downgrade the actual balance ALICE received
222+
uint256 aliceBalance = superToken.balanceOf(ALICE);
223+
superToken.downgrade(aliceBalance);
224+
vm.stopPrank();
225+
226+
// 3 operations: enable deposits + upgrade deposits + downgrade withdraws
227+
_verifyInvariants(true, 3);
228+
}
229+
230+
/// @notice Test withdrawing surplus due to excess underlying balance
231+
function testWithdrawSurplusFromYieldBackendExcessUnderlying() public {
232+
_enableYieldBackend();
233+
234+
// Upgrade tokens to create supply
235+
uint256 upgradeAmount = 1000 * 1e18;
236+
vm.startPrank(ALICE);
237+
superToken.upgrade(upgradeAmount);
238+
vm.stopPrank();
239+
240+
// Manually add excess underlying to SuperToken to simulate surplus
241+
// This could happen if someone accidentally sends tokens to the SuperToken
242+
uint256 surplusAmount = 100 * 1e6; // 100 USDC
243+
deal(USDC, address(superToken), surplusAmount);
244+
245+
uint256 receiverBalanceBefore = IERC20(USDC).balanceOf(SURPLUS_RECEIVER);
246+
uint256 underlyingBalanceBefore = IERC20(USDC).balanceOf(address(superToken));
247+
uint256 aTokenBalanceBefore = IERC20(A_USDC).balanceOf(address(superToken));
248+
249+
// Verify there is excess underlying
250+
(uint256 normalizedTotalSupply,) = superToken.toUnderlyingAmount(superToken.totalSupply());
251+
uint256 totalAssetsBefore = underlyingBalanceBefore + aTokenBalanceBefore;
252+
assertGt(
253+
totalAssetsBefore,
254+
normalizedTotalSupply + 100, // withdrawSurplus uses -100 margin
255+
"Precondition: excess underlying should exist"
256+
);
257+
258+
vm.startPrank(ADMIN);
259+
superToken.withdrawSurplusFromYieldBackend();
260+
vm.stopPrank();
261+
262+
uint256 receiverBalanceAfter = IERC20(USDC).balanceOf(SURPLUS_RECEIVER);
263+
264+
// Surplus should be withdrawn to receiver
265+
uint256 surplusWithdrawn = receiverBalanceAfter - receiverBalanceBefore;
266+
assertGt(surplusWithdrawn, 0, "Surplus should be withdrawn to receiver");
267+
// The surplus withdrawn should be approximately the excess (minus 100 wei margin)
268+
assertGe(
269+
surplusWithdrawn,
270+
surplusAmount - 200,
271+
"Surplus withdrawn should be approximately the excess"
272+
);
273+
274+
// After withdrawing surplus, initial excess is also withdrawn, so don't expect it to be preserved
275+
// 3 operations: enable deposits + upgrade deposits + withdraw surplus
276+
_verifyInvariants(false, 3);
277+
}
278+
279+
/// @notice Test withdrawing surplus generated by yield protocol (fast forward time)
280+
function testWithdrawSurplusFromYieldBackendYieldAccrued(uint256 timeForward) public {
281+
// Bound time forward between 1 hour and 365 days
282+
timeForward = bound(timeForward, 1 hours, 365 days);
283+
284+
_enableYieldBackend();
285+
286+
// Record initial state before yield accrual
287+
(uint256 normalizedTotalSupplyInitial,) = superToken.toUnderlyingAmount(superToken.totalSupply());
288+
289+
// Fast forward time to accrue yield in Aave
290+
vm.warp(block.timestamp + timeForward);
291+
292+
// Calculate total supply after time forward (should be unchanged)
293+
(uint256 normalizedTotalSupply,) =
294+
superToken.toUnderlyingAmount(superToken.totalSupply());
295+
assertEq(
296+
normalizedTotalSupply,
297+
normalizedTotalSupplyInitial,
298+
"Total supply should not change from time forward"
299+
);
300+
301+
uint256 receiverBalanceBefore = IERC20(USDC).balanceOf(SURPLUS_RECEIVER);
302+
uint256 underlyingBalanceBefore = IERC20(USDC).balanceOf(address(superToken));
303+
uint256 aTokenBalanceBefore = IERC20(A_USDC).balanceOf(address(superToken));
304+
305+
// Total assets should be greater than supply due to yield accrual
306+
// Note: Aave yield accrues by increasing the underlying value of aTokens over time
307+
uint256 totalAssetsBefore = underlyingBalanceBefore + aTokenBalanceBefore;
308+
309+
// Check if there's actually surplus to withdraw (after the 100 wei margin used in withdrawSurplus)
310+
bool hasSurplus = totalAssetsBefore > normalizedTotalSupply + 100;
311+
assertTrue(hasSurplus, "no surplus, may need to review the lower bound for timeForward");
312+
313+
vm.startPrank(ADMIN);
314+
superToken.withdrawSurplusFromYieldBackend();
315+
vm.stopPrank();
316+
317+
uint256 receiverBalanceAfter = IERC20(USDC).balanceOf(SURPLUS_RECEIVER);
318+
uint256 aTokenBalanceAfter = IERC20(A_USDC).balanceOf(address(superToken));
319+
320+
// Surplus should be withdrawn to receiver
321+
assertGt(receiverBalanceAfter, receiverBalanceBefore, "Surplus should be withdrawn to receiver");
322+
assertLt(aTokenBalanceAfter, aTokenBalanceBefore, "aToken balance should decrease");
323+
324+
// After withdrawing surplus, initial excess is also withdrawn, so don't expect it to be preserved
325+
// 2 operations: enable deposits + withdraw surplus
326+
_verifyInvariants(false, 2);
327+
}
328+
}
329+

0 commit comments

Comments
 (0)