Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 24 additions & 12 deletions ERC4626.prop.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ abstract contract ERC4626Prop is Test {

bool internal _vaultMayBeEmpty;
bool internal _unlimitedAmount;
bool internal _skipRoundTripShares;

//
// asset
Expand Down Expand Up @@ -246,7 +247,7 @@ abstract contract ERC4626Prop is Test {
//

// redeem(deposit(a)) <= a
function prop_RT_deposit_redeem(address caller, uint assets) public {
function prop_RT_deposit_redeem(address caller, uint assets) public checkNoFreeProfit(caller) {
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);
Expand All @@ -256,51 +257,51 @@ abstract contract ERC4626Prop is Test {
// s = deposit(a)
// s' = withdraw(a)
// s' >= s
function prop_RT_deposit_withdraw(address caller, uint assets) public {
function prop_RT_deposit_withdraw(address caller, uint assets) public checkNoFreeProfit(caller) {
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_);
if (!_skipRoundTripShares) assertApproxGeAbs(shares2, shares1, _delta_);
}

// deposit(redeem(s)) <= s
function prop_RT_redeem_deposit(address caller, uint shares) public {
function prop_RT_redeem_deposit(address caller, uint shares) public checkNoFreeProfit(caller) {
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_);
if (!_skipRoundTripShares) assertApproxLeAbs(shares2, shares, _delta_);
}

// a = redeem(s)
// a' = mint(s)
// a' >= a
function prop_RT_redeem_mint(address caller, uint shares) public {
function prop_RT_redeem_mint(address caller, uint shares) public checkNoFreeProfit(caller) {
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 {
function prop_RT_mint_withdraw(address caller, uint shares) public checkNoFreeProfit(caller) {
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_);
if (!_skipRoundTripShares) assertApproxGeAbs(shares2, shares, _delta_);
}

// a = mint(s)
// a' = redeem(s)
// a' <= a
function prop_RT_mint_redeem(address caller, uint shares) public {
function prop_RT_mint_redeem(address caller, uint shares) public checkNoFreeProfit(caller) {
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 {
function prop_RT_withdraw_mint(address caller, uint assets) public checkNoFreeProfit(caller) {
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);
Expand All @@ -310,11 +311,22 @@ abstract contract ERC4626Prop is Test {
// s = withdraw(a)
// s' = deposit(a)
// s' <= s
function prop_RT_withdraw_deposit(address caller, uint assets) public {
function prop_RT_withdraw_deposit(address caller, uint assets) public checkNoFreeProfit(caller) {
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_);
if (!_skipRoundTripShares) assertApproxLeAbs(shares2, shares1, _delta_);
}

modifier checkNoFreeProfit(address caller) {
uint256 assetsBefore = _getTotalAssets(caller);
_;
uint256 assetsAfter = _getTotalAssets(caller);
assertApproxLeAbs(assetsAfter, assetsBefore, _delta_);
}

function _getTotalAssets(address account) internal returns (uint256) {
return vault_convertToAssets(IERC20(_vault_).balanceOf(account)) + IERC20(_underlying_).balanceOf(account);
}

//
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ contract ERC4626StdTest is ERC4626Test {
_delta_ = 0;
_vaultMayBeEmpty = false;
_unlimitedAmount = false;
_skipRoundTripShares = false;
}
}
```
Expand All @@ -85,6 +86,7 @@ Specifically, set the state variables as follows:
- `_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.)
- `_vaultMayBeEmpty`: when set to false, fuzz inputs that empties the vault are ignored.
- `_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.
- `_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).

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

Expand Down