-
Notifications
You must be signed in to change notification settings - Fork 261
Super Token Yield Backends: Aave & ERC4626 #2125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
f2ced44
7945dc7
5c28327
1d3e9be
d19348c
04a4e1c
19624e6
7db2940
0e4c436
8b9979c
806dba1
a75947d
3621b96
f217e28
82bc629
1987a65
2002d94
955a94f
47e8081
882ec21
5a615d8
8ab52e1
fd0b070
5f695a2
4bbbd2e
061a491
3500962
fd3aa4f
e459e00
121df80
17da953
ce01fa7
61af65b
4272afd
a9bc707
d77a933
f2883eb
886554a
1e49964
d8ca70d
5fdb16e
c044943
d69e4b0
1b04366
fb61d8b
92112ab
ac0b856
2a9428b
8f8ff4e
4d24a83
101b154
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.23; | ||
|
|
||
| /** | ||
| * A yield backend acts as interface between an ERC20 wrapper SuperToken and a yield generating protocol. | ||
| * The underlying token can be deposited on upgrade and withdrawn on downgrade. | ||
| * | ||
| * It is possible to transition from no/one yield backend to another/no yield backend. | ||
| * one -> another could be seen as a composition of one -> no -> another | ||
| * | ||
| * one -> no means withdraw not in the context of a downgrade. | ||
| * | ||
| * Contracts implementing this act as a kind of hot-pluggable library, | ||
| * using delegatecall to execute its logic on the SuperToken contract. | ||
| * This means that underlying tokens are transferred directly between the SuperToken contract and the yield protocol, | ||
| * as are yield protocol tokens representing positions in that protocol. | ||
| * If an implementation requires to hold state, it shall do so using a namespaced storage layout (EIP-7201). | ||
| */ | ||
| interface IYieldBackend { | ||
| /// Invoked by `SuperToken` as delegatecall. | ||
| /// Sets up the SuperToken as needed, e.g. by giving required approvals. | ||
| function enable() external; | ||
|
|
||
| /// Invoked by `SuperToken` as delegatecall. | ||
| /// Restores the prior state, e.g. by revoking given approvals | ||
| function disable() external; | ||
|
|
||
| /// Invoked by `SuperToken` as delegatecall. | ||
| /// Deposits the given amount of the underlying asset into the yield backend. | ||
| function deposit(uint256 amount) external; | ||
| /// Invoked by `SuperToken` as delegatecall. | ||
d10r marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// Deposits the maximum amount of the underlying asset into the yield backend. | ||
| /// Maximum is defined by the underlying asset balance of the SuperToken and the yield backend capacity. | ||
| function depositMax() external; | ||
d10r marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /// Invoked by `SuperToken` as delegatecall. | ||
| /// Withdraws the given amount of the underlying asset from the yield backend. | ||
| function withdraw(uint256 amount) external; | ||
|
|
||
| /// Invoked by `SuperToken` as delegatecall. | ||
| /// Withdraws the maximum amount of the underlying asset from the yield backend. | ||
| /// Maximum is defined by how much can be withdrawn from the yield backend at that point in time. | ||
| function withdrawMax() external; | ||
|
|
||
| /// Invoked by `SuperToken` as delegatecall. | ||
| /// tranfers the deposited asset exceeding totalSupply of the SuperToken to the preset receiver account | ||
| function withdrawSurplus(uint256 totalSupply) external; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| // SPDX-License-Identifier: AGPLv3 | ||
| pragma solidity ^0.8.23; | ||
|
|
||
| import { IYieldBackend } from "../interfaces/superfluid/IYieldBackend.sol"; | ||
|
|
||
|
|
||
| /** | ||
| * @dev Helper to delegatecall yield backend methods. | ||
| * Reverts if the call fails. | ||
| * Does NOT return anything! | ||
| */ | ||
| library YieldBackendHelperLib { | ||
| function dCall(IYieldBackend yieldBackend, bytes memory callData) internal { | ||
| (bool success,) = address(yieldBackend).delegatecall(callData); | ||
| require(success, "yield backend delegatecall failed"); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.23; | ||
|
|
||
| import { IYieldBackend } from "../interfaces/superfluid/IYieldBackend.sol"; | ||
| import { IERC20, ISuperToken } from "../interfaces/superfluid/ISuperfluid.sol"; | ||
| import { IPool } from "aave-v3/interfaces/IPool.sol"; | ||
| import { IWETH } from "aave-v3/helpers/interfaces/IWETH.sol"; | ||
|
|
||
|
|
||
| /** | ||
| * Aave supports a simple deposit/withdraw workflow nicely matching the IYieldBackend interface. | ||
| * Deposits are represented by transferrable aTokens. | ||
| * | ||
| * This contract is conceptually a hot-pluggable library. | ||
| * All methods are supposed to be invoked as delegatecall. | ||
| */ | ||
| contract AaveYieldBackend is IYieldBackend { | ||
| IERC20 public immutable ASSET_TOKEN; | ||
| IPool public immutable AAVE_POOL; | ||
| IERC20 public immutable A_TOKEN; | ||
| bool public immutable USING_WETH; | ||
| // TODO: make an immutable | ||
| address constant SURPLUS_RECEIVER = 0xac808840f02c47C05507f48165d2222FF28EF4e1; // dao.superfluid.eth | ||
|
|
||
| AaveYieldBackend internal immutable _SELF; | ||
|
|
||
| // THIS CONTRACT CANNOT HAVE STATE VARIABLES! | ||
| // IF STATE IS NEEDED, USE NAMESPACED STORAGE LAYOUT (EIP-7201) | ||
|
|
||
| /** | ||
| * @param assetToken the asset (Aave terminology) supplied to Aave for yield. Typically, this will be | ||
| * the underlyingToken of a SuperToken. | ||
| * @param aavePool the Aave pool | ||
| */ | ||
| constructor(IERC20 assetToken, IPool aavePool) { | ||
| // TODO: any checks to be done? | ||
| if (address(assetToken) == address(0)) { | ||
| // native token, need to wrap to WETH | ||
| USING_WETH = true; | ||
| // This implementation currently only supports Base | ||
| if (block.chainid == 10 || block.chainid == 8453) { | ||
| // base, optimism | ||
| ASSET_TOKEN = IERC20(0x4200000000000000000000000000000000000006); | ||
| } else { | ||
| revert("not supported"); | ||
| } | ||
| } else { | ||
| ASSET_TOKEN = assetToken; | ||
| } | ||
| AAVE_POOL = IPool(aavePool); | ||
| A_TOKEN = IERC20(aavePool.getReserveAToken(address(ASSET_TOKEN))); | ||
|
|
||
| _SELF = this; | ||
| } | ||
|
|
||
| function enable() external { | ||
| // approve Aave pool to fetch asset | ||
| ASSET_TOKEN.approve(address(AAVE_POOL), type(uint256).max); | ||
| } | ||
|
|
||
| function disable() external { | ||
| // Revoke approval | ||
| ASSET_TOKEN.approve(address(AAVE_POOL), 0); | ||
| } | ||
|
|
||
| function deposit(uint256 amount) public { | ||
| // TODO: can this constraint break anything? | ||
| require(amount > 0, "amount must be greater than 0"); | ||
| if (USING_WETH) { | ||
| // wrap ETH to WETH | ||
| IWETH(address(ASSET_TOKEN)).deposit{value: amount}(); | ||
| } | ||
| // Deposit asset and get back aTokens | ||
| AAVE_POOL.supply(address(ASSET_TOKEN), amount, address(this), 0); | ||
| } | ||
|
|
||
| function depositMax() external { | ||
| uint256 amount = USING_WETH ? | ||
| address(this).balance : | ||
| ASSET_TOKEN.balanceOf(address(this)); | ||
|
||
| if (amount > 0) { | ||
| deposit(amount); | ||
| } | ||
| } | ||
|
|
||
| function withdraw(uint256 amount) external { | ||
| // withdraw amount asset by redeeming the corresponding aTokens amount | ||
| if (USING_WETH) { | ||
| AAVE_POOL.withdraw(address(ASSET_TOKEN), amount, address(_SELF)); | ||
| _SELF.unwrapAndForwardWETH(amount); | ||
| } else { | ||
| AAVE_POOL.withdraw(address(ASSET_TOKEN), amount, address(this)); | ||
| } | ||
| } | ||
|
|
||
| function withdrawMax() external { | ||
| // We can delegate the max calculation to the Aave pool by setting amount to type(uint256).max | ||
| AAVE_POOL.withdraw(address(ASSET_TOKEN), type(uint256).max, address(this)); | ||
| } | ||
|
|
||
| function withdrawSurplus(uint256 totalSupply) external { | ||
| // totalSupply is always 18 decimals while assetToken and aToken may not | ||
| (uint256 normalizedTotalSupply,) = ISuperToken(address(this)).toUnderlyingAmount(totalSupply); | ||
| // decrement by 1 in order to offset Aave's rounding up | ||
|
||
| uint256 surplusAmount = A_TOKEN.balanceOf(address(this)) - normalizedTotalSupply - 1; | ||
|
||
| AAVE_POOL.withdraw(address(ASSET_TOKEN), surplusAmount, SURPLUS_RECEIVER); | ||
| } | ||
|
|
||
| // ============ functions operating on this contract itself (NOT in delegatecall context) ============ | ||
|
|
||
| // allow unwrapping from WETH to this contract | ||
| receive() external payable {} | ||
|
|
||
| // To be invoked by `withdraw` executed via delegatecall in a SuperToken context. | ||
| // Since WETH never stays in this contract, no validation of msg.sender is necessary. | ||
| function unwrapAndForwardWETH(uint256 amount) external { | ||
| IWETH(address(ASSET_TOKEN)).withdraw(amount); | ||
| (bool success, ) = address(msg.sender).call{value: amount}(""); | ||
| require(success, "call failed"); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,8 @@ import { | |
| IERC20, | ||
| IPoolAdminNFT | ||
| } from "../interfaces/superfluid/ISuperfluid.sol"; | ||
| import { IYieldBackend } from "../interfaces/superfluid/IYieldBackend.sol"; | ||
| import { YieldBackendHelperLib } from "../libs/YieldBackendHelperLib.sol"; | ||
| import { SuperfluidToken } from "./SuperfluidToken.sol"; | ||
| import { ERC777Helper } from "../libs/ERC777Helper.sol"; | ||
| import { SafeERC20 } from "@openzeppelin-v5/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
|
|
@@ -37,6 +39,7 @@ contract SuperToken is | |
| using SafeCast for uint256; | ||
| using ERC777Helper for ERC777Helper.Operators; | ||
| using SafeERC20 for IERC20; | ||
| using YieldBackendHelperLib for IYieldBackend; | ||
|
|
||
| // See: https://eips.ethereum.org/EIPS/eip-1967#admin-address | ||
| bytes32 constant private _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; | ||
|
|
@@ -82,15 +85,17 @@ contract SuperToken is | |
| /// @dev ERC20 Nonces for EIP-2612 (permit) | ||
| mapping(address account => uint256) internal _nonces; | ||
|
|
||
| // TODO: use a randomly located storage slot instead? | ||
| IYieldBackend internal _yieldBackend; | ||
|
|
||
| // NOTE: for future compatibility, these are reserved solidity slots | ||
| // The sub-class of SuperToken solidity slot will start after _reserve22 | ||
| // The sub-class of SuperToken solidity slot will start after _reserve24 | ||
|
|
||
| // NOTE: Whenever modifying the storage layout here it is important to update the validateStorageLayout | ||
| // function in its respective mock contract to ensure that it doesn't break anything or lead to unexpected | ||
| // behaviors/layout when upgrading | ||
|
|
||
| uint256 internal _reserve23; | ||
| uint256 private _reserve24; | ||
| uint256 internal _reserve24; | ||
| uint256 private _reserve25; | ||
| uint256 private _reserve26; | ||
| uint256 private _reserve27; | ||
|
|
@@ -102,6 +107,9 @@ contract SuperToken is | |
| // NOTE: You cannot add more storage here. Refer to CustomSuperTokenBase.sol | ||
| // to see the hard-coded storage padding used by SETH and PureSuperToken | ||
|
|
||
| // set when withdrawing ETH from yield backend in order to avoid a burn/mint loop | ||
| bool transient internal _skipSelfMint; | ||
|
|
||
| constructor( | ||
| ISuperfluid host, | ||
| IPoolAdminNFT poolAdminNFT | ||
|
|
@@ -195,6 +203,31 @@ contract SuperToken is | |
| } | ||
| } | ||
|
|
||
| function enableYieldBackend(IYieldBackend newYieldBackend) external onlyAdmin { | ||
| require(address(_yieldBackend) == address(0), "yield backend already set"); | ||
| _yieldBackend = newYieldBackend; | ||
| _yieldBackend.dCall(abi.encodeCall(IYieldBackend.enable, ())); | ||
| _yieldBackend.dCall(abi.encodeCall(IYieldBackend.depositMax, ())); | ||
| // TODO: emit event | ||
| } | ||
|
|
||
| // withdraws everything and removes allowances | ||
| function disableYieldBackend() external onlyAdmin { | ||
| require(address(_yieldBackend) != address(0), "yield backend not set"); | ||
| _yieldBackend.dCall(abi.encodeCall(IYieldBackend.withdrawMax, ())); | ||
| _yieldBackend.dCall(abi.encodeCall(IYieldBackend.disable, ())); | ||
| _yieldBackend = IYieldBackend(address(0)); | ||
| // TODO: emit event | ||
| } | ||
|
|
||
| function getYieldBackend() external view returns (address) { | ||
| return address(_yieldBackend); | ||
| } | ||
|
|
||
| function withdrawSurplusFromYieldBackend() external onlyAdmin { | ||
| require(address(_yieldBackend) != address(0), "yield backend not set"); | ||
| _yieldBackend.dCall(abi.encodeCall(IYieldBackend.withdrawSurplus, (_totalSupply))); | ||
| } | ||
|
|
||
| /************************************************************************** | ||
| * ERC20 Token Info | ||
|
|
@@ -728,8 +761,15 @@ contract SuperToken is | |
| external virtual override | ||
| onlySelf | ||
| { | ||
| _mint(msg.sender, account, amount, userData.length != 0 /* invokeHook */, | ||
| userData.length != 0 /* requireReceptionAck */, userData, new bytes(0)); | ||
| if (!_skipSelfMint) { | ||
| if (address(_yieldBackend) != address(0)) { | ||
| // TODO: shall we deposit all, or just the upgradeAmount? | ||
| _yieldBackend.dCall(abi.encodeCall(IYieldBackend.deposit, (amount))); | ||
| } | ||
|
|
||
| _mint(msg.sender, account, amount, userData.length != 0 /* invokeHook */, | ||
| userData.length != 0 /* requireReceptionAck */, userData, new bytes(0)); | ||
| } | ||
| } | ||
|
|
||
| function selfBurn( | ||
|
|
@@ -741,6 +781,13 @@ contract SuperToken is | |
| onlySelf | ||
| { | ||
| _burn(msg.sender, account, amount, userData.length != 0 /* invokeHook */, userData, new bytes(0)); | ||
|
|
||
| if (address(_yieldBackend) != address(0)) { | ||
| _skipSelfMint = true; | ||
| // TODO: we may want to skip if enough underlying already in the contract | ||
| _yieldBackend.dCall(abi.encodeCall(IYieldBackend.withdraw, (amount))); | ||
| _skipSelfMint = false; | ||
| } | ||
| } | ||
|
|
||
| function selfApproveFor( | ||
|
|
@@ -839,6 +886,11 @@ contract SuperToken is | |
| uint256 actualUpgradedAmount = amountAfter - amountBefore; | ||
| if (underlyingAmount != actualUpgradedAmount) revert SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED(); | ||
|
|
||
| if (address(_yieldBackend) != address(0)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move to _burn and _mint, so that selfBurn/selfMint could work too (for SETH).
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2 reasons against that:
|
||
| // TODO: shall we deposit all, or just the upgradeAmount? | ||
| _yieldBackend.dCall(abi.encodeCall(IYieldBackend.deposit, (actualUpgradedAmount))); | ||
| } | ||
|
|
||
| _mint(operator, to, adjustedAmount, | ||
| // if `userData.length` is greater than 0, we set invokeHook and requireReceptionAck true | ||
| userData.length != 0, userData.length != 0, userData, operatorData); | ||
|
|
@@ -861,6 +913,11 @@ contract SuperToken is | |
| // _burn will check the (actual) amount availability again | ||
| _burn(operator, account, adjustedAmount, userData.length != 0, userData, operatorData); | ||
|
|
||
| if (address(_yieldBackend) != address(0)) { | ||
| // TODO: we may want to skip if enough underlying already in the contract | ||
| _yieldBackend.dCall(abi.encodeCall(IYieldBackend.withdraw, (underlyingAmount))); | ||
| } | ||
|
|
||
| uint256 amountBefore = _underlyingToken.balanceOf(address(this)); | ||
| _underlyingToken.safeTransfer(to, underlyingAmount); | ||
| uint256 amountAfter = _underlyingToken.balanceOf(address(this)); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.