Skip to content

Commit a7ee248

Browse files
d10rhellwolf
andauthored
Super Token Yield Backends: Aave & ERC4626 (#2125)
--------- Co-authored-by: Miao ZhiCheng <miao@superfluid.finance>
1 parent deeae86 commit a7ee248

24 files changed

+2071
-10
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@
55
path = lib/openzeppelin-contracts
66
url = https://github.com/OpenZeppelin/openzeppelin-contracts.git
77
branch = release-v5.4
8+
[submodule "lib/aave-v3"]
9+
path = lib/aave-v3
10+
url = https://github.com/aave-dao/aave-v3-origin

lib/aave-v3

Submodule aave-v3 added at 1ce897b

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
},
5353
"scripts": {
5454
"prepare": "husky && npm run git-submodule:init",
55-
"postinstall": "ln -fs ../lib/openzeppelin-contracts node_modules/@openzeppelin-v5",
55+
"postinstall": "ln -fs ../lib/openzeppelin-contracts node_modules/@openzeppelin-v5 && ln -fs ../lib/aave-v3 node_modules/aave-v3",
5656
"lint": "run-s -l lint:*",
5757
"lint:syncpack": "syncpack lint",
5858
"lint:shellcheck": "tasks/shellcheck-all-tasks.sh",

packages/ethereum-contracts/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
55

66
## [UNRELEASED]
77

8+
### Added
9+
10+
- `SuperToken`: the contract admin can enable/disable a _Yield Backend_ in order to generate a yield on the underlying asset.
11+
812
### Changed
913

1014
- EVM target changed from _shanghai_ to _cancun_.

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,43 @@ interface ISuperToken is ISuperfluidToken, IERC20Metadata, IERC777, IERC20Permit
6363
*/
6464
function changeAdmin(address newAdmin) external;
6565

66+
6667
event AdminChanged(address indexed oldAdmin, address indexed newAdmin);
6768

6869
/**
6970
* @dev Returns the admin address for the SuperToken
71+
* The admin account has the exclusive privilege of
72+
* - updating the contract (change implementation)
73+
* - enabling/disabling a yield backend
74+
* - setting another admin
75+
* If no admin is set (zero address), this privileges are delegated to the host contract.
7076
*/
7177
function getAdmin() external view returns (address admin);
7278

79+
/**
80+
* @dev Returns the address of the yield backend contract (see `IYieldBackend`).
81+
* The yield backend contract is responsible for managing the yield of the SuperToken.
82+
*/
83+
function getYieldBackend() external view returns (address yieldBackend);
84+
85+
/**
86+
* @dev Yield backend enabled event
87+
* @param yieldBackend The address of the yield backend that was enabled
88+
* @param depositAmount The amount deposited to the yield backend
89+
*/
90+
event YieldBackendEnabled(
91+
address indexed yieldBackend,
92+
uint256 depositAmount
93+
);
94+
95+
/**
96+
* @dev Yield backend disabled event
97+
* @param yieldBackend The address of the yield backend that was disabled
98+
*/
99+
event YieldBackendDisabled(
100+
address indexed yieldBackend
101+
);
102+
73103
/**************************************************************************
74104
* Immutable variables
75105
*************************************************************************/
@@ -614,5 +644,4 @@ interface ISuperToken is ISuperfluidToken, IERC20Metadata, IERC777, IERC20Permit
614644

615645
/// @dev The msg.sender must be the contract itself
616646
//modifier onlySelf() virtual
617-
618647
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity >= 0.8.11;
3+
4+
/**
5+
* A yield backend acts as interface between an ERC20 wrapper SuperToken and a yield generating protocol.
6+
* The underlying token can be deposited on upgrade and withdrawn on downgrade.
7+
*
8+
* It is possible to transition from no/one yield backend to another/no yield backend.
9+
* one -> another could be seen as a composition of one -> no -> another
10+
*
11+
* one -> no means withdraw not in the context of a downgrade.
12+
*
13+
* Contracts implementing this act as a kind of hot-pluggable library,
14+
* using delegatecall to execute its logic on the SuperToken contract.
15+
* This means that underlying tokens are transferred directly between the SuperToken contract and the yield protocol,
16+
* as are yield protocol tokens representing positions in that protocol.
17+
* If an implementation requires to hold state, it shall do so using a namespaced storage layout (EIP-7201).
18+
*/
19+
interface IYieldBackend {
20+
/// Invoked by `SuperToken` as delegatecall.
21+
/// Sets up the SuperToken as needed, e.g. by giving required approvals.
22+
function enable() external;
23+
24+
/// Invoked by `SuperToken` as delegatecall.
25+
/// Restores the prior state, e.g. by revoking given approvals
26+
function disable() external;
27+
28+
/// Invoked by `SuperToken` as delegatecall.
29+
/// Deposits the given amount of the underlying asset into the yield backend.
30+
function deposit(uint256 amount) external;
31+
32+
/// Invoked by `SuperToken` as delegatecall.
33+
/// Withdraws the given amount of the underlying asset from the yield backend.
34+
function withdraw(uint256 amount) external;
35+
36+
/// Invoked by `SuperToken` as delegatecall.
37+
/// Withdraws the maximum withdrawable amount of the underlying asset from the yield backend.
38+
function withdrawMax() external;
39+
40+
/// Invoked by `SuperToken` as delegatecall.
41+
/// tranfers the deposited asset exceeding totalSupply of the SuperToken to the preset receiver account
42+
function withdrawSurplus(uint256 totalSupply) external;
43+
}

packages/ethereum-contracts/contracts/libs/CallUtils.sol

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
// SPDX-License-Identifier: AGPLv3
22
pragma solidity ^0.8.23;
33

4+
5+
/**
6+
* @dev Helper method to delegatecall, reverts if not successful.
7+
* @param target The address to delegatecall to
8+
* @param callData The data to delegatecall with
9+
* Does not return anything!
10+
*/
11+
function delegateCallChecked(address target, bytes memory callData) {
12+
// solhint-disable-next-line avoid-low-level-calls
13+
(bool success, bytes memory returnedData) = target.delegatecall(callData);
14+
if (!success) {
15+
CallUtils.revertFromReturnedData(returnedData);
16+
}
17+
}
18+
419
/**
520
* @title Call utilities library that is absent from the OpenZeppelin
621
* @author Superfluid

packages/ethereum-contracts/contracts/mocks/SuperTokenMock.t.sol

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,14 @@ contract SuperTokenStorageLayoutTester is SuperToken {
6464
require (slot == 18 && offset == 0, "_operators changed location");
6565
// uses 4 slots
6666

67-
assembly { slot:= _reserve23.slot offset := _reserve23.offset }
68-
require (slot == 23 && offset == 0, "_reserve23 changed location");
67+
assembly { slot:= _nonces.slot offset := _nonces.offset }
68+
require (slot == 22 && offset == 0, "_nonces changed location");
69+
70+
assembly { slot:= _yieldBackend.slot offset := _yieldBackend.offset }
71+
require (slot == 23 && offset == 0, "_yieldBackend changed location");
72+
73+
assembly { slot:= _reserve24.slot offset := _reserve24.offset }
74+
require (slot == 24 && offset == 0, "_reserve24 changed location");
6975

7076
assembly { slot:= _reserve31.slot offset := _reserve31.offset }
7177
require (slot == 31 && offset == 0, "_reserve31 changed location");
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.23;
3+
4+
import { AaveYieldBackend } from "./AaveYieldBackend.sol";
5+
import { IERC20 } from "../interfaces/superfluid/ISuperfluid.sol";
6+
import { IPool } from "aave-v3/src/contracts/interfaces/IPool.sol";
7+
import { IWETH } from "aave-v3/src/contracts/helpers/interfaces/IWETH.sol";
8+
9+
/**
10+
* @title a SuperToken yield backend for the Aave protocol for ETH/native tokens.
11+
* This contract extends AaveYieldBackend to support native ETH by wrapping it to WETH.
12+
* WETH addresses are hardcoded by chain id.
13+
*
14+
* NOTE: "WETH" is to be interpreted in a technical sense: the native token wrapper.
15+
* On chains with ETH not being the native token, the ERC20 token with symbol "WETH" may be an ordinary ERC20
16+
* while the ERC20 wrapper of the native token may have a different symbol. We mean the latter!
17+
*
18+
* NOTE: Surplus WETH will NOT be unwrapped by `withdrawSurplus` (which is inherited from the Base contract)
19+
* before transferring it to the configured SURPLUS_RECEIVER.
20+
*/
21+
contract AaveETHYieldBackend is AaveYieldBackend {
22+
AaveETHYieldBackend internal immutable _SELF;
23+
24+
// THIS CONTRACT CANNOT HAVE STATE VARIABLES!
25+
// IF STATE IS NEEDED, USE NAMESPACED STORAGE LAYOUT (EIP-7201)
26+
27+
/**
28+
* @param aavePool the Aave pool
29+
* @param surplusReceiver the address to receive the surplus asset when withdrawing the surplus
30+
*/
31+
constructor(IPool aavePool, address surplusReceiver)
32+
AaveYieldBackend(IERC20(getWETHAddress()), aavePool, surplusReceiver)
33+
{
34+
_SELF = this;
35+
}
36+
37+
/// get the canonical native token ERC20 wrapper contract address based on the chain id and Aave deployment.
38+
/// Implemented for chains with official deployments of Aave and Superfluid.
39+
function getWETHAddress() internal view returns (address) {
40+
if (block.chainid == 1) { // Ethereum
41+
return 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
42+
}
43+
if (block.chainid == 10 || block.chainid == 8453) {
44+
return 0x4200000000000000000000000000000000000006;
45+
}
46+
if (block.chainid == 137) { // Polygon
47+
// Note this token has the symbol WPOL, wrapping the native token POL
48+
return 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270;
49+
}
50+
if (block.chainid == 42161) { // Arbitrum
51+
return 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1;
52+
}
53+
if (block.chainid == 100) { // Gnosis Chain
54+
// Note this token has the symbol WXDAI, wrapping the native token xDAI
55+
return 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d;
56+
}
57+
if (block.chainid == 43114) { // Avalanche C-Chain
58+
// Note this token has the symbol WAVAX, wrapping the native token AVAX
59+
return 0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7;
60+
}
61+
if (block.chainid == 56) { // BNB
62+
// Note this token has the symbol WBNB, wrapping the native token BNB
63+
return 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
64+
}
65+
if (block.chainid == 534352) { // Scroll
66+
return 0x5300000000000000000000000000000000000004;
67+
}
68+
// Celo: WCELO does not implement IWETH
69+
70+
revert("chain not supported");
71+
}
72+
73+
function deposit(uint256 amount) public override {
74+
// wrap ETH to WETH
75+
IWETH(address(ASSET_TOKEN)).deposit{ value: amount }();
76+
// Deposit asset and get back aTokens
77+
super.deposit(amount);
78+
}
79+
80+
function withdraw(uint256 amount) public override {
81+
// withdraw WETH by redeeming the corresponding aTokens amount.
82+
// the receiver is set to the address of the implementation contract in order to not trigger the
83+
// fallback function of the SuperToken contract.
84+
uint256 withdrawnAmount = AAVE_POOL.withdraw(address(ASSET_TOKEN), amount, address(_SELF));
85+
// unwrap to ETH and transfer it to the calling SuperToken contract
86+
_SELF.unwrapWETHAndForwardETH(withdrawnAmount, address(this));
87+
}
88+
89+
// ============ functions operating on this contract itself (NOT in delegatecall context) ============
90+
91+
// allow unwrapping from WETH to this contract
92+
receive() external payable { }
93+
94+
// To be invoked by `withdraw` which is executed via delegatecall in a SuperToken context.
95+
// WETH deposited or withdrawn by the SuperToken never stays in this contract beyond the lifetime of the tx.
96+
// Thus it is not necessary to restrict msg.sender.
97+
// We accept that an alien caller may withdraw WETH deposited to this contract (for whatever reason).
98+
function unwrapWETHAndForwardETH(uint256 amount, address recipient) external {
99+
IWETH(address(ASSET_TOKEN)).withdraw(amount);
100+
(bool success,) = recipient.call{ value: amount }("");
101+
require(success, "call failed");
102+
}
103+
}
104+
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.23;
3+
4+
import { IYieldBackend } from "../interfaces/superfluid/IYieldBackend.sol";
5+
import { IERC20, ISuperToken } from "../interfaces/superfluid/ISuperfluid.sol";
6+
import { IPool } from "aave-v3/src/contracts/interfaces/IPool.sol";
7+
8+
/**
9+
* @title a SuperToken yield backend for the Aave protocol.
10+
* Aave supports a simple deposit/withdraw workflow nicely matching the IYieldBackend interface.
11+
* Deposits are represented by transferrable aTokens.
12+
*
13+
* This contract is conceptually a hot-pluggable library.
14+
* All methods are supposed to be invoked as delegatecall.
15+
*
16+
* In order to learn about the limitations and constraints of this implementation, see
17+
* https://github.com/superfluid-org/protocol-monorepo/wiki/Yield-Backend
18+
*/
19+
contract AaveYieldBackend is IYieldBackend {
20+
IERC20 public immutable ASSET_TOKEN;
21+
IPool public immutable AAVE_POOL;
22+
IERC20 public immutable A_TOKEN;
23+
address public immutable SURPLUS_RECEIVER;
24+
25+
// THIS CONTRACT CANNOT HAVE STATE VARIABLES!
26+
// IF STATE IS NEEDED, USE NAMESPACED STORAGE LAYOUT (EIP-7201)
27+
28+
/**
29+
* @param assetToken the asset (Aave terminology) supplied to Aave for yield. Typically, this will be
30+
* the underlyingToken of a SuperToken. Must be a valid ERC20 token address.
31+
* @param aavePool the Aave pool
32+
* @param surplusReceiver the address to receive the surplus asset when withdrawing the surplus
33+
*/
34+
constructor(IERC20 assetToken, IPool aavePool, address surplusReceiver) {
35+
require(address(assetToken) != address(0), "assetToken cannot be address(0)");
36+
ASSET_TOKEN = assetToken;
37+
AAVE_POOL = IPool(aavePool);
38+
SURPLUS_RECEIVER = surplusReceiver;
39+
A_TOKEN = IERC20(aavePool.getReserveAToken(address(ASSET_TOKEN)));
40+
}
41+
42+
function enable() external {
43+
// approve Aave pool to fetch asset
44+
ASSET_TOKEN.approve(address(AAVE_POOL), type(uint256).max);
45+
}
46+
47+
function disable() external {
48+
// Revoke approval
49+
ASSET_TOKEN.approve(address(AAVE_POOL), 0);
50+
}
51+
52+
function deposit(uint256 amount) public virtual {
53+
// TODO: can this constraint break anything?
54+
require(amount > 0, "amount must be greater than 0");
55+
// Deposit asset and get back aTokens
56+
AAVE_POOL.supply(address(ASSET_TOKEN), amount, address(this), 0);
57+
}
58+
59+
function withdraw(uint256 amount) public virtual {
60+
// withdraw amount asset by redeeming the corresponding aTokens amount
61+
AAVE_POOL.withdraw(address(ASSET_TOKEN), amount, address(this));
62+
}
63+
64+
function withdrawMax() external virtual {
65+
// We can delegate the max calculation to the Aave pool by setting amount to type(uint256).max
66+
withdraw(type(uint256).max);
67+
}
68+
69+
function withdrawSurplus(uint256 totalSupply) external {
70+
// totalSupply is always 18 decimals while assetToken and aToken may not
71+
(uint256 normalizedTotalSupply,) = ISuperToken(address(this)).toUnderlyingAmount(totalSupply);
72+
// decrement by 100 in order to give ample of margin for offsetting Aave's potential rounding error
73+
// If there's no surplus, this will simply revert due to arithmetic underflow.
74+
uint256 surplusAmount = A_TOKEN.balanceOf(address(this)) + ASSET_TOKEN.balanceOf(address(this))
75+
- normalizedTotalSupply - 100;
76+
AAVE_POOL.withdraw(address(ASSET_TOKEN), surplusAmount, SURPLUS_RECEIVER);
77+
}
78+
}

0 commit comments

Comments
 (0)