-
Notifications
You must be signed in to change notification settings - Fork 26
/
ERC4626.prop.sol
404 lines (345 loc) · 18.6 KB
/
ERC4626.prop.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
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
// SPDX-License-Identifier: AGPL-3.0
pragma solidity >=0.8.0 <0.9.0;
import "forge-std/Test.sol";
// TODO: use interface provided by forge-std v1.0.0 or later
// import {IERC20} from "forge-std/interfaces/IERC20.sol";
interface IERC20 {
event Transfer(address indexed from, address indexed to, uint value);
event Approval(address indexed owner, address indexed spender, uint value);
function totalSupply() external view returns (uint);
function balanceOf(address account) external view returns (uint);
function transfer(address to, uint amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint);
function approve(address spender, uint amount) external returns (bool);
function transferFrom(address from, address to, uint amount) external returns (bool);
}
// TODO: use interface provided by forge-std v1.0.0 or later
// import {IERC4626} from "forge-std/interfaces/IERC4626.sol";
interface IERC4626 is IERC20 {
event Deposit(address indexed caller, address indexed owner, uint assets, uint shares);
event Withdraw(address indexed caller, address indexed receiver, address indexed owner, uint assets, uint shares);
function asset() external view returns (address assetTokenAddress);
function totalAssets() external view returns (uint totalManagedAssets);
function convertToShares(uint assets) external view returns (uint shares);
function convertToAssets(uint shares) external view returns (uint assets);
function maxDeposit(address receiver) external view returns (uint maxAssets);
function previewDeposit(uint assets) external view returns (uint shares);
function deposit(uint assets, address receiver) external returns (uint shares);
function maxMint(address receiver) external view returns (uint maxShares);
function previewMint(uint shares) external view returns (uint assets);
function mint(uint shares, address receiver) external returns (uint assets);
function maxWithdraw(address owner) external view returns (uint maxAssets);
function previewWithdraw(uint assets) external view returns (uint shares);
function withdraw(uint assets, address receiver, address owner) external returns (uint shares);
function maxRedeem(address owner) external view returns (uint maxShares);
function previewRedeem(uint shares) external view returns (uint assets);
function redeem(uint shares, address receiver, address owner) external returns (uint assets);
}
abstract contract ERC4626Prop is Test {
uint internal _delta_;
address internal _underlying_;
address internal _vault_;
bool internal _vaultMayBeEmpty;
bool internal _unlimitedAmount;
//
// asset
//
// asset
// "MUST NOT revert."
function prop_asset(address caller) public {
vm.prank(caller); IERC4626(_vault_).asset();
}
// totalAssets
// "MUST NOT revert."
function prop_totalAssets(address caller) public {
vm.prank(caller); IERC4626(_vault_).totalAssets();
}
//
// convert
//
// convertToShares
// "MUST NOT show any variations depending on the caller."
function prop_convertToShares(address caller1, address caller2, uint assets) public {
vm.prank(caller1); uint res1 = vault_convertToShares(assets); // "MAY revert due to integer overflow caused by an unreasonably large input."
vm.prank(caller2); uint res2 = vault_convertToShares(assets); // "MAY revert due to integer overflow caused by an unreasonably large input."
assertEq(res1, res2);
}
// convertToAssets
// "MUST NOT show any variations depending on the caller."
function prop_convertToAssets(address caller1, address caller2, uint shares) public {
vm.prank(caller1); uint res1 = vault_convertToAssets(shares); // "MAY revert due to integer overflow caused by an unreasonably large input."
vm.prank(caller2); uint res2 = vault_convertToAssets(shares); // "MAY revert due to integer overflow caused by an unreasonably large input."
assertEq(res1, res2);
}
//
// deposit
//
// maxDeposit
// "MUST NOT revert."
function prop_maxDeposit(address caller, address receiver) public {
vm.prank(caller); IERC4626(_vault_).maxDeposit(receiver);
}
// previewDeposit
// "MUST return as close to and no more than the exact amount of Vault
// shares that would be minted in a deposit call in the same transaction.
// I.e. deposit should return the same or more shares as previewDeposit if
// called in the same transaction."
function prop_previewDeposit(address caller, address receiver, address other, uint assets) public {
vm.prank(other); uint sharesPreview = vault_previewDeposit(assets); // "MAY revert due to other conditions that would also cause deposit to revert."
vm.prank(caller); uint sharesActual = vault_deposit(assets, receiver);
assertApproxGeAbs(sharesActual, sharesPreview, _delta_);
}
// deposit
function prop_deposit(address caller, address receiver, uint assets) public {
uint oldCallerAsset = IERC20(_underlying_).balanceOf(caller);
uint oldReceiverShare = IERC20(_vault_).balanceOf(receiver);
uint oldAllowance = IERC20(_underlying_).allowance(caller, _vault_);
vm.prank(caller); uint shares = vault_deposit(assets, receiver);
uint newCallerAsset = IERC20(_underlying_).balanceOf(caller);
uint newReceiverShare = IERC20(_vault_).balanceOf(receiver);
uint newAllowance = IERC20(_underlying_).allowance(caller, _vault_);
assertApproxEqAbs(newCallerAsset, oldCallerAsset - assets, _delta_, "asset"); // NOTE: this may fail if the caller is a contract in which the asset is stored
assertApproxEqAbs(newReceiverShare, oldReceiverShare + shares, _delta_, "share");
if (oldAllowance != type(uint).max) assertApproxEqAbs(newAllowance, oldAllowance - assets, _delta_, "allowance");
}
//
// mint
//
// maxMint
// "MUST NOT revert."
function prop_maxMint(address caller, address receiver) public {
vm.prank(caller); IERC4626(_vault_).maxMint(receiver);
}
// previewMint
// "MUST return as close to and no fewer than the exact amount of assets
// that would be deposited in a mint call in the same transaction. I.e. mint
// should return the same or fewer assets as previewMint if called in the
// same transaction."
function prop_previewMint(address caller, address receiver, address other, uint shares) public {
vm.prank(other); uint assetsPreview = vault_previewMint(shares);
vm.prank(caller); uint assetsActual = vault_mint(shares, receiver);
assertApproxLeAbs(assetsActual, assetsPreview, _delta_);
}
// mint
function prop_mint(address caller, address receiver, uint shares) public {
uint oldCallerAsset = IERC20(_underlying_).balanceOf(caller);
uint oldReceiverShare = IERC20(_vault_).balanceOf(receiver);
uint oldAllowance = IERC20(_underlying_).allowance(caller, _vault_);
vm.prank(caller); uint assets = vault_mint(shares, receiver);
uint newCallerAsset = IERC20(_underlying_).balanceOf(caller);
uint newReceiverShare = IERC20(_vault_).balanceOf(receiver);
uint newAllowance = IERC20(_underlying_).allowance(caller, _vault_);
assertApproxEqAbs(newCallerAsset, oldCallerAsset - assets, _delta_, "asset"); // NOTE: this may fail if the caller is a contract in which the asset is stored
assertApproxEqAbs(newReceiverShare, oldReceiverShare + shares, _delta_, "share");
if (oldAllowance != type(uint).max) assertApproxEqAbs(newAllowance, oldAllowance - assets, _delta_, "allowance");
}
//
// withdraw
//
// maxWithdraw
// "MUST NOT revert."
// NOTE: some implementations failed due to arithmetic overflow
function prop_maxWithdraw(address caller, address owner) public {
vm.prank(caller); IERC4626(_vault_).maxWithdraw(owner);
}
// previewWithdraw
// "MUST return as close to and no fewer than the exact amount of Vault
// shares that would be burned in a withdraw call in the same transaction.
// I.e. withdraw should return the same or fewer shares as previewWithdraw
// if called in the same transaction."
function prop_previewWithdraw(address caller, address receiver, address owner, address other, uint assets) public {
vm.prank(other); uint preview = vault_previewWithdraw(assets);
vm.prank(caller); uint actual = vault_withdraw(assets, receiver, owner);
assertApproxLeAbs(actual, preview, _delta_);
}
// withdraw
function prop_withdraw(address caller, address receiver, address owner, uint assets) public {
uint oldReceiverAsset = IERC20(_underlying_).balanceOf(receiver);
uint oldOwnerShare = IERC20(_vault_).balanceOf(owner);
uint oldAllowance = IERC20(_vault_).allowance(owner, caller);
vm.prank(caller); uint shares = vault_withdraw(assets, receiver, owner);
uint newReceiverAsset = IERC20(_underlying_).balanceOf(receiver);
uint newOwnerShare = IERC20(_vault_).balanceOf(owner);
uint newAllowance = IERC20(_vault_).allowance(owner, caller);
assertApproxEqAbs(newOwnerShare, oldOwnerShare - shares, _delta_, "share");
assertApproxEqAbs(newReceiverAsset, oldReceiverAsset + assets, _delta_, "asset"); // NOTE: this may fail if the receiver is a contract in which the asset is stored
if (caller != owner && oldAllowance != type(uint).max) assertApproxEqAbs(newAllowance, oldAllowance - shares, _delta_, "allowance");
assertTrue(caller == owner || oldAllowance != 0 || (shares == 0 && assets == 0), "access control");
}
//
// redeem
//
// maxRedeem
// "MUST NOT revert."
function prop_maxRedeem(address caller, address owner) public {
vm.prank(caller); IERC4626(_vault_).maxRedeem(owner);
}
// previewRedeem
// "MUST return as close to and no more than the exact amount of assets that
// would be withdrawn in a redeem call in the same transaction. I.e. redeem
// should return the same or more assets as previewRedeem if called in the
// same transaction."
function prop_previewRedeem(address caller, address receiver, address owner, address other, uint shares) public {
vm.prank(other); uint preview = vault_previewRedeem(shares);
vm.prank(caller); uint actual = vault_redeem(shares, receiver, owner);
assertApproxGeAbs(actual, preview, _delta_);
}
// redeem
function prop_redeem(address caller, address receiver, address owner, uint shares) public {
uint oldReceiverAsset = IERC20(_underlying_).balanceOf(receiver);
uint oldOwnerShare = IERC20(_vault_).balanceOf(owner);
uint oldAllowance = IERC20(_vault_).allowance(owner, caller);
vm.prank(caller); uint assets = vault_redeem(shares, receiver, owner);
uint newReceiverAsset = IERC20(_underlying_).balanceOf(receiver);
uint newOwnerShare = IERC20(_vault_).balanceOf(owner);
uint newAllowance = IERC20(_vault_).allowance(owner, caller);
assertApproxEqAbs(newOwnerShare, oldOwnerShare - shares, _delta_, "share");
assertApproxEqAbs(newReceiverAsset, oldReceiverAsset + assets, _delta_, "asset"); // NOTE: this may fail if the receiver is a contract in which the asset is stored
if (caller != owner && oldAllowance != type(uint).max) assertApproxEqAbs(newAllowance, oldAllowance - shares, _delta_, "allowance");
assertTrue(caller == owner || oldAllowance != 0 || (shares == 0 && assets == 0), "access control");
}
//
// round trip properties
//
// redeem(deposit(a)) <= a
function prop_RT_deposit_redeem(address caller, uint assets) public {
if (!_vaultMayBeEmpty) vm.assume(IERC20(_vault_).totalSupply() > 0);
vm.prank(caller); uint shares = vault_deposit(assets, caller);
vm.prank(caller); uint assets2 = vault_redeem(shares, caller, caller);
assertApproxLeAbs(assets2, assets, _delta_);
}
// s = deposit(a)
// s' = withdraw(a)
// s' >= s
function prop_RT_deposit_withdraw(address caller, uint assets) public {
if (!_vaultMayBeEmpty) vm.assume(IERC20(_vault_).totalSupply() > 0);
vm.prank(caller); uint shares1 = vault_deposit(assets, caller);
vm.prank(caller); uint shares2 = vault_withdraw(assets, caller, caller);
assertApproxGeAbs(shares2, shares1, _delta_);
}
// deposit(redeem(s)) <= s
function prop_RT_redeem_deposit(address caller, uint shares) public {
vm.prank(caller); uint assets = vault_redeem(shares, caller, caller);
if (!_vaultMayBeEmpty) vm.assume(IERC20(_vault_).totalSupply() > 0);
vm.prank(caller); uint shares2 = vault_deposit(assets, caller);
assertApproxLeAbs(shares2, shares, _delta_);
}
// a = redeem(s)
// a' = mint(s)
// a' >= a
function prop_RT_redeem_mint(address caller, uint shares) public {
vm.prank(caller); uint assets1 = vault_redeem(shares, caller, caller);
if (!_vaultMayBeEmpty) vm.assume(IERC20(_vault_).totalSupply() > 0);
vm.prank(caller); uint assets2 = vault_mint(shares, caller);
assertApproxGeAbs(assets2, assets1, _delta_);
}
// withdraw(mint(s)) >= s
function prop_RT_mint_withdraw(address caller, uint shares) public {
if (!_vaultMayBeEmpty) vm.assume(IERC20(_vault_).totalSupply() > 0);
vm.prank(caller); uint assets = vault_mint(shares, caller);
vm.prank(caller); uint shares2 = vault_withdraw(assets, caller, caller);
assertApproxGeAbs(shares2, shares, _delta_);
}
// a = mint(s)
// a' = redeem(s)
// a' <= a
function prop_RT_mint_redeem(address caller, uint shares) public {
if (!_vaultMayBeEmpty) vm.assume(IERC20(_vault_).totalSupply() > 0);
vm.prank(caller); uint assets1 = vault_mint(shares, caller);
vm.prank(caller); uint assets2 = vault_redeem(shares, caller, caller);
assertApproxLeAbs(assets2, assets1, _delta_);
}
// mint(withdraw(a)) >= a
function prop_RT_withdraw_mint(address caller, uint assets) public {
vm.prank(caller); uint shares = vault_withdraw(assets, caller, caller);
if (!_vaultMayBeEmpty) vm.assume(IERC20(_vault_).totalSupply() > 0);
vm.prank(caller); uint assets2 = vault_mint(shares, caller);
assertApproxGeAbs(assets2, assets, _delta_);
}
// s = withdraw(a)
// s' = deposit(a)
// s' <= s
function prop_RT_withdraw_deposit(address caller, uint assets) public {
vm.prank(caller); uint shares1 = vault_withdraw(assets, caller, caller);
if (!_vaultMayBeEmpty) vm.assume(IERC20(_vault_).totalSupply() > 0);
vm.prank(caller); uint shares2 = vault_deposit(assets, caller);
assertApproxLeAbs(shares2, shares1, _delta_);
}
//
// utils
//
function vault_convertToShares(uint assets) internal returns (uint) {
return _call_vault(abi.encodeWithSelector(IERC4626.convertToShares.selector, assets));
}
function vault_convertToAssets(uint shares) internal returns (uint) {
return _call_vault(abi.encodeWithSelector(IERC4626.convertToAssets.selector, shares));
}
function vault_maxDeposit(address receiver) internal returns (uint) {
return _call_vault(abi.encodeWithSelector(IERC4626.maxDeposit.selector, receiver));
}
function vault_maxMint(address receiver) internal returns (uint) {
return _call_vault(abi.encodeWithSelector(IERC4626.maxMint.selector, receiver));
}
function vault_maxWithdraw(address owner) internal returns (uint) {
return _call_vault(abi.encodeWithSelector(IERC4626.maxWithdraw.selector, owner));
}
function vault_maxRedeem(address owner) internal returns (uint) {
return _call_vault(abi.encodeWithSelector(IERC4626.maxRedeem.selector, owner));
}
function vault_previewDeposit(uint assets) internal returns (uint) {
return _call_vault(abi.encodeWithSelector(IERC4626.previewDeposit.selector, assets));
}
function vault_previewMint(uint shares) internal returns (uint) {
return _call_vault(abi.encodeWithSelector(IERC4626.previewMint.selector, shares));
}
function vault_previewWithdraw(uint assets) internal returns (uint) {
return _call_vault(abi.encodeWithSelector(IERC4626.previewWithdraw.selector, assets));
}
function vault_previewRedeem(uint shares) internal returns (uint) {
return _call_vault(abi.encodeWithSelector(IERC4626.previewRedeem.selector, shares));
}
function vault_deposit(uint assets, address receiver) internal returns (uint) {
return _call_vault(abi.encodeWithSelector(IERC4626.deposit.selector, assets, receiver));
}
function vault_mint(uint shares, address receiver) internal returns (uint) {
return _call_vault(abi.encodeWithSelector(IERC4626.mint.selector, shares, receiver));
}
function vault_withdraw(uint assets, address receiver, address owner) internal returns (uint) {
return _call_vault(abi.encodeWithSelector(IERC4626.withdraw.selector, assets, receiver, owner));
}
function vault_redeem(uint shares, address receiver, address owner) internal returns (uint) {
return _call_vault(abi.encodeWithSelector(IERC4626.redeem.selector, shares, receiver, owner));
}
function _call_vault(bytes memory data) internal returns (uint) {
(bool success, bytes memory retdata) = _vault_.call(data);
if (success) return abi.decode(retdata, (uint));
vm.assume(false); // if reverted, discard the current fuzz inputs, and let the fuzzer to start a new fuzz run
return 0; // silence warning
}
function assertApproxGeAbs(uint a, uint b, uint maxDelta) internal {
if (!(a >= b)) {
uint dt = b - a;
if (dt > maxDelta) {
emit log ("Error: a >=~ b not satisfied [uint]");
emit log_named_uint (" Value a", a);
emit log_named_uint (" Value b", b);
emit log_named_uint (" Max Delta", maxDelta);
emit log_named_uint (" Delta", dt);
fail();
}
}
}
function assertApproxLeAbs(uint a, uint b, uint maxDelta) internal {
if (!(a <= b)) {
uint dt = a - b;
if (dt > maxDelta) {
emit log ("Error: a <=~ b not satisfied [uint]");
emit log_named_uint (" Value a", a);
emit log_named_uint (" Value b", b);
emit log_named_uint (" Max Delta", maxDelta);
emit log_named_uint (" Delta", dt);
fail();
}
}
}
}