diff --git a/examples/curve/src/LlamaLendVaultAssertion.sol b/examples/curve/src/LlamaLendVaultAssertion.sol index 30b4757..c65d47b 100644 --- a/examples/curve/src/LlamaLendVaultAssertion.sol +++ b/examples/curve/src/LlamaLendVaultAssertion.sol @@ -25,7 +25,7 @@ contract LlamaLendVaultAssertion is ERC4626PreviewAssertion, LlamaLendVaultProto } /// @notice Registers preview checks plus controller-side accounting and custody checks. - function triggers() external view override { + function triggers() external view virtual override { _registerPreviewTriggers(); registerFnCallTrigger(this.assertDepositPreview.selector, DEPOSIT_DEFAULT); registerFnCallTrigger(this.assertMintPreview.selector, MINT_DEFAULT); @@ -37,6 +37,17 @@ contract LlamaLendVaultAssertion is ERC4626PreviewAssertion, LlamaLendVaultProto registerTxEndTrigger(this.assertControllerCustodyCoversAvailableBalance.selector); } + /// @dev Pinned LlamaLend vault operations return the pre-state previewed amount exactly. + function _maxPreviewDeviation() internal pure override returns (uint256) { + return 0; + } + + /// @dev LlamaLend vault operations transfer the borrowed token directly through the + /// controller; the vault contract itself never holds the ERC-4626 payment inventory. + function _assetCustodyAccount() internal view override returns (address) { + return controller; + } + /// @notice Checks `totalAssets()` equals controller available balance plus debt minus admin fees. function assertTotalAssetsMatchesControllerAccounting() external { PhEvm.ForkId memory fork = _postTx(); diff --git a/examples/spark/src/SparkVaultAssertion.sol b/examples/spark/src/SparkVaultAssertion.sol index cae2d6c..f0afbc8 100644 --- a/examples/spark/src/SparkVaultAssertion.sol +++ b/examples/spark/src/SparkVaultAssertion.sol @@ -25,10 +25,7 @@ import {SparkVaultHelpers} from "./SparkVaultHelpers.sol"; /// Beyond ERC-4626, Spark's savings-rate model requires mutating accrual paths to fully /// settle pending `chi` growth for the current block, while `take()` must only move /// liquidity and `assetsOutstanding()` without changing liabilities or rate state. -contract SparkVaultAssertion is - ERC4626PreviewAssertion, - SparkVaultHelpers -{ +contract SparkVaultAssertion is ERC4626PreviewAssertion, SparkVaultHelpers { /// @param vault_ Spark vault instance whose selectors this bundle will monitor. constructor(address vault_, address asset_) ERC4626BaseAssertion(vault_, asset_) { registerAssertionSpec(AssertionSpec.Reshiram); @@ -42,13 +39,18 @@ contract SparkVaultAssertion is /// state forks." The inherited `_register*Triggers()` cover the standard /// ERC-4626 invariants; the `_registerSpark*Triggers()` helpers below extend /// that wiring to Spark's non-standard surfaces. - function triggers() external view override { + function triggers() external view virtual override { _registerPreviewTriggers(); _registerSparkReferralOverloadTriggers(); _registerSparkRateAccumulationTriggers(); _registerSparkManagedLiquidityTriggers(); } + /// @dev Pinned Spark vault operations return the pre-state previewed amount exactly. + function _maxPreviewDeviation() internal pure override returns (uint256) { + return 0; + } + /// @notice Reuses the inherited share-price and preview assertions against Spark's /// referral-enabled `deposit`/`mint` overloads. /// @dev The referral forms share the leading `(assets, receiver)` / `(shares, receiver)` diff --git a/src/protection/vault/ERC4626PreviewAssertion.sol b/src/protection/vault/ERC4626PreviewAssertion.sol index 4b7add4..63c2707 100644 --- a/src/protection/vault/ERC4626PreviewAssertion.sol +++ b/src/protection/vault/ERC4626PreviewAssertion.sol @@ -23,12 +23,9 @@ import {ERC4626BaseAssertion} from "./ERC4626BaseAssertion.sol"; /// previewWithdraw rounds UP (returns more shares -> favors vault) /// previewRedeem rounds DOWN (returns fewer assets -> favors vault) /// -/// @dev Uses V2 `registerFnCallTrigger` + `ph.context()` for call-scoped triggers, -/// `ph.callinputAt()` to read call arguments, and `ph.callOutputAt()` to read the -/// actual return value — replacing the totalSupply/totalAssets delta inference from V1. -/// ERC-4626 specifies the conservative direction of previews, but it does not set a generic -/// maximum distance from the state-changing result. Concrete vaults may override -/// `_maxPreviewDeviation()` when their implementation proves a tighter bound. +/// @dev In addition to return values, every operation proves the corresponding receiver/owner +/// share delta, total-supply delta, and underlying-token movement. ERC-4626 does not define a +/// universal preview-distance bound, so every concrete supported adapter must provide one. abstract contract ERC4626PreviewAssertion is ERC4626BaseAssertion { /// @notice Register the default trigger set for preview-consistency invariants. /// @dev Each ERC-4626 operation gets its own assertion function via registerFnCallTrigger. @@ -40,10 +37,15 @@ abstract contract ERC4626PreviewAssertion is ERC4626BaseAssertion { } /// @notice Maximum acceptable deviation between a preview result and the actual result. - /// @dev Defaults to no generic distance cap. Override only when the concrete vault's - /// implementation proves a smaller maximum. - function _maxPreviewDeviation() internal view virtual returns (uint256) { - return type(uint256).max; + /// @dev Must be derived from the concrete vault implementation; no generic default is sound. + function _maxPreviewDeviation() internal view virtual returns (uint256); + + /// @notice Account whose underlying-token balance reflects ERC-4626 payments and payouts. + /// @dev Standard vaults custody assets themselves. Managed-custody adapters such as + /// LlamaLend must override this with the controller that actually receives and sends the + /// underlying token. + function _assetCustodyAccount() internal view virtual returns (address) { + return vault; } // --------------------------------------------------------------- @@ -55,20 +57,31 @@ abstract contract ERC4626PreviewAssertion is ERC4626BaseAssertion { /// actualSharesMinted - previewDeposit(assets) <= maxDeviation function assertDepositPreview() external { PhEvm.TriggerContext memory ctx = ph.context(); - _requireVaultConfigurationAt(_preCall(ctx.callStart)); + PhEvm.ForkId memory pre = _preCall(ctx.callStart); + PhEvm.ForkId memory post = _postCall(ctx.callEnd); + _requireVaultConfigurationAt(pre); bytes memory input = ph.callinputAt(ctx.callStart); uint256 assets = _firstUint256Arg(input); + address receiver = _receiver(input, ctx); + address payer = _triggerCaller(ctx); // Preview at pre-call state - uint256 previewShares = - _readUintAt(vault, abi.encodeCall(IERC4626.previewDeposit, (assets)), _preCall(ctx.callStart)); + uint256 previewShares = _readUintAt(vault, abi.encodeCall(IERC4626.previewDeposit, (assets)), pre); // Actual return value: deposit returns shares minted uint256 actualShares = abi.decode(ph.callOutputAt(ctx.callStart), (uint256)); require(previewShares <= actualShares, "ERC4626: previewDeposit > actual shares"); require(actualShares - previewShares <= _maxPreviewDeviation(), "ERC4626: deposit preview deviates from actual"); + _requireIncrease( + _shareBalanceAt(receiver, pre), + _shareBalanceAt(receiver, post), + actualShares, + "ERC4626: deposit receiver shares mismatch" + ); + _requireIncrease(_totalSupplyAt(pre), _totalSupplyAt(post), actualShares, "ERC4626: deposit supply mismatch"); + _requirePaymentEffects(payer, _assetCustodyAccount(), assets, pre, post, "deposit"); } // --------------------------------------------------------------- @@ -80,19 +93,30 @@ abstract contract ERC4626PreviewAssertion is ERC4626BaseAssertion { /// previewMint(shares) - actualAssetsCharged <= maxDeviation function assertMintPreview() external { PhEvm.TriggerContext memory ctx = ph.context(); - _requireVaultConfigurationAt(_preCall(ctx.callStart)); + PhEvm.ForkId memory pre = _preCall(ctx.callStart); + PhEvm.ForkId memory post = _postCall(ctx.callEnd); + _requireVaultConfigurationAt(pre); bytes memory input = ph.callinputAt(ctx.callStart); uint256 shares = _firstUint256Arg(input); + address receiver = _receiver(input, ctx); + address payer = _triggerCaller(ctx); - uint256 previewAssets = - _readUintAt(vault, abi.encodeCall(IERC4626.previewMint, (shares)), _preCall(ctx.callStart)); + uint256 previewAssets = _readUintAt(vault, abi.encodeCall(IERC4626.previewMint, (shares)), pre); // Actual return value: mint returns assets charged uint256 actualAssets = abi.decode(ph.callOutputAt(ctx.callStart), (uint256)); require(previewAssets >= actualAssets, "ERC4626: previewMint < actual assets"); require(previewAssets - actualAssets <= _maxPreviewDeviation(), "ERC4626: mint preview deviates from actual"); + _requireIncrease( + _shareBalanceAt(receiver, pre), + _shareBalanceAt(receiver, post), + shares, + "ERC4626: mint receiver shares mismatch" + ); + _requireIncrease(_totalSupplyAt(pre), _totalSupplyAt(post), shares, "ERC4626: mint supply mismatch"); + _requirePaymentEffects(payer, _assetCustodyAccount(), actualAssets, pre, post, "mint"); } // --------------------------------------------------------------- @@ -104,13 +128,15 @@ abstract contract ERC4626PreviewAssertion is ERC4626BaseAssertion { /// previewWithdraw(assets) - actualSharesBurned <= maxDeviation function assertWithdrawPreview() external { PhEvm.TriggerContext memory ctx = ph.context(); - _requireVaultConfigurationAt(_preCall(ctx.callStart)); + PhEvm.ForkId memory pre = _preCall(ctx.callStart); + PhEvm.ForkId memory post = _postCall(ctx.callEnd); + _requireVaultConfigurationAt(pre); bytes memory input = ph.callinputAt(ctx.callStart); uint256 assets = _firstUint256Arg(input); + (address receiver, address owner) = _withdrawAccounts(input, ctx); - uint256 previewShares = - _readUintAt(vault, abi.encodeCall(IERC4626.previewWithdraw, (assets)), _preCall(ctx.callStart)); + uint256 previewShares = _readUintAt(vault, abi.encodeCall(IERC4626.previewWithdraw, (assets)), pre); // Actual return value: withdraw returns shares burned uint256 actualShares = abi.decode(ph.callOutputAt(ctx.callStart), (uint256)); @@ -119,6 +145,14 @@ abstract contract ERC4626PreviewAssertion is ERC4626BaseAssertion { require( previewShares - actualShares <= _maxPreviewDeviation(), "ERC4626: withdraw preview deviates from actual" ); + _requireDecrease( + _shareBalanceAt(owner, pre), + _shareBalanceAt(owner, post), + actualShares, + "ERC4626: withdraw owner shares mismatch" + ); + _requireDecrease(_totalSupplyAt(pre), _totalSupplyAt(post), actualShares, "ERC4626: withdraw supply mismatch"); + _requirePayoutEffects(_assetCustodyAccount(), receiver, assets, pre, post, "withdraw"); } // --------------------------------------------------------------- @@ -130,19 +164,26 @@ abstract contract ERC4626PreviewAssertion is ERC4626BaseAssertion { /// actualAssetsReturned - previewRedeem(shares) <= maxDeviation function assertRedeemPreview() external { PhEvm.TriggerContext memory ctx = ph.context(); - _requireVaultConfigurationAt(_preCall(ctx.callStart)); + PhEvm.ForkId memory pre = _preCall(ctx.callStart); + PhEvm.ForkId memory post = _postCall(ctx.callEnd); + _requireVaultConfigurationAt(pre); bytes memory input = ph.callinputAt(ctx.callStart); uint256 shares = _firstUint256Arg(input); + (address receiver, address owner) = _withdrawAccounts(input, ctx); - uint256 previewAssets = - _readUintAt(vault, abi.encodeCall(IERC4626.previewRedeem, (shares)), _preCall(ctx.callStart)); + uint256 previewAssets = _readUintAt(vault, abi.encodeCall(IERC4626.previewRedeem, (shares)), pre); // Actual return value: redeem returns assets returned uint256 actualAssets = abi.decode(ph.callOutputAt(ctx.callStart), (uint256)); require(previewAssets <= actualAssets, "ERC4626: previewRedeem > actual assets"); require(actualAssets - previewAssets <= _maxPreviewDeviation(), "ERC4626: redeem preview deviates from actual"); + _requireDecrease( + _shareBalanceAt(owner, pre), _shareBalanceAt(owner, post), shares, "ERC4626: redeem owner shares mismatch" + ); + _requireDecrease(_totalSupplyAt(pre), _totalSupplyAt(post), shares, "ERC4626: redeem supply mismatch"); + _requirePayoutEffects(_assetCustodyAccount(), receiver, actualAssets, pre, post, "redeem"); } // --------------------------------------------------------------- @@ -167,4 +208,102 @@ abstract contract ERC4626PreviewAssertion is ERC4626BaseAssertion { value := mload(add(input, 36)) } } + + function _receiver(bytes memory input, PhEvm.TriggerContext memory ctx) internal view returns (address) { + return input.length >= 68 ? _addressArg(input, 1) : _triggerCaller(ctx); + } + + function _withdrawAccounts(bytes memory input, PhEvm.TriggerContext memory ctx) + internal + view + returns (address receiver, address owner) + { + if (input.length >= 100) return (_addressArg(input, 1), _addressArg(input, 2)); + address caller = _triggerCaller(ctx); + receiver = input.length >= 68 ? _addressArg(input, 1) : caller; + owner = caller; + } + + function _triggerCaller(PhEvm.TriggerContext memory ctx) internal view returns (address) { + PhEvm.CallFilter memory filter = PhEvm.CallFilter({ + callType: 0, minDepth: 0, maxDepth: type(uint32).max, topLevelOnly: false, successOnly: true + }); + PhEvm.TriggerCall[] memory calls = ph.matchingCalls(vault, ctx.selector, filter, 8); + for (uint256 i; i < calls.length; ++i) { + if (calls[i].callId == ctx.callStart) return calls[i].caller; + } + revert("ERC4626: triggered call not found"); + } + + function _requirePaymentEffects( + address payer, + address custody, + uint256 amount, + PhEvm.ForkId memory pre, + PhEvm.ForkId memory post, + string memory operation + ) internal view { + uint256 payerBefore = _assetBalanceAt(payer, pre); + uint256 payerAfter = _assetBalanceAt(payer, post); + if (payer == custody) { + require(payerAfter == payerBefore, string.concat("ERC4626: ", operation, " asset payment mismatch")); + return; + } + _requireIncrease( + _assetBalanceAt(custody, pre), + _assetBalanceAt(custody, post), + amount, + string.concat("ERC4626: ", operation, " asset payment mismatch") + ); + _requireDecrease( + payerBefore, payerAfter, amount, string.concat("ERC4626: ", operation, " payer assets mismatch") + ); + } + + function _requirePayoutEffects( + address custody, + address receiver, + uint256 amount, + PhEvm.ForkId memory pre, + PhEvm.ForkId memory post, + string memory operation + ) internal view { + uint256 custodyBefore = _assetBalanceAt(custody, pre); + uint256 custodyAfter = _assetBalanceAt(custody, post); + if (custody == receiver) { + require(custodyAfter == custodyBefore, string.concat("ERC4626: ", operation, " asset payout mismatch")); + return; + } + _requireDecrease( + custodyBefore, custodyAfter, amount, string.concat("ERC4626: ", operation, " vault assets mismatch") + ); + _requireIncrease( + _assetBalanceAt(receiver, pre), + _assetBalanceAt(receiver, post), + amount, + string.concat("ERC4626: ", operation, " receiver assets mismatch") + ); + } + + function _addressArg(bytes memory input, uint256 index) internal pure returns (address value) { + uint256 offset = 36 + index * 32; + require(input.length >= 4 + (index + 1) * 32, "ERC4626Preview: address arg missing"); + assembly ("memory-safe") { + value := mload(add(input, offset)) + } + } + + function _requireIncrease(uint256 beforeValue, uint256 afterValue, uint256 amount, string memory reason) + internal + pure + { + require(afterValue >= beforeValue && afterValue - beforeValue == amount, reason); + } + + function _requireDecrease(uint256 beforeValue, uint256 afterValue, uint256 amount, string memory reason) + internal + pure + { + require(beforeValue >= afterValue && beforeValue - afterValue == amount, reason); + } } diff --git a/src/protection/vault/examples/MetaMorphoVaultAssertion.sol b/src/protection/vault/examples/MetaMorphoVaultAssertion.sol index 1307f90..617892b 100644 --- a/src/protection/vault/examples/MetaMorphoVaultAssertion.sol +++ b/src/protection/vault/examples/MetaMorphoVaultAssertion.sol @@ -24,7 +24,12 @@ contract MetaMorphoVaultAssertion is ERC4626PreviewAssertion { /// @notice Registers the ERC-4626 selectors against the MetaMorpho-safe assertion set. /// @dev MetaMorpho-specific managed-asset and loss accounting must be installed separately. - function triggers() external view override { + function triggers() external view virtual override { _registerPreviewTriggers(); } + + /// @dev Pinned MetaMorpho operations return the same pre-state previewed amount. + function _maxPreviewDeviation() internal pure override returns (uint256) { + return 0; + } } diff --git a/test/protection/vault/ERC4626PreviewAssertion.t.sol b/test/protection/vault/ERC4626PreviewAssertion.t.sol new file mode 100644 index 0000000..b4bb8a2 --- /dev/null +++ b/test/protection/vault/ERC4626PreviewAssertion.t.sol @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import {ERC20Mock} from "openzeppelin-contracts/contracts/mocks/token/ERC20Mock.sol"; + +import {CredibleTest} from "../../../src/CredibleTest.sol"; +import {AssertionSpec} from "../../../src/SpecRecorder.sol"; +import {ERC4626BaseAssertion} from "../../../src/protection/vault/ERC4626BaseAssertion.sol"; +import {ERC4626PreviewAssertion} from "../../../src/protection/vault/ERC4626PreviewAssertion.sol"; +import {MetaMorphoVaultAssertion} from "../../../src/protection/vault/examples/MetaMorphoVaultAssertion.sol"; +import {LlamaLendVaultAssertion} from "../../../examples/curve/src/LlamaLendVaultAssertion.sol"; +import {SparkVaultAssertion} from "../../../examples/spark/src/SparkVaultAssertion.sol"; + +contract PreviewAssetCustodian { + function release(ERC20Mock token, address receiver, uint256 amount) external { + token.transfer(receiver, amount); + } +} + +contract PreviewEffectsVault is ERC20 { + ERC20Mock public immutable underlying; + address public immutable custodian; + bool public skipShares; + bool public skipAssets; + address public paymentSource; + address public extraChargeRecipient; + + constructor(ERC20Mock underlying_, address custodian_) ERC20("Vault Share", "VS") { + underlying = underlying_; + custodian = custodian_ == address(0) ? address(this) : custodian_; + } + + function setFaults(bool skipShares_, bool skipAssets_) external { + skipShares = skipShares_; + skipAssets = skipAssets_; + } + + function setPaymentFaults(address paymentSource_, address extraChargeRecipient_) external { + paymentSource = paymentSource_; + extraChargeRecipient = extraChargeRecipient_; + } + + function asset() external view returns (address) { + return address(underlying); + } + + function totalAssets() external view returns (uint256) { + return underlying.balanceOf(address(this)); + } + + function previewDeposit(uint256 assets) external pure returns (uint256) { + return assets; + } + + function previewMint(uint256 shares) external pure returns (uint256) { + return shares; + } + + function previewWithdraw(uint256 assets) external pure returns (uint256) { + return assets; + } + + function previewRedeem(uint256 shares) external pure returns (uint256) { + return shares; + } + + function deposit(uint256 assets, address receiver) external returns (uint256 shares) { + if (!skipAssets) _collect(assets); + if (!skipShares) _mint(receiver, assets); + return assets; + } + + function mint(uint256 shares, address receiver) external returns (uint256 assets) { + if (!skipAssets) _collect(shares); + if (!skipShares) _mint(receiver, shares); + return shares; + } + + function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares) { + if (!skipShares) _burn(owner, assets); + if (!skipAssets) _release(receiver, assets); + return assets; + } + + function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets) { + if (!skipShares) _burn(owner, shares); + if (!skipAssets) _release(receiver, shares); + return shares; + } + + function _release(address receiver, uint256 amount) internal { + if (custodian == address(this)) underlying.transfer(receiver, amount); + else PreviewAssetCustodian(custodian).release(underlying, receiver, amount); + } + + function _collect(uint256 amount) internal { + address payer = paymentSource == address(0) ? msg.sender : paymentSource; + underlying.transferFrom(payer, custodian, amount); + if (extraChargeRecipient != address(0)) underlying.transferFrom(payer, extraChargeRecipient, amount); + } +} + +contract PreviewEffectsAssertion is ERC4626PreviewAssertion { + address internal immutable custody; + + constructor(address vault_, address asset_, address custody_) ERC4626BaseAssertion(vault_, asset_) { + custody = custody_; + registerAssertionSpec(AssertionSpec.Reshiram); + } + + function triggers() external view override { + _registerPreviewTriggers(); + } + + function _maxPreviewDeviation() internal pure override returns (uint256) { + return 0; + } + + function _assetCustodyAccount() internal view override returns (address) { + return custody; + } +} + +contract PreviewMetaMorphoHarness is MetaMorphoVaultAssertion { + constructor(address vault_, address asset_) MetaMorphoVaultAssertion(vault_, asset_) {} + + function triggers() external view override { + _registerPreviewTriggers(); + } +} + +contract PreviewSparkHarness is SparkVaultAssertion { + constructor(address vault_, address asset_) SparkVaultAssertion(vault_, asset_) {} + + function triggers() external view override { + _registerPreviewTriggers(); + } +} + +contract PreviewLlamaHarness is LlamaLendVaultAssertion { + constructor(address vault_, address asset_, address controller_) + LlamaLendVaultAssertion(vault_, asset_, controller_) + {} + + function triggers() external view override { + _registerPreviewTriggers(); + } +} + +contract ERC4626PreviewAssertionTest is Test, CredibleTest { + ERC20Mock internal asset; + PreviewEffectsVault internal vault; + address internal receiver = makeAddr("receiver"); + + function setUp() public { + asset = new ERC20Mock(); + vault = new PreviewEffectsVault(asset, address(0)); + asset.mint(address(this), 1_000 ether); + asset.approve(address(vault), type(uint256).max); + } + + function _arm(bytes4 selector) internal { + bytes memory createData = abi.encodePacked( + type(PreviewEffectsAssertion).creationCode, abi.encode(address(vault), address(asset), vault.custodian()) + ); + cl.assertion(address(vault), createData, selector); + } + + function testDepositProvesAllStateEffects() public { + _arm(ERC4626PreviewAssertion.assertDepositPreview.selector); + vault.deposit(100 ether, receiver); + } + + function testMintProvesAllStateEffects() public { + _arm(ERC4626PreviewAssertion.assertMintPreview.selector); + vault.mint(100 ether, receiver); + } + + function testWithdrawProvesAllStateEffects() public { + vault.deposit(100 ether, address(this)); + _arm(ERC4626PreviewAssertion.assertWithdrawPreview.selector); + vault.withdraw(40 ether, receiver, address(this)); + } + + function testRedeemProvesAllStateEffects() public { + vault.deposit(100 ether, address(this)); + _arm(ERC4626PreviewAssertion.assertRedeemPreview.selector); + vault.redeem(40 ether, receiver, address(this)); + } + + function testDepositSupportsExternalAssetCustody() public { + PreviewAssetCustodian externalCustodian = new PreviewAssetCustodian(); + vault = new PreviewEffectsVault(asset, address(externalCustodian)); + asset.approve(address(vault), type(uint256).max); + + _arm(ERC4626PreviewAssertion.assertDepositPreview.selector); + vault.deposit(100 ether, receiver); + assertEq(asset.balanceOf(address(externalCustodian)), 100 ether); + } + + function testWithdrawSupportsExternalAssetCustody() public { + PreviewAssetCustodian externalCustodian = new PreviewAssetCustodian(); + vault = new PreviewEffectsVault(asset, address(externalCustodian)); + asset.approve(address(vault), type(uint256).max); + vault.deposit(100 ether, address(this)); + + _arm(ERC4626PreviewAssertion.assertWithdrawPreview.selector); + vault.withdraw(40 ether, receiver, address(this)); + assertEq(asset.balanceOf(address(externalCustodian)), 60 ether); + } + + function testFavorableReturnWithoutSharesTrips() public { + vault.setFaults(true, true); + _arm(ERC4626PreviewAssertion.assertDepositPreview.selector); + vm.expectRevert(bytes("ERC4626: deposit receiver shares mismatch")); + vault.deposit(100 ether, receiver); + } + + function testMissingShareEffectTrips() public { + vault.setFaults(true, false); + _arm(ERC4626PreviewAssertion.assertDepositPreview.selector); + vm.expectRevert(bytes("ERC4626: deposit receiver shares mismatch")); + vault.deposit(100 ether, receiver); + } + + function testMissingAssetEffectTrips() public { + vault.setFaults(false, true); + _arm(ERC4626PreviewAssertion.assertDepositPreview.selector); + vm.expectRevert(bytes("ERC4626: deposit asset payment mismatch")); + vault.deposit(100 ether, receiver); + } + + function testDepositOverchargeTripsEvenWhenCustodyReceivesExpectedAmount() public { + vault.setPaymentFaults(address(0), makeAddr("diversion")); + _arm(ERC4626PreviewAssertion.assertDepositPreview.selector); + vm.expectRevert(bytes("ERC4626: deposit payer assets mismatch")); + vault.deposit(100 ether, receiver); + } + + function testDepositWrongPayerTrips() public { + address wrongPayer = makeAddr("wrongPayer"); + asset.mint(wrongPayer, 100 ether); + vm.prank(wrongPayer); + asset.approve(address(vault), type(uint256).max); + vault.setPaymentFaults(wrongPayer, address(0)); + _arm(ERC4626PreviewAssertion.assertDepositPreview.selector); + vm.expectRevert(bytes("ERC4626: deposit payer assets mismatch")); + vault.deposit(100 ether, receiver); + } + + function testMintMissingShareEffectTrips() public { + vault.setFaults(true, false); + _arm(ERC4626PreviewAssertion.assertMintPreview.selector); + vm.expectRevert(bytes("ERC4626: mint receiver shares mismatch")); + vault.mint(100 ether, receiver); + } + + function testMintMissingAssetEffectTrips() public { + vault.setFaults(false, true); + _arm(ERC4626PreviewAssertion.assertMintPreview.selector); + vm.expectRevert(bytes("ERC4626: mint asset payment mismatch")); + vault.mint(100 ether, receiver); + } + + function testWithdrawMissingShareEffectTrips() public { + vault.deposit(100 ether, address(this)); + vault.setFaults(true, false); + _arm(ERC4626PreviewAssertion.assertWithdrawPreview.selector); + vm.expectRevert(bytes("ERC4626: withdraw owner shares mismatch")); + vault.withdraw(40 ether, receiver, address(this)); + } + + function testWithdrawMissingAssetEffectTrips() public { + vault.deposit(100 ether, address(this)); + vault.setFaults(false, true); + _arm(ERC4626PreviewAssertion.assertWithdrawPreview.selector); + vm.expectRevert(bytes("ERC4626: withdraw vault assets mismatch")); + vault.withdraw(40 ether, receiver, address(this)); + } + + function testRedeemMissingShareEffectTrips() public { + vault.deposit(100 ether, address(this)); + vault.setFaults(true, false); + _arm(ERC4626PreviewAssertion.assertRedeemPreview.selector); + vm.expectRevert(bytes("ERC4626: redeem owner shares mismatch")); + vault.redeem(40 ether, receiver, address(this)); + } + + function testRedeemMissingAssetEffectTrips() public { + vault.deposit(100 ether, address(this)); + vault.setFaults(false, true); + _arm(ERC4626PreviewAssertion.assertRedeemPreview.selector); + vm.expectRevert(bytes("ERC4626: redeem vault assets mismatch")); + vault.redeem(40 ether, receiver, address(this)); + } + + function testWithdrawAllowsReceiverToEqualCustody() public { + vault.deposit(100 ether, address(this)); + _arm(ERC4626PreviewAssertion.assertWithdrawPreview.selector); + vault.withdraw(40 ether, address(vault), address(this)); + } + + function testRedeemAllowsReceiverToEqualCustody() public { + vault.deposit(100 ether, address(this)); + _arm(ERC4626PreviewAssertion.assertRedeemPreview.selector); + vault.redeem(40 ether, address(vault), address(this)); + } + + function testMetaMorphoAdapterHonestAndIncorrectEffects() public { + bytes memory createData = + abi.encodePacked(type(PreviewMetaMorphoHarness).creationCode, abi.encode(address(vault), address(asset))); + cl.assertion(address(vault), createData, ERC4626PreviewAssertion.assertDepositPreview.selector); + vault.deposit(10 ether, receiver); + + vault.setFaults(false, true); + cl.assertion(address(vault), createData, ERC4626PreviewAssertion.assertDepositPreview.selector); + vm.expectRevert(bytes("ERC4626: deposit asset payment mismatch")); + vault.deposit(10 ether, receiver); + } + + function testSparkAdapterHonestAndIncorrectEffects() public { + bytes memory createData = + abi.encodePacked(type(PreviewSparkHarness).creationCode, abi.encode(address(vault), address(asset))); + cl.assertion(address(vault), createData, ERC4626PreviewAssertion.assertMintPreview.selector); + vault.mint(10 ether, receiver); + + vault.setFaults(true, false); + cl.assertion(address(vault), createData, ERC4626PreviewAssertion.assertMintPreview.selector); + vm.expectRevert(bytes("ERC4626: mint receiver shares mismatch")); + vault.mint(10 ether, receiver); + } + + function testLlamaAdapterUsesControllerCustodyForHonestAndIncorrectEffects() public { + PreviewAssetCustodian controller = new PreviewAssetCustodian(); + vault = new PreviewEffectsVault(asset, address(controller)); + asset.approve(address(vault), type(uint256).max); + bytes memory createData = abi.encodePacked( + type(PreviewLlamaHarness).creationCode, abi.encode(address(vault), address(asset), address(controller)) + ); + cl.assertion(address(vault), createData, ERC4626PreviewAssertion.assertDepositPreview.selector); + vault.deposit(10 ether, receiver); + assertEq(asset.balanceOf(address(controller)), 10 ether); + + vault.setFaults(false, true); + cl.assertion(address(vault), createData, ERC4626PreviewAssertion.assertDepositPreview.selector); + vm.expectRevert(bytes("ERC4626: deposit asset payment mismatch")); + vault.deposit(10 ether, receiver); + } +}