From 5e8e2ad1d42fcbb69f3608241922fdd637da77a3 Mon Sep 17 00:00:00 2001 From: Kaze Date: Thu, 6 Nov 2025 09:47:37 +0900 Subject: [PATCH 01/10] feat: collateral swap router --- src/CowEvcCollateralSwapWrapper.sol | 342 ++++++ test/CowEvcCollateralSwapWrapper.t.sol | 511 ++++++++ test/helpers/CowBaseTest.sol | 54 +- .../CowEvcCollateralSwapWrapper.unit.t.sol | 1090 +++++++++++++++++ 4 files changed, 1974 insertions(+), 23 deletions(-) create mode 100644 src/CowEvcCollateralSwapWrapper.sol create mode 100644 test/CowEvcCollateralSwapWrapper.t.sol create mode 100644 test/unit/CowEvcCollateralSwapWrapper.unit.t.sol diff --git a/src/CowEvcCollateralSwapWrapper.sol b/src/CowEvcCollateralSwapWrapper.sol new file mode 100644 index 0000000..7b2f193 --- /dev/null +++ b/src/CowEvcCollateralSwapWrapper.sol @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8; + +import {IEVC} from "evc/EthereumVaultConnector.sol"; + +import {CowWrapper, CowSettlement} from "./vendor/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, CowSettlement _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[], CowSettlement.CowTradeData[], CowSettlement.CowInteractionData[][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 _internalSettle to call the settlement contract + // wrapperData is empty since we've already processed it in _wrap + _internalSettle(settleData, remainingWrapperData); + } +} diff --git a/test/CowEvcCollateralSwapWrapper.t.sol b/test/CowEvcCollateralSwapWrapper.t.sol new file mode 100644 index 0000000..e2f1028 --- /dev/null +++ b/test/CowEvcCollateralSwapWrapper.t.sol @@ -0,0 +1,511 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8; + +import {GPv2Order, IERC20 as CowERC20} from "cow/libraries/GPv2Order.sol"; + +import {IEVC} from "evc/EthereumVaultConnector.sol"; +import {IEVault, IERC4626, IBorrowing, IERC20} from "euler-vault-kit/src/EVault/IEVault.sol"; + +import {CowEvcCollateralSwapWrapper} from "../src/CowEvcCollateralSwapWrapper.sol"; +import {CowSettlement, CowWrapper} from "../src/vendor/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; + + 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); + } + + /// @notice Helper to set up an initial leveraged position + /// @dev This creates a position that can then be used in tests + function _setupLeveragedPosition(uint256 borrowAmount, uint256 collateralAmount) internal { + address account = address(uint160(user) ^ uint8(0x01)); + + vm.startPrank(user); + + // User approves SUSDS vault for deposit + IERC20(SUSDS).approve(ESUSDS, type(uint256).max); + + // Enable collateral and controller on the account + evc.enableCollateral(account, ESUSDS); + evc.enableController(account, EWETH); + + // Deposit collateral to the account, and add the approximate amount after swapping the borrowed collateral + IERC4626(ESUSDS).deposit(collateralAmount + borrowAmount * 2500e18 / 0.99e18, account); + + vm.stopPrank(); + + // Borrow assets from the account. And confiscate the borrowed asset (needs to be called with account as onBehalfOf) + vm.startPrank(account); + IBorrowing(EWETH).borrow(borrowAmount, address(this)); + + vm.stopPrank(); + } + + struct SettlementData { + bytes orderUid; + GPv2Order.Data orderData; + address[] tokens; + uint256[] clearingPrices; + CowSettlement.CowTradeData[] trades; + CowSettlement.CowInteractionData[][3] interactions; + } + + /// @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 view returns (SettlementData memory r) { + uint32 validTo = uint32(block.timestamp + 1 hours); + + // Create order data - using KIND_SELL to sell exact amount of collateral + r.orderData = GPv2Order.Data({ + sellToken: CowERC20(sellVaultToken), + buyToken: CowERC20(buyVaultToken), + receiver: receiver, + sellAmount: sellAmount, + buyAmount: buyAmount, + validTo: validTo, + appData: bytes32(0), + feeAmount: 0, + kind: GPv2Order.KIND_SELL, + partiallyFillable: false, + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }); + + // Get order UID + r.orderUid = getOrderUid(owner, r.orderData); + + // Get trade data + r.trades = new CowSettlement.CowTradeData[](1); + r.trades[0] = getTradeData(sellAmount, buyAmount, validTo, owner, r.orderData.receiver, false); + + // 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; + + // Setup interactions - withdraw from sell vault, swap underlying assets, deposit to buy vault + r.interactions = [ + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](4), + new CowSettlement.CowInteractionData[](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] = CowSettlement.CowInteractionData({ + 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); + + vm.startPrank(user); + + // User deposits SUSDS collateral + IERC20(SUSDS).approve(ESUSDS, type(uint256).max); + uint256 depositAmount = 1000e18; + IERC4626(ESUSDS).deposit(depositAmount, user); + + uint256 sellAmount = 500e18; // Sell 500 ESUSDS + uint256 buyAmount = 0.0045e8; // Expect to receive ~0.0045 EWBTC (8 decimals) + + // Get settlement data + SettlementData memory settlement = getCollateralSwapSettlement( + user, + user, // Receiver is user since it's main account + ESUSDS, + EWBTC, + sellAmount, + buyAmount + ); + + // User signs the order on cowswap + COW_SETTLEMENT.setPreSignature(settlement.orderUid, true); + + // User approves vault shares for settlement + 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); + + // Prepare CollateralSwapParams + uint256 deadline = block.timestamp + 1 hours; + ecdsa.setPrivateKey(privateKey); + + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: user, + account: user, // Main account + deadline: deadline, + fromVault: ESUSDS, + toVault: EWBTC, + swapAmount: sellAmount, + kind: GPv2Order.KIND_SELL + }); + + // Sign permit for EVC operator + bytes memory permitSignature = ecdsa.signPermit( + user, + address(collateralSwapWrapper), + uint256(uint160(address(collateralSwapWrapper))), + 0, + deadline, + 0, + collateralSwapWrapper.getSignedCalldata(params) + ); + + // Encode settlement data + bytes memory settleData = abi.encodeCall( + CowSettlement.settle, + (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions) + ); + + // Encode wrapper data with CollateralSwapParams + bytes memory wrapperData = abi.encode(params, permitSignature); + wrapperData = abi.encodePacked(uint16(wrapperData.length), wrapperData); + + // Execute wrapped settlement through solver + address[] memory targets = new address[](1); + bytes[] memory datas = new bytes[](1); + targets[0] = address(collateralSwapWrapper); + datas[0] = abi.encodeCall(collateralSwapWrapper.wrappedSettle, (settleData, wrapperData)); + + // Expect the event to be emitted + vm.expectEmit(true, true, true, true); + emit CowEvcCollateralSwapWrapper.CowEvcCollateralSwapped( + params.owner, params.account, params.fromVault, params.toVault, params.swapAmount, params.kind + ); + + solver.runBatch(targets, datas); + + // Verify the collateral was swapped successfully + assertEq( + IERC20(ESUSDS).balanceOf(user), susdsBalanceBefore - sellAmount, "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)); + + uint256 sellAmount = 500e18; // Sell 500 ESUSDS + uint256 buyAmount = 0.0045e8; // Expect to receive ~0.0045 EWBTC (8 decimals) + + // Prepare CollateralSwapParams + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: user, + account: account, // Subaccount + deadline: block.timestamp + 1 hours, + fromVault: ESUSDS, + toVault: EWBTC, + swapAmount: sellAmount, + kind: GPv2Order.KIND_SELL + }); + + vm.startPrank(user); + + // User deposits SUSDS collateral to subaccount + IERC20(SUSDS).approve(ESUSDS, type(uint256).max); + uint256 depositAmount = 1000e18; + IERC4626(ESUSDS).deposit(depositAmount, account); + + // Get settlement data - receiver is the subaccount + SettlementData memory settlement = getCollateralSwapSettlement( + user, + account, // Receiver is subaccount + ESUSDS, + EWBTC, + sellAmount, + buyAmount + ); + + // User signs the order on cowswap + COW_SETTLEMENT.setPreSignature(settlement.orderUid, true); + + // User approves vault shares for settlement (from main account) + IEVault(ESUSDS).approve(COW_SETTLEMENT.vaultRelayer(), type(uint256).max); + + // For subaccount, user approves transfer of vault shares from the account to main account + { + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](1); + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: account, + targetContract: ESUSDS, + value: 0, + data: abi.encodeCall(IERC20.approve, (address(collateralSwapWrapper), type(uint256).max)) + }); + evc.batch(items); + } + + // User approves the wrapper to be operator (both of the main account and the subaccount) + evc.setAccountOperator(account, address(collateralSwapWrapper), true); + + // User pre-approves the hash for the wrapper operation + bytes32 hash = collateralSwapWrapper.getApprovalHash(params); + collateralSwapWrapper.setPreApprovedHash(hash, true); + + vm.stopPrank(); + + // Record balances before swap + uint256 susdsBalanceBefore = IERC20(ESUSDS).balanceOf(account); + uint256 wbtcBalanceBefore = IERC20(EWBTC).balanceOf(account); + + // Encode settlement data + bytes memory settleData = abi.encodeCall( + CowSettlement.settle, + (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions) + ); + + // Encode wrapper data with CollateralSwapParams + bytes memory signature = new bytes(0); // Empty signature for pre-approved hash + bytes memory wrapperData = abi.encode(params, signature); + wrapperData = abi.encodePacked(uint16(wrapperData.length), wrapperData); + + // Execute wrapped settlement through solver + address[] memory targets = new address[](1); + bytes[] memory datas = new bytes[](1); + targets[0] = address(collateralSwapWrapper); + datas[0] = abi.encodeCall(collateralSwapWrapper.wrappedSettle, (settleData, wrapperData)); + + // Expect the event to be emitted + vm.expectEmit(true, true, true, true); + emit CowEvcCollateralSwapWrapper.CowEvcCollateralSwapped( + params.owner, params.account, params.fromVault, params.toVault, params.swapAmount, params.kind + ); + + solver.runBatch(targets, datas); + + // Verify the collateral was swapped successfully + assertEq( + IERC20(ESUSDS).balanceOf(account), + susdsBalanceBefore - sellAmount, + "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 { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: user, + account: address(uint160(user) ^ uint8(0x01)), + deadline: block.timestamp + 1 hours, + fromVault: ESUSDS, + toVault: EWBTC, + swapAmount: 1000e18, + kind: GPv2Order.KIND_SELL + }); + + 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; + + // Set up a leveraged position + _setupLeveragedPosition(borrowAmount, collateralAmount); + + address account = address(uint160(user) ^ uint8(0x01)); + + uint256 sellAmount = 1000 ether + 2500 ether; // Sell 3500 ESUSDS + uint256 buyAmount = 0.0325e8; // Expect to receive ~0.0325 EWBTC (8 decimals) + + // Prepare CollateralSwapParams + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: user, + account: account, + deadline: block.timestamp + 1 hours, + fromVault: ESUSDS, + toVault: EWBTC, + swapAmount: sellAmount, + kind: GPv2Order.KIND_SELL + }); + + // Now swap some collateral from SUSDS to WBTC (add more WBTC collateral) + vm.startPrank(user); + + // Get settlement data + SettlementData memory settlement = getCollateralSwapSettlement( + user, + account, // Receiver is subaccount + ESUSDS, + EWBTC, + sellAmount, + buyAmount + ); + + // User signs the order on cowswap + COW_SETTLEMENT.setPreSignature(settlement.orderUid, true); + + // User approves vault shares for settlement + IEVault(ESUSDS).approve(COW_SETTLEMENT.vaultRelayer(), type(uint256).max); + + // For subaccount, user approves transfer of vault shares from the account + { + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](1); + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: account, + targetContract: ESUSDS, + value: 0, + data: abi.encodeCall(IERC20.approve, (address(collateralSwapWrapper), type(uint256).max)) + }); + evc.batch(items); + } + + // User approves the wrapper to be operator (both of the main account and the subaccount) + evc.setAccountOperator(account, address(collateralSwapWrapper), true); + + // User pre-approves the hash for the wrapper operation + bytes32 hash = collateralSwapWrapper.getApprovalHash(params); + collateralSwapWrapper.setPreApprovedHash(hash, true); + + vm.stopPrank(); + + // 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 data + bytes memory settleData = abi.encodeCall( + CowSettlement.settle, + (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions) + ); + + // Encode wrapper data with CollateralSwapParams + bytes memory signature = new bytes(0); // Empty signature for pre-approved hash + bytes memory wrapperData = abi.encode(params, signature); + wrapperData = abi.encodePacked(uint16(wrapperData.length), wrapperData); + + // Execute wrapped settlement through solver + address[] memory targets = new address[](1); + bytes[] memory datas = new bytes[](1); + targets[0] = address(collateralSwapWrapper); + datas[0] = abi.encodeCall(collateralSwapWrapper.wrappedSettle, (settleData, wrapperData)); + + // Expect the event to be emitted + vm.expectEmit(true, true, true, true); + emit CowEvcCollateralSwapWrapper.CowEvcCollateralSwapped( + params.owner, params.account, params.fromVault, params.toVault, params.swapAmount, params.kind + ); + + solver.runBatch(targets, datas); + + // 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"); + } +} diff --git a/test/helpers/CowBaseTest.sol b/test/helpers/CowBaseTest.sol index 6c6fda0..b88a418 100644 --- a/test/helpers/CowBaseTest.sol +++ b/test/helpers/CowBaseTest.sol @@ -8,7 +8,7 @@ import {EVaultTestBase} from "lib/euler-vault-kit/test/unit/evault/EVaultTestBas import {IEVault, IVault, IERC4626, IERC20} from "euler-vault-kit/src/EVault/IEVault.sol"; import {GPv2AllowListAuthentication} from "cow/GPv2AllowListAuthentication.sol"; -import {ICowSettlement} from "../../src/CowWrapper.sol"; +import {CowSettlement} from "../../src/vendor/CowWrapper.sol"; import {MilkSwap} from "./MilkSwap.sol"; @@ -32,15 +32,17 @@ contract CowBaseTest is EVaultTestBase { 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 payable constant REAL_EVC = payable(0x0C9a3dd6b8F28529d72d7f9cE918D493519EE383); address internal swapVerifier = 0xae26485ACDDeFd486Fe9ad7C2b34169d360737c7; - ICowSettlement constant COW_SETTLEMENT = ICowSettlement(payable(0x9008D19f58AAbD9eD0D60971565AA8510560ab41)); + CowSettlement constant COW_SETTLEMENT = CowSettlement(payable(0x9008D19f58AAbD9eD0D60971565AA8510560ab41)); MilkSwap public milkSwap; address user; @@ -73,18 +75,22 @@ contract CowBaseTest is EVaultTestBase { // 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 - milkSwap.setPrice(WETH, 1000e18); // 1 ETH = 1,000 USD + 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) // 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(); @@ -99,10 +105,12 @@ contract CowBaseTest is EVaultTestBase { 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(address(COW_SETTLEMENT), "cow settlement"); - vm.label(address(milkSwap), "milkswap"); + vm.label(EWBTC, "eWBTC"); + vm.label(address(COW_SETTLEMENT), "cowSettlement"); + vm.label(address(milkSwap), "milkSwap"); } function getEmptySettlement() @@ -111,18 +119,18 @@ contract CowBaseTest is EVaultTestBase { returns ( IERC20[] memory tokens, uint256[] memory clearingPrices, - ICowSettlement.Trade[] memory trades, - ICowSettlement.Interaction[][3] memory interactions + CowSettlement.CowTradeData[] memory trades, + CowSettlement.CowInteractionData[][3] memory interactions ) { return ( new IERC20[](0), new uint256[](0), - new ICowSettlement.Trade[](0), + new CowSettlement.CowTradeData[](0), [ - new ICowSettlement.Interaction[](0), - new ICowSettlement.Interaction[](0), - new ICowSettlement.Interaction[](0) + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0) ] ); } @@ -138,9 +146,9 @@ contract CowBaseTest is EVaultTestBase { function getSwapInteraction(address sellToken, address buyToken, uint256 sellAmount) public view - returns (ICowSettlement.Interaction memory) + returns (CowSettlement.CowInteractionData memory) { - return ICowSettlement.Interaction({ + return CowSettlement.CowInteractionData({ target: address(milkSwap), value: 0, callData: abi.encodeCall(MilkSwap.swap, (sellToken, buyToken, sellAmount)) @@ -151,9 +159,9 @@ contract CowBaseTest is EVaultTestBase { function getDepositInteraction(address vault, uint256 sellAmount) public view - returns (ICowSettlement.Interaction memory) + returns (CowSettlement.CowInteractionData memory) { - return ICowSettlement.Interaction({ + return CowSettlement.CowInteractionData({ target: address(IEVault(vault).asset()), value: 0, callData: abi.encodeCall(IERC20.transfer, (vault, sellAmount)) @@ -163,17 +171,17 @@ contract CowBaseTest is EVaultTestBase { function getWithdrawInteraction(address vault, uint256 sellAmount) public pure - returns (ICowSettlement.Interaction memory) + returns (CowSettlement.CowInteractionData memory) { - return ICowSettlement.Interaction({ + return CowSettlement.CowInteractionData({ target: vault, value: 0, callData: abi.encodeCall(IERC4626.withdraw, (sellAmount, address(COW_SETTLEMENT), address(COW_SETTLEMENT))) }); } - function getSkimInteraction() public pure returns (ICowSettlement.Interaction memory) { - return ICowSettlement.Interaction({ + function getSkimInteraction() public pure returns (CowSettlement.CowInteractionData memory) { + return CowSettlement.CowInteractionData({ target: address(ESUSDS), value: 0, callData: abi.encodeCall(IVault.skim, (type(uint256).max, address(COW_SETTLEMENT))) @@ -187,13 +195,13 @@ contract CowBaseTest is EVaultTestBase { address owner, address receiver, bool isBuy - ) public pure returns (ICowSettlement.Trade memory) { + ) public pure returns (CowSettlement.CowTradeData memory) { // Set flags for (pre-sign, FoK sell order) // See // https://github.com/cowprotocol/contracts/blob/08f8627d8427c8842ae5d29ed8b44519f7674879/src/contracts/libraries/GPv2Trade.sol#L89-L94 uint256 flags = (3 << 5) | (isBuy ? 1 : 0); // 1100000 - return ICowSettlement.Trade({ + return CowSettlement.CowTradeData({ sellTokenIndex: 0, buyTokenIndex: 1, receiver: receiver, diff --git a/test/unit/CowEvcCollateralSwapWrapper.unit.t.sol b/test/unit/CowEvcCollateralSwapWrapper.unit.t.sol new file mode 100644 index 0000000..ac34a44 --- /dev/null +++ b/test/unit/CowEvcCollateralSwapWrapper.unit.t.sol @@ -0,0 +1,1090 @@ +// 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 {CowSettlement} from "../../src/vendor/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); + + // 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); + + // Helper function to decode signed calldata + 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), CowSettlement(address(mockSettlement))); + emptyWrapper = new EmptyWrapper(CowSettlement(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"); + } + + function test_Constructor_SetsName() public view { + assertEq(wrapper.name(), "Euler EVC - Collateral Swap", "Name not set correctly"); + } + + /*////////////////////////////////////////////////////////////// + PARSE WRAPPER DATA TESTS + //////////////////////////////////////////////////////////////*/ + + function test_ParseWrapperData_EmptySignature() 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 + }); + + 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_WithSignature() 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 + }); + + bytes memory signature = new bytes(65); + bytes memory wrapperData = abi.encode(params, signature); + 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 = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_SELL + }); + + 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_Consistency() 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 + }); + + bytes32 hash1 = wrapper.getApprovalHash(params); + bytes32 hash2 = wrapper.getApprovalHash(params); + + assertEq(hash1, hash2, "Hash should be consistent"); + } + + function test_GetApprovalHash_DifferentForDifferentParams() public view { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params1 = + CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_SELL + }); + + // Same as params1 except owner + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params2 = + CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: ACCOUNT, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_SELL + }); + + // Same as params1 except swapAmount (the last meaningful field) + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params3 = + CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 2000e18, + kind: KIND_SELL + }); + + 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 = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_SELL + }); + + 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 = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_SELL + }); + + 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 = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_SELL + }); + + 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( + CowSettlement.settle, + ( + tokens, + prices, + new CowSettlement.CowTradeData[](0), + [ + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](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( + CowSettlement.settle, + ( + tokens, + prices, + new CowSettlement.CowTradeData[](0), + [ + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](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( + CowSettlement.settle, + ( + tokens, + prices, + new CowSettlement.CowTradeData[](0), + [ + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](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( + CowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new CowSettlement.CowTradeData[](0), + [ + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](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( + CowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new CowSettlement.CowTradeData[](0), + [ + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](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( + CowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new CowSettlement.CowTradeData[](0), + [ + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](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( + CowSettlement.settle, + ( + tokens, + prices, + new CowSettlement.CowTradeData[](0), + [ + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](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( + CowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new CowSettlement.CowTradeData[](0), + [ + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](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( + CowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new CowSettlement.CowTradeData[](0), + [ + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](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 = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: OWNER, + deadline: block.timestamp + 1 hours, + fromVault: address(mockFromVault), + toVault: address(mockToVault), + swapAmount: 1000e18, + kind: KIND_SELL + }); + + address[] memory tokens = new address[](0); + uint256[] memory prices = new uint256[](0); + + bytes memory signature = new bytes(65); + bytes memory settleData = abi.encodeCall( + CowSettlement.settle, + ( + tokens, + prices, + new CowSettlement.CowTradeData[](0), + [ + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](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); + } + + function test_WrappedSettle_WithPreApprovedHash() public { + mockFromVault.mint(OWNER, 2000e18); + + vm.startPrank(OWNER); + mockFromVault.approve(address(wrapper), 2000e18); + vm.stopPrank(); + + 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); + + address[] memory tokens = new address[](0); + uint256[] memory prices = new uint256[](0); + + bytes memory settleData = abi.encodeCall( + CowSettlement.settle, + ( + tokens, + prices, + new CowSettlement.CowTradeData[](0), + [ + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](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); + + assertFalse(wrapper.isHashPreApproved(OWNER, hash), "Hash should be consumed"); + } + + function test_WrappedSettle_PreApprovedHashRevertsIfDeadlineExceeded() public { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: OWNER, + account: OWNER, + deadline: block.timestamp - 1, // Past deadline + 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( + CowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new CowSettlement.CowTradeData[](0), + [ + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + wrapperData = abi.encodePacked(uint16(wrapperData.length), wrapperData); + + 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( + CowSettlement.settle, + ( + tokens, + prices, + new CowSettlement.CowTradeData[](0), + [ + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](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( + CowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new CowSettlement.CowTradeData[](0), + [ + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](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( + CowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new CowSettlement.CowTradeData[](0), + [ + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](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( + CowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new CowSettlement.CowTradeData[](0), + [ + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](0), + new CowSettlement.CowInteractionData[](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) + } +} From 29f92ba9a7e5a9ff2093ea362090105cea9de040 Mon Sep 17 00:00:00 2001 From: Kaze Date: Thu, 6 Nov 2025 10:03:22 +0900 Subject: [PATCH 02/10] fixes from upstream --- src/CowEvcCollateralSwapWrapper.sol | 10 +- test/CowEvcCollateralSwapWrapper.t.sol | 22 +-- test/helpers/CowBaseTest.sol | 40 ++--- .../CowEvcCollateralSwapWrapper.unit.t.sol | 166 +++++++++--------- 4 files changed, 119 insertions(+), 119 deletions(-) diff --git a/src/CowEvcCollateralSwapWrapper.sol b/src/CowEvcCollateralSwapWrapper.sol index 7b2f193..e2e63b0 100644 --- a/src/CowEvcCollateralSwapWrapper.sol +++ b/src/CowEvcCollateralSwapWrapper.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8; import {IEVC} from "evc/EthereumVaultConnector.sol"; -import {CowWrapper, CowSettlement} from "./vendor/CowWrapper.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"; @@ -83,7 +83,7 @@ contract CowEvcCollateralSwapWrapper is CowWrapper, PreApprovedHashes { bytes32 kind ); - constructor(address _evc, CowSettlement _settlement) CowWrapper(_settlement) { + constructor(address _evc, ICowSettlement _settlement) CowWrapper(_settlement) { EVC = IEVC(_evc); NONCE_NAMESPACE = uint256(uint160(address(this))); @@ -278,7 +278,7 @@ contract CowEvcCollateralSwapWrapper is CowWrapper, PreApprovedHashes { returns (uint256 fromVaultPrice, uint256 toVaultPrice) { (address[] memory tokens, uint256[] memory clearingPrices,,) = abi.decode( - settleData[4:], (address[], uint256[], CowSettlement.CowTradeData[], CowSettlement.CowInteractionData[][3]) + settleData[4:], (address[], uint256[], ICowSettlement.Trade[], ICowSettlement.Interaction[][3]) ); for (uint256 i = 0; i < tokens.length; i++) { if (tokens[i] == fromVault) { @@ -335,8 +335,8 @@ contract CowEvcCollateralSwapWrapper is CowWrapper, PreApprovedHashes { ); } - // Use GPv2Wrapper's _internalSettle to call the settlement contract + // Use GPv2Wrapper's _next to call the settlement contract // wrapperData is empty since we've already processed it in _wrap - _internalSettle(settleData, remainingWrapperData); + _next(settleData, remainingWrapperData); } } diff --git a/test/CowEvcCollateralSwapWrapper.t.sol b/test/CowEvcCollateralSwapWrapper.t.sol index e2f1028..8640131 100644 --- a/test/CowEvcCollateralSwapWrapper.t.sol +++ b/test/CowEvcCollateralSwapWrapper.t.sol @@ -7,7 +7,7 @@ import {IEVC} from "evc/EthereumVaultConnector.sol"; import {IEVault, IERC4626, IBorrowing, IERC20} from "euler-vault-kit/src/EVault/IEVault.sol"; import {CowEvcCollateralSwapWrapper} from "../src/CowEvcCollateralSwapWrapper.sol"; -import {CowSettlement, CowWrapper} from "../src/vendor/CowWrapper.sol"; +import {ICowSettlement, CowWrapper} from "../src/CowWrapper.sol"; import {GPv2AllowListAuthentication} from "cow/GPv2AllowListAuthentication.sol"; import {CowBaseTest} from "./helpers/CowBaseTest.sol"; @@ -86,8 +86,8 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { GPv2Order.Data orderData; address[] tokens; uint256[] clearingPrices; - CowSettlement.CowTradeData[] trades; - CowSettlement.CowInteractionData[][3] interactions; + ICowSettlement.Trade[] trades; + ICowSettlement.Interaction[][3] interactions; } /// @notice Create settlement data for swapping collateral between vaults @@ -122,7 +122,7 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { r.orderUid = getOrderUid(owner, r.orderData); // Get trade data - r.trades = new CowSettlement.CowTradeData[](1); + r.trades = new ICowSettlement.Trade[](1); r.trades[0] = getTradeData(sellAmount, buyAmount, validTo, owner, r.orderData.receiver, false); // Get tokens and prices @@ -136,9 +136,9 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { // Setup interactions - withdraw from sell vault, swap underlying assets, deposit to buy vault r.interactions = [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](4), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](4), + new ICowSettlement.Interaction[](0) ]; // Withdraw from sell vault @@ -155,7 +155,7 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { r.interactions[1][2] = getDepositInteraction(buyVaultToken, buyUnderlyingAmount); // Skim to mint vault shares to receiver - r.interactions[1][3] = CowSettlement.CowInteractionData({ + r.interactions[1][3] = ICowSettlement.Interaction({ target: buyVaultToken, value: 0, callData: abi.encodeWithSignature("skim(uint256,address)", type(uint256).max, address(COW_SETTLEMENT)) @@ -225,7 +225,7 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { // Encode settlement data bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions) ); @@ -324,7 +324,7 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { // Encode settlement data bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions) ); @@ -476,7 +476,7 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { // Encode settlement data bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions) ); diff --git a/test/helpers/CowBaseTest.sol b/test/helpers/CowBaseTest.sol index b88a418..c08cbaa 100644 --- a/test/helpers/CowBaseTest.sol +++ b/test/helpers/CowBaseTest.sol @@ -8,7 +8,7 @@ import {EVaultTestBase} from "lib/euler-vault-kit/test/unit/evault/EVaultTestBas import {IEVault, IVault, IERC4626, IERC20} from "euler-vault-kit/src/EVault/IEVault.sol"; import {GPv2AllowListAuthentication} from "cow/GPv2AllowListAuthentication.sol"; -import {CowSettlement} from "../../src/vendor/CowWrapper.sol"; +import {ICowSettlement} from "../../src/CowWrapper.sol"; import {MilkSwap} from "./MilkSwap.sol"; @@ -42,7 +42,7 @@ contract CowBaseTest is EVaultTestBase { address payable constant REAL_EVC = payable(0x0C9a3dd6b8F28529d72d7f9cE918D493519EE383); address internal swapVerifier = 0xae26485ACDDeFd486Fe9ad7C2b34169d360737c7; - CowSettlement constant COW_SETTLEMENT = CowSettlement(payable(0x9008D19f58AAbD9eD0D60971565AA8510560ab41)); + ICowSettlement constant COW_SETTLEMENT = ICowSettlement(payable(0x9008D19f58AAbD9eD0D60971565AA8510560ab41)); MilkSwap public milkSwap; address user; @@ -117,20 +117,20 @@ contract CowBaseTest is EVaultTestBase { public pure returns ( - IERC20[] memory tokens, + address[] memory tokens, uint256[] memory clearingPrices, - CowSettlement.CowTradeData[] memory trades, - CowSettlement.CowInteractionData[][3] memory interactions + ICowSettlement.Trade[] memory trades, + ICowSettlement.Interaction[][3] memory interactions ) { return ( - new IERC20[](0), + new address[](0), new uint256[](0), - new CowSettlement.CowTradeData[](0), + new ICowSettlement.Trade[](0), [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) ] ); } @@ -146,9 +146,9 @@ contract CowBaseTest is EVaultTestBase { function getSwapInteraction(address sellToken, address buyToken, uint256 sellAmount) public view - returns (CowSettlement.CowInteractionData memory) + returns (ICowSettlement.Interaction memory) { - return CowSettlement.CowInteractionData({ + return ICowSettlement.Interaction({ target: address(milkSwap), value: 0, callData: abi.encodeCall(MilkSwap.swap, (sellToken, buyToken, sellAmount)) @@ -159,9 +159,9 @@ contract CowBaseTest is EVaultTestBase { function getDepositInteraction(address vault, uint256 sellAmount) public view - returns (CowSettlement.CowInteractionData memory) + returns (ICowSettlement.Interaction memory) { - return CowSettlement.CowInteractionData({ + return ICowSettlement.Interaction({ target: address(IEVault(vault).asset()), value: 0, callData: abi.encodeCall(IERC20.transfer, (vault, sellAmount)) @@ -171,17 +171,17 @@ contract CowBaseTest is EVaultTestBase { function getWithdrawInteraction(address vault, uint256 sellAmount) public pure - returns (CowSettlement.CowInteractionData memory) + returns (ICowSettlement.Interaction memory) { - return CowSettlement.CowInteractionData({ + return ICowSettlement.Interaction({ target: vault, value: 0, callData: abi.encodeCall(IERC4626.withdraw, (sellAmount, address(COW_SETTLEMENT), address(COW_SETTLEMENT))) }); } - function getSkimInteraction() public pure returns (CowSettlement.CowInteractionData memory) { - return CowSettlement.CowInteractionData({ + function getSkimInteraction() public pure returns (ICowSettlement.Interaction memory) { + return ICowSettlement.Interaction({ target: address(ESUSDS), value: 0, callData: abi.encodeCall(IVault.skim, (type(uint256).max, address(COW_SETTLEMENT))) @@ -195,13 +195,13 @@ contract CowBaseTest is EVaultTestBase { address owner, address receiver, bool isBuy - ) public pure returns (CowSettlement.CowTradeData memory) { + ) public pure returns (ICowSettlement.Trade memory) { // Set flags for (pre-sign, FoK sell order) // See // https://github.com/cowprotocol/contracts/blob/08f8627d8427c8842ae5d29ed8b44519f7674879/src/contracts/libraries/GPv2Trade.sol#L89-L94 uint256 flags = (3 << 5) | (isBuy ? 1 : 0); // 1100000 - return CowSettlement.CowTradeData({ + return ICowSettlement.Trade({ sellTokenIndex: 0, buyTokenIndex: 1, receiver: receiver, diff --git a/test/unit/CowEvcCollateralSwapWrapper.unit.t.sol b/test/unit/CowEvcCollateralSwapWrapper.unit.t.sol index ac34a44..5fe467c 100644 --- a/test/unit/CowEvcCollateralSwapWrapper.unit.t.sol +++ b/test/unit/CowEvcCollateralSwapWrapper.unit.t.sol @@ -5,7 +5,7 @@ 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 {CowSettlement} from "../../src/vendor/CowWrapper.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"; @@ -50,8 +50,8 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { mockFromVault = new MockVault(address(mockAsset), "Mock From Vault", "mFROM"); mockToVault = new MockVault(address(mockAsset), "Mock To Vault", "mTO"); - wrapper = new CowEvcCollateralSwapWrapper(address(mockEvc), CowSettlement(address(mockSettlement))); - emptyWrapper = new EmptyWrapper(CowSettlement(address(mockSettlement))); + wrapper = new CowEvcCollateralSwapWrapper(address(mockEvc), ICowSettlement(address(mockSettlement))); + emptyWrapper = new EmptyWrapper(ICowSettlement(address(mockSettlement))); // Set solver as authenticated mockAuth.setSolver(SOLVER, true); @@ -313,15 +313,15 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { prices[1] = 2e18; bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, ( tokens, prices, - new CowSettlement.CowTradeData[](0), + new ICowSettlement.Trade[](0), [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) ] ) ); @@ -354,15 +354,15 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { prices[0] = 2e18; bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, ( tokens, prices, - new CowSettlement.CowTradeData[](0), + new ICowSettlement.Trade[](0), [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) ] ) ); @@ -398,15 +398,15 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { prices[0] = 1e18; bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, ( tokens, prices, - new CowSettlement.CowTradeData[](0), + new ICowSettlement.Trade[](0), [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) ] ) ); @@ -448,15 +448,15 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { }); bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, ( new address[](0), new uint256[](0), - new CowSettlement.CowTradeData[](0), + new ICowSettlement.Trade[](0), [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) ] ) ); @@ -485,15 +485,15 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { }); bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, ( new address[](0), new uint256[](0), - new CowSettlement.CowTradeData[](0), + new ICowSettlement.Trade[](0), [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) ] ) ); @@ -527,15 +527,15 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { // Create settle data without prices (not needed for KIND_SELL) bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, ( new address[](0), new uint256[](0), - new CowSettlement.CowTradeData[](0), + new ICowSettlement.Trade[](0), [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) ] ) ); @@ -581,15 +581,15 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { prices[1] = 2e18; // toVault price (2x more expensive) bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, ( tokens, prices, - new CowSettlement.CowTradeData[](0), + new ICowSettlement.Trade[](0), [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) ] ) ); @@ -630,15 +630,15 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { mockFromVault.mint(invalidSubaccount, 2000e18); bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, ( new address[](0), new uint256[](0), - new CowSettlement.CowTradeData[](0), + new ICowSettlement.Trade[](0), [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) ] ) ); @@ -671,15 +671,15 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { mockFromVault.mint(OWNER, 2000e18); bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, ( new address[](0), new uint256[](0), - new CowSettlement.CowTradeData[](0), + new ICowSettlement.Trade[](0), [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) ] ) ); @@ -727,15 +727,15 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { bytes memory signature = new bytes(65); bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, ( tokens, prices, - new CowSettlement.CowTradeData[](0), + new ICowSettlement.Trade[](0), [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) ] ) ); @@ -776,15 +776,15 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { uint256[] memory prices = new uint256[](0); bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, ( tokens, prices, - new CowSettlement.CowTradeData[](0), + new ICowSettlement.Trade[](0), [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) ] ) ); @@ -818,15 +818,15 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { mockEvc.setOperator(OWNER, address(wrapper), true); bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, ( new address[](0), new uint256[](0), - new CowSettlement.CowTradeData[](0), + new ICowSettlement.Trade[](0), [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) ] ) ); @@ -906,15 +906,15 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { prices[1] = 3e18; // toVault price (3x more expensive) bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, ( tokens, prices, - new CowSettlement.CowTradeData[](0), + new ICowSettlement.Trade[](0), [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) ] ) ); @@ -987,15 +987,15 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { }); bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, ( new address[](0), new uint256[](0), - new CowSettlement.CowTradeData[](0), + new ICowSettlement.Trade[](0), [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) ] ) ); @@ -1023,15 +1023,15 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { bytes memory signature = new bytes(65); bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, ( new address[](0), new uint256[](0), - new CowSettlement.CowTradeData[](0), + new ICowSettlement.Trade[](0), [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) ] ) ); @@ -1065,15 +1065,15 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { mockEvc.setOperator(OWNER, address(wrapper), true); bytes memory settleData = abi.encodeCall( - CowSettlement.settle, + ICowSettlement.settle, ( new address[](0), new uint256[](0), - new CowSettlement.CowTradeData[](0), + new ICowSettlement.Trade[](0), [ - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0), - new CowSettlement.CowInteractionData[](0) + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) ] ) ); From 718c0bb893a47134ce3055ff310851059c64bccf Mon Sep 17 00:00:00 2001 From: Kaze Date: Thu, 6 Nov 2025 10:04:57 +0900 Subject: [PATCH 03/10] forge fmt --- src/CowEvcCollateralSwapWrapper.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/CowEvcCollateralSwapWrapper.sol b/src/CowEvcCollateralSwapWrapper.sol index e2e63b0..bb4e1f8 100644 --- a/src/CowEvcCollateralSwapWrapper.sol +++ b/src/CowEvcCollateralSwapWrapper.sol @@ -277,9 +277,8 @@ contract CowEvcCollateralSwapWrapper is CowWrapper, PreApprovedHashes { pure returns (uint256 fromVaultPrice, uint256 toVaultPrice) { - (address[] memory tokens, uint256[] memory clearingPrices,,) = abi.decode( - settleData[4:], (address[], uint256[], ICowSettlement.Trade[], ICowSettlement.Interaction[][3]) - ); + (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]; From 9271bee4d83266630072484af671e4a13f32d266 Mon Sep 17 00:00:00 2001 From: Kaze Date: Sat, 8 Nov 2025 05:56:33 +0900 Subject: [PATCH 04/10] refactor: improve collateral swap test suite DRYness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor CowEvcCollateralSwapWrapper test files to reduce code duplication and improve maintainability: Integration tests (CowEvcCollateralSwapWrapper.t.sol): - Add helper functions for common test setup patterns - _createDefaultParams(): standardized param creation - _createPermitSignature(): permit signature creation - _encodeWrapperData(): wrapper data encoding - _executeWrappedSettlement(): settlement execution - _setupSubaccountApprovals(): comprehensive subaccount setup - Add DEFAULT_SWAP_AMOUNT and DEFAULT_BUY_AMOUNT constants - Reduce test function size by ~40% while maintaining clarity Unit tests (CowEvcCollateralSwapWrapper.unit.t.sol): - Add helper functions for common patterns - _getDefaultParams(): returns default params (tests modify as needed) - _getEmptySettleData(): creates empty settlement data - _encodeWrapperData(): wrapper data encoding - _setupPreApprovedHash(): pre-approved hash setup - Add DEFAULT_SWAP_AMOUNT constant - Remove 3 redundant tests with no coverage impact: - test_Constructor_SetsName (trivial getter) - test_GetApprovalHash_Consistency (deterministic function) - test_ParseWrapperData_WithSignature (duplicate code path) - Reduce from 33 to 30 unit tests with no loss in coverage All tests pass: 6/6 integration tests, 30/30 unit tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- test/CowEvcCollateralSwapWrapper.t.sol | 312 ++++++++---------- .../CowEvcCollateralSwapWrapper.unit.t.sol | 294 +++++------------ 2 files changed, 213 insertions(+), 393 deletions(-) diff --git a/test/CowEvcCollateralSwapWrapper.t.sol b/test/CowEvcCollateralSwapWrapper.t.sol index 8640131..819a9bd 100644 --- a/test/CowEvcCollateralSwapWrapper.t.sol +++ b/test/CowEvcCollateralSwapWrapper.t.sol @@ -20,6 +20,8 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { 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(); @@ -90,6 +92,88 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { 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 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(address account, CowEvcCollateralSwapWrapper.CollateralSwapParams memory params) + internal + { + vm.startPrank(user); + + // 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: 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(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( @@ -166,90 +250,50 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { function test_CollateralSwapWrapper_MainAccount() external { vm.skip(bytes(forkRpcUrl).length == 0); - vm.startPrank(user); - // User deposits SUSDS collateral + vm.startPrank(user); IERC20(SUSDS).approve(ESUSDS, type(uint256).max); uint256 depositAmount = 1000e18; IERC4626(ESUSDS).deposit(depositAmount, user); - uint256 sellAmount = 500e18; // Sell 500 ESUSDS - uint256 buyAmount = 0.0045e8; // Expect to receive ~0.0045 EWBTC (8 decimals) + // Create params using helper + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _createDefaultParams(user, user); // Get settlement data - SettlementData memory settlement = getCollateralSwapSettlement( - user, - user, // Receiver is user since it's main account - ESUSDS, - EWBTC, - sellAmount, - buyAmount - ); + SettlementData memory settlement = + getCollateralSwapSettlement(user, user, ESUSDS, EWBTC, DEFAULT_SWAP_AMOUNT, DEFAULT_BUY_AMOUNT); - // User signs the order on cowswap + // User signs the order and approves vault shares for settlement COW_SETTLEMENT.setPreSignature(settlement.orderUid, true); - - // User approves vault shares for settlement 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); - // Prepare CollateralSwapParams - uint256 deadline = block.timestamp + 1 hours; - ecdsa.setPrivateKey(privateKey); - - CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ - owner: user, - account: user, // Main account - deadline: deadline, - fromVault: ESUSDS, - toVault: EWBTC, - swapAmount: sellAmount, - kind: GPv2Order.KIND_SELL - }); - - // Sign permit for EVC operator - bytes memory permitSignature = ecdsa.signPermit( - user, - address(collateralSwapWrapper), - uint256(uint160(address(collateralSwapWrapper))), - 0, - deadline, - 0, - collateralSwapWrapper.getSignedCalldata(params) - ); - - // Encode settlement data + // 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); - // Encode wrapper data with CollateralSwapParams - bytes memory wrapperData = abi.encode(params, permitSignature); - wrapperData = abi.encodePacked(uint16(wrapperData.length), wrapperData); - - // Execute wrapped settlement through solver - address[] memory targets = new address[](1); - bytes[] memory datas = new bytes[](1); - targets[0] = address(collateralSwapWrapper); - datas[0] = abi.encodeCall(collateralSwapWrapper.wrappedSettle, (settleData, wrapperData)); - - // Expect the event to be emitted + // Expect event emission vm.expectEmit(true, true, true, true); emit CowEvcCollateralSwapWrapper.CowEvcCollateralSwapped( params.owner, params.account, params.fromVault, params.toVault, params.swapAmount, params.kind ); - solver.runBatch(targets, datas); + // Execute wrapped settlement + _executeWrappedSettlement(settleData, wrapperData); // Verify the collateral was swapped successfully assertEq( - IERC20(ESUSDS).balanceOf(user), susdsBalanceBefore - sellAmount, "User should have less ESUSDS after swap" + 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"); } @@ -260,97 +304,50 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { address account = address(uint160(user) ^ uint8(0x01)); - uint256 sellAmount = 500e18; // Sell 500 ESUSDS - uint256 buyAmount = 0.0045e8; // Expect to receive ~0.0045 EWBTC (8 decimals) - - // Prepare CollateralSwapParams - CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ - owner: user, - account: account, // Subaccount - deadline: block.timestamp + 1 hours, - fromVault: ESUSDS, - toVault: EWBTC, - swapAmount: sellAmount, - kind: GPv2Order.KIND_SELL - }); - - vm.startPrank(user); + // Create params using helper + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _createDefaultParams(user, account); // User deposits SUSDS collateral to subaccount + vm.startPrank(user); IERC20(SUSDS).approve(ESUSDS, type(uint256).max); uint256 depositAmount = 1000e18; IERC4626(ESUSDS).deposit(depositAmount, account); // Get settlement data - receiver is the subaccount - SettlementData memory settlement = getCollateralSwapSettlement( - user, - account, // Receiver is subaccount - ESUSDS, - EWBTC, - sellAmount, - buyAmount - ); + SettlementData memory settlement = + getCollateralSwapSettlement(user, account, ESUSDS, EWBTC, DEFAULT_SWAP_AMOUNT, DEFAULT_BUY_AMOUNT); // User signs the order on cowswap COW_SETTLEMENT.setPreSignature(settlement.orderUid, true); - - // User approves vault shares for settlement (from main account) - IEVault(ESUSDS).approve(COW_SETTLEMENT.vaultRelayer(), type(uint256).max); - - // For subaccount, user approves transfer of vault shares from the account to main account - { - IEVC.BatchItem[] memory items = new IEVC.BatchItem[](1); - items[0] = IEVC.BatchItem({ - onBehalfOfAccount: account, - targetContract: ESUSDS, - value: 0, - data: abi.encodeCall(IERC20.approve, (address(collateralSwapWrapper), type(uint256).max)) - }); - evc.batch(items); - } - - // User approves the wrapper to be operator (both of the main account and the subaccount) - evc.setAccountOperator(account, address(collateralSwapWrapper), true); - - // User pre-approves the hash for the wrapper operation - bytes32 hash = collateralSwapWrapper.getApprovalHash(params); - collateralSwapWrapper.setPreApprovedHash(hash, true); - vm.stopPrank(); + // Setup subaccount approvals and pre-approved hash + _setupSubaccountApprovals(account, params); + // Record balances before swap uint256 susdsBalanceBefore = IERC20(ESUSDS).balanceOf(account); uint256 wbtcBalanceBefore = IERC20(EWBTC).balanceOf(account); - // Encode settlement data + // 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)); - // Encode wrapper data with CollateralSwapParams - bytes memory signature = new bytes(0); // Empty signature for pre-approved hash - bytes memory wrapperData = abi.encode(params, signature); - wrapperData = abi.encodePacked(uint16(wrapperData.length), wrapperData); - - // Execute wrapped settlement through solver - address[] memory targets = new address[](1); - bytes[] memory datas = new bytes[](1); - targets[0] = address(collateralSwapWrapper); - datas[0] = abi.encodeCall(collateralSwapWrapper.wrappedSettle, (settleData, wrapperData)); - - // Expect the event to be emitted + // Expect event emission vm.expectEmit(true, true, true, true); emit CowEvcCollateralSwapWrapper.CowEvcCollateralSwapped( params.owner, params.account, params.fromVault, params.toVault, params.swapAmount, params.kind ); - solver.runBatch(targets, datas); + // Execute wrapped settlement + _executeWrappedSettlement(settleData, wrapperData); // Verify the collateral was swapped successfully assertEq( IERC20(ESUSDS).balanceOf(account), - susdsBalanceBefore - sellAmount, + susdsBalanceBefore - DEFAULT_SWAP_AMOUNT, "Subaccount should have less ESUSDS after swap" ); assertGt(IERC20(EWBTC).balanceOf(account), wbtcBalanceBefore, "Subaccount should have more EWBTC after swap"); @@ -385,15 +382,8 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { /// @notice Test parseWrapperData function function test_CollateralSwapWrapper_ParseWrapperData() external view { - CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ - owner: user, - account: address(uint160(user) ^ uint8(0x01)), - deadline: block.timestamp + 1 hours, - fromVault: ESUSDS, - toVault: EWBTC, - swapAmount: 1000e18, - kind: GPv2Order.KIND_SELL - }); + 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); @@ -418,86 +408,42 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { uint256 sellAmount = 1000 ether + 2500 ether; // Sell 3500 ESUSDS uint256 buyAmount = 0.0325e8; // Expect to receive ~0.0325 EWBTC (8 decimals) - // Prepare CollateralSwapParams - CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ - owner: user, - account: account, - deadline: block.timestamp + 1 hours, - fromVault: ESUSDS, - toVault: EWBTC, - swapAmount: sellAmount, - kind: GPv2Order.KIND_SELL - }); - - // Now swap some collateral from SUSDS to WBTC (add more WBTC collateral) - vm.startPrank(user); + // 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, // Receiver is subaccount - ESUSDS, - EWBTC, - sellAmount, - buyAmount - ); + SettlementData memory settlement = + getCollateralSwapSettlement(user, account, ESUSDS, EWBTC, sellAmount, buyAmount); // User signs the order on cowswap + vm.startPrank(user); COW_SETTLEMENT.setPreSignature(settlement.orderUid, true); - - // User approves vault shares for settlement - IEVault(ESUSDS).approve(COW_SETTLEMENT.vaultRelayer(), type(uint256).max); - - // For subaccount, user approves transfer of vault shares from the account - { - IEVC.BatchItem[] memory items = new IEVC.BatchItem[](1); - items[0] = IEVC.BatchItem({ - onBehalfOfAccount: account, - targetContract: ESUSDS, - value: 0, - data: abi.encodeCall(IERC20.approve, (address(collateralSwapWrapper), type(uint256).max)) - }); - evc.batch(items); - } - - // User approves the wrapper to be operator (both of the main account and the subaccount) - evc.setAccountOperator(account, address(collateralSwapWrapper), true); - - // User pre-approves the hash for the wrapper operation - bytes32 hash = collateralSwapWrapper.getApprovalHash(params); - collateralSwapWrapper.setPreApprovedHash(hash, true); - vm.stopPrank(); + // Setup subaccount approvals and pre-approved hash + _setupSubaccountApprovals(account, 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 data + // 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)); - // Encode wrapper data with CollateralSwapParams - bytes memory signature = new bytes(0); // Empty signature for pre-approved hash - bytes memory wrapperData = abi.encode(params, signature); - wrapperData = abi.encodePacked(uint16(wrapperData.length), wrapperData); - - // Execute wrapped settlement through solver - address[] memory targets = new address[](1); - bytes[] memory datas = new bytes[](1); - targets[0] = address(collateralSwapWrapper); - datas[0] = abi.encodeCall(collateralSwapWrapper.wrappedSettle, (settleData, wrapperData)); - - // Expect the event to be emitted + // Expect event emission vm.expectEmit(true, true, true, true); emit CowEvcCollateralSwapWrapper.CowEvcCollateralSwapped( params.owner, params.account, params.fromVault, params.toVault, params.swapAmount, params.kind ); - solver.runBatch(targets, datas); + // Execute wrapped settlement + _executeWrappedSettlement(settleData, wrapperData); // Verify the collateral was swapped successfully while maintaining debt assertEq( diff --git a/test/unit/CowEvcCollateralSwapWrapper.unit.t.sol b/test/unit/CowEvcCollateralSwapWrapper.unit.t.sol index 5fe467c..faa49a4 100644 --- a/test/unit/CowEvcCollateralSwapWrapper.unit.t.sol +++ b/test/unit/CowEvcCollateralSwapWrapper.unit.t.sol @@ -26,6 +26,8 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { 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"; @@ -33,7 +35,59 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { event PreApprovedHash(address indexed owner, bytes32 indexed hash, bool approved); event PreApprovedHashConsumed(address indexed owner, bytes32 indexed hash); - // Helper function to decode signed calldata + /// @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++) { @@ -86,24 +140,12 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { assertEq(wrapper.DOMAIN_SEPARATOR(), expectedDomainSeparator, "DOMAIN_SEPARATOR incorrect"); } - function test_Constructor_SetsName() public view { - assertEq(wrapper.name(), "Euler EVC - Collateral Swap", "Name not set correctly"); - } - /*////////////////////////////////////////////////////////////// PARSE WRAPPER DATA TESTS //////////////////////////////////////////////////////////////*/ function test_ParseWrapperData_EmptySignature() 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 - }); + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _getDefaultParams(); bytes memory wrapperData = abi.encode(params, new bytes(0)); bytes memory remaining = wrapper.parseWrapperData(wrapperData); @@ -111,34 +153,8 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { assertEq(remaining.length, 0, "Should have no remaining data"); } - function test_ParseWrapperData_WithSignature() 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 - }); - - bytes memory signature = new bytes(65); - bytes memory wrapperData = abi.encode(params, signature); - 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 = CowEvcCollateralSwapWrapper.CollateralSwapParams({ - owner: OWNER, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - fromVault: address(mockFromVault), - toVault: address(mockToVault), - swapAmount: 1000e18, - kind: KIND_SELL - }); + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _getDefaultParams(); bytes memory signature = new bytes(0); bytes memory wrapperData = abi.encode(params, signature); @@ -155,58 +171,16 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { APPROVAL HASH TESTS //////////////////////////////////////////////////////////////*/ - function test_GetApprovalHash_Consistency() 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 - }); - - bytes32 hash1 = wrapper.getApprovalHash(params); - bytes32 hash2 = wrapper.getApprovalHash(params); + function test_GetApprovalHash_DifferentForDifferentParams() public view { + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params1 = _getDefaultParams(); - assertEq(hash1, hash2, "Hash should be consistent"); - } + // Change owner field + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params2 = _getDefaultParams(); + params2.owner = ACCOUNT; - function test_GetApprovalHash_DifferentForDifferentParams() public view { - CowEvcCollateralSwapWrapper.CollateralSwapParams memory params1 = - CowEvcCollateralSwapWrapper.CollateralSwapParams({ - owner: OWNER, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - fromVault: address(mockFromVault), - toVault: address(mockToVault), - swapAmount: 1000e18, - kind: KIND_SELL - }); - - // Same as params1 except owner - CowEvcCollateralSwapWrapper.CollateralSwapParams memory params2 = - CowEvcCollateralSwapWrapper.CollateralSwapParams({ - owner: ACCOUNT, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - fromVault: address(mockFromVault), - toVault: address(mockToVault), - swapAmount: 1000e18, - kind: KIND_SELL - }); - - // Same as params1 except swapAmount (the last meaningful field) - CowEvcCollateralSwapWrapper.CollateralSwapParams memory params3 = - CowEvcCollateralSwapWrapper.CollateralSwapParams({ - owner: OWNER, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - fromVault: address(mockFromVault), - toVault: address(mockToVault), - swapAmount: 2000e18, - kind: KIND_SELL - }); + // Change swapAmount field + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params3 = _getDefaultParams(); + params3.swapAmount = 2000e18; bytes32 hash1 = wrapper.getApprovalHash(params1); bytes32 hash2 = wrapper.getApprovalHash(params2); @@ -217,15 +191,7 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { } function test_GetApprovalHash_MatchesEIP712() 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 - }); + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _getDefaultParams(); bytes32 structHash = keccak256( abi.encode( @@ -250,15 +216,7 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { //////////////////////////////////////////////////////////////*/ function test_GetSignedCalldata_EnablesNewCollateral() 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 - }); + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _getDefaultParams(); bytes memory signedCalldata = wrapper.getSignedCalldata(params); IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); @@ -273,15 +231,7 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { } function test_GetSignedCalldata_UsesCorrectAccount() 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 - }); + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _getDefaultParams(); bytes memory signedCalldata = wrapper.getSignedCalldata(params); IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); @@ -712,35 +662,12 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { vm.prank(OWNER); mockFromVault.approve(address(wrapper), 2000e18); - 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 - }); - - address[] memory tokens = new address[](0); - uint256[] memory prices = new uint256[](0); + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _getDefaultParams(); + params.account = OWNER; // Same account bytes memory signature = new bytes(65); - 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, signature); - wrapperData = abi.encodePacked(uint16(wrapperData.length), wrapperData); + bytes memory settleData = _getEmptySettleData(); + bytes memory wrapperData = _encodeWrapperData(params, signature); mockEvc.setSuccessfulBatch(true); @@ -751,45 +678,16 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { function test_WrappedSettle_WithPreApprovedHash() public { mockFromVault.mint(OWNER, 2000e18); - vm.startPrank(OWNER); - mockFromVault.approve(address(wrapper), 2000e18); - vm.stopPrank(); - - 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); + mockFromVault.approve(address(wrapper), 2000e18); - mockEvc.setOperator(OWNER, address(wrapper), true); + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _getDefaultParams(); + params.account = OWNER; // Same account - address[] memory tokens = new address[](0); - uint256[] memory prices = new uint256[](0); + bytes32 hash = _setupPreApprovedHash(params); - 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)); - wrapperData = abi.encodePacked(uint16(wrapperData.length), wrapperData); + bytes memory settleData = _getEmptySettleData(); + bytes memory wrapperData = _encodeWrapperData(params, new bytes(0)); mockEvc.setSuccessfulBatch(true); @@ -800,38 +698,14 @@ contract CowEvcCollateralSwapWrapperUnitTest is Test { } function test_WrappedSettle_PreApprovedHashRevertsIfDeadlineExceeded() public { - CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = CowEvcCollateralSwapWrapper.CollateralSwapParams({ - owner: OWNER, - account: OWNER, - deadline: block.timestamp - 1, // Past deadline - fromVault: address(mockFromVault), - toVault: address(mockToVault), - swapAmount: 1000e18, - kind: KIND_SELL - }); - - bytes32 hash = wrapper.getApprovalHash(params); - - vm.prank(OWNER); - wrapper.setPreApprovedHash(hash, true); + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params = _getDefaultParams(); + params.account = OWNER; // Same account + params.deadline = block.timestamp - 1; // Past deadline - mockEvc.setOperator(OWNER, address(wrapper), true); + _setupPreApprovedHash(params); - 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); + bytes memory settleData = _getEmptySettleData(); + bytes memory wrapperData = _encodeWrapperData(params, new bytes(0)); vm.prank(SOLVER); vm.expectRevert( From 0f01f86fb75fc12fb7584a12cccc24de5de4336e Mon Sep 17 00:00:00 2001 From: Kaze Date: Sat, 8 Nov 2025 07:30:44 +0900 Subject: [PATCH 05/10] forge fmt --- test/CowEvcCollateralSwapWrapper.t.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/CowEvcCollateralSwapWrapper.t.sol b/test/CowEvcCollateralSwapWrapper.t.sol index 4625ccb..4042f08 100644 --- a/test/CowEvcCollateralSwapWrapper.t.sol +++ b/test/CowEvcCollateralSwapWrapper.t.sol @@ -197,9 +197,8 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { // 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 - ); + (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 = [ From 7c293bffe82db1acdf30408d802ee70871a9b908 Mon Sep 17 00:00:00 2001 From: Kaze Date: Sat, 8 Nov 2025 13:25:32 +0900 Subject: [PATCH 06/10] partially implemented COW test for collateral swap also found a bug which caused another test to succeed when it shouldn't (afaict) will have to finish this tomorrow --- test/CowEvcCollateralSwapWrapper.t.sol | 206 ++++++++++++++++++++++++- test/helpers/CowBaseTest.sol | 6 +- 2 files changed, 204 insertions(+), 8 deletions(-) diff --git a/test/CowEvcCollateralSwapWrapper.t.sol b/test/CowEvcCollateralSwapWrapper.t.sol index 4042f08..56c528f 100644 --- a/test/CowEvcCollateralSwapWrapper.t.sol +++ b/test/CowEvcCollateralSwapWrapper.t.sol @@ -83,6 +83,28 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { vm.stopPrank(); } + /// @notice Helper to set up a leveraged position for any user + /// @dev More flexible version that accepts owner, account, and vault parameters + function _setupLeveragedPositionFor( + address owner, + address account, + address collateralAsset, + address collateralVault, + address borrowVault, + uint256 collateralAmount, + uint256 borrowAmount + ) internal { + vm.startPrank(owner); + IERC20(collateralAsset).approve(collateralVault, type(uint256).max); + EVC.enableCollateral(account, collateralVault); + EVC.enableController(account, borrowVault); + IERC4626(collateralVault).deposit(collateralAmount, account); + vm.stopPrank(); + + vm.prank(account); + IBorrowing(borrowVault).borrow(borrowAmount, owner); + } + struct SettlementData { bytes orderUid; GPv2Order.Data orderData; @@ -126,6 +148,23 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { ); } + /// @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 @@ -146,10 +185,10 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { } /// @notice Setup user approvals for collateral swap on subaccount - function _setupSubaccountApprovals(address account, CowEvcCollateralSwapWrapper.CollateralSwapParams memory params) + function _setupSubaccountApprovals(CowEvcCollateralSwapWrapper.CollateralSwapParams memory params) internal { - vm.startPrank(user); + vm.startPrank(params.owner); // Approve vault shares from main account for settlement IEVault(params.fromVault).approve(COW_SETTLEMENT.vaultRelayer(), type(uint256).max); @@ -157,7 +196,7 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { // Approve transfer of vault shares from the subaccount to wrapper IEVC.BatchItem[] memory items = new IEVC.BatchItem[](1); items[0] = IEVC.BatchItem({ - onBehalfOfAccount: account, + onBehalfOfAccount: params.account, targetContract: params.fromVault, value: 0, data: abi.encodeCall(IERC20.approve, (address(collateralSwapWrapper), type(uint256).max)) @@ -165,7 +204,7 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { EVC.batch(items); // Set wrapper as operator for the subaccount - EVC.setAccountOperator(account, address(collateralSwapWrapper), true); + EVC.setAccountOperator(params.account, address(collateralSwapWrapper), true); // Pre-approve the operation hash bytes32 hash = collateralSwapWrapper.getApprovalHash(params); @@ -305,7 +344,7 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { vm.stopPrank(); // Setup subaccount approvals and pre-approved hash - _setupSubaccountApprovals(account, params); + _setupSubaccountApprovals(params); // Record balances before swap uint256 susdsBalanceBefore = IERC20(ESUSDS).balanceOf(account); @@ -402,7 +441,7 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { // User signs the order on cowswap (already done in setupCowOrder) // Setup subaccount approvals and pre-approved hash - _setupSubaccountApprovals(account, params); + _setupSubaccountApprovals(params); // Record balances and debt before swap uint256 susdsBalanceBefore = IERC20(ESUSDS).balanceOf(account); @@ -434,4 +473,159 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { 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(ESUSDS).governorAdmin()); + IEVault(ESUSDS).setLTV(EWETH, 0.9e4, 0.9e4, 0); + IEVault(ESUSDS).setLTV(EWBTC, 0.9e4, 0.9e4, 0); + vm.stopPrank(); + 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); + + // Setup User1: Long SUSDS (SUSDS collateral, WETH debt). ~1 ETH debt + deal(SUSDS, user, 10000 ether); + _setupLeveragedPositionFor(user, account1, SUSDS, ESUSDS, EWETH, 3500 ether, 1 ether); + + // Setup User2: Long SUSDS (SUSDS collateral, WETH debt). ~3 ETH debt + deal(SUSDS, user2, 10000 ether); + _setupLeveragedPositionFor(user2, account2, SUSDS, ESUSDS, EWETH, 10000 ether, 3 ether); + + // Setup User3: Long WETH (WETH collateral, SUSDS debt). ~5000 SUSDS debt + deal(WETH, user3, 3 ether); + _setupLeveragedPositionFor(user3, account3, WETH, EWETH, ESUSDS, 3 ether, 5000 ether); + + // Verify positions exist + 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(ESUSDS).debtOf(account3), 5000 ether, "User3 should have SUSDS debt"); + + // Create params for all users + CowEvcCollateralSwapWrapper.CollateralSwapParams memory params1 = CowEvcCollateralSwapWrapper.CollateralSwapParams({ + owner: user, + account: account1, + deadline: block.timestamp + 1 hours, + fromVault: ESUSDS, + toVault: EWETH, + swapAmount: 1000 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: EWETH, + toVault: ESUSDS, + swapAmount: 1000 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[](3); + tokens[0] = ESUSDS; + tokens[1] = EWETH; + tokens[2] = EWBTC; + + uint256[] memory clearingPrices = new uint256[](3); + clearingPrices[0] = 1 ether; // eSUSDS price + clearingPrices[1] = 2500 ether; // eWETH price + clearingPrices[2] = 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, 2, 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 (we only need to withdraw for the BTC exchange) + interactions[1][0] = getWithdrawInteraction( + ESUSDS, 500 ether + ); + // we dont need to withdraw the WETH vault because we already have the WETH to direct exchange + //interactions[1][1] = getWithdrawInteraction(EWETH, ); + + // We swap. Since WETH <> SUSD trades are already coincidence of wants, we trade only the SUSD -> WBTC trade + interactions[1][1] = getSwapInteraction(SUSDS, WBTC, 500 ether); + + // We deposit back into WBTC + interactions[1][2] = getDepositInteraction(EWBTC, 0.005e8); + + // We "skim" to get the tokens + interactions[1][3] = getSkimInteraction(EWBTC); + + // 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(ESUSDS).debtOf(account3), 5000 ether, "User3 should have SUSDS debt"); + + // TODO: check collaterals + } } diff --git a/test/helpers/CowBaseTest.sol b/test/helpers/CowBaseTest.sol index ab4bb1d..7e459dd 100644 --- a/test/helpers/CowBaseTest.sol +++ b/test/helpers/CowBaseTest.sol @@ -92,9 +92,11 @@ contract CowBaseTest is Test { // deal small amount to the settlement contract that serve as buffer (just makes tests easier...) deal(SUSDS, address(COW_SETTLEMENT), 100e18); - deal(WETH, address(COW_SETTLEMENT), 100e18); + deal(WETH, address(COW_SETTLEMENT), 0.05e18); + deal(WBTC, address(COW_SETTLEMENT), 0.001e8); deal(ESUSDS, address(COW_SETTLEMENT), 100e18); - deal(EWETH, address(COW_SETTLEMENT), 100e18); + deal(EWETH, address(COW_SETTLEMENT), 0.05e18); + deal(EWBTC, address(COW_SETTLEMENT), 0.001e8); // Set the approval for MilkSwap in the settlement as a convenience vm.startPrank(address(COW_SETTLEMENT)); From 582410dfee08487570bd58cc0a0e24a69982e122 Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 10 Nov 2025 01:16:40 +0900 Subject: [PATCH 07/10] complete working test --- test/CowEvcCollateralSwapWrapper.t.sol | 134 ++++++++++++------------- 1 file changed, 64 insertions(+), 70 deletions(-) diff --git a/test/CowEvcCollateralSwapWrapper.t.sol b/test/CowEvcCollateralSwapWrapper.t.sol index 56c528f..21a73d6 100644 --- a/test/CowEvcCollateralSwapWrapper.t.sol +++ b/test/CowEvcCollateralSwapWrapper.t.sol @@ -88,12 +88,15 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { function _setupLeveragedPositionFor( address owner, address account, - address collateralAsset, address collateralVault, address borrowVault, uint256 collateralAmount, uint256 borrowAmount ) internal { + address collateralAsset = address(IEVault(collateralVault).asset()); + + deal(collateralAsset, owner, collateralAmount); + vm.startPrank(owner); IERC20(collateralAsset).approve(collateralVault, type(uint256).max); EVC.enableCollateral(account, collateralVault); @@ -185,9 +188,7 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { } /// @notice Setup user approvals for collateral swap on subaccount - function _setupSubaccountApprovals(CowEvcCollateralSwapWrapper.CollateralSwapParams memory params) - internal - { + function _setupSubaccountApprovals(CowEvcCollateralSwapWrapper.CollateralSwapParams memory params) internal { vm.startPrank(params.owner); // Approve vault shares from main account for settlement @@ -480,10 +481,6 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { vm.skip(bytes(forkRpcUrl).length == 0); // Configure vault LTVs for both directions - vm.startPrank(IEVault(ESUSDS).governorAdmin()); - IEVault(ESUSDS).setLTV(EWETH, 0.9e4, 0.9e4, 0); - IEVault(ESUSDS).setLTV(EWBTC, 0.9e4, 0.9e4, 0); - vm.stopPrank(); vm.startPrank(IEVault(EWETH).governorAdmin()); IEVault(EWETH).setLTV(ESUSDS, 0.9e4, 0.9e4, 0); IEVault(EWETH).setLTV(EWBTC, 0.9e4, 0.9e4, 0); @@ -494,53 +491,59 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { address account2 = address(uint160(user2) ^ 1); address account3 = address(uint160(user3) ^ 1); - // Setup User1: Long SUSDS (SUSDS collateral, WETH debt). ~1 ETH debt - deal(SUSDS, user, 10000 ether); - _setupLeveragedPositionFor(user, account1, SUSDS, ESUSDS, EWETH, 3500 ether, 1 ether); + vm.label(account1, "account 1"); + vm.label(account2, "account 2"); + vm.label(account3, "account 3"); - // Setup User2: Long SUSDS (SUSDS collateral, WETH debt). ~3 ETH debt - deal(SUSDS, user2, 10000 ether); - _setupLeveragedPositionFor(user2, account2, SUSDS, ESUSDS, EWETH, 10000 ether, 3 ether); + // Setup User1: Long SUSDS (SUSDS collateral, WETH debt). 1 ETH debt + _setupLeveragedPositionFor(user, account1, ESUSDS, EWETH, 3500 ether, 1 ether); - // Setup User3: Long WETH (WETH collateral, SUSDS debt). ~5000 SUSDS debt - deal(WETH, user3, 3 ether); - _setupLeveragedPositionFor(user3, account3, WETH, EWETH, ESUSDS, 3 ether, 5000 ether); + // Setup User2: Long SUSDS (SUSDS collateral, WETH debt). 3 ETH debt + _setupLeveragedPositionFor(user2, account2, ESUSDS, EWETH, 10000 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, "User1 should have WETH debt"); assertEq(IEVault(EWETH).debtOf(account2), 3 ether, "User2 should have WETH debt"); - assertEq(IEVault(ESUSDS).debtOf(account3), 5000 ether, "User3 should have SUSDS debt"); - - // Create params for all users - CowEvcCollateralSwapWrapper.CollateralSwapParams memory params1 = CowEvcCollateralSwapWrapper.CollateralSwapParams({ - owner: user, - account: account1, - deadline: block.timestamp + 1 hours, - fromVault: ESUSDS, - toVault: EWETH, - swapAmount: 1000 ether, - kind: GPv2Order.KIND_SELL - }); + assertEq(IEVault(EWETH).debtOf(account3), 2 ether, "User3 should have WETH debt"); - 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 - }); + // TODO: verify collaterals - CowEvcCollateralSwapWrapper.CollateralSwapParams memory params3 = CowEvcCollateralSwapWrapper.CollateralSwapParams({ - owner: user3, - account: account3, - deadline: block.timestamp + 1 hours, - fromVault: EWETH, - toVault: ESUSDS, - swapAmount: 1000 ether, - kind: GPv2Order.KIND_BUY - }); + // 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); @@ -555,23 +558,18 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { // Create settlement with all three trades uint32 validTo = uint32(block.timestamp + 1 hours); - address[] memory tokens = new address[](3); + address[] memory tokens = new address[](2); tokens[0] = ESUSDS; - tokens[1] = EWETH; - tokens[2] = EWBTC; + tokens[1] = EWBTC; - uint256[] memory clearingPrices = new uint256[](3); + uint256[] memory clearingPrices = new uint256[](2); clearingPrices[0] = 1 ether; // eSUSDS price - clearingPrices[1] = 2500 ether; // eWETH price - clearingPrices[2] = 100000 ether * 1e10; // eWBTC 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, 2, 1e24, params2.swapAmount, validTo, user2, account2, true); - (trades[2],,) = - setupCowOrder(tokens, 1, 0, 1e24, params3.swapAmount, validTo, user3, account3, true); + (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; @@ -579,21 +577,17 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { interactions[1] = new ICowSettlement.Interaction[](4); interactions[2] = new ICowSettlement.Interaction[](0); - // We pull the money out of the euler vaults (we only need to withdraw for the BTC exchange) - interactions[1][0] = getWithdrawInteraction( - ESUSDS, 500 ether - ); - // we dont need to withdraw the WETH vault because we already have the WETH to direct exchange - //interactions[1][1] = getWithdrawInteraction(EWETH, ); + // We pull the money out of the euler vaults + interactions[1][0] = getWithdrawInteraction(EWBTC, 0.01e8); - // We swap. Since WETH <> SUSD trades are already coincidence of wants, we trade only the SUSD -> WBTC trade - interactions[1][1] = getSwapInteraction(SUSDS, WBTC, 500 ether); + // 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(EWBTC, 0.005e8); + interactions[1][2] = getDepositInteraction(ESUSDS, 1000 ether); // We "skim" to get the tokens - interactions[1][3] = getSkimInteraction(EWBTC); + interactions[1][3] = getSkimInteraction(ESUSDS); // Encode settlement data bytes memory settleData = abi.encodeCall(ICowSettlement.settle, (tokens, clearingPrices, trades, interactions)); @@ -624,7 +618,7 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { // 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(ESUSDS).debtOf(account3), 5000 ether, "User3 should have SUSDS debt"); + assertEq(IEVault(EWETH).debtOf(account3), 2 ether, "User3 should have WETH debt"); // TODO: check collaterals } From 3385fd5e66213f359bf3a8822383ed7691fdda7f Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 10 Nov 2025 01:52:04 +0900 Subject: [PATCH 08/10] tighten up conditions --- test/CowEvcCollateralSwapWrapper.t.sol | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/test/CowEvcCollateralSwapWrapper.t.sol b/test/CowEvcCollateralSwapWrapper.t.sol index 21a73d6..7235490 100644 --- a/test/CowEvcCollateralSwapWrapper.t.sol +++ b/test/CowEvcCollateralSwapWrapper.t.sol @@ -496,20 +496,23 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { vm.label(account3, "account 3"); // Setup User1: Long SUSDS (SUSDS collateral, WETH debt). 1 ETH debt - _setupLeveragedPositionFor(user, account1, ESUSDS, EWETH, 3500 ether, 1 ether); + _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, 10000 ether, 3 ether); + _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, "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"); + 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"); - // TODO: verify collaterals + // 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 = @@ -620,6 +623,14 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { assertEq(IEVault(EWETH).debtOf(account2), 3 ether, "User2 should have WETH debt"); assertEq(IEVault(EWETH).debtOf(account3), 2 ether, "User3 should have WETH debt"); - // TODO: check collaterals + // 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"); } } From 2a4af9bfa6e6066e67641a9495ae61825f524b41 Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 10 Nov 2025 02:13:55 +0900 Subject: [PATCH 09/10] refactor `_createLeveragedPosition` --- test/CowEvcClosePositionWrapper.t.sol | 77 ++++----------------- test/CowEvcCollateralSwapWrapper.t.sol | 95 ++++++++------------------ test/helpers/CowBaseTest.sol | 29 +++++++- 3 files changed, 71 insertions(+), 130 deletions(-) diff --git a/test/CowEvcClosePositionWrapper.t.sol b/test/CowEvcClosePositionWrapper.t.sol index 494cea7..2dfd6f6 100644 --- a/test/CowEvcClosePositionWrapper.t.sol +++ b/test/CowEvcClosePositionWrapper.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8; import {GPv2Order, IERC20 as CowERC20} from "cow/libraries/GPv2Order.sol"; import {IEVC} from "evc/EthereumVaultConnector.sol"; -import {IEVault, IERC4626, IBorrowing, IERC20} from "euler-vault-kit/src/EVault/IEVault.sol"; +import {IEVault, IERC4626, IERC20} from "euler-vault-kit/src/EVault/IEVault.sol"; import {CowEvcClosePositionWrapper} from "../src/CowEvcClosePositionWrapper.sol"; import {ICowSettlement, CowWrapper} from "../src/CowWrapper.sol"; @@ -164,54 +164,6 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { solver.runBatch(targets, datas); } - /// @notice Helper to set up an initial leveraged position - /// @dev This creates a position that can then be closed in the tests - function _setupLeveragedPosition(uint256 borrowAmount, uint256 collateralAmount) internal { - address account = address(uint160(user) ^ uint8(0x01)); - - vm.startPrank(user); - - // User approves SUSDS vault for deposit - IERC20(SUSDS).approve(ESUSDS, type(uint256).max); - - // Enable collateral and controller on the account - EVC.enableCollateral(account, ESUSDS); - EVC.enableController(account, EWETH); - - // Deposit collateral to the account - IERC4626(ESUSDS).deposit(collateralAmount, account); - - vm.stopPrank(); - - // Borrow assets from the account (needs to be called with account as onBehalfOf) - vm.startPrank(account); - IBorrowing(EWETH).borrow(borrowAmount, address(this)); - - vm.stopPrank(); - } - - /// @notice Helper to set up a leveraged position for any user - /// @dev More flexible version that accepts owner, account, and vault parameters - function _setupLeveragedPositionFor( - address owner, - address account, - address collateralAsset, - address collateralVault, - address borrowVault, - uint256 collateralAmount, - uint256 borrowAmount - ) internal { - vm.startPrank(owner); - IERC20(collateralAsset).approve(collateralVault, type(uint256).max); - EVC.enableCollateral(account, collateralVault); - EVC.enableController(account, borrowVault); - IERC4626(collateralVault).deposit(collateralAmount, account); - vm.stopPrank(); - - vm.prank(account); - IBorrowing(borrowVault).borrow(borrowAmount, owner); - } - /// @notice Setup approvals for a specific user to close their position function _setupClosePositionApprovalsFor( address owner, @@ -302,11 +254,11 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { uint256 borrowAmount = 1e18; uint256 collateralAmount = SUSDS_MARGIN + 2495e18; - // First, set up a leveraged position - _setupLeveragedPosition(borrowAmount, collateralAmount); - address account = address(uint160(user) ^ uint8(0x01)); + // First, set up a leveraged position + setupLeveragedPositionFor(user, account, ESUSDS, EWETH, collateralAmount, borrowAmount); + // Verify position exists uint256 debtBefore = IEVault(EWETH).debtOf(account); assertEq(debtBefore, borrowAmount, "Position should have debt"); @@ -394,11 +346,11 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { uint256 sellAmount = 2500e18; uint256 buyAmount = 0.98e18; - // First, set up a leveraged position - _setupLeveragedPosition(borrowAmount, collateralAmount); - address account = address(uint160(user) ^ uint8(0x01)); + // First, set up a leveraged position + setupLeveragedPositionFor(user, account, ESUSDS, EWETH, collateralAmount, borrowAmount); + // Create params with custom amounts and KIND_SELL CowEvcClosePositionWrapper.ClosePositionParams memory params = _createDefaultParams(user, account); params.collateralAmount = sellAmount; @@ -498,11 +450,11 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { uint256 borrowAmount = 1e18; uint256 collateralAmount = SUSDS_MARGIN + 2495e18; - // First, set up a leveraged position - _setupLeveragedPosition(borrowAmount, collateralAmount); - address account = address(uint160(user) ^ uint8(0x01)); + // First, set up a leveraged position + setupLeveragedPositionFor(user, account, ESUSDS, EWETH, collateralAmount, borrowAmount); + // Create params using helper CowEvcClosePositionWrapper.ClosePositionParams memory params = _createDefaultParams(user, account); @@ -562,16 +514,13 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { address account3 = address(uint160(user3) ^ 1); // Setup User1: Long SUSDS (SUSDS collateral, WETH debt). ~1 ETH debt - deal(SUSDS, user, 10000 ether); - _setupLeveragedPositionFor(user, account1, SUSDS, ESUSDS, EWETH, 3500 ether, 1 ether); + setupLeveragedPositionFor(user, account1, ESUSDS, EWETH, 3500 ether, 1 ether); // Setup User2: Long SUSDS (SUSDS collateral, WETH debt). ~3 ETH debt - deal(SUSDS, user2, 10000 ether); - _setupLeveragedPositionFor(user2, account2, SUSDS, ESUSDS, EWETH, 10000 ether, 3 ether); + setupLeveragedPositionFor(user2, account2, ESUSDS, EWETH, 10000 ether, 3 ether); // Setup User3: Long WETH (WETH collateral, SUSDS debt). ~5000 SUSDS debt - deal(WETH, user3, 3 ether); - _setupLeveragedPositionFor(user3, account3, WETH, EWETH, ESUSDS, 3 ether, 5000 ether); + setupLeveragedPositionFor(user3, account3, EWETH, ESUSDS, 3 ether, 5000 ether); // Verify positions exist assertEq(IEVault(EWETH).debtOf(account1), 1 ether, "User1 should have WETH debt"); diff --git a/test/CowEvcCollateralSwapWrapper.t.sol b/test/CowEvcCollateralSwapWrapper.t.sol index 7235490..4464de3 100644 --- a/test/CowEvcCollateralSwapWrapper.t.sol +++ b/test/CowEvcCollateralSwapWrapper.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8; import {GPv2Order, IERC20 as CowERC20} from "cow/libraries/GPv2Order.sol"; import {IEVC} from "evc/EthereumVaultConnector.sol"; -import {IEVault, IERC4626, IBorrowing, IERC20} from "euler-vault-kit/src/EVault/IEVault.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"; @@ -57,57 +57,6 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { IERC20(WBTC).approve(vaultRelayer, type(uint256).max); } - /// @notice Helper to set up an initial leveraged position - /// @dev This creates a position that can then be used in tests - function _setupLeveragedPosition(uint256 borrowAmount, uint256 collateralAmount) internal { - address account = address(uint160(user) ^ uint8(0x01)); - - vm.startPrank(user); - - // User approves SUSDS vault for deposit - IERC20(SUSDS).approve(ESUSDS, type(uint256).max); - - // Enable collateral and controller on the account - EVC.enableCollateral(account, ESUSDS); - EVC.enableController(account, EWETH); - - // Deposit collateral to the account, and add the approximate amount after swapping the borrowed collateral - IERC4626(ESUSDS).deposit(collateralAmount + borrowAmount * 2500e18 / 0.99e18, account); - - vm.stopPrank(); - - // Borrow assets from the account. And confiscate the borrowed asset (needs to be called with account as onBehalfOf) - vm.startPrank(account); - IBorrowing(EWETH).borrow(borrowAmount, address(this)); - - vm.stopPrank(); - } - - /// @notice Helper to set up a leveraged position for any user - /// @dev More flexible version that accepts owner, account, and vault parameters - function _setupLeveragedPositionFor( - address owner, - address account, - address collateralVault, - address borrowVault, - uint256 collateralAmount, - uint256 borrowAmount - ) internal { - address collateralAsset = address(IEVault(collateralVault).asset()); - - deal(collateralAsset, owner, collateralAmount); - - vm.startPrank(owner); - IERC20(collateralAsset).approve(collateralVault, type(uint256).max); - EVC.enableCollateral(account, collateralVault); - EVC.enableController(account, borrowVault); - IERC4626(collateralVault).deposit(collateralAmount, account); - vm.stopPrank(); - - vm.prank(account); - IBorrowing(borrowVault).borrow(borrowAmount, owner); - } - struct SettlementData { bytes orderUid; GPv2Order.Data orderData; @@ -423,11 +372,13 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { uint256 borrowAmount = 1e18; // Borrow 1 WETH uint256 collateralAmount = 1000e18; - // Set up a leveraged position - _setupLeveragedPosition(borrowAmount, collateralAmount); - 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) @@ -496,13 +447,13 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { 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); + 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); + 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); + 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"); @@ -510,9 +461,15 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { 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"); + 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 = @@ -624,12 +581,20 @@ contract CowEvcCollateralSwapWrapperTest is CowBaseTest { 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"); + 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"); + 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 f2506fb..dacdaa5 100644 --- a/test/helpers/CowBaseTest.sol +++ b/test/helpers/CowBaseTest.sol @@ -7,7 +7,7 @@ import {IERC20 as CowERC20} from "cow/interfaces/IERC20.sol"; import {EthereumVaultConnector} from "evc/EthereumVaultConnector.sol"; import {Test} from "forge-std/Test.sol"; -import {IEVault, IVault, IERC4626, IERC20} from "euler-vault-kit/src/EVault/IEVault.sol"; +import {IEVault, IVault, IBorrowing, IERC4626, IERC20} from "euler-vault-kit/src/EVault/IEVault.sol"; import {GPv2AllowListAuthentication} from "cow/GPv2AllowListAuthentication.sol"; import {ICowSettlement} from "../../src/CowWrapper.sol"; @@ -269,4 +269,31 @@ contract CowBaseTest is Test { clearingPrices[0] = 2495; // WETH price (if it was against SUSD then 2500) clearingPrices[1] = 1; // eSUSDS price } + + /// @notice Helper to set up a leveraged position for any user + /// @dev More flexible version that accepts owner, account, and vault parameters + /// The proceeds of the `borrow` are *NOT* deposited in the account for convienience of setup. + /// So make sure that `collateralAmount` is margin + borrowValue if that is something you care about. + function setupLeveragedPositionFor( + address owner, + address account, + address collateralVault, + address borrowVault, + uint256 collateralAmount, + uint256 borrowAmount + ) internal { + address collateralAsset = address(IEVault(collateralVault).asset()); + + deal(collateralAsset, owner, collateralAmount); + + vm.startPrank(owner); + IERC20(collateralAsset).approve(collateralVault, type(uint256).max); + EVC.enableCollateral(account, collateralVault); + EVC.enableController(account, borrowVault); + IERC4626(collateralVault).deposit(collateralAmount, account); + vm.stopPrank(); + + vm.prank(account); + IBorrowing(borrowVault).borrow(borrowAmount, address(1)); + } } From e2e00d8d8a810f75bdd83ec811074724a7b45d0e Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 10 Nov 2025 02:27:58 +0900 Subject: [PATCH 10/10] refactor more duplicated functions, clean out warnings/notes --- test/CowEvcClosePositionWrapper.t.sol | 81 ++++---------------------- test/CowEvcCollateralSwapWrapper.t.sol | 2 +- test/CowEvcOpenPositionWrapper.t.sol | 4 +- test/helpers/CowBaseTest.sol | 16 ++++- 4 files changed, 29 insertions(+), 74 deletions(-) diff --git a/test/CowEvcClosePositionWrapper.t.sol b/test/CowEvcClosePositionWrapper.t.sol index 2dfd6f6..c786dab 100644 --- a/test/CowEvcClosePositionWrapper.t.sol +++ b/test/CowEvcClosePositionWrapper.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8; -import {GPv2Order, IERC20 as CowERC20} from "cow/libraries/GPv2Order.sol"; +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"; @@ -75,29 +75,6 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { }); } - /// @notice Setup account approvals for closing position - function _setupClosePositionApprovals(address account) internal { - vm.startPrank(user); - - // Approve vault shares from subaccount - IEVC.BatchItem[] memory items = new IEVC.BatchItem[](1); - items[0] = IEVC.BatchItem({ - onBehalfOfAccount: account, - targetContract: ESUSDS, - value: 0, - data: abi.encodeCall(IERC20.approve, (address(closePositionWrapper), type(uint256).max)) - }); - EVC.batch(items); - - // Approve vault shares for settlement - IEVault(ESUSDS).approve(COW_SETTLEMENT.vaultRelayer(), type(uint256).max); - - // Approve wrapper to spend WETH for repayment - IERC20(WETH).approve(address(closePositionWrapper), type(uint256).max); - - vm.stopPrank(); - } - /// @notice Setup pre-approved hash flow for close position function _setupPreApprovedFlow(address account, bytes32 hash) internal { vm.startPrank(user); @@ -128,42 +105,6 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { vm.stopPrank(); } - /// @notice Create permit signature for EVC operator - function _createPermitSignature(CowEvcClosePositionWrapper.ClosePositionParams memory params) - internal - returns (bytes memory) - { - ecdsa.setPrivateKey(privateKey); - return ecdsa.signPermit( - params.owner, - address(closePositionWrapper), - uint256(uint160(address(closePositionWrapper))), - 0, - params.deadline, - 0, - closePositionWrapper.getSignedCalldata(params) - ); - } - - /// @notice Encode wrapper data with length prefix - function _encodeWrapperData(CowEvcClosePositionWrapper.ClosePositionParams 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(closePositionWrapper); - datas[0] = abi.encodeCall(closePositionWrapper.wrappedSettle, (settleData, wrapperData)); - solver.runBatch(targets, datas); - } - /// @notice Setup approvals for a specific user to close their position function _setupClosePositionApprovalsFor( address owner, @@ -273,10 +214,10 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { // User signs order (already done in setupCowOrder) // Setup approvals - _setupClosePositionApprovals(account); + _setupClosePositionApprovalsFor(user, account, ESUSDS, WETH); // Create permit signature - bytes memory permitSignature = _createPermitSignature(params); + bytes memory permitSignature = _createPermitSignatureFor(params, privateKey); // Record balances before closing uint256 collateralBefore = IERC20(ESUSDS).balanceOf(user); @@ -287,7 +228,7 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { ICowSettlement.settle, (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions) ); - bytes memory wrapperData = _encodeWrapperData(params, permitSignature); + bytes memory wrapperData = encodeWrapperData(abi.encode(params, permitSignature)); // Expect event emission vm.expectEmit(true, true, true, true); @@ -302,7 +243,7 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { ); // Execute wrapped settlement - _executeWrappedSettlement(settleData, wrapperData); + executeWrappedSettlement(address(closePositionWrapper), settleData, wrapperData); // Verify the position was closed successfully assertEq(IEVault(EWETH).debtOf(account), 0, "User should have no debt after closing"); @@ -363,17 +304,17 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { // User signs order (already done in setupCowOrder) // Setup approvals - _setupClosePositionApprovals(account); + _setupClosePositionApprovalsFor(user, account, ESUSDS, WETH); // Create permit signature - bytes memory permitSignature = _createPermitSignature(params); + bytes memory permitSignature = _createPermitSignatureFor(params, privateKey); // Encode settlement and wrapper data bytes memory settleData = abi.encodeCall( ICowSettlement.settle, (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions) ); - bytes memory wrapperData = _encodeWrapperData(params, permitSignature); + bytes memory wrapperData = encodeWrapperData(abi.encode(params, permitSignature)); // Expect event emission vm.expectEmit(true, true, true, true); @@ -388,7 +329,7 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { ); // Execute wrapped settlement - _executeWrappedSettlement(settleData, wrapperData); + executeWrappedSettlement(address(closePositionWrapper), settleData, wrapperData); // Verify partial repayment uint256 debtAfter = IEVault(EWETH).debtOf(account); @@ -476,7 +417,7 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { ICowSettlement.settle, (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions) ); - bytes memory wrapperData = _encodeWrapperData(params, new bytes(0)); + bytes memory wrapperData = encodeWrapperData(abi.encode(params, new bytes(0))); // Expect event emission vm.expectEmit(true, true, true, true); @@ -491,7 +432,7 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { ); // Execute wrapped settlement - _executeWrappedSettlement(settleData, wrapperData); + executeWrappedSettlement(address(closePositionWrapper), settleData, wrapperData); // Verify the position was closed successfully assertEq(IEVault(EWETH).debtOf(account), 0, "User should have no debt after closing"); diff --git a/test/CowEvcCollateralSwapWrapper.t.sol b/test/CowEvcCollateralSwapWrapper.t.sol index 4464de3..a1f3db8 100644 --- a/test/CowEvcCollateralSwapWrapper.t.sol +++ b/test/CowEvcCollateralSwapWrapper.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8; -import {GPv2Order, IERC20 as CowERC20} from "cow/libraries/GPv2Order.sol"; +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"; diff --git a/test/CowEvcOpenPositionWrapper.t.sol b/test/CowEvcOpenPositionWrapper.t.sol index 9a6832a..928301c 100644 --- a/test/CowEvcOpenPositionWrapper.t.sol +++ b/test/CowEvcOpenPositionWrapper.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8; -import {GPv2Order, IERC20 as CowERC20} from "cow/libraries/GPv2Order.sol"; +import {GPv2Order} from "cow/libraries/GPv2Order.sol"; -import {IEVault, IERC4626, IERC20, IVault} from "euler-vault-kit/src/EVault/IEVault.sol"; +import {IEVault, IERC4626, IERC20} from "euler-vault-kit/src/EVault/IEVault.sol"; import {CowEvcOpenPositionWrapper} from "../src/CowEvcOpenPositionWrapper.sol"; import {ICowSettlement, CowWrapper} from "../src/CowWrapper.sol"; diff --git a/test/helpers/CowBaseTest.sol b/test/helpers/CowBaseTest.sol index dacdaa5..2457202 100644 --- a/test/helpers/CowBaseTest.sol +++ b/test/helpers/CowBaseTest.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8; import {GPv2Order} from "cow/libraries/GPv2Order.sol"; -import {GPv2Trade} from "cow/libraries/GPv2Trade.sol"; import {IERC20 as CowERC20} from "cow/interfaces/IERC20.sol"; import {EthereumVaultConnector} from "evc/EthereumVaultConnector.sol"; @@ -296,4 +295,19 @@ contract CowBaseTest is Test { vm.prank(account); IBorrowing(borrowVault).borrow(borrowAmount, address(1)); } + + /// @notice Encode wrapper data with length prefix + /// @dev Takes already abi.encoded params and signature + function encodeWrapperData(bytes memory paramsAndSignature) internal pure returns (bytes memory) { + return abi.encodePacked(uint16(paramsAndSignature.length), paramsAndSignature); + } + + /// @notice Execute wrapped settlement through solver + function executeWrappedSettlement(address wrapper, bytes memory settleData, bytes memory wrapperData) internal { + address[] memory targets = new address[](1); + bytes[] memory datas = new bytes[](1); + targets[0] = wrapper; + datas[0] = abi.encodeWithSignature("wrappedSettle(bytes,bytes)", settleData, wrapperData); + solver.runBatch(targets, datas); + } }