diff --git a/src/CowEvcCollateralSwapWrapper.sol b/src/CowEvcCollateralSwapWrapper.sol new file mode 100644 index 0000000..bb4e1f8 --- /dev/null +++ b/src/CowEvcCollateralSwapWrapper.sol @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8; + +import {IEVC} from "evc/EthereumVaultConnector.sol"; + +import {CowWrapper, ICowSettlement} from "./CowWrapper.sol"; +import {IERC20} from "euler-vault-kit/src/EVault/IEVault.sol"; +import {SafeERC20Lib} from "euler-vault-kit/src/EVault/shared/lib/SafeERC20Lib.sol"; +import {PreApprovedHashes} from "./PreApprovedHashes.sol"; + +/// @title CowEvcCollateralSwapWrapper +/// @notice A specialized wrapper for swapping collateral between vaults with EVC +/// @dev This wrapper enables atomic collateral swaps: +/// 1. Enable new collateral vault +/// 2. Transfer collateral from EVC subaccount to main account (if using subaccount) +/// 3. Execute settlement to swap collateral (new collateral is deposited directly into user's account) +/// All operations are atomic within EVC batch +contract CowEvcCollateralSwapWrapper is CowWrapper, PreApprovedHashes { + IEVC public immutable EVC; + + /// @dev The EIP-712 domain type hash used for computing the domain + /// separator. + bytes32 private constant DOMAIN_TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + /// @dev The EIP-712 domain name used for computing the domain separator. + bytes32 private constant DOMAIN_NAME = keccak256("CowEvcCollateralSwapWrapper"); + + /// @dev The EIP-712 domain version used for computing the domain separator. + bytes32 private constant DOMAIN_VERSION = keccak256("1"); + + /// @dev The marker value for a sell order for computing the order struct + /// hash. This allows the EIP-712 compatible wallets to display a + /// descriptive string for the order kind (instead of 0 or 1). + /// + /// This value is pre-computed from the following expression: + /// ``` + /// keccak256("sell") + /// ``` + bytes32 private constant KIND_SELL = hex"f3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775"; + + /// @dev The OrderKind marker value for a buy order for computing the order + /// struct hash. + /// + /// This value is pre-computed from the following expression: + /// ``` + /// keccak256("buy") + /// ``` + bytes32 private constant KIND_BUY = hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc"; + + /// @dev The domain separator used for signing orders that gets mixed in + /// making signatures for different domains incompatible. This domain + /// separator is computed following the EIP-712 standard and has replay + /// protection mixed in so that signed orders are only valid for specific + /// this contract. + bytes32 public immutable DOMAIN_SEPARATOR; + + //// @dev The EVC nonce namespace to use when calling `EVC.permit` to authorize this contract. + uint256 public immutable NONCE_NAMESPACE; + + /// @dev A descriptive label for this contract, as required by CowWrapper + string public override name = "Euler EVC - Collateral Swap"; + + /// @dev Indicates that the current operation cannot be completed with the given msgSender + error Unauthorized(address msgSender); + + /// @dev Indicates that the pre-approved hash is no longer able to be executed because the block timestamp is too old + error OperationDeadlineExceeded(uint256 validToTimestamp, uint256 currentTimestamp); + + /// @dev Indicates that the collateral swap cannot be executed because the necessary pricing data is not present in the `tokens`/`clearingPrices` variable + error PricesNotFoundInSettlement(address fromVault, address toVault); + + /// @dev Indicates that a user attempted to interact with an account that is not their own + error SubaccountMustBeControlledByOwner(address subaccount, address owner); + + /// @dev Emitted when collateral is swapped via this wrapper + event CowEvcCollateralSwapped( + address indexed owner, + address account, + address indexed fromVault, + address indexed toVault, + uint256 swapAmount, + bytes32 kind + ); + + constructor(address _evc, ICowSettlement _settlement) CowWrapper(_settlement) { + EVC = IEVC(_evc); + NONCE_NAMESPACE = uint256(uint160(address(this))); + + DOMAIN_SEPARATOR = + keccak256(abi.encode(DOMAIN_TYPE_HASH, DOMAIN_NAME, DOMAIN_VERSION, block.chainid, address(this))); + } + + /** + * @notice A command to swap collateral between vaults + * @dev This structure is used, combined with domain separator, to indicate a pre-approved hash. + * the `deadline` is used for deduplication checking, so be careful to ensure this value is unique. + */ + struct CollateralSwapParams { + /** + * @dev The ethereum address that has permission to operate upon the account + */ + address owner; + + /** + * @dev The subaccount to swap collateral from. Learn more about Euler subaccounts https://evc.wtf/docs/concepts/internals/sub-accounts + */ + address account; + + /** + * @dev A date by which this operation must be completed + */ + uint256 deadline; + + /** + * @dev The source collateral vault (what we're swapping from) + */ + address fromVault; + + /** + * @dev The destination collateral vault (what we're swapping to) + */ + address toVault; + + /** + * @dev The amount of collateral to swap from the source vault + */ + uint256 swapAmount; + + /** + * @dev Effectively determines whether this is an exactIn or exactOut order. Must be either KIND_BUY or KIND_SELL as defined in GPv2Order. Should be the same as whats in the actual order. + */ + bytes32 kind; + } + + function _parseCollateralSwapParams(bytes calldata wrapperData) + internal + pure + returns (CollateralSwapParams memory params, bytes memory signature, bytes calldata remainingWrapperData) + { + (params, signature) = abi.decode(wrapperData, (CollateralSwapParams, bytes)); + + // Calculate consumed bytes for abi.encode(CollateralSwapParams, bytes) + // Structure: + // - 32 bytes: offset to params (0x40) + // - 32 bytes: offset to signature + // - 224 bytes: params data (7 fields × 32 bytes) + // - 32 bytes: signature length + // - N bytes: signature data (padded to 32-byte boundary) + uint256 consumed = 224 + 64 + ((signature.length + 31) & ~uint256(31)); + + remainingWrapperData = wrapperData[consumed:]; + } + + /// @notice Helper function to compute the hash that would be approved + /// @param params The CollateralSwapParams to hash + /// @return The hash of the signed calldata for these params + function getApprovalHash(CollateralSwapParams memory params) external view returns (bytes32) { + return _getApprovalHash(params); + } + + function _getApprovalHash(CollateralSwapParams memory params) internal view returns (bytes32 digest) { + bytes32 structHash; + bytes32 separator = DOMAIN_SEPARATOR; + assembly ("memory-safe") { + structHash := keccak256(params, 224) + let ptr := mload(0x40) + mstore(ptr, "\x19\x01") + mstore(add(ptr, 0x02), separator) + mstore(add(ptr, 0x22), structHash) + digest := keccak256(ptr, 0x42) + } + } + + function parseWrapperData(bytes calldata wrapperData) + external + pure + override + returns (bytes calldata remainingWrapperData) + { + (,, remainingWrapperData) = _parseCollateralSwapParams(wrapperData); + } + + function getSignedCalldata(CollateralSwapParams memory params) external view returns (bytes memory) { + return abi.encodeCall(IEVC.batch, _getSignedCalldata(params)); + } + + function _getSignedCalldata(CollateralSwapParams memory params) + internal + view + returns (IEVC.BatchItem[] memory items) + { + items = new IEVC.BatchItem[](1); + + // Enable the destination collateral vault for the account + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: address(0), + targetContract: address(EVC), + value: 0, + data: abi.encodeCall(IEVC.enableCollateral, (params.account, params.toVault)) + }); + } + + /// @notice Implementation of GPv2Wrapper._wrap - executes EVC operations to swap collateral + /// @param settleData Data which will be used for the parameters in a call to `CowSettlement.settle` + /// @param wrapperData Additional data containing CollateralSwapParams + function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData) + internal + override + { + // Decode wrapper data into CollateralSwapParams + CollateralSwapParams memory params; + bytes memory signature; + (params, signature,) = _parseCollateralSwapParams(wrapperData); + + // Check if the signed calldata hash is pre-approved + IEVC.BatchItem[] memory signedItems = _getSignedCalldata(params); + bool isPreApproved = signature.length == 0 && _consumePreApprovedHash(params.owner, _getApprovalHash(params)); + + // Build the EVC batch items for swapping collateral + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](isPreApproved ? signedItems.length + 1 : 2); + + uint256 itemIndex = 0; + + // 1. There are two ways this contract can be executed: either the user approves this contract as + // an operator and supplies a pre-approved hash for the operation to take, or they submit a permit hash + // for this specific instance + if (!isPreApproved) { + items[itemIndex++] = IEVC.BatchItem({ + onBehalfOfAccount: address(0), + targetContract: address(EVC), + value: 0, + data: abi.encodeCall( + IEVC.permit, + ( + params.owner, + address(this), + uint256(NONCE_NAMESPACE), + EVC.getNonce(bytes19(bytes20(params.owner)), NONCE_NAMESPACE), + params.deadline, + 0, + abi.encodeCall(EVC.batch, signedItems), + signature + ) + ) + }); + } else { + require(params.deadline >= block.timestamp, OperationDeadlineExceeded(params.deadline, block.timestamp)); + // copy the operations to execute. we can operate on behalf of the user directly + for (; itemIndex < signedItems.length; itemIndex++) { + items[itemIndex] = signedItems[itemIndex]; + } + } + + // 2. Settlement call + items[itemIndex] = IEVC.BatchItem({ + onBehalfOfAccount: address(this), + targetContract: address(this), + value: 0, + data: abi.encodeCall(this.evcInternalSwap, (settleData, wrapperData, remainingWrapperData)) + }); + + // 3. Account status check (automatically done by EVC at end of batch) + // For more info, see: https://evc.wtf/docs/concepts/internals/account-status-checks + // No explicit item needed - EVC handles this + + // Execute all items in a single batch + EVC.batch(items); + + emit CowEvcCollateralSwapped( + params.owner, params.account, params.fromVault, params.toVault, params.swapAmount, params.kind + ); + } + + function _findRatePrices(bytes calldata settleData, address fromVault, address toVault) + internal + pure + returns (uint256 fromVaultPrice, uint256 toVaultPrice) + { + (address[] memory tokens, uint256[] memory clearingPrices,,) = + abi.decode(settleData[4:], (address[], uint256[], ICowSettlement.Trade[], ICowSettlement.Interaction[][3])); + for (uint256 i = 0; i < tokens.length; i++) { + if (tokens[i] == fromVault) { + fromVaultPrice = clearingPrices[i]; + } else if (tokens[i] == toVault) { + toVaultPrice = clearingPrices[i]; + } + } + require(fromVaultPrice != 0 && toVaultPrice != 0, PricesNotFoundInSettlement(fromVault, toVault)); + } + + /// @notice Internal swap function called by EVC + function evcInternalSwap( + bytes calldata settleData, + bytes calldata wrapperData, + bytes calldata remainingWrapperData + ) external payable { + require(msg.sender == address(EVC), Unauthorized(msg.sender)); + (address onBehalfOfAccount,) = EVC.getCurrentOnBehalfOfAccount(address(0)); + require(onBehalfOfAccount == address(this), Unauthorized(onBehalfOfAccount)); + + CollateralSwapParams memory params; + (params,,) = _parseCollateralSwapParams(wrapperData); + _evcInternalSwap(settleData, remainingWrapperData, params); + } + + function _evcInternalSwap( + bytes calldata settleData, + bytes calldata remainingWrapperData, + CollateralSwapParams memory params + ) internal { + // If a subaccount is being used, we need to transfer the required amount of collateral for the trade into the owner's wallet. + // This is required becuase the settlement contract can only pull funds from the wallet that signed the transaction. + // Since its not possible for a subaccount to sign a transaction due to the private key not existing and their being no + // contract deployed to the subaccount address, transferring to the owner's account is the only option. + // Additionally, we don't transfer this collateral directly to the settlement contract because the settlement contract + // requires receiving of funds from the user's wallet, and cannot be put in the contract in advance. + if (params.owner != params.account) { + require( + bytes19(bytes20(params.owner)) == bytes19(bytes20(params.account)), + SubaccountMustBeControlledByOwner(params.account, params.owner) + ); + + uint256 transferAmount = params.swapAmount; + + if (params.kind == KIND_BUY) { + (uint256 fromVaultPrice, uint256 toVaultPrice) = + _findRatePrices(settleData, params.fromVault, params.toVault); + transferAmount = params.swapAmount * toVaultPrice / fromVaultPrice; + } + + SafeERC20Lib.safeTransferFrom( + IERC20(params.fromVault), params.account, params.owner, transferAmount, address(0) + ); + } + + // Use GPv2Wrapper's _next to call the settlement contract + // wrapperData is empty since we've already processed it in _wrap + _next(settleData, remainingWrapperData); + } +} diff --git a/test/CowEvcCollateralSwapWrapper.t.sol b/test/CowEvcCollateralSwapWrapper.t.sol new file mode 100644 index 0000000..a1f3db8 --- /dev/null +++ b/test/CowEvcCollateralSwapWrapper.t.sol @@ -0,0 +1,601 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8; + +import {GPv2Order} from "cow/libraries/GPv2Order.sol"; + +import {IEVC} from "evc/EthereumVaultConnector.sol"; +import {IEVault, IERC4626, IERC20} from "euler-vault-kit/src/EVault/IEVault.sol"; + +import {CowEvcCollateralSwapWrapper} from "../src/CowEvcCollateralSwapWrapper.sol"; +import {ICowSettlement, CowWrapper} from "../src/CowWrapper.sol"; +import {GPv2AllowListAuthentication} from "cow/GPv2AllowListAuthentication.sol"; + +import {CowBaseTest} from "./helpers/CowBaseTest.sol"; +import {SignerECDSA} from "./helpers/SignerECDSA.sol"; + +/// @title E2E Test for CowEvcCollateralSwapWrapper +/// @notice Tests the full flow of swapping collateral between vaults +contract CowEvcCollateralSwapWrapperTest is CowBaseTest { + CowEvcCollateralSwapWrapper public collateralSwapWrapper; + SignerECDSA internal ecdsa; + + uint256 constant SUSDS_MARGIN = 2000e18; + uint256 constant DEFAULT_SWAP_AMOUNT = 500e18; + uint256 constant DEFAULT_BUY_AMOUNT = 0.0045e8; + + function setUp() public override { + super.setUp(); + + // Deploy the collateral swap wrapper + collateralSwapWrapper = new CowEvcCollateralSwapWrapper(address(EVC), COW_SETTLEMENT); + + // Add wrapper as a solver + GPv2AllowListAuthentication allowList = GPv2AllowListAuthentication(address(COW_SETTLEMENT.authenticator())); + address manager = allowList.manager(); + vm.startPrank(manager); + allowList.addSolver(address(collateralSwapWrapper)); + vm.stopPrank(); + + ecdsa = new SignerECDSA(EVC); + + // sUSDS is not currently a collateral for WETH borrow, fix it + vm.startPrank(IEVault(EWETH).governorAdmin()); + IEVault(EWETH).setLTV(ESUSDS, 0.9e4, 0.9e4, 0); + vm.stopPrank(); + + // WBTC is not currently a collateral for WETH borrow, fix it + vm.startPrank(IEVault(EWETH).governorAdmin()); + IEVault(EWETH).setLTV(EWBTC, 0.9e4, 0.9e4, 0); + vm.stopPrank(); + + // Setup user with SUSDS + deal(SUSDS, user, 10000e18); + + // User has approved WBTC for COW Protocol + address vaultRelayer = COW_SETTLEMENT.vaultRelayer(); + vm.prank(user); + IERC20(WBTC).approve(vaultRelayer, type(uint256).max); + } + + struct SettlementData { + bytes orderUid; + GPv2Order.Data orderData; + address[] tokens; + uint256[] clearingPrices; + ICowSettlement.Trade[] trades; + ICowSettlement.Interaction[][3] interactions; + } + + /// @notice Create default CollateralSwapParams for testing + function _createDefaultParams(address owner, address account) + internal + view + returns (CowEvcCollateralSwapWrapper.CollateralSwapParams memory) + { + return CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: owner, + account: account, + deadline: block.timestamp + 1 hours, + fromVault: ESUSDS, + toVault: EWBTC, + swapAmount: DEFAULT_SWAP_AMOUNT, + kind: GPv2Order.KIND_SELL + }); + } + + /// @notice Create permit signature for EVC operator + function _createPermitSignature(CowEvcCollateralSwapWrapper.CollateralSwapParams memory params) + internal + returns (bytes memory) + { + ecdsa.setPrivateKey(privateKey); + return ecdsa.signPermit( + params.owner, + address(collateralSwapWrapper), + uint256(uint160(address(collateralSwapWrapper))), + 0, + params.deadline, + 0, + collateralSwapWrapper.getSignedCalldata(params) + ); + } + + /// @notice Create permit signature for any user + function _createPermitSignatureFor( + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params, + uint256 userPrivateKey + ) internal returns (bytes memory) { + ecdsa.setPrivateKey(userPrivateKey); + return ecdsa.signPermit( + params.owner, + address(collateralSwapWrapper), + uint256(uint160(address(collateralSwapWrapper))), + 0, + params.deadline, + 0, + collateralSwapWrapper.getSignedCalldata(params) + ); + } + + /// @notice Encode wrapper data with length prefix + function _encodeWrapperData(CowEvcCollateralSwapWrapper.CollateralSwapParams memory params, bytes memory signature) + internal + pure + returns (bytes memory) + { + bytes memory wrapperData = abi.encode(params, signature); + return abi.encodePacked(uint16(wrapperData.length), wrapperData); + } + + /// @notice Execute wrapped settlement through solver + function _executeWrappedSettlement(bytes memory settleData, bytes memory wrapperData) internal { + address[] memory targets = new address[](1); + bytes[] memory datas = new bytes[](1); + targets[0] = address(collateralSwapWrapper); + datas[0] = abi.encodeCall(collateralSwapWrapper.wrappedSettle, (settleData, wrapperData)); + solver.runBatch(targets, datas); + } + + /// @notice Setup user approvals for collateral swap on subaccount + function _setupSubaccountApprovals(CowEvcCollateralSwapWrapper.CollateralSwapParams memory params) internal { + vm.startPrank(params.owner); + + // Approve vault shares from main account for settlement + IEVault(params.fromVault).approve(COW_SETTLEMENT.vaultRelayer(), type(uint256).max); + + // Approve transfer of vault shares from the subaccount to wrapper + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](1); + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: params.account, + targetContract: params.fromVault, + value: 0, + data: abi.encodeCall(IERC20.approve, (address(collateralSwapWrapper), type(uint256).max)) + }); + EVC.batch(items); + + // Set wrapper as operator for the subaccount + EVC.setAccountOperator(params.account, address(collateralSwapWrapper), true); + + // Pre-approve the operation hash + bytes32 hash = collateralSwapWrapper.getApprovalHash(params); + collateralSwapWrapper.setPreApprovedHash(hash, true); + + vm.stopPrank(); + } + + /// @notice Create settlement data for swapping collateral between vaults + /// @dev Sells vault shares from one vault to buy shares in another + function getCollateralSwapSettlement( + address owner, + address receiver, + address sellVaultToken, + address buyVaultToken, + uint256 sellAmount, + uint256 buyAmount + ) public returns (SettlementData memory r) { + uint32 validTo = uint32(block.timestamp + 1 hours); + + // Get tokens and prices + r.tokens = new address[](2); + r.tokens[0] = sellVaultToken; + r.tokens[1] = buyVaultToken; + + r.clearingPrices = new uint256[](2); + r.clearingPrices[0] = milkSwap.prices(IERC4626(sellVaultToken).asset()); + r.clearingPrices[1] = milkSwap.prices(IERC4626(buyVaultToken).asset()) * 1 ether / 0.98 ether; + + // Get trade data + r.trades = new ICowSettlement.Trade[](1); + (r.trades[0], r.orderData, r.orderUid) = + setupCowOrder(r.tokens, 0, 1, sellAmount, buyAmount, validTo, owner, receiver, false); + + // Setup interactions - withdraw from sell vault, swap underlying assets, deposit to buy vault + r.interactions = [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](4), + new ICowSettlement.Interaction[](0) + ]; + + // Withdraw from sell vault + r.interactions[1][0] = getWithdrawInteraction(sellVaultToken, sellAmount); + + // Swap underlying assets + uint256 swapAmount = sellAmount * 0.999 ether / 1 ether; + r.interactions[1][1] = + getSwapInteraction(IERC4626(sellVaultToken).asset(), IERC4626(buyVaultToken).asset(), swapAmount); + + // Deposit to buy vault (transfer underlying to vault) + uint256 buyUnderlyingAmount = + sellAmount * r.clearingPrices[0] / milkSwap.prices(IERC4626(buyVaultToken).asset()); + r.interactions[1][2] = getDepositInteraction(buyVaultToken, buyUnderlyingAmount); + + // Skim to mint vault shares to receiver + r.interactions[1][3] = ICowSettlement.Interaction({ + target: buyVaultToken, + value: 0, + callData: abi.encodeWithSignature("skim(uint256,address)", type(uint256).max, address(COW_SETTLEMENT)) + }); + } + + /// @notice Test swapping collateral from main account + function test_CollateralSwapWrapper_MainAccount() external { + vm.skip(bytes(forkRpcUrl).length == 0); + + // Create params using helper + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _createDefaultParams(user, user); + + // Get settlement data + SettlementData memory settlement = + getCollateralSwapSettlement(user, user, ESUSDS, EWBTC, DEFAULT_SWAP_AMOUNT, DEFAULT_BUY_AMOUNT); + + // User deposits SUSDS collateral + vm.startPrank(user); + IERC20(SUSDS).approve(ESUSDS, type(uint256).max); + uint256 depositAmount = 1000e18; + IERC4626(ESUSDS).deposit(depositAmount, user); + + // User signs the order and approves vault shares for settlement (already done in setupCowOrder) + + // Approve spending of the ESUSDS to repay debt + IEVault(ESUSDS).approve(COW_SETTLEMENT.vaultRelayer(), type(uint256).max); + vm.stopPrank(); + + // Record balances before swap + uint256 susdsBalanceBefore = IERC20(ESUSDS).balanceOf(user); + uint256 wbtcBalanceBefore = IERC20(EWBTC).balanceOf(user); + + // Create permit signature and encode data + bytes memory permitSignature = _createPermitSignature(params); + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions) + ); + bytes memory wrapperData = _encodeWrapperData(params, permitSignature); + + // Expect event emission + vm.expectEmit(true, true, true, true); + emit CowEvcCollateralSwapWrapper.CowEvcCollateralSwapped( + params.owner, params.account, params.fromVault, params.toVault, params.swapAmount, params.kind + ); + + // Execute wrapped settlement + _executeWrappedSettlement(settleData, wrapperData); + + // Verify the collateral was swapped successfully + assertEq( + IERC20(ESUSDS).balanceOf(user), + susdsBalanceBefore - DEFAULT_SWAP_AMOUNT, + "User should have less ESUSDS after swap" + ); + assertGt(IERC20(EWBTC).balanceOf(user), wbtcBalanceBefore, "User should have more EWBTC after swap"); + } + + /// @notice Test swapping collateral from subaccount + function test_CollateralSwapWrapper_Subaccount() external { + vm.skip(bytes(forkRpcUrl).length == 0); + + address account = address(uint160(user) ^ uint8(0x01)); + + // Create params using helper + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _createDefaultParams(user, account); + + // Get settlement data - receiver is the subaccount + SettlementData memory settlement = + getCollateralSwapSettlement(user, account, ESUSDS, EWBTC, DEFAULT_SWAP_AMOUNT, DEFAULT_BUY_AMOUNT); + + // User deposits SUSDS collateral to subaccount + vm.startPrank(user); + IERC20(SUSDS).approve(ESUSDS, type(uint256).max); + uint256 depositAmount = 1000e18; + IERC4626(ESUSDS).deposit(depositAmount, account); + + // User signs the order on cowswap (already done in setupCowOrder) + + vm.stopPrank(); + + // Setup subaccount approvals and pre-approved hash + _setupSubaccountApprovals(params); + + // Record balances before swap + uint256 susdsBalanceBefore = IERC20(ESUSDS).balanceOf(account); + uint256 wbtcBalanceBefore = IERC20(EWBTC).balanceOf(account); + + // Encode settlement and wrapper data (empty signature for pre-approved hash) + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions) + ); + bytes memory wrapperData = _encodeWrapperData(params, new bytes(0)); + + // Expect event emission + vm.expectEmit(true, true, true, true); + emit CowEvcCollateralSwapWrapper.CowEvcCollateralSwapped( + params.owner, params.account, params.fromVault, params.toVault, params.swapAmount, params.kind + ); + + // Execute wrapped settlement + _executeWrappedSettlement(settleData, wrapperData); + + // Verify the collateral was swapped successfully + assertEq( + IERC20(ESUSDS).balanceOf(account), + susdsBalanceBefore - DEFAULT_SWAP_AMOUNT, + "Subaccount should have less ESUSDS after swap" + ); + assertGt(IERC20(EWBTC).balanceOf(account), wbtcBalanceBefore, "Subaccount should have more EWBTC after swap"); + + // Main account balance should remain unchanged (transfer is atomic through settlement) + assertEq(IERC20(ESUSDS).balanceOf(user), 0, "Main account ESUSDS balance should be 0"); + } + + /// @notice Test that unauthorized users cannot call evcInternalSwap directly + function test_CollateralSwapWrapper_UnauthorizedInternalSwap() external { + vm.skip(bytes(forkRpcUrl).length == 0); + + bytes memory settleData = ""; + bytes memory wrapperData = ""; + + // Try to call evcInternalSwap directly (not through EVC) + vm.expectRevert(abi.encodeWithSelector(CowEvcCollateralSwapWrapper.Unauthorized.selector, address(this))); + collateralSwapWrapper.evcInternalSwap(settleData, wrapperData, wrapperData); + } + + /// @notice Test that non-solvers cannot call wrappedSettle + function test_CollateralSwapWrapper_NonSolverCannotSettle() external { + vm.skip(bytes(forkRpcUrl).length == 0); + + bytes memory settleData = ""; + bytes memory wrapperData = hex"0000"; + + // Try to call wrappedSettle as non-solver + vm.expectRevert(abi.encodeWithSelector(CowWrapper.NotASolver.selector, address(this))); + collateralSwapWrapper.wrappedSettle(settleData, wrapperData); + } + + /// @notice Test parseWrapperData function + function test_CollateralSwapWrapper_ParseWrapperData() external view { + address account = address(uint160(user) ^ uint8(0x01)); + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _createDefaultParams(user, account); + + bytes memory signature = new bytes(0); + bytes memory wrapperData = abi.encode(params, signature); + bytes memory remainingData = collateralSwapWrapper.parseWrapperData(wrapperData); + + // After parsing CollateralSwapParams, remaining data should be empty + assertEq(remainingData.length, 0, "Remaining data should be empty"); + } + + /// @notice Test swapping with a leveraged position (ensuring account health is maintained) + function test_CollateralSwapWrapper_WithLeveragedPosition() external { + vm.skip(bytes(forkRpcUrl).length == 0); + + uint256 borrowAmount = 1e18; // Borrow 1 WETH + uint256 collateralAmount = 1000e18; + + address account = address(uint160(user) ^ uint8(0x01)); + + // Set up a leveraged position + setupLeveragedPositionFor( + user, account, ESUSDS, EWETH, collateralAmount + borrowAmount * 2500e18 / 0.99e18, borrowAmount + ); + + uint256 sellAmount = 1000 ether + 2500 ether; // Sell 3500 ESUSDS + uint256 buyAmount = 0.0325e8; // Expect to receive ~0.0325 EWBTC (8 decimals) + + // Create params using helper + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _createDefaultParams(user, account); + params.swapAmount = sellAmount; // Override swap amount for this test + + // Get settlement data + SettlementData memory settlement = + getCollateralSwapSettlement(user, account, ESUSDS, EWBTC, sellAmount, buyAmount); + + // User signs the order on cowswap (already done in setupCowOrder) + + // Setup subaccount approvals and pre-approved hash + _setupSubaccountApprovals(params); + + // Record balances and debt before swap + uint256 susdsBalanceBefore = IERC20(ESUSDS).balanceOf(account); + uint256 wbtcBalanceBefore = IERC20(EWBTC).balanceOf(account); + uint256 debtBefore = IEVault(EWETH).debtOf(account); + + // Encode settlement and wrapper data (empty signature for pre-approved hash) + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions) + ); + bytes memory wrapperData = _encodeWrapperData(params, new bytes(0)); + + // Expect event emission + vm.expectEmit(true, true, true, true); + emit CowEvcCollateralSwapWrapper.CowEvcCollateralSwapped( + params.owner, params.account, params.fromVault, params.toVault, params.swapAmount, params.kind + ); + + // Execute wrapped settlement + _executeWrappedSettlement(settleData, wrapperData); + + // Verify the collateral was swapped successfully while maintaining debt + assertEq( + IERC20(ESUSDS).balanceOf(account), + susdsBalanceBefore - sellAmount, + "Account should have less ESUSDS after swap" + ); + assertGt(IERC20(EWBTC).balanceOf(account), wbtcBalanceBefore, "Account should have more EWBTC after swap"); + assertEq(IEVault(EWETH).debtOf(account), debtBefore, "Debt should remain unchanged after swap"); + } + + /// @notice Test that the wrapper can handle being called three times in the same chain + /// @dev Two users close positions in the same direction (long SUSDS), one user closes opposite (long WETH) + function test_CollateralSwapWrapper_ThreeUsers_TwoSameOneOpposite() external { + vm.skip(bytes(forkRpcUrl).length == 0); + + // Configure vault LTVs for both directions + vm.startPrank(IEVault(EWETH).governorAdmin()); + IEVault(EWETH).setLTV(ESUSDS, 0.9e4, 0.9e4, 0); + IEVault(EWETH).setLTV(EWBTC, 0.9e4, 0.9e4, 0); + vm.stopPrank(); + + // Setup accounts + address account1 = address(uint160(user) ^ 1); + address account2 = address(uint160(user2) ^ 1); + address account3 = address(uint160(user3) ^ 1); + + vm.label(account1, "account 1"); + vm.label(account2, "account 2"); + vm.label(account3, "account 3"); + + // Setup User1: Long SUSDS (SUSDS collateral, WETH debt). 1 ETH debt + setupLeveragedPositionFor(user, account1, ESUSDS, EWETH, 2750 ether, 1 ether); + + // Setup User2: Long SUSDS (SUSDS collateral, WETH debt). 3 ETH debt + setupLeveragedPositionFor(user2, account2, ESUSDS, EWETH, 8500 ether, 3 ether); + + // Setup User3: Long WBTC (WETH collateral, WBTC debt). 2 ETH debt + setupLeveragedPositionFor(user3, account3, EWBTC, EWETH, 0.075e8, 2 ether); + + // Verify positions exist + assertEq(IEVault(EWETH).debtOf(account1), 1 ether, "Account 1 should have WETH debt"); + assertEq(IEVault(EWETH).debtOf(account2), 3 ether, "Account 2 should have WETH debt"); + assertEq(IEVault(EWETH).debtOf(account3), 2 ether, "Account 3 should have WETH debt"); + + // Verify collaterals + assertApproxEqRel( + IEVault(ESUSDS).balanceOf(account1), 2750 ether, 0.01 ether, "Account 1 should have SUSDS collateral" + ); + assertApproxEqRel( + IEVault(ESUSDS).balanceOf(account2), 8500 ether, 0.01 ether, "Account 2 should have SUSDS collateral" + ); + assertApproxEqRel( + IEVault(EWBTC).balanceOf(account3), 0.075e8, 0.01 ether, "Account 3 should have WBTC collateral" + ); + + // Create params for all users + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params1 = + CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: user, + account: account1, + deadline: block.timestamp + 1 hours, + fromVault: ESUSDS, + toVault: EWBTC, + swapAmount: 500 ether, + kind: GPv2Order.KIND_SELL + }); + + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params2 = + CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: user2, + account: account2, + deadline: block.timestamp + 1 hours, + fromVault: ESUSDS, + toVault: EWBTC, + swapAmount: 0.005e8, // about 500 ESUSDS + kind: GPv2Order.KIND_BUY + }); + + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params3 = + CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: user3, + account: account3, + deadline: block.timestamp + 1 hours, + fromVault: EWBTC, + toVault: ESUSDS, + swapAmount: 2000 ether, + kind: GPv2Order.KIND_BUY + }); + + // Create permit signatures for all users + bytes memory permitSignature1 = _createPermitSignatureFor(params1, privateKey); + bytes memory permitSignature2 = _createPermitSignatureFor(params2, privateKey2); + bytes memory permitSignature3 = _createPermitSignatureFor(params3, privateKey3); + + // Setup approvals for all users + _setupSubaccountApprovals(params1); + _setupSubaccountApprovals(params2); + _setupSubaccountApprovals(params3); + + // Create settlement with all three trades + uint32 validTo = uint32(block.timestamp + 1 hours); + + address[] memory tokens = new address[](2); + tokens[0] = ESUSDS; + tokens[1] = EWBTC; + + uint256[] memory clearingPrices = new uint256[](2); + clearingPrices[0] = 1 ether; // eSUSDS price + clearingPrices[1] = 100000 ether * 1e10; // eWBTC price + + ICowSettlement.Trade[] memory trades = new ICowSettlement.Trade[](3); + (trades[0],,) = setupCowOrder(tokens, 0, 1, params1.swapAmount, 0, validTo, user, account1, false); + (trades[1],,) = setupCowOrder(tokens, 0, 1, 1e24, params2.swapAmount, validTo, user2, account2, true); + (trades[2],,) = setupCowOrder(tokens, 1, 0, 1e24, params3.swapAmount, validTo, user3, account3, true); + + // Setup interactions + ICowSettlement.Interaction[][3] memory interactions; + interactions[0] = new ICowSettlement.Interaction[](0); + interactions[1] = new ICowSettlement.Interaction[](4); + interactions[2] = new ICowSettlement.Interaction[](0); + + // We pull the money out of the euler vaults + interactions[1][0] = getWithdrawInteraction(EWBTC, 0.01e8); + + // We swap all of the WBTC we need + interactions[1][1] = getSwapInteraction(WBTC, SUSDS, 0.01e8); + + // We deposit back into WBTC + interactions[1][2] = getDepositInteraction(ESUSDS, 1000 ether); + + // We "skim" to get the tokens + interactions[1][3] = getSkimInteraction(ESUSDS); + + // Encode settlement data + bytes memory settleData = abi.encodeCall(ICowSettlement.settle, (tokens, clearingPrices, trades, interactions)); + + // Chain wrapper data + bytes memory wrapper1Data = abi.encode(params1, permitSignature1); + bytes memory wrapper2Data = abi.encode(params2, permitSignature2); + bytes memory wrapper3Data = abi.encode(params3, permitSignature3); + + bytes memory wrapperData = abi.encodePacked( + uint16(wrapper1Data.length), + wrapper1Data, + address(collateralSwapWrapper), + uint16(wrapper2Data.length), + wrapper2Data, + address(collateralSwapWrapper), + uint16(wrapper3Data.length), + wrapper3Data + ); + + // Execute wrapped settlement + address[] memory targets = new address[](1); + bytes[] memory datas = new bytes[](1); + targets[0] = address(collateralSwapWrapper); + datas[0] = abi.encodeCall(CowWrapper.wrappedSettle, (settleData, wrapperData)); + solver.runBatch(targets, datas); + + // Verify all positions closed successfully + assertEq(IEVault(EWETH).debtOf(account1), 1 ether, "User1 should have WETH debt"); + assertEq(IEVault(EWETH).debtOf(account2), 3 ether, "User2 should have WETH debt"); + assertEq(IEVault(EWETH).debtOf(account3), 2 ether, "User3 should have WETH debt"); + + // Verify original collaterals + assertApproxEqRel( + IEVault(ESUSDS).balanceOf(account1), 2250 ether, 0.01 ether, "Account 1 should have less SUSDS collateral" + ); + assertApproxEqRel( + IEVault(ESUSDS).balanceOf(account2), 8000 ether, 0.01 ether, "Account 2 should have less SUSDS collateral" + ); + assertApproxEqRel( + IEVault(EWBTC).balanceOf(account3), 0.055e8, 0.01 ether, "Account 3 should have less WBTC collateral" + ); + + // Verify new collaterals + assertApproxEqRel( + IEVault(EWBTC).balanceOf(account1), 0.005e8, 0.01 ether, "Account 1 should have some WBTC collateral" + ); + assertEq(IEVault(EWBTC).balanceOf(account2), 0.005e8, "Account 2 should have some WBTC collateral"); + assertEq(IEVault(ESUSDS).balanceOf(account3), 2000 ether, "Account 3 should have some SUSD collateral"); + } +} diff --git a/test/helpers/CowBaseTest.sol b/test/helpers/CowBaseTest.sol index 9e4a38a..2457202 100644 --- a/test/helpers/CowBaseTest.sol +++ b/test/helpers/CowBaseTest.sol @@ -33,10 +33,12 @@ contract CowBaseTest is Test { address constant SUSDS = 0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD; address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address constant WBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; // Vaults address internal constant ESUSDS = 0x1e548CfcE5FCF17247E024eF06d32A01841fF404; address internal constant EWETH = 0xD8b27CF359b7D15710a5BE299AF6e7Bf904984C2; + address internal constant EWBTC = 0x998D761eC1BAdaCeb064624cc3A1d37A46C88bA4; address internal swapVerifier = 0xae26485ACDDeFd486Fe9ad7C2b34169d360737c7; @@ -80,24 +82,30 @@ contract CowBaseTest is Test { // Setup some liquidity for MilkSwap milkSwap = new MilkSwap(); - deal(SUSDS, address(milkSwap), 10000e18); // Add SUSDS to MilkSwap - deal(WETH, address(milkSwap), 10000e18); // Add WETH to MilkSwap + deal(SUSDS, address(milkSwap), 100000e18); // Add SUSDS to MilkSwap + deal(WETH, address(milkSwap), 100000e18); // Add WETH to MilkSwap + deal(WBTC, address(milkSwap), 100000e8); // Add WBTC to MilkSwap (8 decimals) milkSwap.setPrice(WETH, 2500e18); // 1 ETH = 2,500 USD milkSwap.setPrice(SUSDS, 1e18); // 1 USDS = 1 USD + milkSwap.setPrice(WBTC, 100000e18 * 1e10); // 1 BTC = 100,000 USD (8 decimals) // deal small amount to the settlement contract that serve as buffer (just makes tests easier...) deal(SUSDS, address(COW_SETTLEMENT), 200e18); - deal(WETH, address(COW_SETTLEMENT), 0.2e18); + deal(WETH, address(COW_SETTLEMENT), 0.1e18); + deal(WBTC, address(COW_SETTLEMENT), 0.002e8); deal(ESUSDS, address(COW_SETTLEMENT), 200e18); - deal(EWETH, address(COW_SETTLEMENT), 0.2e18); + deal(EWETH, address(COW_SETTLEMENT), 0.1e18); + deal(EWBTC, address(COW_SETTLEMENT), 0.002e8); // Set the approval for MilkSwap in the settlement as a convenience vm.startPrank(address(COW_SETTLEMENT)); IERC20(WETH).approve(address(milkSwap), type(uint256).max); IERC20(SUSDS).approve(address(milkSwap), type(uint256).max); + IERC20(WBTC).approve(address(milkSwap), type(uint256).max); IERC20(ESUSDS).approve(address(ESUSDS), type(uint256).max); IERC20(EWETH).approve(address(EWETH), type(uint256).max); + IERC20(EWBTC).approve(address(EWBTC), type(uint256).max); vm.stopPrank(); @@ -112,8 +120,10 @@ contract CowBaseTest is Test { vm.label(user, "user"); vm.label(SUSDS, "SUSDS"); vm.label(WETH, "WETH"); + vm.label(WBTC, "WBTC"); vm.label(ESUSDS, "eSUSDS"); vm.label(EWETH, "eWETH"); + vm.label(EWBTC, "eWBTC"); vm.label(address(COW_SETTLEMENT), "CoW"); vm.label(address(COW_SETTLEMENT.authenticator()), "CoW Auth"); vm.label(address(COW_SETTLEMENT.authenticator()), "CoW Vault Relayer"); @@ -125,14 +135,14 @@ contract CowBaseTest is Test { public pure returns ( - IERC20[] memory tokens, + address[] memory tokens, uint256[] memory clearingPrices, ICowSettlement.Trade[] memory trades, ICowSettlement.Interaction[][3] memory interactions ) { return ( - new IERC20[](0), + new address[](0), new uint256[](0), new ICowSettlement.Trade[](0), [ diff --git a/test/unit/CowEvcCollateralSwapWrapper.unit.t.sol b/test/unit/CowEvcCollateralSwapWrapper.unit.t.sol new file mode 100644 index 0000000..faa49a4 --- /dev/null +++ b/test/unit/CowEvcCollateralSwapWrapper.unit.t.sol @@ -0,0 +1,964 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8; + +import {Test} from "forge-std/Test.sol"; +import {IEVC} from "evc/EthereumVaultConnector.sol"; +import {CowEvcCollateralSwapWrapper} from "../../src/CowEvcCollateralSwapWrapper.sol"; +import {EmptyWrapper} from "../EmptyWrapper.sol"; +import {ICowSettlement} from "../../src/CowWrapper.sol"; +import {MockEVC} from "./mocks/MockEVC.sol"; +import {MockCowAuthentication, MockCowSettlement} from "./mocks/MockCowProtocol.sol"; +import {MockERC20, MockVault} from "./mocks/MockERC20AndVaults.sol"; + +/// @title Unit tests for CowEvcCollateralSwapWrapper +/// @notice Comprehensive unit tests focusing on isolated functionality testing with mocks +contract CowEvcCollateralSwapWrapperUnitTest is Test { + CowEvcCollateralSwapWrapper public wrapper; + EmptyWrapper public emptyWrapper; + MockEVC public mockEvc; + MockCowSettlement public mockSettlement; + MockCowAuthentication public mockAuth; + MockERC20 public mockAsset; + MockVault public mockFromVault; + MockVault public mockToVault; + + address constant OWNER = address(0x1111); + address constant ACCOUNT = address(0x1112); + address constant SOLVER = address(0x3333); + + uint256 constant DEFAULT_SWAP_AMOUNT = 1000e18; + + // Constants from the contract + bytes32 private constant KIND_SELL = hex"f3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775"; + bytes32 private constant KIND_BUY = hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc"; + + event PreApprovedHash(address indexed owner, bytes32 indexed hash, bool approved); + event PreApprovedHashConsumed(address indexed owner, bytes32 indexed hash); + + /// @notice Get default CollateralSwapParams for testing + function _getDefaultParams() internal view returns (CowEvcCollateralSwapWrapper.CollateralSwapParams memory) { + return CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: DEFAULT_SWAP_AMOUNT, + kind: KIND_SELL + }); + } + + /// @notice Create empty settle data + function _getEmptySettleData() internal pure returns (bytes memory) { + return abi.encodeCall( + ICowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + } + + /// @notice Encode wrapper data with length prefix + function _encodeWrapperData(CowEvcCollateralSwapWrapper.CollateralSwapParams memory params, bytes memory signature) + internal + pure + returns (bytes memory) + { + bytes memory wrapperData = abi.encode(params, signature); + return abi.encodePacked(uint16(wrapperData.length), wrapperData); + } + + /// @notice Setup pre-approved hash flow + function _setupPreApprovedHash(CowEvcCollateralSwapWrapper.CollateralSwapParams memory params) + internal + returns (bytes32) + { + bytes32 hash = wrapper.getApprovalHash(params); + vm.prank(OWNER); + wrapper.setPreApprovedHash(hash, true); + mockEvc.setOperator(OWNER, address(wrapper), true); + return hash; + } + + /// @notice Decode signed calldata helper + function _decodeSignedCalldata(bytes memory signedCalldata) internal pure returns (IEVC.BatchItem[] memory) { + bytes memory encodedItems = new bytes(signedCalldata.length - 4); + for (uint256 i = 4; i < signedCalldata.length; i++) { + encodedItems[i - 4] = signedCalldata[i]; + } + return abi.decode(encodedItems, (IEVC.BatchItem[])); + } + + function setUp() public { + mockAuth = new MockCowAuthentication(); + mockSettlement = new MockCowSettlement(address(mockAuth)); + mockEvc = new MockEVC(); + mockAsset = new MockERC20("Mock Asset", "MOCK"); + mockFromVault = new MockVault(address(mockAsset), "Mock From Vault", "mFROM"); + mockToVault = new MockVault(address(mockAsset), "Mock To Vault", "mTO"); + + wrapper = new CowEvcCollateralSwapWrapper(address(mockEvc), ICowSettlement(address(mockSettlement))); + emptyWrapper = new EmptyWrapper(ICowSettlement(address(mockSettlement))); + + // Set solver as authenticated + mockAuth.setSolver(SOLVER, true); + mockAuth.setSolver(address(wrapper), true); + mockAuth.setSolver(address(emptyWrapper), true); + + // Set the correct onBehalfOfAccount for evcInternalSwap calls + mockEvc.setOnBehalfOf(address(wrapper)); + } + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR TESTS + //////////////////////////////////////////////////////////////*/ + + function test_Constructor_SetsImmutables() public view { + assertEq(address(wrapper.EVC()), address(mockEvc), "EVC not set correctly"); + assertEq(address(wrapper.SETTLEMENT()), address(mockSettlement), "SETTLEMENT not set correctly"); + assertEq(address(wrapper.AUTHENTICATOR()), address(mockAuth), "AUTHENTICATOR not set correctly"); + assertEq(wrapper.NONCE_NAMESPACE(), uint256(uint160(address(wrapper))), "NONCE_NAMESPACE incorrect"); + } + + function test_Constructor_SetsDomainSeparator() public view { + bytes32 expectedDomainSeparator = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256("CowEvcCollateralSwapWrapper"), + keccak256("1"), + block.chainid, + address(wrapper) + ) + ); + assertEq(wrapper.DOMAIN_SEPARATOR(), expectedDomainSeparator, "DOMAIN_SEPARATOR incorrect"); + } + + /*////////////////////////////////////////////////////////////// + PARSE WRAPPER DATA TESTS + //////////////////////////////////////////////////////////////*/ + + function test_ParseWrapperData_EmptySignature() public view { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _getDefaultParams(); + + bytes memory wrapperData = abi.encode(params, new bytes(0)); + bytes memory remaining = wrapper.parseWrapperData(wrapperData); + + assertEq(remaining.length, 0, "Should have no remaining data"); + } + + function test_ParseWrapperData_WithExtraData() public view { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _getDefaultParams(); + + bytes memory signature = new bytes(0); + bytes memory wrapperData = abi.encode(params, signature); + bytes memory extraData = hex"deadbeef"; + wrapperData = abi.encodePacked(wrapperData, extraData); + + bytes memory remaining = wrapper.parseWrapperData(wrapperData); + + assertEq(remaining.length, 4, "Should have 4 bytes remaining"); + assertEq(remaining, extraData, "Extra data should match"); + } + + /*////////////////////////////////////////////////////////////// + APPROVAL HASH TESTS + //////////////////////////////////////////////////////////////*/ + + function test_GetApprovalHash_DifferentForDifferentParams() public view { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params1 = _getDefaultParams(); + + // Change owner field + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params2 = _getDefaultParams(); + params2.owner = ACCOUNT; + + // Change swapAmount field + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params3 = _getDefaultParams(); + params3.swapAmount = 2000e18; + + bytes32 hash1 = wrapper.getApprovalHash(params1); + bytes32 hash2 = wrapper.getApprovalHash(params2); + bytes32 hash3 = wrapper.getApprovalHash(params3); + + assertNotEq(hash1, hash2, "Hash should differ for different params"); + assertNotEq(hash1, hash3, "Hash should differ for different params"); + } + + function test_GetApprovalHash_MatchesEIP712() public view { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _getDefaultParams(); + + bytes32 structHash = keccak256( + abi.encode( + params.owner, + params.account, + params.deadline, + params.fromVault, + params.toVault, + params.swapAmount, + params.kind + ) + ); + + bytes32 expectedDigest = keccak256(abi.encodePacked("\x19\x01", wrapper.DOMAIN_SEPARATOR(), structHash)); + bytes32 actualDigest = wrapper.getApprovalHash(params); + + assertEq(actualDigest, expectedDigest, "Hash should match EIP-712 format"); + } + + /*////////////////////////////////////////////////////////////// + GET SIGNED CALLDATA TESTS + //////////////////////////////////////////////////////////////*/ + + function test_GetSignedCalldata_EnablesNewCollateral() public view { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _getDefaultParams(); + + bytes memory signedCalldata = wrapper.getSignedCalldata(params); + IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); + + assertEq(items.length, 1, "Should have 1 batch item"); + assertEq(items[0].targetContract, address(mockEvc), "Should target EVC"); + assertEq( + items[0].data, + abi.encodeCall(IEVC.enableCollateral, (params.account, params.toVault)), + "Should call enableCollateral" + ); + } + + function test_GetSignedCalldata_UsesCorrectAccount() public view { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _getDefaultParams(); + + bytes memory signedCalldata = wrapper.getSignedCalldata(params); + IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); + + assertEq(items[0].onBehalfOfAccount, address(0), "Should have zero onBehalfOfAccount"); + } + + /*////////////////////////////////////////////////////////////// + FIND RATE PRICES TESTS + //////////////////////////////////////////////////////////////*/ + + function test_FindRatePrices_SuccessfulLookup() public { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: OWNER, // Same account to avoid transfer logic + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_BUY + }); + + address[] memory tokens = new address[](2); + tokens[0] = address(mockFromVault); + tokens[1] = address(mockToVault); + + uint256[] memory prices = new uint256[](2); + prices[0] = 1e18; + prices[1] = 2e18; + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + tokens, + prices, + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + + mockSettlement.setSuccessfulSettle(true); + + vm.prank(address(mockEvc)); + wrapper.evcInternalSwap(settleData, wrapperData, ""); + + // If we get here, prices were found successfully + } + + function test_FindRatePrices_MissingFromVaultPrice() public { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_BUY + }); + + // Only include toVault in tokens, not fromVault + address[] memory tokens = new address[](1); + tokens[0] = address(mockToVault); + + uint256[] memory prices = new uint256[](1); + prices[0] = 2e18; + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + tokens, + prices, + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + + vm.prank(address(mockEvc)); + vm.expectRevert( + abi.encodeWithSelector( + CowEvcCollateralSwapWrapper.PricesNotFoundInSettlement.selector, + address(mockFromVault), + address(mockToVault) + ) + ); + wrapper.evcInternalSwap(settleData, wrapperData, ""); + } + + function test_FindRatePrices_MissingToVaultPrice() public { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_BUY + }); + + // Only include fromVault in tokens, not toVault + address[] memory tokens = new address[](1); + tokens[0] = address(mockFromVault); + + uint256[] memory prices = new uint256[](1); + prices[0] = 1e18; + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + tokens, + prices, + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + + vm.prank(address(mockEvc)); + vm.expectRevert( + abi.encodeWithSelector( + CowEvcCollateralSwapWrapper.PricesNotFoundInSettlement.selector, + address(mockFromVault), + address(mockToVault) + ) + ); + wrapper.evcInternalSwap(settleData, wrapperData, ""); + } + + /*////////////////////////////////////////////////////////////// + EVC INTERNAL SWAP TESTS + //////////////////////////////////////////////////////////////*/ + + function test_EvcInternalSwap_OnlyEVC() public { + bytes memory settleData = ""; + bytes memory wrapperData = ""; + bytes memory remainingWrapperData = ""; + + vm.expectRevert(abi.encodeWithSelector(CowEvcCollateralSwapWrapper.Unauthorized.selector, address(this))); + wrapper.evcInternalSwap(settleData, wrapperData, remainingWrapperData); + } + + function test_EvcInternalSwap_RequiresCorrectOnBehalfOfAccount() public { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: OWNER, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_SELL + }); + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + bytes memory remainingWrapperData = ""; + + mockSettlement.setSuccessfulSettle(true); + + // Set incorrect onBehalfOfAccount (not address(wrapper)) + mockEvc.setOnBehalfOf(address(0x9999)); + + vm.prank(address(mockEvc)); + vm.expectRevert(abi.encodeWithSelector(CowEvcCollateralSwapWrapper.Unauthorized.selector, address(0x9999))); + wrapper.evcInternalSwap(settleData, wrapperData, remainingWrapperData); + } + + function test_EvcInternalSwap_CanBeCalledByEVC() public { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: OWNER, // Same account, no transfer needed + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_SELL + }); + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + bytes memory remainingWrapperData = ""; + + mockSettlement.setSuccessfulSettle(true); + + vm.prank(address(mockEvc)); + wrapper.evcInternalSwap(settleData, wrapperData, remainingWrapperData); + } + + function test_EvcInternalSwap_WithSubaccount_KindSell() public { + // Set up scenario where owner != account + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: ACCOUNT, // Different from owner + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_SELL + }); + + // Give account some from vault tokens + mockFromVault.mint(ACCOUNT, 2000e18); + + // These tokens need to be spendable by the wrapper + vm.prank(ACCOUNT); + mockFromVault.approve(address(wrapper), 2000e18); + + // Create settle data without prices (not needed for KIND_SELL) + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + bytes memory remainingWrapperData = ""; + + mockSettlement.setSuccessfulSettle(true); + + vm.prank(address(mockEvc)); + wrapper.evcInternalSwap(settleData, wrapperData, remainingWrapperData); + + // Verify transfer occurred from account to owner (exact swapAmount for SELL) + assertEq(mockFromVault.balanceOf(ACCOUNT), 1000e18, "Account balance should decrease by swapAmount"); + assertEq(mockFromVault.balanceOf(OWNER), 1000e18, "Owner should receive swapAmount"); + } + + function test_EvcInternalSwap_WithSubaccount_KindBuy() public { + // Set up scenario where owner != account with KIND_BUY + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: ACCOUNT, // Different from owner + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, // This is the buy amount (what we want to receive) + kind: KIND_BUY + }); + + // Give account some from vault tokens + mockFromVault.mint(ACCOUNT, 3000e18); + + // These tokens need to be spendable by the wrapper + vm.prank(ACCOUNT); + mockFromVault.approve(address(wrapper), 3000e18); + + // Create settle data with prices for KIND_BUY calculation + address[] memory tokens = new address[](2); + tokens[0] = address(mockFromVault); + tokens[1] = address(mockToVault); + + uint256[] memory prices = new uint256[](2); + prices[0] = 1e18; // fromVault price + prices[1] = 2e18; // toVault price (2x more expensive) + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + tokens, + prices, + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + bytes memory remainingWrapperData = ""; + + mockSettlement.setSuccessfulSettle(true); + + vm.prank(address(mockEvc)); + wrapper.evcInternalSwap(settleData, wrapperData, remainingWrapperData); + + // For KIND_BUY: transferAmount = swapAmount * toVaultPrice / fromVaultPrice + // transferAmount = 1000e18 * 2e18 / 1e18 = 2000e18 + assertEq(mockFromVault.balanceOf(ACCOUNT), 1000e18, "Account balance should decrease by 2000e18"); + assertEq(mockFromVault.balanceOf(OWNER), 2000e18, "Owner should receive calculated amount"); + } + + function test_EvcInternalSwap_SubaccountMustBeControlledByOwner() public { + // Create an account that is NOT a valid subaccount of the owner + // Valid subaccount would share first 19 bytes, but this one doesn't + address invalidSubaccount = address(0x9999999999999999999999999999999999999999); + + // Approve the wrapper to transfer from the subaccount + vm.prank(invalidSubaccount); + mockFromVault.approve(address(wrapper), type(uint256).max); + + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: invalidSubaccount, // Invalid subaccount + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_SELL + }); + + // Give account some from vault tokens + mockFromVault.mint(invalidSubaccount, 2000e18); + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + bytes memory remainingWrapperData = ""; + + mockSettlement.setSuccessfulSettle(true); + + vm.prank(address(mockEvc)); + vm.expectRevert( + abi.encodeWithSelector( + CowEvcCollateralSwapWrapper.SubaccountMustBeControlledByOwner.selector, invalidSubaccount, OWNER + ) + ); + wrapper.evcInternalSwap(settleData, wrapperData, remainingWrapperData); + } + + function test_EvcInternalSwap_SameOwnerAndAccount() public { + // When owner == account, no transfer should occur + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: OWNER, // Same as owner + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_SELL + }); + + mockFromVault.mint(OWNER, 2000e18); + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + + mockSettlement.setSuccessfulSettle(true); + + vm.prank(address(mockEvc)); + wrapper.evcInternalSwap(settleData, wrapperData, ""); + + // No transfer should occur, so balance should remain unchanged + assertEq(mockFromVault.balanceOf(OWNER), 2000e18, "Owner balance should remain unchanged"); + } + + /*////////////////////////////////////////////////////////////// + WRAPPED SETTLE TESTS + //////////////////////////////////////////////////////////////*/ + + function test_WrappedSettle_OnlySolver() public { + bytes memory settleData = ""; + bytes memory wrapperData = hex"0000"; + + vm.expectRevert(abi.encodeWithSignature("NotASolver(address)", address(this))); + wrapper.wrappedSettle(settleData, wrapperData); + } + + function test_WrappedSettle_WithPermitSignature() public { + mockFromVault.mint(OWNER, 2000e18); + + vm.prank(OWNER); + mockFromVault.approve(address(wrapper), 2000e18); + + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _getDefaultParams(); + params.account = OWNER; // Same account + + bytes memory signature = new bytes(65); + bytes memory settleData = _getEmptySettleData(); + bytes memory wrapperData = _encodeWrapperData(params, signature); + + mockEvc.setSuccessfulBatch(true); + + vm.prank(SOLVER); + wrapper.wrappedSettle(settleData, wrapperData); + } + + function test_WrappedSettle_WithPreApprovedHash() public { + mockFromVault.mint(OWNER, 2000e18); + + vm.prank(OWNER); + mockFromVault.approve(address(wrapper), 2000e18); + + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _getDefaultParams(); + params.account = OWNER; // Same account + + bytes32 hash = _setupPreApprovedHash(params); + + bytes memory settleData = _getEmptySettleData(); + bytes memory wrapperData = _encodeWrapperData(params, new bytes(0)); + + mockEvc.setSuccessfulBatch(true); + + vm.prank(SOLVER); + wrapper.wrappedSettle(settleData, wrapperData); + + assertFalse(wrapper.isHashPreApproved(OWNER, hash), "Hash should be consumed"); + } + + function test_WrappedSettle_PreApprovedHashRevertsIfDeadlineExceeded() public { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _getDefaultParams(); + params.account = OWNER; // Same account + params.deadline = block.timestamp - 1; // Past deadline + + _setupPreApprovedHash(params); + + bytes memory settleData = _getEmptySettleData(); + bytes memory wrapperData = _encodeWrapperData(params, new bytes(0)); + + vm.prank(SOLVER); + vm.expectRevert( + abi.encodeWithSelector( + CowEvcCollateralSwapWrapper.OperationDeadlineExceeded.selector, params.deadline, block.timestamp + ) + ); + wrapper.wrappedSettle(settleData, wrapperData); + } + + /*////////////////////////////////////////////////////////////// + EDGE CASE TESTS + //////////////////////////////////////////////////////////////*/ + + function test_SwapAmount_Zero() public view { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: OWNER, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 0, // Zero swap amount + kind: KIND_SELL + }); + + bytes memory signedCalldata = wrapper.getSignedCalldata(params); + IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); + + // Should still have the enable collateral item + assertEq(items.length, 1, "Should have 1 item even with zero swap amount"); + } + + function test_SwapAmount_Max() public view { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: OWNER, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: type(uint256).max, + kind: KIND_SELL + }); + + bytes memory signedCalldata = wrapper.getSignedCalldata(params); + IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); + + assertEq(items.length, 1, "Should have 1 item with max swap amount"); + } + + function test_KindBuy_WithDifferentPrices() public { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 500e18, + kind: KIND_BUY + }); + + mockFromVault.mint(ACCOUNT, 3000e18); + vm.prank(ACCOUNT); + mockFromVault.approve(address(wrapper), 3000e18); + + // toVault is 3x more expensive than fromVault + address[] memory tokens = new address[](2); + tokens[0] = address(mockFromVault); + tokens[1] = address(mockToVault); + + uint256[] memory prices = new uint256[](2); + prices[0] = 1e18; // fromVault price + prices[1] = 3e18; // toVault price (3x more expensive) + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + tokens, + prices, + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + + mockSettlement.setSuccessfulSettle(true); + + vm.prank(address(mockEvc)); + wrapper.evcInternalSwap(settleData, wrapperData, ""); + + // For KIND_BUY: transferAmount = 500e18 * 3e18 / 1e18 = 1500e18 + assertEq(mockFromVault.balanceOf(ACCOUNT), 1500e18, "Account balance should decrease by 1500e18"); + assertEq(mockFromVault.balanceOf(OWNER), 1500e18, "Owner should receive 1500e18"); + } + + function test_DifferentVaults() public { + // Create another set of vaults + MockVault anotherFromVault = new MockVault(address(mockAsset), "Another From", "aFROM"); + MockVault anotherToVault = new MockVault(address(mockAsset), "Another To", "aTO"); + + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: OWNER, + deadline: block.timestamp + 1 hours, + fromVault: address(anotherFromVault), + toVault: address(anotherToVault), + swapAmount: 1000e18, + kind: KIND_SELL + }); + + bytes memory signedCalldata = wrapper.getSignedCalldata(params); + IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); + + // Verify it's enabling the correct toVault + assertEq( + items[0].data, + abi.encodeCall(IEVC.enableCollateral, (OWNER, address(anotherToVault))), + "Should enable correct toVault" + ); + } + + function test_ParseWrapperData_LongSignature() public view { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_SELL + }); + + // Create a signature longer than 65 bytes + bytes memory signature = new bytes(128); + bytes memory wrapperData = abi.encode(params, signature); + bytes memory remaining = wrapper.parseWrapperData(wrapperData); + + assertEq(remaining.length, 0, "Should have no remaining data with long signature"); + } + + function test_EvcInternalSwap_WithRemainingWrapperData() public { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: OWNER, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_SELL + }); + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + bytes memory remainingWrapperData = abi.encodePacked(emptyWrapper, hex"0004deadbeef"); + + mockSettlement.setSuccessfulSettle(true); + + vm.prank(address(mockEvc)); + wrapper.evcInternalSwap(settleData, wrapperData, remainingWrapperData); + + // Should handle remaining wrapper data gracefully + } + + function test_WrappedSettle_BuildsCorrectBatchWithPermit() public { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: OWNER, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_SELL + }); + + bytes memory signature = new bytes(65); + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, signature); + wrapperData = abi.encodePacked(uint16(wrapperData.length), wrapperData); + + mockEvc.setSuccessfulBatch(true); + + vm.prank(SOLVER); + wrapper.wrappedSettle(settleData, wrapperData); + + // Should build a batch with permit + evcInternalSwap (2 items) + } + + function test_WrappedSettle_BuildsCorrectBatchWithPreApproved() public { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: OWNER, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_SELL + }); + + bytes32 hash = wrapper.getApprovalHash(params); + + vm.prank(OWNER); + wrapper.setPreApprovedHash(hash, true); + + mockEvc.setOperator(OWNER, address(wrapper), true); + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + wrapperData = abi.encodePacked(uint16(wrapperData.length), wrapperData); + + mockEvc.setSuccessfulBatch(true); + + vm.prank(SOLVER); + wrapper.wrappedSettle(settleData, wrapperData); + + // Should build a batch with enableCollateral + evcInternalSwap (2 items) + } +}