From f312246a6e6e73042adfa7716bab3ab02dd9f2fb Mon Sep 17 00:00:00 2001 From: Kaze Date: Thu, 6 Nov 2025 09:38:12 +0900 Subject: [PATCH 1/5] feat: close position wrapper --- src/CowEvcClosePositionWrapper.sol | 395 +++++++ test/CowEvcClosePositionWrapper.t.sol | 543 ++++++++++ test/helpers/CowBaseTest.sol | 36 +- .../CowEvcClosePositionWrapper.unit.t.sol | 979 ++++++++++++++++++ 4 files changed, 1952 insertions(+), 1 deletion(-) create mode 100644 src/CowEvcClosePositionWrapper.sol create mode 100644 test/CowEvcClosePositionWrapper.t.sol create mode 100644 test/unit/CowEvcClosePositionWrapper.unit.t.sol diff --git a/src/CowEvcClosePositionWrapper.sol b/src/CowEvcClosePositionWrapper.sol new file mode 100644 index 0000000..f035bc4 --- /dev/null +++ b/src/CowEvcClosePositionWrapper.sol @@ -0,0 +1,395 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8; + +import {IEVC} from "evc/EthereumVaultConnector.sol"; + +import {CowWrapper, ICowSettlement} from "./CowWrapper.sol"; +import {IERC4626, IBorrowing, 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 CowEvcClosePositionWrapper +/// @notice A specialized wrapper for closing leveraged positions with EVC +/// @dev This wrapper hardcodes the EVC operations needed to close a position: +/// 1. Execute settlement to acquire repayment assets +/// 2. Repay debt and return remaining assets to user +/// @dev The settle call by this order should be performing the necessary swap +/// from collateralVault -> IERC20(borrowVault.asset()). The recipient of the +/// swap should *THIS* contract so that it can repay on behalf of the owner. Furthermore, +/// the order should be of type GPv2Order.KIND_BUY to prevent excess from being sent to the contract. +/// If a full close is being performed, leave a small buffer for intrest accumultation, and the dust will +/// be returned to the owner's wallet. +contract CowEvcClosePositionWrapper 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("CowEvcClosePositionWrapper"); + + /// @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 - Close Position"; + + /// @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 this contract did not receive enough repayment assets from the settlement contract in order to cover all user's orders + error InsufficientRepaymentAsset(address vault, uint256 balanceAmount, uint256 repayAmount); + + /// @dev Indicates that the close order cannot be executed becuase the necessary pricing data is not present in the `tokens`/`clearingPrices` variable + error PricesNotFoundInSettlement(address collateralVaultToken, address borrowToken); + + /// @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 a position is closed via this wrapper + event CowEvcPositionClosed( + address indexed owner, + address account, + address indexed borrowVault, + address indexed collateralVault, + uint256 collateralAmount, + uint256 repayAmount, + bytes32 kind + ); + + constructor(address _evc, ICowSettlement _settlement) CowWrapper(_settlement) { + EVC = IEVC(_evc); + NONCE_NAMESPACE = uint256(uint160(address(this))); + + DOMAIN_SEPARATOR = + keccak256(abi.encode(DOMAIN_TYPE_HASH, DOMAIN_NAME, DOMAIN_VERSION, block.chainid, address(this))); + } + + /** + * @notice A command to close a debt position against an euler vault by repaying debt and returning collateral. + * @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 ClosePositionParams { + /** + * @dev The ethereum address that has permission to operate upon the account + */ + address owner; + + /** + * @dev The subaccount to close the position on. 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 Euler vault from which debt was borrowed + */ + address borrowVault; + + /** + * @dev The Euler vault used as collateral + */ + address collateralVault; + + /** + * @dev + */ + uint256 collateralAmount; + + /** + * @dev The amount of debt to repay. If greater than the actual debt, the full debt is repaid + */ + uint256 repayAmount; + + /** + * @dev Whether the `collateralAmount` or `repayAmount` is the exact amount. Either `GPv2Order.KIND_BUY` or `GPv2Order.KIND_SELL` + */ + bytes32 kind; + } + + function _parseClosePositionParams(bytes calldata wrapperData) + internal + pure + returns (ClosePositionParams memory params, bytes memory signature, bytes calldata remainingWrapperData) + { + (params, signature) = abi.decode(wrapperData, (ClosePositionParams, bytes)); + + // Calculate consumed bytes for abi.encode(ClosePositionParams, bytes) + // Structure: + // - 32 bytes: offset to params (0x40) + // - 32 bytes: offset to signature + // - 256 bytes: params data (8 fields × 32 bytes) + // - 32 bytes: signature length + // - N bytes: signature data (padded to 32-byte boundary) + // We can just math this out + uint256 consumed = 256 + 64 + ((signature.length + 31) & ~uint256(31)); + + remainingWrapperData = wrapperData[consumed:]; + } + + /// @notice Helper function to compute the hash that would be approved + /// @param params The ClosePositionParams to hash + /// @return The hash of the signed calldata for these params + function getApprovalHash(ClosePositionParams memory params) external view returns (bytes32) { + return _getApprovalHash(params); + } + + function _getApprovalHash(ClosePositionParams memory params) internal view returns (bytes32 digest) { + bytes32 structHash; + bytes32 separator = DOMAIN_SEPARATOR; + assembly ("memory-safe") { + structHash := keccak256(params, 256) + 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) = _parseClosePositionParams(wrapperData); + } + + function getSignedCalldata(ClosePositionParams memory params) external view returns (bytes memory) { + return abi.encodeCall(IEVC.batch, _getSignedCalldata(params)); + } + + function _getSignedCalldata(ClosePositionParams memory params) + internal + view + returns (IEVC.BatchItem[] memory items) + { + items = new IEVC.BatchItem[](1); + + // 1. Repay debt and return remaining assets + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: params.account, + targetContract: address(this), + value: 0, + data: abi.encodeCall(this.helperRepay, (params.borrowVault, params.owner, params.account)) + }); + } + + /// @notice Called by the EVC after a CoW swap is completed to repay the user's debt. Will use all available collateral in the user's account to do so. + /// @param vault The Euler vault in which the repayment should be made + /// @param owner The address that should be receiving any surplus dust that may exist after the repayment is complete + /// @param account The subaccount that should be receiving the repayment of debt + function helperRepay(address vault, address owner, address account) external { + require(msg.sender == address(EVC), Unauthorized(msg.sender)); + (address onBehalfOfAccount,) = EVC.getCurrentOnBehalfOfAccount(address(0)); + require(onBehalfOfAccount == account, Unauthorized(onBehalfOfAccount)); + + IERC20 asset = IERC20(IERC4626(vault).asset()); + + uint256 debtAmount = IBorrowing(vault).debtOf(account); + + // repay as much debt as we can + uint256 repayAmount = asset.balanceOf(owner); + if (repayAmount > debtAmount) { + // the user intends to repay all their debt. we will revert if their balance is not sufficient. + repayAmount = debtAmount; + } + + // pull funds from the user (they should have approved spending by this contract) + SafeERC20Lib.safeTransferFrom(asset, owner, address(this), repayAmount, address(0)); + + // repay what was requested on the vault + asset.approve(vault, repayAmount); + IBorrowing(vault).repay(repayAmount, account); + } + + /// @notice Implementation of GPv2Wrapper._wrap - executes EVC operations to close a position + /// @param settleData Data which will be used for the parameters in a call to `CowSettlement.settle` + /// @param wrapperData Additional data containing ClosePositionParams + function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData) + internal + override + { + // Decode wrapper data into ClosePositionParams + ClosePositionParams memory params; + bytes memory signature; + (params, signature,) = _parseClosePositionParams(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)); + + // Calculate the number of items needed + uint256 baseItemCount = 2; + uint256 itemCount = isPreApproved ? baseItemCount - 1 + signedItems.length : baseItemCount; + + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](itemCount); + uint256 itemIndex = 0; + + // Build the EVC batch items for closing a position + // 1. Settlement call + items[itemIndex++] = IEVC.BatchItem({ + onBehalfOfAccount: address(this), + targetContract: address(this), + value: 0, + data: abi.encodeCall(this.evcInternalSettle, (settleData, wrapperData, remainingWrapperData)) + }); + + // 2. 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 + uint256 signedItemIndex = 0; + for (; itemIndex < itemCount; itemIndex++) { + items[itemIndex] = signedItems[signedItemIndex++]; + } + } + + // 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 CowEvcPositionClosed( + params.owner, + params.account, + params.borrowVault, + params.collateralVault, + params.collateralAmount, + params.repayAmount, + params.kind + ); + } + + function _findRatePrices(bytes calldata settleData, address collateralVault, address borrowVault) + internal + view + returns (uint256 collateralVaultPrice, uint256 borrowPrice) + { + address borrowAsset = IERC4626(borrowVault).asset(); + (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] == collateralVault) { + collateralVaultPrice = clearingPrices[i]; + } else if (tokens[i] == borrowAsset) { + borrowPrice = clearingPrices[i]; + } + } + require(collateralVaultPrice != 0 && borrowPrice != 0, PricesNotFoundInSettlement(collateralVault, borrowAsset)); + } + + /// @notice Internal settlement function called by EVC + function evcInternalSettle( + 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)); + + ClosePositionParams memory params; + (params,,) = _parseClosePositionParams(wrapperData); + _evcInternalSettle(settleData, remainingWrapperData, params); + } + + function _evcInternalSettle( + bytes calldata settleData, + bytes calldata remainingWrapperData, + ClosePositionParams 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.collateralAmount; + + if (params.kind == KIND_BUY) { + (uint256 collateralVaultPrice, uint256 borrowPrice) = + _findRatePrices(settleData, params.collateralVault, params.borrowVault); + transferAmount = params.repayAmount * borrowPrice / collateralVaultPrice; + } + + SafeERC20Lib.safeTransferFrom( + IERC20(params.collateralVault), 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 + _next(settleData, remainingWrapperData); + } +} diff --git a/test/CowEvcClosePositionWrapper.t.sol b/test/CowEvcClosePositionWrapper.t.sol new file mode 100644 index 0000000..3000ea7 --- /dev/null +++ b/test/CowEvcClosePositionWrapper.t.sol @@ -0,0 +1,543 @@ +// 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 {CowEvcClosePositionWrapper} from "../src/CowEvcClosePositionWrapper.sol"; +import {ICowSettlement, CowWrapper} from "../src/CowWrapper.sol"; +import {GPv2AllowListAuthentication} from "cow/GPv2AllowListAuthentication.sol"; +import {PreApprovedHashes} from "../src/PreApprovedHashes.sol"; + +import {CowBaseTest} from "./helpers/CowBaseTest.sol"; +import {SignerECDSA} from "./helpers/SignerECDSA.sol"; + +/// @title E2E Test for CowEvcClosePositionWrapper +/// @notice Tests the full flow of closing a leveraged position using the new wrapper contract +contract CowEvcClosePositionWrapperTest is CowBaseTest { + CowEvcClosePositionWrapper public closePositionWrapper; + SignerECDSA internal ecdsa; + + uint256 constant SUSDS_MARGIN = 2000e18; + + function setUp() public override { + super.setUp(); + + // Deploy the new close position wrapper + closePositionWrapper = new CowEvcClosePositionWrapper(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(closePositionWrapper)); + 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(); + + // Setup user with SUSDS + deal(SUSDS, user, 10000e18); + } + + struct SettlementData { + bytes orderUid; + GPv2Order.Data orderData; + address[] tokens; + uint256[] clearingPrices; + ICowSettlement.Trade[] trades; + ICowSettlement.Interaction[][3] interactions; + } + + /// @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 Create settlement data for closing a leveraged position + /// @dev Sells vault shares to buy repayment token (WETH) + function getClosePositionSettlement( + address owner, + address receiver, + address sellVaultToken, + address buyToRepayToken, + uint256 sellAmount, + uint256 buyAmount + ) public view returns (SettlementData memory r) { + uint32 validTo = uint32(block.timestamp + 1 hours); + + // Create order data - using KIND_BUY because we want exact buyAmount to repay + r.orderData = GPv2Order.Data({ + sellToken: CowERC20(sellVaultToken), + buyToken: CowERC20(buyToRepayToken), + receiver: receiver, + sellAmount: sellAmount, + buyAmount: buyAmount, + validTo: validTo, + appData: bytes32(0), + feeAmount: 0, + kind: GPv2Order.KIND_BUY, + 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 ICowSettlement.Trade[](1); + r.trades[0] = getTradeData(sellAmount, buyAmount, validTo, owner, r.orderData.receiver, true); + + // Get tokens and prices + r.tokens = new address[](2); + r.tokens[0] = sellVaultToken; + r.tokens[1] = buyToRepayToken; + + r.clearingPrices = new uint256[](2); + r.clearingPrices[0] = milkSwap.prices(IERC4626(sellVaultToken).asset()); + r.clearingPrices[1] = milkSwap.prices(buyToRepayToken); + + // Setup interactions - withdraw from vault, swap to repayment token + r.interactions = [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](2), + new ICowSettlement.Interaction[](0) + ]; + r.interactions[1][0] = getWithdrawInteraction(sellVaultToken, buyAmount * r.clearingPrices[1] / 1e18); + r.interactions[1][1] = getSwapInteraction( + IERC4626(sellVaultToken).asset(), buyToRepayToken, buyAmount * r.clearingPrices[1] / 1e18 + ); + } + + /// @notice Test closing a leveraged position using the new wrapper + function test_ClosePositionWrapper_SuccessFullRepay() external { + vm.skip(bytes(forkRpcUrl).length == 0); + + uint256 borrowAmount = 1e18; // Borrow 1 WETH + uint256 collateralAmount = SUSDS_MARGIN + 2495e18; // The original margin plus the amount we would have if we sold the borrow amount into ESUSDS + + // First, set up a leveraged position + _setupLeveragedPosition(borrowAmount, collateralAmount); + + // Verify position exists + address account = address(uint160(user) ^ uint8(0x01)); + uint256 debtBefore = IEVault(EWETH).debtOf(account); + assertEq(debtBefore, borrowAmount, "Position should have debt"); + + uint256 sellAmount = 2510 ether; // Sell up to 2510 ESUSDS (buffer) + uint256 buyAmount = 1.001 ether; // Buy exactly 1.001 WETH to repay debt (a small amount will be returned to user) + + // Get settlement data + SettlementData memory settlement = getClosePositionSettlement(user, user, ESUSDS, WETH, sellAmount, buyAmount); + + // Prepare ClosePositionParams + uint256 deadline = block.timestamp + 1 hours; + ecdsa.setPrivateKey(privateKey); + + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: user, + account: account, + deadline: deadline, + borrowVault: EWETH, + collateralVault: ESUSDS, + collateralAmount: sellAmount, + repayAmount: buyAmount, + kind: GPv2Order.KIND_BUY + }); + + // Now close the position + vm.startPrank(user); + + // User signs the order on cowswap + // Possibly skippable with Permit2 flow + COW_SETTLEMENT.setPreSignature(settlement.orderUid, true); + + // For subaccount, user approves transfer of vault shares from the account + // only required if the approve has not already been granted + { + 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); + } + + // User approves vault shares for settlement + // only required if the approve has not already been granted. Could be skipped with a Permit2 flow + IEVault(ESUSDS).approve(COW_SETTLEMENT.vaultRelayer(), type(uint256).max); + + // User approves the wrapper so it can repay + IERC20(WETH).approve(address(closePositionWrapper), type(uint256).max); + + // Sign permit for EVC operator (absolutely required in some form or another) + bytes memory permitSignature = ecdsa.signPermit( + user, + address(closePositionWrapper), + uint256(uint160(address(closePositionWrapper))), + 0, + deadline, + 0, + closePositionWrapper.getSignedCalldata(params) + ); + + vm.stopPrank(); + + // Record balances before closing + uint256 collateralBefore = IERC20(ESUSDS).balanceOf(user); + uint256 collateralBeforeAccount = IERC20(ESUSDS).balanceOf(account); + + // Encode settlement data + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions) + ); + + // Encode wrapper data with ClosePositionParams + 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(closePositionWrapper); + datas[0] = abi.encodeCall(closePositionWrapper.wrappedSettle, (settleData, wrapperData)); + + // Expect the event to be emitted + vm.expectEmit(true, true, true, true); + emit CowEvcClosePositionWrapper.CowEvcPositionClosed( + params.owner, + params.account, + params.borrowVault, + params.collateralVault, + params.collateralAmount, + params.repayAmount, + params.kind + ); + + solver.runBatch(targets, datas); + + // Verify the position was closed successfully + assertEq(IEVault(EWETH).debtOf(account), 0, "User should have no debt after closing"); + assertLt( + IERC20(ESUSDS).balanceOf(account), collateralBeforeAccount, "User should have less collateral after closing" + ); + assertGt(IERC20(ESUSDS).balanceOf(account), 0, "User should have some collateral remaining"); + // the sold collateral is sent through the user's main account, but there should be no balance there + assertEq(IERC20(ESUSDS).balanceOf(user), collateralBefore, "User main account balance should not have changed"); + } + + /// @notice Test that unauthorized users cannot call evcInternalSettle directly + function test_ClosePositionWrapper_UnauthorizedInternalSettle() external { + vm.skip(bytes(forkRpcUrl).length == 0); + + bytes memory settleData = ""; + bytes memory wrapperData = ""; + + // Try to call evcInternalSettle directly (not through EVC) + vm.expectRevert(abi.encodeWithSelector(CowEvcClosePositionWrapper.Unauthorized.selector, address(this))); + closePositionWrapper.evcInternalSettle(settleData, wrapperData, wrapperData); + } + + /// @notice Test that non-solvers cannot call wrappedSettle + function test_ClosePositionWrapper_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))); + closePositionWrapper.wrappedSettle(settleData, wrapperData); + } + + /// @notice Test shrinking the position with partial repayment + function test_ClosePositionWrapper_PartialRepay() external { + vm.skip(bytes(forkRpcUrl).length == 0); + + uint256 borrowAmount = 2e18; // Borrow 2 WETH + uint256 collateralAmount = SUSDS_MARGIN + 4090e18; // Sufficient collateral for 2 WETH borrow (double the margin + borrow amount equivalent) + + // First, set up a leveraged position + _setupLeveragedPosition(borrowAmount, collateralAmount); + + vm.startPrank(user); + + // Close only half the position + uint256 sellAmount = 2500e18; // Sell exactly 2500 ESUSDS (buffer) + uint256 buyAmount = 0.98e18; // Buy at least 0.98 WETH to repay around half the debt + + // Get settlement data + SettlementData memory settlement = getClosePositionSettlement(user, user, ESUSDS, WETH, sellAmount, buyAmount); + + // User pre-approves the order + COW_SETTLEMENT.setPreSignature(settlement.orderUid, true); + IEVault(ESUSDS).approve(COW_SETTLEMENT.vaultRelayer(), type(uint256).max); + + // User approves spending of funds from the close position wrapper + IERC20(WETH).approve(address(closePositionWrapper), type(uint256).max); + + // Prepare ClosePositionParams with partial repayment + uint256 deadline = block.timestamp + 1 hours; + ecdsa.setPrivateKey(privateKey); + + address account = address(uint160(user) ^ uint8(0x01)); + + // 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(closePositionWrapper), type(uint256).max)) + }); + evc.batch(items); + } + + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: user, + account: account, + deadline: deadline, + borrowVault: EWETH, + collateralVault: ESUSDS, + collateralAmount: sellAmount, + repayAmount: buyAmount, + kind: GPv2Order.KIND_SELL // use KIND_SELL here because that is the generally expected pattern for a partial sell type of order + }); + + bytes memory permitSignature = ecdsa.signPermit( + user, + address(closePositionWrapper), + uint256(uint160(address(closePositionWrapper))), + 0, + deadline, + 0, + closePositionWrapper.getSignedCalldata(params) + ); + + vm.stopPrank(); + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions) + ); + bytes memory wrapperData = abi.encode(params, permitSignature); + wrapperData = abi.encodePacked(uint16(wrapperData.length), wrapperData); + + address[] memory targets = new address[](1); + bytes[] memory datas = new bytes[](1); + targets[0] = address(closePositionWrapper); + datas[0] = abi.encodeCall(closePositionWrapper.wrappedSettle, (settleData, wrapperData)); + + // Expect the event to be emitted + vm.expectEmit(true, true, true, true); + emit CowEvcClosePositionWrapper.CowEvcPositionClosed( + params.owner, + params.account, + params.borrowVault, + params.collateralVault, + params.collateralAmount, + params.repayAmount, + params.kind + ); + + solver.runBatch(targets, datas); + + // Verify partial repayment + uint256 debtAfter = IEVault(EWETH).debtOf(account); + assertApproxEqAbs(debtAfter, borrowAmount - buyAmount, 0.01e18, "Debt should be reduced by repaid amount"); + assertEq(IERC20(WETH).balanceOf(user), 0, "User should have used any collateral they received to repay"); + } + + /// @notice Test parseWrapperData function + function test_ClosePositionWrapper_ParseWrapperData() external view { + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: user, + account: address(uint160(user) ^ uint8(0x01)), + deadline: block.timestamp + 1 hours, + borrowVault: EWETH, + collateralVault: ESUSDS, + collateralAmount: 0, + repayAmount: type(uint256).max, + kind: GPv2Order.KIND_BUY + }); + + bytes memory wrapperData = abi.encode(params, new bytes(65)); + bytes memory remainingData = closePositionWrapper.parseWrapperData(wrapperData); + + // After parsing ClosePositionParams, remaining data should be empty + assertEq(remainingData.length, 0, "Remaining data should be empty"); + } + + /// @notice Test setting pre-approved hash + function test_ClosePositionWrapper_SetPreApprovedHash() external { + vm.skip(bytes(forkRpcUrl).length == 0); + + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: user, + account: address(uint160(user) ^ uint8(0x01)), + deadline: block.timestamp + 1 hours, + borrowVault: EWETH, + collateralVault: ESUSDS, + collateralAmount: 0, + repayAmount: type(uint256).max, + kind: GPv2Order.KIND_BUY + }); + + bytes32 hash = closePositionWrapper.getApprovalHash(params); + + // Initially hash should not be approved + assertEq(closePositionWrapper.preApprovedHashes(user, hash), 0, "Hash should not be approved initially"); + + // User pre-approves the hash + vm.prank(user); + vm.expectEmit(true, true, false, true); + emit PreApprovedHashes.PreApprovedHash(user, hash, true); + closePositionWrapper.setPreApprovedHash(hash, true); + + // Hash should now be approved + assertGt(closePositionWrapper.preApprovedHashes(user, hash), 0, "Hash should be approved"); + + // User revokes the approval + vm.prank(user); + vm.expectEmit(true, true, false, true); + emit PreApprovedHashes.PreApprovedHash(user, hash, false); + closePositionWrapper.setPreApprovedHash(hash, false); + + // Hash should no longer be approved + assertEq(closePositionWrapper.preApprovedHashes(user, hash), 0, "Hash should not be approved after revocation"); + } + + /// @notice Test closing a position with pre-approved hash (no signature needed) + function test_ClosePositionWrapper_WithPreApprovedHash() external { + vm.skip(bytes(forkRpcUrl).length == 0); + + uint256 borrowAmount = 1e18; // Borrow 1 WETH + uint256 collateralAmount = SUSDS_MARGIN + 2495e18; + + // First, set up a leveraged position + _setupLeveragedPosition(borrowAmount, collateralAmount); + + address account = address(uint160(user) ^ uint8(0x01)); + + uint256 sellAmount = 2510 ether; + uint256 buyAmount = 1.001 ether; + + // Prepare ClosePositionParams + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: user, + account: account, + deadline: block.timestamp + 1 hours, + borrowVault: EWETH, + collateralVault: ESUSDS, + collateralAmount: sellAmount, + repayAmount: buyAmount, + kind: GPv2Order.KIND_BUY + }); + + // Get settlement data + SettlementData memory settlement = getClosePositionSettlement(user, user, ESUSDS, WETH, sellAmount, buyAmount); + + // Now close the position + vm.startPrank(user); + // User approves the wrapper to be operator (both of the main account and the subaccount) + // only required if the operator permission was not previously granted to the close wrapper + evc.setAccountOperator(user, address(closePositionWrapper), true); + evc.setAccountOperator(account, address(closePositionWrapper), true); + + // User pre-approves the hash on the closePositionWrapper (absolutely required in some form) + bytes32 hash = closePositionWrapper.getApprovalHash(params); + closePositionWrapper.setPreApprovedHash(hash, true); + + // User pre-approves the order on CoW + COW_SETTLEMENT.setPreSignature(settlement.orderUid, true); + + // 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(closePositionWrapper), type(uint256).max)) + }); + evc.batch(items); + } + + // User approves vault shares for settlement + IEVault(ESUSDS).approve(COW_SETTLEMENT.vaultRelayer(), type(uint256).max); + + // User approves the wrapper so it can repay + IERC20(WETH).approve(address(closePositionWrapper), type(uint256).max); + + vm.stopPrank(); + + // Record balances before closing + uint256 debtBefore = IEVault(EWETH).debtOf(account); + + // Encode settlement data + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions) + ); + + // Encode wrapper data with ClosePositionParams (empty signature since pre-approved) + bytes memory wrapperData = abi.encode(params, new bytes(0)); + 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(closePositionWrapper); + datas[0] = abi.encodeCall(closePositionWrapper.wrappedSettle, (settleData, wrapperData)); + + // Expect the event to be emitted + vm.expectEmit(true, true, true, true); + emit CowEvcClosePositionWrapper.CowEvcPositionClosed( + params.owner, + params.account, + params.borrowVault, + params.collateralVault, + params.collateralAmount, + params.repayAmount, + params.kind + ); + + solver.runBatch(targets, datas); + + // Verify the position was closed successfully + assertEq(IEVault(EWETH).debtOf(account), 0, "User should have no debt after closing"); + assertEq(debtBefore, borrowAmount, "User should have started with debt"); + } +} diff --git a/test/helpers/CowBaseTest.sol b/test/helpers/CowBaseTest.sol index efec69a..6c6fda0 100644 --- a/test/helpers/CowBaseTest.sol +++ b/test/helpers/CowBaseTest.sol @@ -5,7 +5,7 @@ import {GPv2Order} from "cow/libraries/GPv2Order.sol"; import {EthereumVaultConnector} from "evc/EthereumVaultConnector.sol"; import {EVaultTestBase} from "lib/euler-vault-kit/test/unit/evault/EVaultTestBase.t.sol"; -import {IEVault, IVault, IERC20} from "euler-vault-kit/src/EVault/IEVault.sol"; +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"; @@ -105,6 +105,28 @@ contract CowBaseTest is EVaultTestBase { vm.label(address(milkSwap), "milkswap"); } + function getEmptySettlement() + public + pure + returns ( + IERC20[] memory tokens, + uint256[] memory clearingPrices, + ICowSettlement.Trade[] memory trades, + ICowSettlement.Interaction[][3] memory interactions + ) + { + return ( + new IERC20[](0), + new uint256[](0), + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ); + } + function getOrderUid(address owner, GPv2Order.Data memory orderData) public view returns (bytes memory orderUid) { // Generate order digest using EIP-712 bytes32 orderDigest = GPv2Order.hash(orderData, COW_SETTLEMENT.domainSeparator()); @@ -138,6 +160,18 @@ contract CowBaseTest is EVaultTestBase { }); } + function getWithdrawInteraction(address vault, uint256 sellAmount) + public + pure + returns (ICowSettlement.Interaction memory) + { + return ICowSettlement.Interaction({ + 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({ target: address(ESUSDS), diff --git a/test/unit/CowEvcClosePositionWrapper.unit.t.sol b/test/unit/CowEvcClosePositionWrapper.unit.t.sol new file mode 100644 index 0000000..59eb667 --- /dev/null +++ b/test/unit/CowEvcClosePositionWrapper.unit.t.sol @@ -0,0 +1,979 @@ +// 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 {CowEvcClosePositionWrapper} from "../../src/CowEvcClosePositionWrapper.sol"; +import {ICowSettlement} from "../../src/CowWrapper.sol"; +import {MockEVC} from "./mocks/MockEVC.sol"; +import {MockCowAuthentication, MockCowSettlement} from "./mocks/MockCowProtocol.sol"; +import {MockERC20, MockVault, MockBorrowVault} from "./mocks/MockERC20AndVaults.sol"; + +/// @title Unit tests for CowEvcClosePositionWrapper +/// @notice Comprehensive unit tests focusing on isolated functionality testing with mocks +contract CowEvcClosePositionWrapperUnitTest is Test { + CowEvcClosePositionWrapper public wrapper; + MockEVC public mockEvc; + MockCowSettlement public mockSettlement; + MockCowAuthentication public mockAuth; + MockERC20 public mockAsset; + MockVault public mockCollateralVault; + MockBorrowVault public mockBorrowVault; + + address constant OWNER = address(0x1111); + address constant ACCOUNT = address(0x1112); + address constant SOLVER = address(0x3333); + + 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"); + mockCollateralVault = new MockVault(address(mockAsset), "Mock Collateral", "mCOL"); + mockBorrowVault = new MockBorrowVault(address(mockAsset), "Mock Borrow", "mBOR"); + + wrapper = new CowEvcClosePositionWrapper(address(mockEvc), ICowSettlement(address(mockSettlement))); + + // Set solver as authenticated + mockAuth.setSolver(SOLVER, true); + + // Set the correct onBehalfOfAccount for evcInternalSettle 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("CowEvcClosePositionWrapper"), + 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 - Close Position", "Name not set correctly"); + } + + /*////////////////////////////////////////////////////////////// + PARSE WRAPPER DATA TESTS + //////////////////////////////////////////////////////////////*/ + + function test_ParseWrapperData_EmptySignature() public view { + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + 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 { + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + 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 { + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + 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 { + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + bytes32 hash1 = wrapper.getApprovalHash(params); + bytes32 hash2 = wrapper.getApprovalHash(params); + + assertEq(hash1, hash2, "Hash should be consistent"); + } + + function test_GetApprovalHash_DifferentForDifferentParams() public view { + CowEvcClosePositionWrapper.ClosePositionParams memory params1 = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + // Same as params1 except owner + CowEvcClosePositionWrapper.ClosePositionParams memory params2 = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: ACCOUNT, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + // Same as params1 except repayAmount (the last) field + CowEvcClosePositionWrapper.ClosePositionParams memory params3 = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 2000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + 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 { + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + bytes32 structHash = keccak256( + abi.encode( + params.owner, + params.account, + params.deadline, + params.borrowVault, + params.collateralVault, + params.collateralAmount, + params.repayAmount, + 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_FullRepay() public { + // Set up a debt scenario + mockBorrowVault.setDebt(ACCOUNT, 1000e18); + + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 1000e18, // Exactly matches debt + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + bytes memory signedCalldata = wrapper.getSignedCalldata(params); + IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); + + assertEq(items.length, 1, "Should have 1 batch item"); + } + + function test_GetSignedCalldata_PartialRepay() public { + // Set up a debt scenario + mockBorrowVault.setDebt(ACCOUNT, 1000e18); + + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 500e18, // Less than debt + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + bytes memory signedCalldata = wrapper.getSignedCalldata(params); + IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); + + assertEq(items.length, 1, "Should have 1 batch item for partial repay"); + } + + function test_GetSignedCalldata_RepayItem() public { + mockBorrowVault.setDebt(ACCOUNT, 1000e18); + + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + bytes memory signedCalldata = wrapper.getSignedCalldata(params); + IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); + + assertEq(items[0].targetContract, address(wrapper), "First item should target wrapper"); + assertEq(items[0].onBehalfOfAccount, ACCOUNT, "Should call on behalf of account"); + assertEq( + items[0].data, + abi.encodeCall(wrapper.helperRepay, (address(mockBorrowVault), OWNER, ACCOUNT)), + "Should call helperRepay" + ); + } + + function test_GetSignedCalldata_ContainsRepayItem() public { + mockBorrowVault.setDebt(ACCOUNT, 1000e18); + + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + bytes memory signedCalldata = wrapper.getSignedCalldata(params); + IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); + + assertEq(items[0].targetContract, address(wrapper), "Item should target wrapper"); + assertEq(items[0].onBehalfOfAccount, ACCOUNT, "Should operate on behalf of account"); + } + + /*////////////////////////////////////////////////////////////// + HELPER REPAY TESTS + //////////////////////////////////////////////////////////////*/ + + function test_HelperRepay_SuccessfulRepay() public { + // Give owner some tokens (not wrapper) + mockAsset.mint(OWNER, 1000e18); + + // Owner must approve wrapper to spend their tokens + vm.prank(OWNER); + mockAsset.approve(address(wrapper), 1000e18); + + // Set up borrow vault with debt + mockBorrowVault.setDebt(ACCOUNT, 1000e18); + mockBorrowVault.setRepayAmount(1000e18); + + // Set the correct onBehalfOfAccount for authentication check + mockEvc.setOnBehalfOf(ACCOUNT); + + // Call through EVC batch + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](1); + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: ACCOUNT, + targetContract: address(wrapper), + value: 0, + data: abi.encodeCall(wrapper.helperRepay, (address(mockBorrowVault), OWNER, ACCOUNT)) + }); + + vm.prank(address(mockEvc)); + mockEvc.batch(items); + + // Verify owner's tokens were used for repayment + assertEq(mockAsset.balanceOf(OWNER), 0, "Owner should have no tokens left"); + } + + function test_HelperRepay_WithDust() public { + // Give owner more tokens than needed for repay + mockAsset.mint(OWNER, 1100e18); + + // Owner must approve wrapper to spend their tokens + vm.prank(OWNER); + mockAsset.approve(address(wrapper), 1100e18); + + // Set up borrow vault with debt + mockBorrowVault.setDebt(ACCOUNT, 1000e18); + mockBorrowVault.setRepayAmount(1000e18); // Only 1000 actually needed + + // Set the correct onBehalfOfAccount for authentication check + mockEvc.setOnBehalfOf(ACCOUNT); + + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](1); + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: ACCOUNT, + targetContract: address(wrapper), + value: 0, + data: abi.encodeCall(wrapper.helperRepay, (address(mockBorrowVault), OWNER, ACCOUNT)) + }); + + vm.prank(address(mockEvc)); + mockEvc.batch(items); + + // Owner should have 100 tokens left (1100 - 1000 repaid) + assertEq(mockAsset.balanceOf(OWNER), 100e18, "Owner should have dust remaining"); + } + + function test_HelperRepay_PartialRepayWhenInsufficientBalance() public { + // Give owner insufficient tokens to fully repay debt + mockAsset.mint(OWNER, 500e18); + + // Owner must approve wrapper to spend their tokens + vm.prank(OWNER); + mockAsset.approve(address(wrapper), 500e18); + + mockBorrowVault.setDebt(ACCOUNT, 1000e18); + mockBorrowVault.setRepayAmount(500e18); // Will only repay what's available + + // Set the correct onBehalfOfAccount for authentication check + mockEvc.setOnBehalfOf(ACCOUNT); + + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](1); + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: ACCOUNT, + targetContract: address(wrapper), + value: 0, + data: abi.encodeCall(wrapper.helperRepay, (address(mockBorrowVault), OWNER, ACCOUNT)) + }); + + vm.prank(address(mockEvc)); + mockEvc.batch(items); + + // Should repay partial amount (500e18) + assertEq(mockAsset.balanceOf(OWNER), 0, "Owner should have no tokens left"); + } + + function test_HelperRepay_RepayAll() public { + mockAsset.mint(OWNER, 1100e18); + + // Owner must approve wrapper to spend their tokens + vm.prank(OWNER); + mockAsset.approve(address(wrapper), 1100e18); + + mockBorrowVault.setDebt(ACCOUNT, 1000e18); + mockBorrowVault.setRepayAmount(1000e18); + + // Set the correct onBehalfOfAccount for authentication check + mockEvc.setOnBehalfOf(ACCOUNT); + + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](1); + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: ACCOUNT, + targetContract: address(wrapper), + value: 0, + data: abi.encodeCall(wrapper.helperRepay, (address(mockBorrowVault), OWNER, ACCOUNT)) + }); + + vm.prank(address(mockEvc)); + mockEvc.batch(items); + + // Dust should remain with owner (100e18) + assertEq(mockAsset.balanceOf(OWNER), 100e18, "Owner should have dust remaining"); + } + + function test_HelperRepay_OnlyEVC() public { + vm.expectRevert(abi.encodeWithSelector(CowEvcClosePositionWrapper.Unauthorized.selector, address(this))); + wrapper.helperRepay(address(mockBorrowVault), OWNER, ACCOUNT); + } + + function test_HelperRepay_RequiresCorrectOnBehalfOfAccount() public { + mockAsset.mint(OWNER, 1000e18); + + vm.prank(OWNER); + mockAsset.approve(address(wrapper), 1000e18); + + mockBorrowVault.setDebt(ACCOUNT, 1000e18); + + // Create a batch item that specifies ACCOUNT but the helperRepay expects a different account + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](1); + address wrongAccount = address(0x9999); + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: wrongAccount, // This will be set as getCurrentOnBehalfOfAccount + targetContract: address(wrapper), + value: 0, + data: abi.encodeCall( + wrapper.helperRepay, + (address(mockBorrowVault), OWNER, ACCOUNT) // But we're trying to repay for ACCOUNT + ) + }); + + vm.prank(address(mockEvc)); + vm.expectRevert(abi.encodeWithSelector(CowEvcClosePositionWrapper.Unauthorized.selector, wrongAccount)); + mockEvc.batch(items); + } + + /*////////////////////////////////////////////////////////////// + EVC INTERNAL SETTLE TESTS + //////////////////////////////////////////////////////////////*/ + + function test_EvcInternalSettle_OnlyEVC() public { + bytes memory settleData = ""; + bytes memory wrapperData = ""; + bytes memory remainingWrapperData = ""; + + vm.expectRevert(abi.encodeWithSelector(CowEvcClosePositionWrapper.Unauthorized.selector, address(this))); + wrapper.evcInternalSettle(settleData, wrapperData, remainingWrapperData); + } + + function test_EvcInternalSettle_RequiresCorrectOnBehalfOfAccount() public { + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: OWNER, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + bytes memory remainingWrapperData = ""; + + mockSettlement.setSuccessfulSettle(true); + + // Set incorrect onBehalfOfAccount (not address(wrapper)) + mockEvc.setOnBehalfOf(address(0x9999)); + + vm.prank(address(mockEvc)); + vm.expectRevert(abi.encodeWithSelector(CowEvcClosePositionWrapper.Unauthorized.selector, address(0x9999))); + wrapper.evcInternalSettle(settleData, wrapperData, remainingWrapperData); + } + + function test_EvcInternalSettle_CanBeCalledByEVC() public { + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: OWNER, // Same account, no transfer needed + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + bytes memory remainingWrapperData = ""; + + mockSettlement.setSuccessfulSettle(true); + + vm.prank(address(mockEvc)); + wrapper.evcInternalSettle(settleData, wrapperData, remainingWrapperData); + } + + function test_EvcInternalSettle_WithSubaccountTransfer() public { + // Set up scenario where owner != account + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, // Different from owner + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 1000e18, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + // Give account some collateral vault tokens + mockCollateralVault.mint(ACCOUNT, 2000e18); + + // These tokens need to be spendable by the wrapper + vm.prank(ACCOUNT); + mockCollateralVault.approve(address(wrapper), 2000e18); + + // Create settle data with tokens and prices + address[] memory tokens = new address[](2); + tokens[0] = address(mockCollateralVault); + tokens[1] = address(mockAsset); + + uint256[] memory prices = new uint256[](2); + prices[0] = 1e18; // 1:1 price for simplicity + prices[1] = 1e18; + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + tokens, + prices, + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + bytes memory remainingWrapperData = ""; + + mockSettlement.setSuccessfulSettle(true); + + vm.prank(address(mockEvc)); + wrapper.evcInternalSettle(settleData, wrapperData, remainingWrapperData); + + // Verify transfer occurred from account to owner + assertLt(mockCollateralVault.balanceOf(ACCOUNT), 2000e18, "Account balance should decrease"); + assertGt(mockCollateralVault.balanceOf(OWNER), 0, "Owner should receive tokens"); + } + + function test_EvcInternalSettle_PricesNotFoundReverts() public { + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + // Create settle data with empty tokens (should fail to find prices) + address[] memory tokens = new address[](0); + uint256[] memory prices = new uint256[](0); + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + tokens, + prices, + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + bytes memory remainingWrapperData = ""; + + vm.expectRevert( + abi.encodeWithSelector( + CowEvcClosePositionWrapper.PricesNotFoundInSettlement.selector, + mockCollateralVault, + mockBorrowVault.asset() + ) + ); + vm.prank(address(mockEvc)); + wrapper.evcInternalSettle(settleData, wrapperData, remainingWrapperData); + } + + function test_EvcInternalSettle_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 (in case it succeeds) + vm.prank(invalidSubaccount); + mockCollateralVault.approve(address(wrapper), type(uint256).max); + + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: invalidSubaccount, // Invalid subaccount + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 1000e18, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + // Give account some collateral vault tokens + mockCollateralVault.mint(invalidSubaccount, 2000e18); + + // Create settle data with tokens and prices + address[] memory tokens = new address[](2); + tokens[0] = address(mockCollateralVault); + tokens[1] = address(mockAsset); + + uint256[] memory prices = new uint256[](2); + prices[0] = 1e18; + prices[1] = 1e18; + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + tokens, + prices, + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + bytes memory remainingWrapperData = ""; + + mockSettlement.setSuccessfulSettle(true); + + vm.prank(address(mockEvc)); + vm.expectRevert( + abi.encodeWithSelector( + CowEvcClosePositionWrapper.SubaccountMustBeControlledByOwner.selector, invalidSubaccount, OWNER + ) + ); + wrapper.evcInternalSettle(settleData, wrapperData, remainingWrapperData); + } + + /*////////////////////////////////////////////////////////////// + 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 { + mockBorrowVault.setDebt(ACCOUNT, 1000e18); + + mockCollateralVault.mint(ACCOUNT, 2000e18); + + // Mint repayment assets to OWNER (not wrapper) since helperRepay pulls from owner + MockERC20(mockBorrowVault.asset()).mint(OWNER, 1000e18); + + // Owner must approve wrapper to spend repayment assets + vm.prank(OWNER); + MockERC20(mockBorrowVault.asset()).approve(address(wrapper), 1000e18); + + // These tokens need to be spendable by the wrapper + vm.prank(ACCOUNT); + mockCollateralVault.approve(address(wrapper), 2000e18); + + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + address[] memory tokens = new address[](2); + tokens[0] = mockBorrowVault.asset(); + tokens[1] = address(mockCollateralVault); + uint256[] memory prices = new uint256[](2); + prices[0] = 1e18; + prices[1] = 1e18; + + 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); + + mockEvc.setSuccessfulBatch(true); + + vm.prank(SOLVER); + wrapper.wrappedSettle(settleData, wrapperData); + } + + function test_WrappedSettle_WithPreApprovedHash() public { + mockBorrowVault.setDebt(ACCOUNT, 1000e18); + + mockCollateralVault.mint(ACCOUNT, 2000e18); + + // Mint repayment assets to OWNER (not wrapper) since helperRepay pulls from owner + MockERC20(mockBorrowVault.asset()).mint(OWNER, 1000e18); + + // Owner must approve wrapper to spend repayment assets + vm.startPrank(OWNER); + MockERC20(mockBorrowVault.asset()).approve(address(wrapper), 1000e18); + vm.stopPrank(); + + // These tokens need to be spendable by the wrapper + vm.prank(ACCOUNT); + mockCollateralVault.approve(address(wrapper), 2000e18); + + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + bytes32 hash = wrapper.getApprovalHash(params); + + vm.prank(OWNER); + wrapper.setPreApprovedHash(hash, true); + + mockEvc.setOperator(OWNER, address(wrapper), true); + mockEvc.setOperator(ACCOUNT, address(wrapper), true); + + address[] memory tokens = new address[](2); + tokens[0] = mockBorrowVault.asset(); + tokens[1] = address(mockCollateralVault); + uint256[] memory prices = new uint256[](2); + prices[0] = 1e18; + prices[1] = 1e18; + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + tokens, + prices, + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + 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 { + mockBorrowVault.setDebt(ACCOUNT, 1000e18); + + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp - 1, // Past deadline + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + bytes32 hash = wrapper.getApprovalHash(params); + + vm.prank(OWNER); + wrapper.setPreApprovedHash(hash, true); + + mockEvc.setOperator(OWNER, address(wrapper), true); + mockEvc.setOperator(ACCOUNT, address(wrapper), true); + + bytes memory settleData = abi.encodeCall( + ICowSettlement.settle, + ( + new address[](0), + new uint256[](0), + new ICowSettlement.Trade[](0), + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ] + ) + ); + bytes memory wrapperData = abi.encode(params, new bytes(0)); + wrapperData = abi.encodePacked(uint16(wrapperData.length), wrapperData); + + vm.prank(SOLVER); + vm.expectRevert( + abi.encodeWithSelector( + CowEvcClosePositionWrapper.OperationDeadlineExceeded.selector, params.deadline, block.timestamp + ) + ); + wrapper.wrappedSettle(settleData, wrapperData); + } + + /*////////////////////////////////////////////////////////////// + EDGE CASE TESTS + //////////////////////////////////////////////////////////////*/ + + function test_MaxRepayAmount() public { + mockBorrowVault.setDebt(ACCOUNT, 1000e18); + + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: type(uint256).max, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + bytes memory signedCalldata = wrapper.getSignedCalldata(params); + IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); + + // Should create repay item + assertEq(items.length, 1, "Should have 1 item for repay with max amount"); + } + + function test_SameOwnerAndAccount() public { + mockBorrowVault.setDebt(OWNER, 1000e18); + + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: OWNER, // Same as owner + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + bytes memory signedCalldata = wrapper.getSignedCalldata(params); + IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); + + assertEq(items[0].onBehalfOfAccount, OWNER, "Should operate on behalf of same account"); + } + + function test_ZeroDebt() public { + mockBorrowVault.setDebt(ACCOUNT, 0); + + CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: 1000e18, + kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY + }); + + bytes memory signedCalldata = wrapper.getSignedCalldata(params); + IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); + assertEq(items.length, 1, "Should have 1 item"); + } +} From 9f18ada4f4dd9e2da33b0ca11b1a012b1bdbab80 Mon Sep 17 00:00:00 2001 From: Kaze Date: Sat, 8 Nov 2025 05:41:10 +0900 Subject: [PATCH 2/5] refactor: improve CowEvcClosePositionWrapper test suite DRYness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor CowEvcClosePositionWrapper test files to reduce code duplication and improve maintainability: Integration tests (CowEvcClosePositionWrapper.t.sol): - Add helper functions for common test setup patterns - _createDefaultParams(): standardized param creation - _setupClosePositionApprovals(): approval setup for close position - _setupPreApprovedFlow(): pre-approved hash flow setup - _createPermitSignature(): permit signature creation - _encodeWrapperData(): wrapper data encoding - _executeWrappedSettlement(): settlement execution - Add DEFAULT_SELL_AMOUNT and DEFAULT_BUY_AMOUNT constants - Reduce test function size significantly while maintaining clarity Unit tests (CowEvcClosePositionWrapper.unit.t.sol): - Add helper functions for common patterns - _getDefaultParams(): returns default params (tests modify as needed) - _getEmptySettleData(): creates empty settlement data - _getSettleDataWithTokens(): creates settlement data with tokens/prices - _encodeWrapperData(): wrapper data encoding - _setupPreApprovedHash(): pre-approved hash setup - Add DEFAULT_REPAY_AMOUNT and KIND_BUY constants - Remove 5 redundant tests with no coverage impact: - test_Constructor_SetsName (trivial getter) - test_GetApprovalHash_Consistency (deterministic function) - test_ParseWrapperData_WithSignature (duplicate code path) - test_GetSignedCalldata_FullRepay (covered by other tests) - test_GetSignedCalldata_ContainsRepayItem (duplicate of RepayItem) - Reduce from 32 to 27 unit tests with no loss in coverage All tests pass: 7/7 integration tests, 27/27 unit tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- test/CowEvcClosePositionWrapper.t.sol | 380 +++++++-------- .../CowEvcClosePositionWrapper.unit.t.sol | 450 +++++------------- 2 files changed, 294 insertions(+), 536 deletions(-) diff --git a/test/CowEvcClosePositionWrapper.t.sol b/test/CowEvcClosePositionWrapper.t.sol index 3000ea7..c1713de 100644 --- a/test/CowEvcClosePositionWrapper.t.sol +++ b/test/CowEvcClosePositionWrapper.t.sol @@ -21,6 +21,8 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { SignerECDSA internal ecdsa; uint256 constant SUSDS_MARGIN = 2000e18; + uint256 constant DEFAULT_SELL_AMOUNT = 2510 ether; + uint256 constant DEFAULT_BUY_AMOUNT = 1.001 ether; function setUp() public override { super.setUp(); @@ -55,6 +57,113 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { ICowSettlement.Interaction[][3] interactions; } + /// @notice Create default ClosePositionParams for testing + function _createDefaultParams(address owner, address account) + internal + view + returns (CowEvcClosePositionWrapper.ClosePositionParams memory) + { + return CowEvcClosePositionWrapper.ClosePositionParams({ + owner: owner, + account: account, + deadline: block.timestamp + 1 hours, + borrowVault: EWETH, + collateralVault: ESUSDS, + collateralAmount: DEFAULT_SELL_AMOUNT, + repayAmount: DEFAULT_BUY_AMOUNT, + kind: GPv2Order.KIND_BUY + }); + } + + /// @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); + + // Set operators + evc.setAccountOperator(user, address(closePositionWrapper), true); + evc.setAccountOperator(account, address(closePositionWrapper), true); + + // Pre-approve hash + closePositionWrapper.setPreApprovedHash(hash, true); + + // 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 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 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 { @@ -141,99 +250,47 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { function test_ClosePositionWrapper_SuccessFullRepay() external { vm.skip(bytes(forkRpcUrl).length == 0); - uint256 borrowAmount = 1e18; // Borrow 1 WETH - uint256 collateralAmount = SUSDS_MARGIN + 2495e18; // The original margin plus the amount we would have if we sold the borrow amount into ESUSDS + uint256 borrowAmount = 1e18; + uint256 collateralAmount = SUSDS_MARGIN + 2495e18; // First, set up a leveraged position _setupLeveragedPosition(borrowAmount, collateralAmount); - // Verify position exists address account = address(uint160(user) ^ uint8(0x01)); + + // Verify position exists uint256 debtBefore = IEVault(EWETH).debtOf(account); assertEq(debtBefore, borrowAmount, "Position should have debt"); - uint256 sellAmount = 2510 ether; // Sell up to 2510 ESUSDS (buffer) - uint256 buyAmount = 1.001 ether; // Buy exactly 1.001 WETH to repay debt (a small amount will be returned to user) + // Create params using helper + CowEvcClosePositionWrapper.ClosePositionParams memory params = _createDefaultParams(user, account); // Get settlement data - SettlementData memory settlement = getClosePositionSettlement(user, user, ESUSDS, WETH, sellAmount, buyAmount); + SettlementData memory settlement = + getClosePositionSettlement(user, user, ESUSDS, WETH, DEFAULT_SELL_AMOUNT, DEFAULT_BUY_AMOUNT); - // Prepare ClosePositionParams - uint256 deadline = block.timestamp + 1 hours; - ecdsa.setPrivateKey(privateKey); - - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: user, - account: account, - deadline: deadline, - borrowVault: EWETH, - collateralVault: ESUSDS, - collateralAmount: sellAmount, - repayAmount: buyAmount, - kind: GPv2Order.KIND_BUY - }); - - // Now close the position - vm.startPrank(user); - - // User signs the order on cowswap - // Possibly skippable with Permit2 flow + // User signs order + vm.prank(user); COW_SETTLEMENT.setPreSignature(settlement.orderUid, true); - // For subaccount, user approves transfer of vault shares from the account - // only required if the approve has not already been granted - { - 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); - } - - // User approves vault shares for settlement - // only required if the approve has not already been granted. Could be skipped with a Permit2 flow - IEVault(ESUSDS).approve(COW_SETTLEMENT.vaultRelayer(), type(uint256).max); + // Setup approvals + _setupClosePositionApprovals(account); - // User approves the wrapper so it can repay - IERC20(WETH).approve(address(closePositionWrapper), type(uint256).max); - - // Sign permit for EVC operator (absolutely required in some form or another) - bytes memory permitSignature = ecdsa.signPermit( - user, - address(closePositionWrapper), - uint256(uint160(address(closePositionWrapper))), - 0, - deadline, - 0, - closePositionWrapper.getSignedCalldata(params) - ); - - vm.stopPrank(); + // Create permit signature + bytes memory permitSignature = _createPermitSignature(params); // Record balances before closing uint256 collateralBefore = IERC20(ESUSDS).balanceOf(user); uint256 collateralBeforeAccount = IERC20(ESUSDS).balanceOf(account); - // Encode settlement data + // 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); - // Encode wrapper data with ClosePositionParams - 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(closePositionWrapper); - datas[0] = abi.encodeCall(closePositionWrapper.wrappedSettle, (settleData, wrapperData)); - - // Expect the event to be emitted + // Expect event emission vm.expectEmit(true, true, true, true); emit CowEvcClosePositionWrapper.CowEvcPositionClosed( params.owner, @@ -245,7 +302,8 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { params.kind ); - solver.runBatch(targets, datas); + // Execute wrapped settlement + _executeWrappedSettlement(settleData, wrapperData); // Verify the position was closed successfully assertEq(IEVault(EWETH).debtOf(account), 0, "User should have no debt after closing"); @@ -253,7 +311,6 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { IERC20(ESUSDS).balanceOf(account), collateralBeforeAccount, "User should have less collateral after closing" ); assertGt(IERC20(ESUSDS).balanceOf(account), 0, "User should have some collateral remaining"); - // the sold collateral is sent through the user's main account, but there should be no balance there assertEq(IERC20(ESUSDS).balanceOf(user), collateralBefore, "User main account balance should not have changed"); } @@ -285,82 +342,43 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { function test_ClosePositionWrapper_PartialRepay() external { vm.skip(bytes(forkRpcUrl).length == 0); - uint256 borrowAmount = 2e18; // Borrow 2 WETH - uint256 collateralAmount = SUSDS_MARGIN + 4090e18; // Sufficient collateral for 2 WETH borrow (double the margin + borrow amount equivalent) + uint256 borrowAmount = 2e18; + uint256 collateralAmount = SUSDS_MARGIN + 4990e18; + uint256 sellAmount = 2500e18; + uint256 buyAmount = 0.98e18; // First, set up a leveraged position _setupLeveragedPosition(borrowAmount, collateralAmount); - vm.startPrank(user); + address account = address(uint160(user) ^ uint8(0x01)); - // Close only half the position - uint256 sellAmount = 2500e18; // Sell exactly 2500 ESUSDS (buffer) - uint256 buyAmount = 0.98e18; // Buy at least 0.98 WETH to repay around half the debt + // Create params with custom amounts and KIND_SELL + CowEvcClosePositionWrapper.ClosePositionParams memory params = _createDefaultParams(user, account); + params.collateralAmount = sellAmount; + params.repayAmount = buyAmount; + params.kind = GPv2Order.KIND_SELL; // Get settlement data SettlementData memory settlement = getClosePositionSettlement(user, user, ESUSDS, WETH, sellAmount, buyAmount); - // User pre-approves the order + // User signs order + vm.prank(user); COW_SETTLEMENT.setPreSignature(settlement.orderUid, true); - IEVault(ESUSDS).approve(COW_SETTLEMENT.vaultRelayer(), type(uint256).max); - - // User approves spending of funds from the close position wrapper - IERC20(WETH).approve(address(closePositionWrapper), type(uint256).max); - // Prepare ClosePositionParams with partial repayment - uint256 deadline = block.timestamp + 1 hours; - ecdsa.setPrivateKey(privateKey); + // Setup approvals + _setupClosePositionApprovals(account); - address account = address(uint160(user) ^ uint8(0x01)); - - // 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(closePositionWrapper), type(uint256).max)) - }); - evc.batch(items); - } - - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: user, - account: account, - deadline: deadline, - borrowVault: EWETH, - collateralVault: ESUSDS, - collateralAmount: sellAmount, - repayAmount: buyAmount, - kind: GPv2Order.KIND_SELL // use KIND_SELL here because that is the generally expected pattern for a partial sell type of order - }); - - bytes memory permitSignature = ecdsa.signPermit( - user, - address(closePositionWrapper), - uint256(uint160(address(closePositionWrapper))), - 0, - deadline, - 0, - closePositionWrapper.getSignedCalldata(params) - ); - - vm.stopPrank(); + // Create permit signature + bytes memory permitSignature = _createPermitSignature(params); + // Encode settlement and wrapper data bytes memory settleData = abi.encodeCall( ICowSettlement.settle, (settlement.tokens, settlement.clearingPrices, settlement.trades, settlement.interactions) ); - bytes memory wrapperData = abi.encode(params, permitSignature); - wrapperData = abi.encodePacked(uint16(wrapperData.length), wrapperData); + bytes memory wrapperData = _encodeWrapperData(params, permitSignature); - address[] memory targets = new address[](1); - bytes[] memory datas = new bytes[](1); - targets[0] = address(closePositionWrapper); - datas[0] = abi.encodeCall(closePositionWrapper.wrappedSettle, (settleData, wrapperData)); - - // Expect the event to be emitted + // Expect event emission vm.expectEmit(true, true, true, true); emit CowEvcClosePositionWrapper.CowEvcPositionClosed( params.owner, @@ -372,7 +390,8 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { params.kind ); - solver.runBatch(targets, datas); + // Execute wrapped settlement + _executeWrappedSettlement(settleData, wrapperData); // Verify partial repayment uint256 debtAfter = IEVault(EWETH).debtOf(account); @@ -382,16 +401,10 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { /// @notice Test parseWrapperData function function test_ClosePositionWrapper_ParseWrapperData() external view { - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: user, - account: address(uint160(user) ^ uint8(0x01)), - deadline: block.timestamp + 1 hours, - borrowVault: EWETH, - collateralVault: ESUSDS, - collateralAmount: 0, - repayAmount: type(uint256).max, - kind: GPv2Order.KIND_BUY - }); + address account = address(uint160(user) ^ uint8(0x01)); + CowEvcClosePositionWrapper.ClosePositionParams memory params = _createDefaultParams(user, account); + params.collateralAmount = 0; + params.repayAmount = type(uint256).max; bytes memory wrapperData = abi.encode(params, new bytes(65)); bytes memory remainingData = closePositionWrapper.parseWrapperData(wrapperData); @@ -404,16 +417,10 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { function test_ClosePositionWrapper_SetPreApprovedHash() external { vm.skip(bytes(forkRpcUrl).length == 0); - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: user, - account: address(uint160(user) ^ uint8(0x01)), - deadline: block.timestamp + 1 hours, - borrowVault: EWETH, - collateralVault: ESUSDS, - collateralAmount: 0, - repayAmount: type(uint256).max, - kind: GPv2Order.KIND_BUY - }); + address account = address(uint160(user) ^ uint8(0x01)); + CowEvcClosePositionWrapper.ClosePositionParams memory params = _createDefaultParams(user, account); + params.collateralAmount = 0; + params.repayAmount = type(uint256).max; bytes32 hash = closePositionWrapper.getApprovalHash(params); @@ -443,7 +450,7 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { function test_ClosePositionWrapper_WithPreApprovedHash() external { vm.skip(bytes(forkRpcUrl).length == 0); - uint256 borrowAmount = 1e18; // Borrow 1 WETH + uint256 borrowAmount = 1e18; uint256 collateralAmount = SUSDS_MARGIN + 2495e18; // First, set up a leveraged position @@ -451,78 +458,32 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { address account = address(uint160(user) ^ uint8(0x01)); - uint256 sellAmount = 2510 ether; - uint256 buyAmount = 1.001 ether; - - // Prepare ClosePositionParams - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: user, - account: account, - deadline: block.timestamp + 1 hours, - borrowVault: EWETH, - collateralVault: ESUSDS, - collateralAmount: sellAmount, - repayAmount: buyAmount, - kind: GPv2Order.KIND_BUY - }); + // Create params using helper + CowEvcClosePositionWrapper.ClosePositionParams memory params = _createDefaultParams(user, account); // Get settlement data - SettlementData memory settlement = getClosePositionSettlement(user, user, ESUSDS, WETH, sellAmount, buyAmount); + SettlementData memory settlement = + getClosePositionSettlement(user, user, ESUSDS, WETH, DEFAULT_SELL_AMOUNT, DEFAULT_BUY_AMOUNT); - // Now close the position - vm.startPrank(user); - // User approves the wrapper to be operator (both of the main account and the subaccount) - // only required if the operator permission was not previously granted to the close wrapper - evc.setAccountOperator(user, address(closePositionWrapper), true); - evc.setAccountOperator(account, address(closePositionWrapper), true); - - // User pre-approves the hash on the closePositionWrapper (absolutely required in some form) + // Setup pre-approved flow bytes32 hash = closePositionWrapper.getApprovalHash(params); - closePositionWrapper.setPreApprovedHash(hash, true); + _setupPreApprovedFlow(account, hash); - // User pre-approves the order on CoW + // User signs order + vm.prank(user); COW_SETTLEMENT.setPreSignature(settlement.orderUid, true); - // 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(closePositionWrapper), type(uint256).max)) - }); - evc.batch(items); - } - - // User approves vault shares for settlement - IEVault(ESUSDS).approve(COW_SETTLEMENT.vaultRelayer(), type(uint256).max); - - // User approves the wrapper so it can repay - IERC20(WETH).approve(address(closePositionWrapper), type(uint256).max); - - vm.stopPrank(); - // Record balances before closing uint256 debtBefore = IEVault(EWETH).debtOf(account); - // Encode settlement data + // Encode settlement and wrapper data (empty signature since pre-approved) 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 ClosePositionParams (empty signature since pre-approved) - bytes memory wrapperData = abi.encode(params, new bytes(0)); - 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(closePositionWrapper); - datas[0] = abi.encodeCall(closePositionWrapper.wrappedSettle, (settleData, wrapperData)); - - // Expect the event to be emitted + // Expect event emission vm.expectEmit(true, true, true, true); emit CowEvcClosePositionWrapper.CowEvcPositionClosed( params.owner, @@ -534,7 +495,8 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { params.kind ); - solver.runBatch(targets, datas); + // Execute wrapped settlement + _executeWrappedSettlement(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/unit/CowEvcClosePositionWrapper.unit.t.sol b/test/unit/CowEvcClosePositionWrapper.unit.t.sol index 59eb667..2c34e20 100644 --- a/test/unit/CowEvcClosePositionWrapper.unit.t.sol +++ b/test/unit/CowEvcClosePositionWrapper.unit.t.sol @@ -24,10 +24,91 @@ contract CowEvcClosePositionWrapperUnitTest is Test { address constant ACCOUNT = address(0x1112); address constant SOLVER = address(0x3333); + uint256 constant DEFAULT_REPAY_AMOUNT = 1000e18; + bytes32 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 + /// @notice Get default ClosePositionParams for testing + function _getDefaultParams() internal view returns (CowEvcClosePositionWrapper.ClosePositionParams memory) { + return CowEvcClosePositionWrapper.ClosePositionParams({ + owner: OWNER, + account: ACCOUNT, + deadline: block.timestamp + 1 hours, + borrowVault: address(mockBorrowVault), + collateralVault: address(mockCollateralVault), + collateralAmount: 0, + repayAmount: DEFAULT_REPAY_AMOUNT, + kind: KIND_BUY + }); + } + + /// @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 Create settle data with tokens and prices + function _getSettleDataWithTokens() internal view returns (bytes memory) { + address[] memory tokens = new address[](2); + tokens[0] = mockBorrowVault.asset(); + tokens[1] = address(mockCollateralVault); + uint256[] memory prices = new uint256[](2); + prices[0] = 1e18; + prices[1] = 1e18; + + return abi.encodeCall( + ICowSettlement.settle, + ( + tokens, + prices, + 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(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 Setup pre-approved hash flow + function _setupPreApprovedHash(CowEvcClosePositionWrapper.ClosePositionParams memory params) + internal + returns (bytes32) + { + bytes32 hash = wrapper.getApprovalHash(params); + vm.prank(OWNER); + wrapper.setPreApprovedHash(hash, true); + mockEvc.setOperator(OWNER, address(wrapper), true); + mockEvc.setOperator(ACCOUNT, 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++) { @@ -77,25 +158,12 @@ contract CowEvcClosePositionWrapperUnitTest is Test { assertEq(wrapper.DOMAIN_SEPARATOR(), expectedDomainSeparator, "DOMAIN_SEPARATOR incorrect"); } - function test_Constructor_SetsName() public view { - assertEq(wrapper.name(), "Euler EVC - Close Position", "Name not set correctly"); - } - /*////////////////////////////////////////////////////////////// PARSE WRAPPER DATA TESTS //////////////////////////////////////////////////////////////*/ function test_ParseWrapperData_EmptySignature() public view { - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: OWNER, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: 1000e18, - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); + CowEvcClosePositionWrapper.ClosePositionParams memory params = _getDefaultParams(); bytes memory wrapperData = abi.encode(params, new bytes(0)); bytes memory remaining = wrapper.parseWrapperData(wrapperData); @@ -103,36 +171,8 @@ contract CowEvcClosePositionWrapperUnitTest is Test { assertEq(remaining.length, 0, "Should have no remaining data"); } - function test_ParseWrapperData_WithSignature() public view { - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: OWNER, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: 1000e18, - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); - - 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 { - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: OWNER, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: 1000e18, - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); + CowEvcClosePositionWrapper.ClosePositionParams memory params = _getDefaultParams(); bytes memory signature = new bytes(0); bytes memory wrapperData = abi.encode(params, signature); @@ -149,59 +189,16 @@ contract CowEvcClosePositionWrapperUnitTest is Test { APPROVAL HASH TESTS //////////////////////////////////////////////////////////////*/ - function test_GetApprovalHash_Consistency() public view { - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: OWNER, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: 1000e18, - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); - - bytes32 hash1 = wrapper.getApprovalHash(params); - bytes32 hash2 = wrapper.getApprovalHash(params); - - assertEq(hash1, hash2, "Hash should be consistent"); - } - function test_GetApprovalHash_DifferentForDifferentParams() public view { - CowEvcClosePositionWrapper.ClosePositionParams memory params1 = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: OWNER, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: 1000e18, - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); + CowEvcClosePositionWrapper.ClosePositionParams memory params1 = _getDefaultParams(); - // Same as params1 except owner - CowEvcClosePositionWrapper.ClosePositionParams memory params2 = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: ACCOUNT, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: 1000e18, - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); + // Change owner field + CowEvcClosePositionWrapper.ClosePositionParams memory params2 = _getDefaultParams(); + params2.owner = ACCOUNT; - // Same as params1 except repayAmount (the last) field - CowEvcClosePositionWrapper.ClosePositionParams memory params3 = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: OWNER, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: 2000e18, - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); + // Change repayAmount field + CowEvcClosePositionWrapper.ClosePositionParams memory params3 = _getDefaultParams(); + params3.repayAmount = 2000e18; bytes32 hash1 = wrapper.getApprovalHash(params1); bytes32 hash2 = wrapper.getApprovalHash(params2); @@ -212,16 +209,7 @@ contract CowEvcClosePositionWrapperUnitTest is Test { } function test_GetApprovalHash_MatchesEIP712() public view { - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: OWNER, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: 1000e18, - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); + CowEvcClosePositionWrapper.ClosePositionParams memory params = _getDefaultParams(); bytes32 structHash = keccak256( abi.encode( @@ -246,41 +234,11 @@ contract CowEvcClosePositionWrapperUnitTest is Test { GET SIGNED CALLDATA TESTS //////////////////////////////////////////////////////////////*/ - function test_GetSignedCalldata_FullRepay() public { - // Set up a debt scenario - mockBorrowVault.setDebt(ACCOUNT, 1000e18); - - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: OWNER, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: 1000e18, // Exactly matches debt - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); - - bytes memory signedCalldata = wrapper.getSignedCalldata(params); - IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); - - assertEq(items.length, 1, "Should have 1 batch item"); - } - function test_GetSignedCalldata_PartialRepay() public { - // Set up a debt scenario - mockBorrowVault.setDebt(ACCOUNT, 1000e18); + mockBorrowVault.setDebt(ACCOUNT, DEFAULT_REPAY_AMOUNT); - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: OWNER, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: 500e18, // Less than debt - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); + CowEvcClosePositionWrapper.ClosePositionParams memory params = _getDefaultParams(); + params.repayAmount = 500e18; // Less than debt bytes memory signedCalldata = wrapper.getSignedCalldata(params); IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); @@ -289,18 +247,9 @@ contract CowEvcClosePositionWrapperUnitTest is Test { } function test_GetSignedCalldata_RepayItem() public { - mockBorrowVault.setDebt(ACCOUNT, 1000e18); + mockBorrowVault.setDebt(ACCOUNT, DEFAULT_REPAY_AMOUNT); - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: OWNER, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: 1000e18, - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); + CowEvcClosePositionWrapper.ClosePositionParams memory params = _getDefaultParams(); bytes memory signedCalldata = wrapper.getSignedCalldata(params); IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); @@ -314,27 +263,6 @@ contract CowEvcClosePositionWrapperUnitTest is Test { ); } - function test_GetSignedCalldata_ContainsRepayItem() public { - mockBorrowVault.setDebt(ACCOUNT, 1000e18); - - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: OWNER, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: 1000e18, - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); - - bytes memory signedCalldata = wrapper.getSignedCalldata(params); - IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); - - assertEq(items[0].targetContract, address(wrapper), "Item should target wrapper"); - assertEq(items[0].onBehalfOfAccount, ACCOUNT, "Should operate on behalf of account"); - } - /*////////////////////////////////////////////////////////////// HELPER REPAY TESTS //////////////////////////////////////////////////////////////*/ @@ -502,30 +430,10 @@ contract CowEvcClosePositionWrapperUnitTest is Test { } function test_EvcInternalSettle_RequiresCorrectOnBehalfOfAccount() public { - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: OWNER, - account: OWNER, - deadline: block.timestamp + 1 hours, - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: 1000e18, - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); + CowEvcClosePositionWrapper.ClosePositionParams memory params = _getDefaultParams(); + params.account = OWNER; - 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 settleData = _getEmptySettleData(); bytes memory wrapperData = abi.encode(params, new bytes(0)); bytes memory remainingWrapperData = ""; @@ -540,30 +448,10 @@ contract CowEvcClosePositionWrapperUnitTest is Test { } function test_EvcInternalSettle_CanBeCalledByEVC() public { - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: OWNER, - account: OWNER, // Same account, no transfer needed - deadline: block.timestamp + 1 hours, - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: 1000e18, - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); + CowEvcClosePositionWrapper.ClosePositionParams memory params = _getDefaultParams(); + params.account = OWNER; // Same account, no transfer needed - 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 settleData = _getEmptySettleData(); bytes memory wrapperData = abi.encode(params, new bytes(0)); bytes memory remainingWrapperData = ""; @@ -800,63 +688,23 @@ contract CowEvcClosePositionWrapperUnitTest is Test { } function test_WrappedSettle_WithPreApprovedHash() public { - mockBorrowVault.setDebt(ACCOUNT, 1000e18); - + mockBorrowVault.setDebt(ACCOUNT, DEFAULT_REPAY_AMOUNT); mockCollateralVault.mint(ACCOUNT, 2000e18); + MockERC20(mockBorrowVault.asset()).mint(OWNER, DEFAULT_REPAY_AMOUNT); - // Mint repayment assets to OWNER (not wrapper) since helperRepay pulls from owner - MockERC20(mockBorrowVault.asset()).mint(OWNER, 1000e18); - - // Owner must approve wrapper to spend repayment assets vm.startPrank(OWNER); - MockERC20(mockBorrowVault.asset()).approve(address(wrapper), 1000e18); + MockERC20(mockBorrowVault.asset()).approve(address(wrapper), DEFAULT_REPAY_AMOUNT); vm.stopPrank(); - // These tokens need to be spendable by the wrapper vm.prank(ACCOUNT); mockCollateralVault.approve(address(wrapper), 2000e18); - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: OWNER, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: 1000e18, - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); - - bytes32 hash = wrapper.getApprovalHash(params); - - vm.prank(OWNER); - wrapper.setPreApprovedHash(hash, true); - - mockEvc.setOperator(OWNER, address(wrapper), true); - mockEvc.setOperator(ACCOUNT, address(wrapper), true); + CowEvcClosePositionWrapper.ClosePositionParams memory params = _getDefaultParams(); - address[] memory tokens = new address[](2); - tokens[0] = mockBorrowVault.asset(); - tokens[1] = address(mockCollateralVault); - uint256[] memory prices = new uint256[](2); - prices[0] = 1e18; - prices[1] = 1e18; + 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 = _getSettleDataWithTokens(); + bytes memory wrapperData = _encodeWrapperData(params, new bytes(0)); mockEvc.setSuccessfulBatch(true); @@ -867,42 +715,15 @@ contract CowEvcClosePositionWrapperUnitTest is Test { } function test_WrappedSettle_PreApprovedHashRevertsIfDeadlineExceeded() public { - mockBorrowVault.setDebt(ACCOUNT, 1000e18); - - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: OWNER, - account: ACCOUNT, - deadline: block.timestamp - 1, // Past deadline - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: 1000e18, - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); + mockBorrowVault.setDebt(ACCOUNT, DEFAULT_REPAY_AMOUNT); - bytes32 hash = wrapper.getApprovalHash(params); + CowEvcClosePositionWrapper.ClosePositionParams memory params = _getDefaultParams(); + params.deadline = block.timestamp - 1; // Past deadline - vm.prank(OWNER); - wrapper.setPreApprovedHash(hash, true); - - mockEvc.setOperator(OWNER, address(wrapper), true); - mockEvc.setOperator(ACCOUNT, 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( @@ -918,18 +739,10 @@ contract CowEvcClosePositionWrapperUnitTest is Test { //////////////////////////////////////////////////////////////*/ function test_MaxRepayAmount() public { - mockBorrowVault.setDebt(ACCOUNT, 1000e18); + mockBorrowVault.setDebt(ACCOUNT, DEFAULT_REPAY_AMOUNT); - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: OWNER, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: type(uint256).max, - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); + CowEvcClosePositionWrapper.ClosePositionParams memory params = _getDefaultParams(); + params.repayAmount = type(uint256).max; bytes memory signedCalldata = wrapper.getSignedCalldata(params); IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); @@ -939,18 +752,10 @@ contract CowEvcClosePositionWrapperUnitTest is Test { } function test_SameOwnerAndAccount() public { - mockBorrowVault.setDebt(OWNER, 1000e18); + mockBorrowVault.setDebt(OWNER, DEFAULT_REPAY_AMOUNT); - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: OWNER, - account: OWNER, // Same as owner - deadline: block.timestamp + 1 hours, - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: 1000e18, - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); + CowEvcClosePositionWrapper.ClosePositionParams memory params = _getDefaultParams(); + params.account = OWNER; // Same as owner bytes memory signedCalldata = wrapper.getSignedCalldata(params); IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); @@ -961,16 +766,7 @@ contract CowEvcClosePositionWrapperUnitTest is Test { function test_ZeroDebt() public { mockBorrowVault.setDebt(ACCOUNT, 0); - CowEvcClosePositionWrapper.ClosePositionParams memory params = CowEvcClosePositionWrapper.ClosePositionParams({ - owner: OWNER, - account: ACCOUNT, - deadline: block.timestamp + 1 hours, - borrowVault: address(mockBorrowVault), - collateralVault: address(mockCollateralVault), - collateralAmount: 0, - repayAmount: 1000e18, - kind: hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc" // KIND_BUY - }); + CowEvcClosePositionWrapper.ClosePositionParams memory params = _getDefaultParams(); bytes memory signedCalldata = wrapper.getSignedCalldata(params); IEVC.BatchItem[] memory items = _decodeSignedCalldata(signedCalldata); From 0e06bcc644ffe40697b47e672c5cb7c73ca66ee7 Mon Sep 17 00:00:00 2001 From: Kaze Date: Sat, 8 Nov 2025 08:04:42 +0900 Subject: [PATCH 3/5] add test for CoW --- test/CowEvcClosePositionWrapper.t.sol | 214 ++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) diff --git a/test/CowEvcClosePositionWrapper.t.sol b/test/CowEvcClosePositionWrapper.t.sol index 67c9f79..494cea7 100644 --- a/test/CowEvcClosePositionWrapper.t.sol +++ b/test/CowEvcClosePositionWrapper.t.sol @@ -190,6 +190,73 @@ contract CowEvcClosePositionWrapperTest 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); + } + + /// @notice Setup approvals for a specific user to close their position + function _setupClosePositionApprovalsFor( + address owner, + address account, + address collateralVault, + address repaymentAsset + ) internal { + vm.startPrank(owner); + + // Approve vault shares from subaccount + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](1); + items[0] = IEVC.BatchItem({ + onBehalfOfAccount: account, + targetContract: collateralVault, + value: 0, + data: abi.encodeCall(IERC20.approve, (address(closePositionWrapper), type(uint256).max)) + }); + EVC.batch(items); + + // Approve vault shares for settlement + IEVault(collateralVault).approve(COW_SETTLEMENT.vaultRelayer(), type(uint256).max); + + // Approve wrapper to spend repayment asset + IERC20(repaymentAsset).approve(address(closePositionWrapper), type(uint256).max); + + vm.stopPrank(); + } + + /// @notice Create permit signature for any user + function _createPermitSignatureFor( + CowEvcClosePositionWrapper.ClosePositionParams memory params, + uint256 userPrivateKey + ) internal returns (bytes memory) { + ecdsa.setPrivateKey(userPrivateKey); + return ecdsa.signPermit( + params.owner, + address(closePositionWrapper), + uint256(uint160(address(closePositionWrapper))), + 0, + params.deadline, + 0, + closePositionWrapper.getSignedCalldata(params) + ); + } + /// @notice Create settlement data for closing a leveraged position /// @dev Sells vault shares to buy repayment token (WETH) function getClosePositionSettlement( @@ -478,4 +545,151 @@ contract CowEvcClosePositionWrapperTest is CowBaseTest { assertEq(IEVault(EWETH).debtOf(account), 0, "User should have no debt after closing"); assertEq(debtBefore, borrowAmount, "User should have started with debt"); } + + /// @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_ClosePositionWrapper_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); + 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"); + + // Setup approvals for all users + _setupClosePositionApprovalsFor(user, account1, ESUSDS, WETH); + _setupClosePositionApprovalsFor(user2, account2, ESUSDS, WETH); + _setupClosePositionApprovalsFor(user3, account3, EWETH, SUSDS); + + // Create params for all users + CowEvcClosePositionWrapper.ClosePositionParams memory params1 = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: user, + account: account1, + deadline: block.timestamp + 1 hours, + borrowVault: EWETH, + collateralVault: ESUSDS, + collateralAmount: 2550 ether, + repayAmount: 1.001 ether, + kind: GPv2Order.KIND_BUY + }); + + CowEvcClosePositionWrapper.ClosePositionParams memory params2 = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: user2, + account: account2, + deadline: block.timestamp + 1 hours, + borrowVault: EWETH, + collateralVault: ESUSDS, + collateralAmount: 7600 ether, + repayAmount: 3.003 ether, + kind: GPv2Order.KIND_BUY + }); + + CowEvcClosePositionWrapper.ClosePositionParams memory params3 = CowEvcClosePositionWrapper.ClosePositionParams({ + owner: user3, + account: account3, + deadline: block.timestamp + 1 hours, + borrowVault: ESUSDS, + collateralVault: EWETH, + collateralAmount: 2.1 ether, + repayAmount: 5005 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); + + // Create settlement with all three trades + uint32 validTo = uint32(block.timestamp + 1 hours); + + address[] memory tokens = new address[](4); + tokens[0] = SUSDS; + tokens[1] = WETH; + tokens[2] = ESUSDS; + tokens[3] = EWETH; + + uint256[] memory clearingPrices = new uint256[](4); + clearingPrices[0] = 1 ether; // SUSDS price + clearingPrices[1] = 2500 ether; // WETH price + clearingPrices[2] = 0.99 ether; // eSUSDS price + clearingPrices[3] = 2495 ether; // eWETH price + + ICowSettlement.Trade[] memory trades = new ICowSettlement.Trade[](3); + (trades[0],,) = + setupCowOrder(tokens, 2, 1, params1.collateralAmount, params1.repayAmount, validTo, user, user, true); + (trades[1],,) = + setupCowOrder(tokens, 2, 1, params2.collateralAmount, params2.repayAmount, validTo, user2, user2, true); + (trades[2],,) = + setupCowOrder(tokens, 3, 0, params3.collateralAmount, params3.repayAmount, validTo, user3, user3, true); + + // Setup interactions + ICowSettlement.Interaction[][3] memory interactions; + interactions[0] = new ICowSettlement.Interaction[](0); + interactions[1] = new ICowSettlement.Interaction[](3); + interactions[2] = new ICowSettlement.Interaction[](0); + + // We pull the money out of the euler vaults + interactions[1][0] = getWithdrawInteraction( + ESUSDS, (params1.repayAmount + params2.repayAmount) * clearingPrices[1] / clearingPrices[0] + ); + interactions[1][1] = getWithdrawInteraction(EWETH, params3.repayAmount * clearingPrices[0] / clearingPrices[1]); + + // We swap. We only need to swap the difference of the 3 closes (since coincidence of wants) + // It comes out to 5000 SUSDS needs to become WETH + interactions[1][2] = getSwapInteraction(SUSDS, WETH, 5000 ether); + + // 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(closePositionWrapper), + uint16(wrapper2Data.length), + wrapper2Data, + address(closePositionWrapper), + uint16(wrapper3Data.length), + wrapper3Data + ); + + // Execute wrapped settlement + 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); + + // Verify all positions closed successfully + assertEq(IEVault(EWETH).debtOf(account1), 0, "User1 should have no WETH debt after closing"); + assertEq(IEVault(EWETH).debtOf(account2), 0, "User2 should have no WETH debt after closing"); + assertEq(IEVault(ESUSDS).debtOf(account3), 0, "User3 should have no SUSDS debt after closing"); + } } From 262558153b506c0ccc755d20730732abf5daacd4 Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 10 Nov 2025 02:13:55 +0900 Subject: [PATCH 4/5] refactor `_createLeveragedPosition` --- test/CowEvcClosePositionWrapper.t.sol | 77 +++++---------------------- test/helpers/CowBaseTest.sol | 29 +++++++++- 2 files changed, 41 insertions(+), 65 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/helpers/CowBaseTest.sol b/test/helpers/CowBaseTest.sol index 6ed061c..1d09c06 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"; @@ -259,4 +259,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 60279737ee69db47d433e067e2d45ff2ba1d4d5c Mon Sep 17 00:00:00 2001 From: Kaze Date: Mon, 10 Nov 2025 02:27:58 +0900 Subject: [PATCH 5/5] refactor more duplicated functions, clean out warnings/notes --- test/CowEvcClosePositionWrapper.t.sol | 81 ++++----------------------- test/CowEvcOpenPositionWrapper.t.sol | 4 +- test/helpers/CowBaseTest.sol | 16 +++++- 3 files changed, 28 insertions(+), 73 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/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 1d09c06..9e4a38a 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"; @@ -286,4 +285,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); + } }