Skip to content

Commit d19348c

Browse files
committed
added method for withdrawing surplus
1 parent 1d3e9be commit d19348c

File tree

8 files changed

+104
-89
lines changed

8 files changed

+104
-89
lines changed

packages/ethereum-contracts/contracts/gov/SuperfluidGovernanceBase.sol

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,6 @@ abstract contract SuperfluidGovernanceBase is ISuperfluidGovernance
141141
host.changeSuperTokenAdmin(token, newAdmin);
142142
}
143143

144-
function setSuperTokenYieldBackend(ISuperfluid host, ISuperToken token, address yieldBackend)
145-
external
146-
onlyAuthorized(host)
147-
{
148-
token.setYieldBackend(yieldBackend);
149-
}
150-
151144
function batchChangeSuperTokenAdmin(ISuperfluid host, ISuperToken[] calldata token, address[] calldata newAdmins)
152145
external
153146
onlyAuthorized(host)

packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,6 @@ interface ISuperToken is ISuperfluidToken, IERC20Metadata, IERC777, IERC20Permit
7171
*/
7272
function getAdmin() external view returns (address admin);
7373

74-
/**
75-
* @notice Sets the yield backend for the SuperToken
76-
* @dev Only the admin can call this function
77-
* @param yieldBackend Address of the yield backend contract, or address(0) to disable the yield backend
78-
*/
79-
function setYieldBackend(address yieldBackend) external;
80-
8174
function getYieldBackend() external view returns (address yieldBackend);
8275

8376
/**************************************************************************

packages/ethereum-contracts/contracts/interfaces/superfluid/IYieldBackend.sol

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,19 @@ pragma solidity ^0.8.23;
1111
* one -> no means withdraw not in the context of a downgrade.
1212
*/
1313
interface IYieldBackend {
14-
function init() external;
15-
function deinit() external;
14+
/// Invoked by `SuperToken.enableYieldBackend()` as delegatecall.
15+
/// Sets up the SuperToken as needed, e.g. by giving required approvals.
16+
function enable() external;
17+
18+
/// Invoked by `SuperToken.disableYieldBackend()` as delegatecall.
19+
/// Restores the prior state, e.g. by revoking given approvals
20+
function disable() external;
1621

1722
function deposit(uint256 amount) external;
1823
function depositMax() external;
1924
function withdraw(uint256 amount) external;
2025
function withdrawMax() external;
26+
27+
/// tranfers the deposited asset exceeding the required underlying to the preset treasury account
28+
function withdrawSurplus(uint256 totalSupply) external;
2129
}

packages/ethereum-contracts/contracts/superfluid/AaveYieldBackend.sol

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
pragma solidity ^0.8.23;
33

44
import { IYieldBackend } from "../interfaces/superfluid/IYieldBackend.sol";
5+
import { IERC20, ISuperToken } from "../interfaces/superfluid/ISuperfluid.sol";
56
import { IPool } from "aave-v3/interfaces/IPool.sol";
6-
import { IERC20 } from "../interfaces/superfluid/ISuperfluid.sol";
77

88

99
/**
@@ -17,6 +17,8 @@ contract AaveYieldBackend is IYieldBackend {
1717
IERC20 public immutable ASSET_TOKEN;
1818
IPool public immutable AAVE_POOL;
1919
IERC20 public immutable A_TOKEN;
20+
// TODO: make an immutable
21+
address constant SURPLUS_RECEIVER = 0xac808840f02c47C05507f48165d2222FF28EF4e1; // dao.superfluid.eth
2022

2123
// THIS CONTRACT CANNOT HAVE STATE VARIABLES!
2224
// IF STATE IS NEEDED, USE NAMESPACED STORAGE LAYOUT (EIP-7201)
@@ -33,12 +35,12 @@ contract AaveYieldBackend is IYieldBackend {
3335
A_TOKEN = IERC20(aavePool.getReserveAToken(address(assetToken)));
3436
}
3537

36-
function init() external {
38+
function enable() external {
3739
// approve Aave pool to fetch asset
3840
ASSET_TOKEN.approve(address(AAVE_POOL), type(uint256).max);
3941
}
4042

41-
function deinit() external {
43+
function disable() external {
4244
// Revoke approval
4345
ASSET_TOKEN.approve(address(AAVE_POOL), 0);
4446
}
@@ -66,4 +68,12 @@ contract AaveYieldBackend is IYieldBackend {
6668
// We can delegate the max calculation to the Aave pool by setting amount to type(uint256).max
6769
AAVE_POOL.withdraw(address(ASSET_TOKEN), type(uint256).max, address(this));
6870
}
71+
72+
function withdrawSurplus(uint256 totalSupply) external {
73+
// totalSupply is always 18 decimals while assetToken and aToken may not
74+
(uint256 normalizedTotalSupply,) = ISuperToken(address(this)).toUnderlyingAmount(totalSupply);
75+
// decrement by 1 in order to offset Aave's rounding up
76+
uint256 surplusAmount = A_TOKEN.balanceOf(address(this)) - normalizedTotalSupply - 1;
77+
AAVE_POOL.withdraw(address(ASSET_TOKEN), surplusAmount, SURPLUS_RECEIVER);
78+
}
6979
}

packages/ethereum-contracts/contracts/superfluid/SuperToken.sol

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -198,20 +198,11 @@ contract SuperToken is
198198
}
199199
}
200200

201-
function setYieldBackend(address newYieldBackend) external onlyAdmin {
202-
if (address(_yieldBackend) != address(0)) {
203-
_disableYieldBackend();
204-
}
205-
if (address(newYieldBackend) != address(0)) {
206-
_enableYieldBackend(IYieldBackend(newYieldBackend));
207-
}
208-
}
209-
210-
function _enableYieldBackend(IYieldBackend newYieldBackend) internal {
201+
function enableYieldBackend(IYieldBackend newYieldBackend) external onlyAdmin {
211202
require(address(_yieldBackend) == address(0), "yield backend already set");
212203
_yieldBackend = newYieldBackend;
213204
(bool success, ) = address(_yieldBackend).delegatecall(
214-
abi.encodeCall(IYieldBackend.initSuperToken, ())
205+
abi.encodeCall(IYieldBackend.enable, ())
215206
);
216207
require(success, "delegatecall failed");
217208
(success, ) = address(_yieldBackend).delegatecall(
@@ -222,13 +213,14 @@ contract SuperToken is
222213
}
223214

224215
// withdraws everything and removes allowances
225-
function _disableYieldBackend() internal {
216+
function disableYieldBackend() external onlyAdmin {
217+
require(address(_yieldBackend) != address(0), "yield backend not set");
226218
(bool success, ) = address(_yieldBackend).delegatecall(
227219
abi.encodeCall(IYieldBackend.withdrawMax, ())
228220
);
229221
require(success, "delegatecall failed");
230222
(success, ) = address(_yieldBackend).delegatecall(
231-
abi.encodeCall(IYieldBackend.deinitSuperToken, ())
223+
abi.encodeCall(IYieldBackend.disable, ())
232224
);
233225
// TODO: should this be allowed to fail?
234226
require(success, "delegatecall failed");
@@ -240,6 +232,14 @@ contract SuperToken is
240232
return address(_yieldBackend);
241233
}
242234

235+
function withdrawSurplusFromYieldBackend() external onlyAdmin {
236+
require(address(_yieldBackend) != address(0), "yield backend not set");
237+
(bool success, ) = address(_yieldBackend).delegatecall(
238+
abi.encodeCall(IYieldBackend.withdrawSurplus, (_totalSupply))
239+
);
240+
require(success, "delegatecall failed");
241+
}
242+
243243
/**************************************************************************
244244
* ERC20 Token Info
245245
*************************************************************************/

packages/ethereum-contracts/contracts/superfluid/Superfluid.sol

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
ISuperTokenFactory,
2020
IAccessControl
2121
} from "../interfaces/superfluid/ISuperfluid.sol";
22-
import { IYieldBackend } from "../interfaces/superfluid/IYieldBackend.sol";
2322
import { GeneralDistributionAgreementV1 } from "../agreements/gdav1/GeneralDistributionAgreementV1.sol";
2423
import { SuperfluidUpgradeableBeacon } from "../upgradability/SuperfluidUpgradeableBeacon.sol";
2524
import { CallUtils } from "../libs/CallUtils.sol";
@@ -333,10 +332,6 @@ contract Superfluid is
333332
token.changeAdmin(newAdmin);
334333
}
335334

336-
function setSuperTokenYieldBackend(ISuperToken token, address yieldBackend) external onlyGovernance {
337-
token.setYieldBackend(yieldBackend);
338-
}
339-
340335
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
341336
// Superfluid Upgradeable Beacon
342337
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

packages/ethereum-contracts/test/foundry/superfluid/SuperTokenYield.t.sol

Lines changed: 68 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { IPool } from "aave-v3/interfaces/IPool.sol";
1515
*/
1616
contract SuperTokenYieldForkTest is Test {
1717
address constant ALICE = address(0x420);
18+
address constant ADMIN = address(0xAAA);
1819

1920
// Base network constants
2021
uint256 internal constant CHAIN_ID = 8453;
@@ -37,11 +38,6 @@ contract SuperTokenYieldForkTest is Test {
3738
/// @notice Aave V3 Pool contract
3839
IPool public aavePool;
3940

40-
/// @notice Admin address (this contract)
41-
address public admin;
42-
/// @notice Test user address
43-
address public user;
44-
4541
/// @notice Set up the test environment by forking Base and deploying AaveYieldBackend
4642
function setUp() public {
4743
// Fork Base using public RPC
@@ -50,10 +46,6 @@ contract SuperTokenYieldForkTest is Test {
5046
// Verify we're on Base
5147
assertEq(block.chainid, CHAIN_ID, "Chainid mismatch");
5248

53-
// Initialize test accounts
54-
admin = address(this);
55-
user = address(0x1234);
56-
5749
// Get Aave Pool
5850
aavePool = IPool(AAVE_POOL);
5951

@@ -77,14 +69,33 @@ contract SuperTokenYieldForkTest is Test {
7769
superToken.updateCode(address(newSuperTokenLogic));
7870
vm.stopPrank();
7971

72+
// designate an admin for the SuperToken
73+
vm.startPrank(address(superToken.getHost()));
74+
superToken.changeAdmin(ADMIN);
75+
vm.stopPrank();
76+
77+
// provide ALICE with underlying and let her approve for upgrade
78+
deal(USDC, ALICE, type(uint128).max);
79+
vm.startPrank(ALICE);
80+
IERC20(USDC).approve(address(superToken), type(uint256).max);
81+
8082
console.log("aaveBackend address", address(aaveBackend));
8183
}
8284

8385
function _enableYieldBackend() public {
84-
vm.startPrank(address(superToken.getHost()));
85-
superToken.setYieldBackend(address(aaveBackend));
86+
vm.startPrank(ADMIN);
87+
superToken.enableYieldBackend(aaveBackend);
8688
vm.stopPrank();
8789
}
90+
91+
function _verifyInvariants() internal view {
92+
// underlyingBalance + aTokenBalance >= superToken.supply()
93+
uint256 underlyingBalance = IERC20(USDC).balanceOf(address(superToken));
94+
uint256 aTokenBalance = IERC20(aUSDC).balanceOf(address(superToken));
95+
(uint256 superTokenNormalizedSupply,) = superToken.toUnderlyingAmount(superToken.totalSupply());
96+
97+
assertGe(underlyingBalance + aTokenBalance, superTokenNormalizedSupply, "invariant failed: underlyingBalance + aTokenBalance insufficient");
98+
}
8899

89100
/// @notice Test that we're forking the correct Base network
90101
function testForkBaseNetwork() public view {
@@ -116,37 +127,30 @@ contract SuperTokenYieldForkTest is Test {
116127
console.log("aUSDC balance of SuperToken", IERC20(aUSDC).balanceOf(address(superToken)));
117128
// TODO: We'd want asset balance to equal aToken balance. But that's not exactly the case.
118129
// what else shall be require?
130+
_verifyInvariants();
119131
}
120132

121133
function testDisableYieldBackend() public {
122-
// store underlying balance before enabling yield backend
123-
uint256 underlyingBalanceBefore = IERC20(USDC).balanceOf(address(superToken));
124-
125134
_enableYieldBackend();
126135

127-
vm.startPrank(address(superToken.getHost()));
128-
superToken.setYieldBackend(address(0));
136+
vm.startPrank(ADMIN);
137+
superToken.disableYieldBackend();
129138
vm.stopPrank();
130139
assertEq(address(superToken.getYieldBackend()), address(0), "Yield backend mismatch");
131140

132141
// the SuperToken should now have a non-zero USDC balance and a zero aUSDC balance
133142
assertGt(IERC20(USDC).balanceOf(address(superToken)), 0, "USDC balance should be non-zero");
134143
assertEq(IERC20(aUSDC).balanceOf(address(superToken)), 0, "aUSDC balance should be zero");
135144

136-
// get underlying balance after disabling yield backend
137-
uint256 underlyingBalanceAfter = IERC20(USDC).balanceOf(address(superToken));
138-
//assertEq(underlyingBalanceAfter, underlyingBalanceBefore, "Underlying balance should be the same");
145+
_verifyInvariants();
139146
}
140147

141148
// TODO: bool fuzz arg for disabled/enabled backend
142149
function testUpgradeDowngrade() public {
143150
_enableYieldBackend();
144151

145-
deal(USDC, ALICE, 1000 ether);
146-
147152
uint256 aTokenBalanceBefore = IERC20(aUSDC).balanceOf(address(superToken));
148153
vm.startPrank(ALICE);
149-
IERC20(USDC).approve(address(superToken), type(uint256).max);
150154
superToken.upgrade(1 ether);
151155
vm.stopPrank();
152156

@@ -167,7 +171,7 @@ contract SuperTokenYieldForkTest is Test {
167171
superToken.downgrade(1 ether);
168172
vm.stopPrank();
169173

170-
uint256 aTokenBalanceAfterDowngrade = IERC20(aUSDC).balanceOf(address(superToken));
174+
_verifyInvariants();
171175
}
172176

173177
// ============ Gas Benchmarking Tests ============
@@ -176,19 +180,13 @@ contract SuperTokenYieldForkTest is Test {
176180
/// @dev Separate test function to avoid cold/warm storage slot interference
177181
function testGasUpgrade_WithoutYieldBackend() public {
178182
// Ensure yield backend is NOT set
179-
vm.startPrank(address(superToken.getHost()));
180-
superToken.setYieldBackend(address(0));
181-
vm.stopPrank();
182183
assertEq(address(superToken.getYieldBackend()), address(0), "Yield backend should not be set");
183184

184185
// Prepare test state
185186
// 1000 USDC = 1000 * 1e6 (USDC has 6 decimals)
186187
// In SuperToken units (18 decimals), this is 1000 * 1e18
187188
uint256 upgradeAmount = 1000 * 1e18;
188-
deal(USDC, ALICE, 1000 * 1e6);
189189
vm.startPrank(ALICE);
190-
IERC20(USDC).approve(address(superToken), type(uint256).max);
191-
192190
// Measure gas for upgrade
193191
uint256 gasBefore = gasleft();
194192
superToken.upgrade(upgradeAmount);
@@ -211,10 +209,7 @@ contract SuperTokenYieldForkTest is Test {
211209
// 1000 USDC = 1000 * 1e6 (USDC has 6 decimals)
212210
// In SuperToken units (18 decimals), this is 1000 * 1e18
213211
uint256 upgradeAmount = 1000 * 1e18;
214-
deal(USDC, ALICE, 1000 * 1e6);
215212
vm.startPrank(ALICE);
216-
IERC20(USDC).approve(address(superToken), type(uint256).max);
217-
218213
// Measure gas for upgrade
219214
uint256 gasBefore = gasleft();
220215
superToken.upgrade(upgradeAmount);
@@ -230,17 +225,13 @@ contract SuperTokenYieldForkTest is Test {
230225
/// @dev Separate test function to avoid cold/warm storage slot interference
231226
function testGasDowngrade_WithoutYieldBackend() public {
232227
// Ensure yield backend is NOT set
233-
vm.startPrank(address(superToken.getHost()));
234-
superToken.setYieldBackend(address(0));
235-
vm.stopPrank();
228+
assertEq(address(superToken.getYieldBackend()), address(0), "Yield backend should not be set");
236229

237230
// First, upgrade some tokens for ALICE to downgrade later
238231
// 1000 USDC = 1000 * 1e6 (USDC has 6 decimals)
239232
// In SuperToken units (18 decimals), this is 1000 * 1e18
240233
uint256 initialUpgradeAmount = 1000 * 1e18;
241-
deal(USDC, ALICE, 1000 * 1e6);
242234
vm.startPrank(ALICE);
243-
IERC20(USDC).approve(address(superToken), type(uint256).max);
244235
superToken.upgrade(initialUpgradeAmount);
245236
vm.stopPrank();
246237

@@ -270,9 +261,7 @@ contract SuperTokenYieldForkTest is Test {
270261
// 1000 USDC = 1000 * 1e6 (USDC has 6 decimals)
271262
// In SuperToken units (18 decimals), this is 1000 * 1e18
272263
uint256 initialUpgradeAmount = 1000 * 1e18;
273-
deal(USDC, ALICE, 1000 * 1e6);
274264
vm.startPrank(ALICE);
275-
IERC20(USDC).approve(address(superToken), type(uint256).max);
276265
superToken.upgrade(initialUpgradeAmount);
277266
vm.stopPrank();
278267

@@ -291,5 +280,45 @@ contract SuperTokenYieldForkTest is Test {
291280
console.log("Gas used", gasUsed);
292281
console.log("Amount downgraded", amountToDowngrade);
293282
}
283+
284+
function testWithdrawSurplusFromYieldBackend() public {
285+
address SURPLUS_RECEIVER = 0xac808840f02c47C05507f48165d2222FF28EF4e1;
286+
287+
// Simulate yield accumulation by transferring extra underlying to SuperToken
288+
uint256 surplusAmount = 100 * 1e6; // 100 USDC
289+
deal(USDC, address(this), surplusAmount);
290+
291+
_enableYieldBackend();
292+
293+
// Upgrade tokens to create supply
294+
uint256 upgradeAmount = 1000 * 1e18;
295+
vm.startPrank(ALICE);
296+
superToken.upgrade(upgradeAmount);
297+
vm.stopPrank();
298+
299+
uint256 receiverBalanceBefore = IERC20(USDC).balanceOf(SURPLUS_RECEIVER);
300+
uint256 aTokenBalanceBefore = IERC20(aUSDC).balanceOf(address(superToken));
301+
302+
// log USDC and aUSDC balances of SuperToken
303+
console.log("USDC balance of SuperToken", IERC20(USDC).balanceOf(address(superToken)));
304+
console.log("aUSDC balance of SuperToken", IERC20(aUSDC).balanceOf(address(superToken)));
305+
// log normalized total supply
306+
(uint256 normalizedTotalSupply, uint256 adjustedAmount) = superToken.toUnderlyingAmount(superToken.totalSupply());
307+
console.log("normalized total supply", normalizedTotalSupply);
308+
console.log("adjusted amount", adjustedAmount);
309+
310+
vm.startPrank(ADMIN);
311+
superToken.withdrawSurplusFromYieldBackend();
312+
vm.stopPrank();
313+
314+
uint256 receiverBalanceAfter = IERC20(USDC).balanceOf(SURPLUS_RECEIVER);
315+
uint256 aTokenBalanceAfter = IERC20(aUSDC).balanceOf(address(superToken));
316+
console.log("aToken balance after", aTokenBalanceAfter);
317+
console.log("aToken balance diff", aTokenBalanceBefore - aTokenBalanceAfter);
318+
319+
assertGt(receiverBalanceAfter, receiverBalanceBefore, "Surplus should be withdrawn to receiver");
320+
assertLt(aTokenBalanceAfter, aTokenBalanceBefore, "aToken balance should decrease");
321+
_verifyInvariants();
322+
}
294323
}
295324

0 commit comments

Comments
 (0)