Skip to content

Commit db4eaa5

Browse files
StakeWise Reinvestment Extension (#120)
1 parent a607e7a commit db4eaa5

File tree

11 files changed

+528
-14
lines changed

11 files changed

+528
-14
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
Copyright 2022 Index Cooperative.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
16+
SPDX-License-Identifier: Apache License, Version 2.0
17+
*/
18+
19+
pragma solidity 0.6.10;
20+
pragma experimental ABIEncoderV2;
21+
22+
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
23+
import { SafeCast } from "@openzeppelin/contracts/utils/SafeCast.sol";
24+
25+
import { BaseExtension } from "../lib/BaseExtension.sol";
26+
import { PreciseUnitMath } from "../lib/PreciseUnitMath.sol";
27+
import { IBaseManager } from "../interfaces/IBaseManager.sol";
28+
import { ISetToken } from "../interfaces/ISetToken.sol";
29+
import { IAirdropModule } from "../interfaces/IAirdropModule.sol";
30+
import { ITradeModule } from "../interfaces/ITradeModule.sol";
31+
32+
/**
33+
* @title StakeWiseReinvestmentExtension
34+
* @author FlattestWhite
35+
*
36+
* Smart contract that enables reinvesting the accrued rETH2 into a SetToken into sETH2.
37+
*/
38+
contract StakeWiseReinvestmentExtension is BaseExtension {
39+
40+
using Address for address;
41+
using SafeCast for int256;
42+
43+
/* ============ Constants ============= */
44+
45+
address public constant S_ETH2 = 0xFe2e637202056d30016725477c5da089Ab0A043A;
46+
address public constant R_ETH2 = 0x20BC832ca081b91433ff6c17f85701B6e92486c5;
47+
48+
/* ========== Structs ================= */
49+
50+
struct ExecutionSettings {
51+
// Name of the exchange adapter stored in the IntegrationRegistry. Typically the name of the contract
52+
// such as UniswapV3ExchangeAdapterV2.
53+
string exchangeName;
54+
// The callData that needs to be passed along to the exchange adapter. This is usually generated with
55+
// a external view call on the adapter contract using the function generateDataParam.
56+
bytes exchangeCallData;
57+
}
58+
59+
/* ========== State Variables ========= */
60+
61+
ISetToken public immutable setToken;
62+
IAirdropModule public immutable airdropModule;
63+
ITradeModule public immutable tradeModule;
64+
ExecutionSettings public settings;
65+
66+
/* ============ Constructor ============ */
67+
/**
68+
* Sets state variables
69+
*
70+
* @param _manager // The manager contract. Used to invoke calls on the underlying SetToken.
71+
* @param _airdropModule // The airdropModule contract. Used to absorb tokens into the SetToken so that it's part of SetToken's accounting.
72+
* @param _tradeModule // The tradeModule contract. Used to trade the absorbed rETH2 into sETH2.
73+
* @param _settings // Determines which exchange adapter is used to execute the trade through the TradeModule contract.
74+
*/
75+
constructor(
76+
IBaseManager _manager,
77+
IAirdropModule _airdropModule,
78+
ITradeModule _tradeModule,
79+
ExecutionSettings memory _settings
80+
) public BaseExtension(_manager) {
81+
setToken = _manager.setToken();
82+
airdropModule = _airdropModule;
83+
tradeModule = _tradeModule;
84+
settings = _settings;
85+
}
86+
87+
/* ============ External Functions ============ */
88+
89+
/**
90+
* Initializes the extension by:
91+
* 1. Calling initialize on the AirdropModule for the SetToken with required airdrop settings.
92+
* 2. Initializing the TradeModule for the SetToken to allow trading trading with the SetToken.
93+
*/
94+
function initialize() external onlyOperator {
95+
address[] memory tokens = new address[](1);
96+
tokens[0] = R_ETH2;
97+
IAirdropModule.AirdropSettings memory airdropSettings = IAirdropModule.AirdropSettings ({
98+
airdrops: tokens,
99+
feeRecipient: address(setToken),
100+
airdropFee: 0,
101+
anyoneAbsorb: false
102+
});
103+
bytes memory airdropModuleData = abi.encodeWithSelector(airdropModule.initialize.selector, setToken, airdropSettings);
104+
invokeManager(address(airdropModule), airdropModuleData);
105+
106+
bytes memory tradeModuleData = abi.encodeWithSelector(tradeModule.initialize.selector, setToken);
107+
invokeManager(address(tradeModule), tradeModuleData);
108+
}
109+
110+
/**
111+
*
112+
* 1. Absorbs rETH2 into the SetToken
113+
* 2. Trades rETH2 into sETH2 with _minReceivedQuantity
114+
*
115+
* We considered removing the _minReceivedQuantity parameter and storing the slippage parameter as part of
116+
* ExecutionSettings. However, in the event of a black swan event where rETH2 de-pegs, we'd need to updateExecutionSettings
117+
* which would involve a multi-sig txn. Once rETH2 can be redeemed for sETH2 directly, the exchange rate is guaranteed to be at least 1:1.
118+
* Therefore, _minReceiveQuantity can be removed and this function can be made public.
119+
*/
120+
function reinvest(uint256 _minReceiveQuantity) external onlyAllowedCaller(msg.sender) {
121+
bytes memory absorbCallData = abi.encodeWithSelector(
122+
IAirdropModule.absorb.selector,
123+
setToken,
124+
R_ETH2
125+
);
126+
invokeManager(address(airdropModule), absorbCallData);
127+
128+
uint256 rEthUnits = uint256(setToken.getTotalComponentRealUnits(R_ETH2));
129+
require(rEthUnits > 0, "rETH2 units must be greater than zero");
130+
bytes memory tradeCallData = abi.encodeWithSelector(
131+
ITradeModule.trade.selector,
132+
setToken,
133+
settings.exchangeName,
134+
R_ETH2,
135+
rEthUnits,
136+
S_ETH2,
137+
_minReceiveQuantity,
138+
settings.exchangeCallData
139+
);
140+
invokeManager(address(tradeModule), tradeCallData);
141+
}
142+
143+
function updateExecutionSettings(ExecutionSettings memory _settings) external onlyAllowedCaller(msg.sender) {
144+
settings = _settings;
145+
}
146+
}

contracts/interfaces/IAirdropModule.sol

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,40 @@
1515
*/
1616

1717
pragma solidity 0.6.10;
18+
pragma experimental "ABIEncoderV2";
1819

19-
import { ISetToken } from "../interfaces/ISetToken.sol";
20+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
21+
22+
import { AddressArrayUtils } from "../lib/AddressArrayUtils.sol";
23+
import { ISetToken } from "./ISetToken.sol";
2024

2125
interface IAirdropModule {
26+
using AddressArrayUtils for address[];
2227

2328
struct AirdropSettings {
24-
address[] airdrops;
29+
address[] airdrops; // Array of tokens manager is allowing to be absorbed
30+
address feeRecipient; // Address airdrop fees are sent to
31+
uint256 airdropFee; // Percentage in preciseUnits of airdrop sent to feeRecipient (1e16 = 1%)
32+
bool anyoneAbsorb; // Boolean indicating if any address can call absorb or just the manager
33+
}
34+
35+
struct AirdropReturnSettings {
2536
address feeRecipient;
2637
uint256 airdropFee;
2738
bool anyoneAbsorb;
2839
}
29-
40+
41+
function initialize(ISetToken _setToken, AirdropSettings memory _airdropSettings) external;
42+
43+
function airdropSettings(ISetToken _setToken) external view returns(AirdropReturnSettings memory);
3044
function batchAbsorb(ISetToken _setToken, address[] memory _tokens) external;
31-
function getAirdrops(ISetToken _setToken) external view returns (address[] memory);
32-
}
45+
function absorb(ISetToken _setToken, IERC20 _token) external;
46+
function addAirdrop(ISetToken _setToken, IERC20 _airdrop) external;
47+
function removeAirdrop(ISetToken _setToken, IERC20 _airdrop) external;
48+
function updateAnyoneAbsorb(ISetToken _setToken, bool _anyoneAbsorb) external;
49+
function updateFeeRecipient(ISetToken _setToken, address _newFeeRecipient) external;
50+
function updateAirdropFee(ISetToken _setToken, uint256 _newFee) external;
51+
function removeModule() external;
52+
function getAirdrops(ISetToken _setToken) external returns(address[] memory);
53+
function isAirdropToken(ISetToken _setToken, IERC20 _token) external returns(bool);
54+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
Copyright 2022 Index Cooperative.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
pragma solidity 0.6.10;
18+
19+
import { ISetToken } from "../interfaces/ISetToken.sol";
20+
21+
interface ITradeModule {
22+
function initialize(ISetToken _setToken) external;
23+
24+
function trade(
25+
ISetToken _setToken,
26+
string memory _exchangeName,
27+
address _sendToken,
28+
uint256 _sendQuantity,
29+
address _receiveToken,
30+
uint256 _minReceiveQuantity,
31+
bytes memory _data
32+
) external;
33+
}

external/abi/index-protocol/TradeModule.json

Lines changed: 10 additions & 0 deletions
Large diffs are not rendered by default.

hardhat.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const optimismForkingConfig = {
2424

2525
const mainnetForkingConfig = {
2626
url: "https://eth-mainnet.alchemyapi.io/v2/" + process.env.ALCHEMY_TOKEN,
27-
blockNumber: process.env.LATESTBLOCK ? undefined : 15981100,
27+
blockNumber: process.env.LATESTBLOCK ? undefined : 16180859,
2828
};
2929

3030
const forkingConfig =
@@ -49,7 +49,7 @@ const gasOption =
4949
}
5050
: {
5151
gas: 12000000,
52-
blockGasLimit: 12000000,
52+
blockGasLimit: 30000000,
5353
};
5454

5555
const config: HardhatUserConfig = {

test/integration/ethereum/addresses.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export const PRODUCTION_ADDRESSES = {
1313
cUSDC: "0x39aa39c021dfbae8fac545936693ac917d5e7563",
1414
cDAI: "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643",
1515
fixedDai: "0x015558c3aB97c9e5a9c8c437C71Bb498B2e5afB3",
16+
wsETH2: "0x5dA21D9e63F1EA13D34e48B7223bcc97e3ecD687",
17+
rETH2: "0x20BC832ca081b91433ff6c17f85701B6e92486c5",
18+
sETH2: "0xFe2e637202056d30016725477c5da089Ab0A043A",
1619
},
1720
whales: {
1821
stEth: "0xdc24316b9ae028f1497c275eb9192a3ea0f67022",
@@ -52,6 +55,8 @@ export const PRODUCTION_ADDRESSES = {
5255
controller: "0xD2463675a099101E36D85278494268261a66603A",
5356
debtIssuanceModuleV2: "0xa0a98EB7Af028BE00d04e46e1316808A62a8fd59",
5457
notionalTradeModule: "0x600d9950c6ecAef98Cc42fa207E92397A6c43416",
58+
tradeModule: "0xFaAB3F8f3678f68AA0d307B66e71b636F82C28BF",
59+
airdropModule: "0x09b9e7c7e2daf40fCb286fE6b863e517d5d5c40F",
5560
},
5661
lending: {
5762
aave: {

test/integration/ethereum/fixedRebalanceExtension.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ if (process.env.INTEGRATIONTEST) {
7272
);
7373

7474
componentMaturities = await Promise.all(
75-
(await setToken.getComponents()).map((c) => {
75+
(await setToken.getComponents()).map(c => {
7676
const wrappedfCash = IWrappedfCashComplete__factory.connect(c, operator);
7777
return wrappedfCash.getMaturity();
7878
}),
@@ -128,7 +128,7 @@ if (process.env.INTEGRATIONTEST) {
128128
assetToken = addresses.tokens.cDAI;
129129
assetTokenContract = IERC20__factory.connect(assetToken, operator);
130130
const maturitiesMonths = [3, 6];
131-
maturities = maturitiesMonths.map((m) => ONE_MONTH_IN_SECONDS.mul(m));
131+
maturities = maturitiesMonths.map(m => ONE_MONTH_IN_SECONDS.mul(m));
132132
sixMonthAllocation = ether(0.75);
133133
threeMonthAllocation = ether(0.25);
134134
allocations = [threeMonthAllocation, sixMonthAllocation];
@@ -326,7 +326,7 @@ if (process.env.INTEGRATIONTEST) {
326326
});
327327
});
328328

329-
[false, true].forEach((tradeViaUnderlying) => {
329+
[false, true].forEach(tradeViaUnderlying => {
330330
describe(`When trading via the ${
331331
tradeViaUnderlying ? "underlying" : "asset"
332332
} token`, () => {

0 commit comments

Comments
 (0)