Skip to content

Commit ac48546

Browse files
authored
feat: optional shares validation for round trip properties (#15)
1 parent 6641fa2 commit ac48546

File tree

2 files changed

+26
-12
lines changed

2 files changed

+26
-12
lines changed

ERC4626.prop.sol

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ abstract contract ERC4626Prop is Test {
4747

4848
bool internal _vaultMayBeEmpty;
4949
bool internal _unlimitedAmount;
50+
bool internal _skipRoundTripShares;
5051

5152
//
5253
// asset
@@ -246,7 +247,7 @@ abstract contract ERC4626Prop is Test {
246247
//
247248

248249
// redeem(deposit(a)) <= a
249-
function prop_RT_deposit_redeem(address caller, uint assets) public {
250+
function prop_RT_deposit_redeem(address caller, uint assets) public checkNoFreeProfit(caller) {
250251
if (!_vaultMayBeEmpty) vm.assume(IERC20(_vault_).totalSupply() > 0);
251252
vm.prank(caller); uint shares = vault_deposit(assets, caller);
252253
vm.prank(caller); uint assets2 = vault_redeem(shares, caller, caller);
@@ -256,51 +257,51 @@ abstract contract ERC4626Prop is Test {
256257
// s = deposit(a)
257258
// s' = withdraw(a)
258259
// s' >= s
259-
function prop_RT_deposit_withdraw(address caller, uint assets) public {
260+
function prop_RT_deposit_withdraw(address caller, uint assets) public checkNoFreeProfit(caller) {
260261
if (!_vaultMayBeEmpty) vm.assume(IERC20(_vault_).totalSupply() > 0);
261262
vm.prank(caller); uint shares1 = vault_deposit(assets, caller);
262263
vm.prank(caller); uint shares2 = vault_withdraw(assets, caller, caller);
263-
assertApproxGeAbs(shares2, shares1, _delta_);
264+
if (!_skipRoundTripShares) assertApproxGeAbs(shares2, shares1, _delta_);
264265
}
265266

266267
// deposit(redeem(s)) <= s
267-
function prop_RT_redeem_deposit(address caller, uint shares) public {
268+
function prop_RT_redeem_deposit(address caller, uint shares) public checkNoFreeProfit(caller) {
268269
vm.prank(caller); uint assets = vault_redeem(shares, caller, caller);
269270
if (!_vaultMayBeEmpty) vm.assume(IERC20(_vault_).totalSupply() > 0);
270271
vm.prank(caller); uint shares2 = vault_deposit(assets, caller);
271-
assertApproxLeAbs(shares2, shares, _delta_);
272+
if (!_skipRoundTripShares) assertApproxLeAbs(shares2, shares, _delta_);
272273
}
273274

274275
// a = redeem(s)
275276
// a' = mint(s)
276277
// a' >= a
277-
function prop_RT_redeem_mint(address caller, uint shares) public {
278+
function prop_RT_redeem_mint(address caller, uint shares) public checkNoFreeProfit(caller) {
278279
vm.prank(caller); uint assets1 = vault_redeem(shares, caller, caller);
279280
if (!_vaultMayBeEmpty) vm.assume(IERC20(_vault_).totalSupply() > 0);
280281
vm.prank(caller); uint assets2 = vault_mint(shares, caller);
281282
assertApproxGeAbs(assets2, assets1, _delta_);
282283
}
283284

284285
// withdraw(mint(s)) >= s
285-
function prop_RT_mint_withdraw(address caller, uint shares) public {
286+
function prop_RT_mint_withdraw(address caller, uint shares) public checkNoFreeProfit(caller) {
286287
if (!_vaultMayBeEmpty) vm.assume(IERC20(_vault_).totalSupply() > 0);
287288
vm.prank(caller); uint assets = vault_mint(shares, caller);
288289
vm.prank(caller); uint shares2 = vault_withdraw(assets, caller, caller);
289-
assertApproxGeAbs(shares2, shares, _delta_);
290+
if (!_skipRoundTripShares) assertApproxGeAbs(shares2, shares, _delta_);
290291
}
291292

292293
// a = mint(s)
293294
// a' = redeem(s)
294295
// a' <= a
295-
function prop_RT_mint_redeem(address caller, uint shares) public {
296+
function prop_RT_mint_redeem(address caller, uint shares) public checkNoFreeProfit(caller) {
296297
if (!_vaultMayBeEmpty) vm.assume(IERC20(_vault_).totalSupply() > 0);
297298
vm.prank(caller); uint assets1 = vault_mint(shares, caller);
298299
vm.prank(caller); uint assets2 = vault_redeem(shares, caller, caller);
299300
assertApproxLeAbs(assets2, assets1, _delta_);
300301
}
301302

302303
// mint(withdraw(a)) >= a
303-
function prop_RT_withdraw_mint(address caller, uint assets) public {
304+
function prop_RT_withdraw_mint(address caller, uint assets) public checkNoFreeProfit(caller) {
304305
vm.prank(caller); uint shares = vault_withdraw(assets, caller, caller);
305306
if (!_vaultMayBeEmpty) vm.assume(IERC20(_vault_).totalSupply() > 0);
306307
vm.prank(caller); uint assets2 = vault_mint(shares, caller);
@@ -310,11 +311,22 @@ abstract contract ERC4626Prop is Test {
310311
// s = withdraw(a)
311312
// s' = deposit(a)
312313
// s' <= s
313-
function prop_RT_withdraw_deposit(address caller, uint assets) public {
314+
function prop_RT_withdraw_deposit(address caller, uint assets) public checkNoFreeProfit(caller) {
314315
vm.prank(caller); uint shares1 = vault_withdraw(assets, caller, caller);
315316
if (!_vaultMayBeEmpty) vm.assume(IERC20(_vault_).totalSupply() > 0);
316317
vm.prank(caller); uint shares2 = vault_deposit(assets, caller);
317-
assertApproxLeAbs(shares2, shares1, _delta_);
318+
if (!_skipRoundTripShares) assertApproxLeAbs(shares2, shares1, _delta_);
319+
}
320+
321+
modifier checkNoFreeProfit(address caller) {
322+
uint256 assetsBefore = _getTotalAssets(caller);
323+
_;
324+
uint256 assetsAfter = _getTotalAssets(caller);
325+
assertApproxLeAbs(assetsAfter, assetsBefore, _delta_);
326+
}
327+
328+
function _getTotalAssets(address account) internal returns (uint256) {
329+
return vault_convertToAssets(IERC20(_vault_).balanceOf(account)) + IERC20(_underlying_).balanceOf(account);
318330
}
319331

320332
//

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ contract ERC4626StdTest is ERC4626Test {
7575
_delta_ = 0;
7676
_vaultMayBeEmpty = false;
7777
_unlimitedAmount = false;
78+
_skipRoundTripShares = false;
7879
}
7980
}
8081
```
@@ -85,6 +86,7 @@ Specifically, set the state variables as follows:
8586
- `_delta_`: the maximum approximation error size to be passed to [`assertApproxEqAbs()`]. It must be given as an absolute value (not a percentage) in the smallest unit (e.g., Wei or Satoshi). Note that all the tests are expected to pass with `__delta__ == 0` as long as your vault follows the [preferred rounding direction] as specified in the standard. If your vault doesn't follow the preferred rounding direction, you can set `__delta__` to a reasonable size of rounding errors where the adversarial profit of exploiting such rounding errors stays sufficiently small compared to the gas cost. (You can read our [post] for more about the adversarial profit.)
8687
- `_vaultMayBeEmpty`: when set to false, fuzz inputs that empties the vault are ignored.
8788
- `_unlimitedAmount`: when set to false, fuzz inputs are restricted to the currently available amount from the caller. Limiting the amount can speed up fuzzing, but may miss some edge cases.
89+
- `_skipRoundTripShares`: when set to true, shares inequality assertions in round trip properties are skipped. This is useful for testing vaults where shares comparison may not yield expected results due to implementation-specific behavior (see issue [#13](https://github.com/a16z/erc4626-tests/issues/13) for examples).
8890

8991
[`assertApproxEqAbs()`]: <https://book.getfoundry.sh/reference/forge-std/assertApproxEqAbs>
9092

0 commit comments

Comments
 (0)