-
Notifications
You must be signed in to change notification settings - Fork 6
/
wfCashERC4626.sol
269 lines (236 loc) · 10.6 KB
/
wfCashERC4626.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import "./wfCashLogic.sol";
import "../interfaces/IERC4626.sol";
contract wfCashERC4626 is IERC4626, wfCashLogic {
constructor(INotionalV2 _notional, WETH9 _weth) wfCashLogic(_notional, _weth) {}
/** @dev See {IERC4626-asset} */
function asset() public view override returns (address) {
(IERC20 underlyingToken, bool isETH) = getToken(true);
return isETH ? address(WETH) : address(underlyingToken);
}
/**
* @notice Although not explicitly required by ERC4626 standards, this totalAssets method
* is expected to be manipulation resistant because it queries an internal Notional V2 TWAP
* of the fCash interest rate. This means that the value here along with `convertToAssets`
* and `convertToShares` can be used as an on-chain price oracle.
*
* If the wrapper is holding a cash balance prior to maturity, the total value of assets held
* by the contract will exceed what is returned by this function. The value of the excess value
* should never be accessible by Wrapped fCash holders due to the redemption mechanism, therefore
* the lower reported value is correct.
*
* @dev See {IERC4626-totalAssets}
*/
function totalAssets() public view override returns (uint256 pvExternal) {
(/* */, pvExternal) = _getPresentCashValue(totalSupply());
}
/** @dev See {IERC4626-convertToShares} */
function convertToShares(uint256 assets) public view override returns (uint256 shares) {
uint256 supply = totalSupply();
if (supply == 0) {
// Scales assets by the value of a single unit of fCash
(/* */, uint256 unitfCashValue) = _getPresentCashValue(uint256(Constants.INTERNAL_TOKEN_PRECISION));
return (assets * uint256(Constants.INTERNAL_TOKEN_PRECISION)) / unitfCashValue;
}
return (assets * supply) / totalAssets();
}
/** @dev See {IERC4626-convertToAssets} */
function convertToAssets(uint256 shares) public view override returns (uint256 assets) {
uint256 supply = totalSupply();
if (supply == 0) {
// Catch the edge case where totalSupply causes a divide by zero error
(/* */, assets) = _getPresentCashValue(shares);
} else {
assets = (shares * totalAssets()) / supply;
}
}
/** @dev See {IERC4626-maxDeposit} */
function maxDeposit(address) external view override returns (uint256) {
return hasMatured() ? 0 : type(uint256).max;
}
/** @dev See {IERC4626-maxMint} */
function maxMint(address) external view override returns (uint256) {
return hasMatured() ? 0 : type(uint88).max;
}
/** @dev See {IERC4626-maxWithdraw} */
function maxWithdraw(address owner) external view override returns (uint256) {
return previewRedeem(balanceOf(owner));
}
/** @dev See {IERC4626-maxRedeem} */
function maxRedeem(address owner) external view override returns (uint256) {
return balanceOf(owner);
}
/** @dev See {IERC4626-previewDeposit} */
function _previewDeposit(uint256 assets) internal view returns (uint256 shares, uint256 maxFCash) {
if (hasMatured()) return (0, 0);
// This is how much fCash received from depositing assets
(uint16 currencyId, uint40 maturity) = getDecodedID();
(/* */, maxFCash) = getTotalFCashAvailable();
// This method reverts when lending cannot successfully occur.
try NotionalV2.getfCashLendFromDeposit(
currencyId,
assets,
maturity,
0,
block.timestamp,
true
) returns (uint88 s, uint8, bytes32) {
shares = s;
} catch {
(/* */, int256 precision) = getUnderlyingToken();
require(precision > 0);
// In this case, will lend at zero which is 1-1 with assets.
shares = assets * uint256(Constants.INTERNAL_TOKEN_PRECISION) / uint256(precision);
}
}
function previewDeposit(uint256 assets) public view override returns (uint256 shares) {
(shares, /* */) = _previewDeposit(assets);
}
/** @dev See {IERC4626-previewMint} */
function _previewMint(uint256 shares) internal view returns (uint256 assets, uint256 maxFCash) {
if (hasMatured()) return (0, 0);
// This is how much fCash received from depositing assets
(uint16 currencyId, uint40 maturity) = getDecodedID();
(/* */, maxFCash) = getTotalFCashAvailable();
if (maxFCash < shares) {
(/* */, int256 precision) = getUnderlyingToken();
require(precision > 0);
// Lending at zero interest means that 1 fCash unit is equivalent to 1 asset unit
assets = shares * uint256(precision) / uint256(Constants.INTERNAL_TOKEN_PRECISION);
} else {
// This method will round up when calculating the depositAmountUnderlying (happens inside
// CalculationViews._convertToAmountExternal).
(assets, /* */, /* */, /* */) = NotionalV2.getDepositFromfCashLend(
currencyId,
shares,
maturity,
0,
block.timestamp
);
}
}
function previewMint(uint256 shares) public view override returns (uint256 assets) {
(assets, /* */) = _previewMint(shares);
}
/** @dev See {IERC4626-previewWithdraw} */
function previewWithdraw(uint256 assets) public view override returns (uint256 shares) {
if (assets == 0) return 0;
// Although the ERC4626 standard suggests that shares is rounded up in this calculation,
// it would not have much of an effect for wrapped fCash in practice. The actual amount
// of assets returned to the user is not dictated by the `assets` parameter supplied here
// but is actually calculated inside _burnInternal (rounding against the user) when fCash
// has matured or inside the NotionalV2 AMM when withdrawing fCash early. In either case,
// the number of shares returned here will be burned exactly and the user will receive close
// to the assets input, but not exactly.
if (hasMatured()) {
shares = convertToShares(assets);
} else {
// If withdrawing non-matured assets, we sell them on the market (i.e. borrow)
(uint16 currencyId, uint40 maturity) = getDecodedID();
(shares, /* */, /* */) = NotionalV2.getfCashBorrowFromPrincipal(
currencyId,
assets,
maturity,
0,
block.timestamp,
true
);
}
}
/** @dev See {IERC4626-previewRedeem} */
function previewRedeem(uint256 shares) public view override returns (uint256 assets) {
if (shares == 0) return 0;
if (hasMatured()) {
assets = convertToAssets(shares);
} else {
// If withdrawing non-matured assets, we sell them on the market (i.e. borrow)
(uint16 currencyId, uint40 maturity) = getDecodedID();
(assets, /* */, /* */, /* */) = NotionalV2.getPrincipalFromfCashBorrow(
currencyId,
shares,
maturity,
0,
block.timestamp
);
}
}
/** @dev See {IERC4626-deposit} */
function deposit(uint256 assets, address receiver) external override returns (uint256) {
(uint256 shares, uint256 maxFCash) = _previewDeposit(assets);
// Short circuit zero shares minted as well as matured fCash
if (shares == 0) return 0;
// This returns unused assets, since Notional always performs the equivalent of a
// mint action, excess assets are turned to the msg.sender. In that case, the "assets"
// passed into this function are not actually used to mint shares, actually slightly less
// shares are minted than the amount of "assets" passed in.
_mintInternal(assets, _safeUint88(shares), receiver, 0, maxFCash);
emit Deposit(msg.sender, receiver, assets, shares);
return shares;
}
/** @dev See {IERC4626-mint} */
function mint(uint256 shares, address receiver) external override returns (uint256) {
(uint256 assets, uint256 maxFCash) = _previewMint(shares);
// Will revert if matured
_mintInternal(assets, _safeUint88(shares), receiver, 0, maxFCash);
emit Deposit(msg.sender, receiver, assets, shares);
return assets;
}
/** @dev See {IERC4626-withdraw} */
function withdraw(
uint256 assets,
address receiver,
address owner
) external override returns (uint256) {
// This is a noop if the account has already been settled, cheaper to call this than cache
// it locally in storage.
NotionalV2.settleAccount(address(this));
// Attempts to calculate how many shares are required to withdraw assets, however,
// _redeemInternal will always return to the receiver how much assets are raised by
// selling shares. This means that receiver may receive slightly less than the value
// of assets passed in.
uint256 shares = previewWithdraw(assets);
if (msg.sender != owner) {
_spendAllowance(owner, msg.sender, shares);
}
_redeemInternal(shares, receiver, owner);
emit Withdraw(msg.sender, receiver, owner, assets, shares);
return shares;
}
/** @dev See {IERC4626-redeem} */
function redeem(
uint256 shares,
address receiver,
address owner
) external override returns (uint256) {
// It is more accurate and gas efficient to check the balance of the
// receiver here than rely on the previewRedeem method.
IERC20 token = IERC20(asset());
uint256 balanceBefore = token.balanceOf(receiver);
if (msg.sender != owner) {
_spendAllowance(owner, msg.sender, shares);
}
_redeemInternal(shares, receiver, owner);
uint256 balanceAfter = token.balanceOf(receiver);
uint256 assets = balanceAfter - balanceBefore;
emit Withdraw(msg.sender, receiver, owner, assets, shares);
return assets;
}
function _redeemInternal(
uint256 shares,
address receiver,
address owner
) private {
_burnInternal(
owner,
shares,
RedeemOpts({
redeemToUnderlying: true,
transferfCash: false,
receiver: receiver,
// No slippage protecting in ERC4626 natively
minUnderlyingOut: 0
})
);
}
}