Skip to content

Conversation

@kaze-cow
Copy link
Collaborator

@kaze-cow kaze-cow commented Oct 31, 2025

Description

Implements the CowEvcCollateralSwapWrapper in order to satisfy a usecase for the Euler integration.

Context

Read up on notion

Considerations

  • Overall this wrapper looks a lot like a condensed version of the CowEvcClosePositionWrapper. It has to enable the destination collateral, however.
  • This flow is supposed to support both KIND_BUY and KIND_SELL from the COW side so that the user can select either an amount exact in or exact out, and swap it just like they would on a regular DEX.

Out of Scope

Every line of code in this PR should be considered in-scope.

Testing Instructions

Follow the test command instructions in the README

We want to make sure:

  • the flow works as expected for the established cases of Euler finance.
  • there are not security opportunities an arbitrary user on the open internet could take advantage of
  • there are not security opportunities that a solver could take advantage that would give access to a significant amount of funds

fixes https://linear.app/cowswap/issue/COW-93/initial-merge-contract-of-cowevccollateralswapwrapper

@kaze-cow kaze-cow self-assigned this Oct 31, 2025
@claude
Copy link

claude bot commented Oct 31, 2025

Claude encountered an error —— View job


Claude encountered an error —— View job


Code Review in Progress

Performing comprehensive code review focusing on:

  • Read repository guidelines and previous feedback
  • Examine current code state and recent changes
  • Review code quality and best practices
  • Analyze security considerations
  • Evaluate performance implications
  • Assess test coverage and quality
  • Check documentation completeness
  • Provide consolidated feedback

if (params.kind == KIND_BUY) {
(uint256 fromVaultPrice, uint256 toVaultPrice) =
_findRatePrices(settleData, params.fromVault, params.toVault);
transferAmount = params.swapAmount * toVaultPrice / fromVaultPrice;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High Priority: Potential precision loss in price calculation

The calculation params.swapAmount * toVaultPrice / fromVaultPrice can suffer from precision loss, especially when:

  1. Tokens have different decimal places
  2. Prices have significant magnitude differences
  3. Division truncates instead of rounding up

Consider:

  • Adding a small buffer/margin to ensure sufficient tokens are transferred
  • Rounding up instead of down for transferAmount
  • Documenting assumptions about token decimals and price precision

This could cause transactions to fail if the calculated transferAmount is slightly less than what's actually needed for the swap.


// 2. Settlement call
items[itemIndex] = IEVC.BatchItem({
onBehalfOfAccount: address(this),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent with other wrappers: Should use onBehalfOfAccount: address(this) for consistency.

Both other wrappers use address(this):

  • CowEvcOpenPositionWrapper.sol:206
  • CowEvcClosePositionWrapper.sol:262

Using address(this) is the correct approach for maintaining proper EVC operation context.

}

/// @notice Test swapping collateral from main account
function test_CollateralSwapWrapper_MainAccount() external {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing comprehensive test coverage

This test suite only covers happy paths. Consider adding tests for:

Security:

  • Unauthorized users attempting to swap another user's collateral
  • Replay attacks with consumed pre-approved hashes

Edge Cases:

  • Expired deadlines
  • Insufficient collateral in account
  • Invalid/non-existent vault addresses
  • Zero swap amounts
  • Maximum swap amounts

Health Factor:

  • Swaps that would violate collateralization requirements
  • Swaps during unhealthy account states

Order Types:

  • KIND_BUY orders (all current tests use KIND_SELL)
  • Mixed order scenarios

The current security tests (lines 351-392) are minimal and don't cover these important scenarios.

evc.enableController(account, EWETH);

// Deposit collateral to the account, and add the approximate amount after swapping the borrowed collateral
IERC4626(ESUSDS).deposit(collateralAmount + borrowAmount * 2500e18 / 0.99e18, account);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test uses hardcoded price instead of actual prices

The calculation borrowAmount * 2500e18 / 0.99e18 hardcodes:

  1. WETH price as exactly 2500 sUSDS
  2. Slippage as 0.99 (1%)

This should use milkSwap.prices(WETH) to match actual test conditions and prevent flakiness if prices change. The hardcoded values may not accurately reflect the vault share amounts needed.


// Borrow assets from the account. And confiscate the borrowed asset (needs to be called with account as onBehalfOf)
vm.startPrank(account);
IBorrowing(EWETH).borrow(borrowAmount, address(this));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suspicious borrow receiver

The borrowed WETH goes to address(this) (the test contract) instead of the account:

IBorrowing(EWETH).borrow(borrowAmount, address(this));

This seems incorrect for "setting up a leveraged position" for the user's account. The borrowed assets should either:

  1. Go to the account
  2. Be transferred to the account after borrowing
  3. Have a comment explaining why the test contract receives them

This doesn't match the expected production flow where users borrow into their own accounts.


r.clearingPrices = new uint256[](2);
r.clearingPrices[0] = milkSwap.prices(IERC4626(sellVaultToken).asset());
r.clearingPrices[1] = milkSwap.prices(IERC4626(buyVaultToken).asset()) * 1 ether / 0.98 ether;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic number without explanation

The * 1 ether / 0.98 ether multiplier applies 2% slippage but lacks explanation.

Consider:

  1. Using a named constant: uint256 constant TEST_SLIPPAGE = 0.98 ether;
  2. Adding a comment explaining why 2% slippage is applied
  3. Testing different slippage values to ensure robustness

Same issue at line 148 with 0.999 ether (0.1%).

/// @title CowEvcCollateralSwapWrapper
/// @notice A specialized wrapper for swapping collateral between vaults with EVC
/// @dev This wrapper enables atomic collateral swaps:
/// 1. Enable new collateral vault
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation doesn't match execution order

The documented order:

  1. Enable new collateral vault
  2. Transfer collateral from subaccount
  3. Execute settlement

Actual execution order:

  1. Enable vault (line 190)
  2. Execute settlement (line 246-251)
  3. Transfer happens during settlement (line 319-321)

Consider either:

  • Updating the documentation to match actual execution order
  • Adding a note that this describes the logical workflow, not execution order

if (params.kind == KIND_BUY) {
(uint256 fromVaultPrice, uint256 toVaultPrice) =
_findRatePrices(settleData, params.fromVault, params.toVault);
transferAmount = params.swapAmount * toVaultPrice / fromVaultPrice;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: Potential precision loss in price calculation

transferAmount = params.swapAmount * toVaultPrice / fromVaultPrice;

This calculation can suffer from precision loss, especially when:

  1. Tokens have different decimal places (e.g., WBTC has 8 decimals, sUSDS has 18)
  2. Prices have significant magnitude differences
  3. Division truncates instead of rounding up

Risk: The calculated transferAmount could be slightly less than what's actually needed for the swap to succeed, causing the transaction to revert.

Recommendation: Consider either:

  • Adding a small buffer (e.g., transferAmount = (params.swapAmount * toVaultPrice / fromVaultPrice) + 1)
  • Rounding up instead of down
  • Documenting the precision assumptions and decimal requirements

evc.enableController(account, EWETH);

// Deposit collateral to the account, and add the approximate amount after swapping the borrowed collateral
IERC4626(ESUSDS).deposit(collateralAmount + borrowAmount * 2500e18 / 0.99e18, account);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High Priority: Hardcoded price instead of actual prices

IERC4626(ESUSDS).deposit(collateralAmount + borrowAmount * 2500e18 / 0.99e18, account);

This hardcodes:

  1. WETH price as exactly 2500 sUSDS
  2. Slippage as 0.99 (1%)

Issue: This doesn't use milkSwap.prices(WETH) to match actual test conditions, which could lead to test flakiness if prices change or calculations don't match reality.

Recommendation:

uint256 wethPrice = milkSwap.prices(WETH);
uint256 susdsPrice = milkSwap.prices(SUSDS);
uint256 approximateSwapOutput = borrowAmount * wethPrice / susdsPrice * 99 / 100; // 1% slippage
IERC4626(ESUSDS).deposit(collateralAmount + approximateSwapOutput, account);


// Borrow assets from the account. And confiscate the borrowed asset (needs to be called with account as onBehalfOf)
vm.startPrank(account);
IBorrowing(EWETH).borrow(borrowAmount, address(this));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High Priority: Suspicious borrow receiver

IBorrowing(EWETH).borrow(borrowAmount, address(this));

The borrowed WETH goes to address(this) (the test contract) instead of account.

Issue: This doesn't match the stated goal of "setting up a leveraged position" for the user's account. In a real scenario, users would borrow into their own accounts.

Recommendation: Either:

  1. Borrow to account if that's the intended behavior
  2. Transfer the borrowed assets to account after borrowing
  3. Add a comment explaining why the test contract receives the borrowed assets

@kaze-cow kaze-cow requested a review from fedgiac December 3, 2025 08:43
fedgiac and others added 5 commits December 4, 2025 12:42
## Description

A proposal to simplify the current contracts by moving repeating code
across different wrappers to the same contract.

This is achieved by creating a base abstract contract
`CowEvcBaseWrapper` which is supposed to be inherited by all Euler
wrappers.
It aggregates code related to three main features:
- ERC712 hashing (in the form of constants, immutables, constructors and
hashing)
- EVC batch execution.
- Errors.

The purpose is making it easier to spot what is different and what is
the same between wrappers when reading the code.
Ideally, the remaining single-instance wrappers should have little
remaining occurrences of remaining code. (One exception is
`_evcInternalSettle`. I'm sure it can be simplified but I don't
understand well enough the balance checks to make sure things are
working after my changes, so I'm skipping it.)

The result is kind of nice imho: it makes clear(er) that what matters in
the various instances is not how the batch is executed, but rather
what's the content of the batches (i.e., the implementation of
`_encodeSignedBatchItems`). It also makes it more visible that the
settlement is executed before other operations when closing a position
and after in the other two wrappers. I considered using a
"itemsBefore/itemsAfter" model instead but I decided we can just switch
to it if we ever find the need and just went for a flag.

The bad part of this PR is that Solidity doesn't have generics. This
means that all external accessors like `getSignedCalldata` still need to
be repeated on each individual wrapper instance, causing annoying code
repetition. Because of that, I also had to play with raw memory
pointers, which may be dangerous to do if we abuse assembly to generate
structures in memory.

Note that I tried to make the minimal changes needed to the existing
code base to show the difference, so some design choices I made may be
questionable. I don't expect this PR to be merged, if anything because
it would make a mess in the current PR stack.

## Out of Scope

There should be no changes in the implementation, only in the code
structure.

## Testing Instructions

To be fair, I didn't go in depth when building the code, I'm trusting CI
to tell me if something is off.

You may also want to compare the gas cost between the two runs. Here is
the diff between PR base e8e9e6c and
0c98278.

<details><summary>forge snapshot --diff</summary>

```
Ran 10 test suites in 336.42ms (1.07s CPU time): 124 tests passed, 0 failed, 0 skipped (124 total tests)
━ CowWrapperTest::test_integration_ThreeWrappersChained() (gas: 99125 → 99125 | 0 0.000%)
━ CowWrapperTest::test_next_CallsWrapperAndThenNextSettlement() (gas: 39431 → 39431 | 0 0.000%)
━ CowWrapperTest::test_wrappedSettle_RevertsOnInvalidSettleSelector() (gas: 18822 → 18822 | 0 0.000%)
━ CowWrapperTest::test_wrappedSettle_RevertsWithNotASolver() (gas: 20222 → 20222 | 0 0.000%)
━ CowWrapperHelpersTest::test_immutableAuthenticators() (gas: 10869 → 10869 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_EmptyArrays() (gas: 10605 → 10605 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_EmptyWrapperData() (gas: 40352 → 40352 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_MixedWrapperDataSizes() (gas: 64717 → 64717 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_MultipleWrappers() (gas: 64380 → 64380 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_RevertsOnNotAWrapper() (gas: 22855 → 22855 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_RevertsOnNotAWrapper_SecondWrapper() (gas: 29118 → 29118 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_RevertsOnSettlementContractShouldNotBeSolver() (gas: 60187 → 60187 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_RevertsOnSettlementMismatch() (gas: 667039 → 667039 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_RevertsOnWrapperDataMalformed() (gas: 24869 → 24869 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_RevertsOnWrapperDataNotFullyConsumed() (gas: 29565 → 29565 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_RevertsOnWrapperDataTooLong_FirstWrapper() (gas: 565333 → 565333 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_RevertsOnWrapperDataTooLong_SecondWrapper() (gas: 580581 → 580581 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_SingleWrapper() (gas: 34243 → 34243 | 0 0.000%)
━ CowWrapperHelpersTest::test_verifyAndBuildWrapperData_SucceedsWithMaxLengthData() (gas: 680581 → 680581 | 0 0.000%)
━ PreApprovedHashesUnitTest::testFuzz_ConsumePreApprovedHash(address,bytes32) (gas: 38101 → 38101 | 0 0.000%)
━ PreApprovedHashesUnitTest::testFuzz_MultipleUsersAndHashes(address,address,bytes32,bytes32) (gas: 69188 → 69188 | 0 0.000%)
━ PreApprovedHashesUnitTest::testFuzz_SetPreApprovedHash(address,bytes32) (gas: 27329 → 27329 | 0 0.000%)
━ PreApprovedHashesUnitTest::test_ConsumePreApprovedHash_CannotConsumedTwice() (gas: 37254 → 37254 | 0 0.000%)
━ PreApprovedHashesUnitTest::test_ConsumePreApprovedHash_EmitsEvent() (gas: 37959 → 37959 | 0 0.000%)
━ PreApprovedHashesUnitTest::test_PreApprovedHashesStorage() (gas: 51111 → 51111 | 0 0.000%)
━ PreApprovedHashesUnitTest::test_SetPreApprovedHash_CannotApproveConsumed() (gas: 38401 → 38401 | 0 0.000%)
━ PreApprovedHashesUnitTest::test_SetPreApprovedHash_CannotRevokeConsumed() (gas: 38424 → 38424 | 0 0.000%)
━ PreApprovedHashesUnitTest::test_SetPreApprovedHash_EmitsEvent() (gas: 28524 → 28524 | 0 0.000%)
━ PreApprovedHashesUnitTest::test_SetPreApprovedHash_RevokeAndReapprove() (gas: 46036 → 46036 | 0 0.000%)
━ TestablePreApprovedHashes::testConsumeHash(address,bytes32) (gas: 2611 → 2611 | 0 0.000%)
↓ CowEvcCollateralSwapWrapperUnitTest::test_Constructor_SetsImmutables() (gas: 15749 → 15748 | -1 -0.006%)
↓ CowEvcCollateralSwapWrapperUnitTest::test_ParseWrapperData_EmptySignature() (gas: 13669 → 13668 | -1 -0.007%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_DifferentVaults() (gas: 1235312 → 1235840 | 528 0.043%)
↑ CowEvcClosePositionWrapperUnitTest::test_HelperRepay_RequiresCorrectOnBehalfOfAccount() (gas: 122723 → 122797 | 74 0.060%)
↑ CowEvcClosePositionWrapperTest::test_ClosePositionWrapper_UnauthorizedInternalSettle() (gas: 13376 → 13386 | 10 0.075%)
↑ CowEvcClosePositionWrapperUnitTest::test_EvcInternalSettle_SubaccountMustBeControlledByOwner() (gas: 108564 → 108665 | 101 0.093%)
↑ CowEvcClosePositionWrapperUnitTest::test_EvcInternalSettle_OnlyEVC() (gas: 10313 → 10323 | 10 0.097%)
↑ CowEvcClosePositionWrapperUnitTest::test_Constructor_SetsDomainSeparator() (gas: 6495 → 6502 | 7 0.108%)
↓ CowEvcCollateralSwapWrapperUnitTest::test_Constructor_SetsDomainSeparator() (gas: 6487 → 6480 | -7 -0.108%)
↑ CowEvcOpenPositionWrapperUnitTest::test_Constructor_SetsDomainSeparator() (gas: 6297 → 6304 | 7 0.111%)
↑ CowEvcCollateralSwapWrapperTest::test_CollateralSwapWrapper_WithLeveragedPosition() (gas: 1257541 → 1259196 | 1655 0.132%)
↓ CowEvcCollateralSwapWrapperUnitTest::test_ParseWrapperData_LongSignature() (gas: 13681 → 13659 | -22 -0.161%)
↑ CowEvcClosePositionWrapperUnitTest::test_HelperRepay_SuccessfulRepay() (gas: 180219 → 180533 | 314 0.174%)
↑ CowEvcClosePositionWrapperUnitTest::test_HelperRepay_WithDust() (gas: 179908 → 180222 | 314 0.175%)
↑ CowEvcClosePositionWrapperUnitTest::test_HelperRepay_RepayAll() (gas: 179749 → 180064 | 315 0.175%)
↑ CowEvcClosePositionWrapperUnitTest::test_HelperRepay_PartialRepayWhenInsufficientBalance() (gas: 179708 → 180023 | 315 0.175%)
↑ CowEvcClosePositionWrapperUnitTest::test_EvcInternalSettle_RequiresCorrectCalldata() (gas: 40302 → 40378 | 76 0.189%)
↑ CowEvcClosePositionWrapperUnitTest::test_Constructor_SetsImmutables() (gas: 15806 → 15836 | 30 0.190%)
↑ CowEvcOpenPositionWrapperUnitTest::test_Constructor_SetsImmutables() (gas: 15556 → 15586 | 30 0.193%)
↑ CowEvcCollateralSwapWrapperTest::test_CollateralSwapWrapper_Subaccount() (gas: 832224 → 833879 | 1655 0.199%)
↑ CowEvcClosePositionWrapperUnitTest::test_HelperRepay_OnlyEVC() (gas: 11850 → 11876 | 26 0.219%)
↑ CowEvcCollateralSwapWrapperTest::test_CollateralSwapWrapper_MainAccount() (gas: 754466 → 756251 | 1785 0.237%)
↑ CowEvcCollateralSwapWrapperTest::test_CollateralSwapWrapper_NonSolverCannotSettle() (gas: 23877 → 23935 | 58 0.243%)
↑ CowEvcClosePositionWrapperTest::test_ClosePositionWrapper_PartialRepay() (gas: 1160452 → 1163316 | 2864 0.247%)
↑ CowEvcCollateralSwapWrapperTest::test_CollateralSwapWrapper_ThreeUsers_TwoSameOneOpposite() (gas: 3227984 → 3236108 | 8124 0.252%)
↑ CowEvcClosePositionWrapperTest::test_ClosePositionWrapper_WithPreApprovedHash() (gas: 1121302 → 1124410 | 3108 0.277%)
↑ CowEvcClosePositionWrapperTest::test_ClosePositionWrapper_SuccessFullRepay() (gas: 1150134 → 1153699 | 3565 0.310%)
↑ CowEvcOpenPositionWrapperUnitTest::test_GetSignedCalldata_EnableControllerItem() (gas: 346019 → 347147 | 1128 0.326%)
↑ CowEvcOpenPositionWrapperUnitTest::test_GetSignedCalldata_EnableCollateralItem() (gas: 345765 → 346893 | 1128 0.326%)
↑ CowEvcOpenPositionWrapperUnitTest::test_GetSignedCalldata_BorrowItem() (gas: 344090 → 345218 | 1128 0.328%)
↑ CowEvcOpenPositionWrapperUnitTest::test_GetSignedCalldata_DepositItem() (gas: 343713 → 344841 | 1128 0.328%)
↑ CowEvcOpenPositionWrapperUnitTest::test_MaxBorrowAmount() (gas: 343645 → 344773 | 1128 0.328%)
↑ CowEvcOpenPositionWrapperUnitTest::test_ZeroCollateralAmount() (gas: 343183 → 344311 | 1128 0.329%)
↓ CowEvcCollateralSwapWrapperTest::test_CollateralSwapWrapper_UnauthorizedInternalSwap() (gas: 13642 → 13597 | -45 -0.330%)
↑ CowEvcOpenPositionWrapperUnitTest::test_SameOwnerAndAccount() (gas: 339821 → 340949 | 1128 0.332%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_ParseWrapperData_WithExtraData() (gas: 18082 → 18148 | 66 0.365%)
↑ CowEvcClosePositionWrapperTest::test_ClosePositionWrapper_ThreeUsers_TwoSameOneOpposite() (gas: 2924476 → 2935336 | 10860 0.371%)
↑ CowEvcOpenPositionWrapperTest::test_OpenPositionWrapper_WithPreApprovedHash() (gas: 808296 → 811528 | 3232 0.400%)
↑ CowEvcClosePositionWrapperTest::test_ClosePositionWrapper_NonSolverCannotSettle() (gas: 23858 → 23956 | 98 0.411%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_GetSignedCalldata_EnablesNewCollateral() (gas: 115346 → 115830 | 484 0.420%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_GetSignedCalldata_UsesCorrectAccount() (gas: 109357 → 109841 | 484 0.443%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_SwapAmount_Max() (gas: 109040 → 109524 | 484 0.444%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_SwapAmount_Zero() (gas: 108444 → 108928 | 484 0.446%)
↑ CowEvcClosePositionWrapperUnitTest::test_GetSignedCalldata_RepayItem() (gas: 147780 → 148444 | 664 0.449%)
↑ CowEvcClosePositionWrapperUnitTest::test_GetSignedCalldata_PartialRepay() (gas: 143447 → 144111 | 664 0.463%)
↑ CowEvcClosePositionWrapperUnitTest::test_MaxRepayAmount() (gas: 143445 → 144109 | 664 0.463%)
↑ CowEvcClosePositionWrapperUnitTest::test_SameOwnerAndAccount() (gas: 143044 → 143708 | 664 0.464%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_WrappedSettle_OnlySolver() (gas: 16349 → 16429 | 80 0.489%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_GetApprovalHash_MatchesEIP712() (gas: 13509 → 13577 | 68 0.503%)
↑ CowEvcOpenPositionWrapperTest::test_OpenPositionWrapper_Success() (gas: 831084 → 835269 | 4185 0.504%)
↑ CowEvcClosePositionWrapperUnitTest::test_ZeroDebt() (gas: 123403 → 124067 | 664 0.538%)
↑ CowEvcOpenPositionWrapperTest::test_OpenPositionWrapper_NonSolverCannotSettle() (gas: 23338 → 23474 | 136 0.583%)
↑ CowEvcClosePositionWrapperUnitTest::test_WrappedSettle_OnlySolver() (gas: 16330 → 16428 | 98 0.600%)
↑ CowEvcOpenPositionWrapperTest::test_OpenPositionWrapper_ThreeUsers_TwoSameOneOpposite() (gas: 2120651 → 2133415 | 12764 0.602%)
↑ CowEvcClosePositionWrapperUnitTest::test_ParseWrapperData_WithExtraData() (gas: 18188 → 18298 | 110 0.605%)
↑ CowEvcClosePositionWrapperUnitTest::test_WrappedSettle_WithPreApprovedHash() (gas: 370724 → 373062 | 2338 0.631%)
↑ CowEvcClosePositionWrapperUnitTest::test_EvcInternalSettle_WithSubaccountTransfer() (gas: 145792 → 146712 | 920 0.631%)
↑ CowEvcOpenPositionWrapperTest::test_OpenPositionWrapper_SetPreApprovedHash() (gas: 39028 → 39277 | 249 0.638%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_WrappedSettle_WithPreApprovedHash() (gas: 182305 → 183499 | 1194 0.655%)
↑ CowEvcOpenPositionWrapperUnitTest::test_ParseWrapperData_WithExtraData() (gas: 13672 → 13770 | 98 0.717%)
↑ CowEvcClosePositionWrapperUnitTest::test_EvcInternalSettle_CanBeCalledByEVC() (gas: 40396 → 40701 | 305 0.755%)
↑ CowEvcCollateralSwapWrapperTest::test_CollateralSwapWrapper_ParseWrapperData() (gas: 12157 → 12249 | 92 0.757%)
↑ CowEvcClosePositionWrapperUnitTest::test_WrappedSettle_WithPermitSignature() (gas: 244682 → 246540 | 1858 0.759%)
↑ CowEvcClosePositionWrapperUnitTest::test_ParseWrapperData_EmptySignature() (gas: 13772 → 13882 | 110 0.799%)
↑ CowEvcClosePositionWrapperTest::test_ClosePositionWrapper_SetPreApprovedHash() (gas: 39086 → 39416 | 330 0.844%)
↑ CowEvcOpenPositionWrapperUnitTest::test_WrappedSettle_OnlySolver() (gas: 16095 → 16231 | 136 0.845%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_WrappedSettle_WithPermitSignature() (gas: 130701 → 131839 | 1138 0.871%)
↑ CowEvcClosePositionWrapperUnitTest::test_WrappedSettle_PreApprovedHashRevertsIfDeadlineExceeded() (gas: 137808 → 139020 | 1212 0.879%)
↑ CowEvcOpenPositionWrapperUnitTest::test_ParseWrapperData_EmptySignature() (gas: 9281 → 9379 | 98 1.056%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_WrappedSettle_BuildsCorrectBatchWithPreApproved() (gas: 108444 → 109693 | 1249 1.152%)
↑ CowEvcOpenPositionWrapperUnitTest::test_GetApprovalHash_MatchesEIP712() (gas: 8805 → 8913 | 108 1.227%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_GetApprovalHash_DifferentForDifferentParams() (gas: 17733 → 17980 | 247 1.393%)
↑ CowEvcOpenPositionWrapperTest::test_OpenPositionWrapper_ParseWrapperData() (gas: 12271 → 12462 | 191 1.557%)
↑ CowEvcClosePositionWrapperUnitTest::test_GetApprovalHash_MatchesEIP712() (gas: 13434 → 13653 | 219 1.630%)
↑ CowEvcClosePositionWrapperTest::test_ClosePositionWrapper_ParseWrapperData() (gas: 12446 → 12649 | 203 1.631%)
↑ CowEvcOpenPositionWrapperTest::test_OpenPositionWrapper_UnauthorizedInternalSettle() (gas: 13306 → 13563 | 257 1.931%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_WrappedSettle_BuildsCorrectBatchWithPermit() (gas: 58149 → 59309 | 1160 1.995%)
↑ CowEvcOpenPositionWrapperUnitTest::test_EvcInternalSettle_RequiresCorrectCalldata() (gas: 33537 → 34244 | 707 2.108%)
↑ CowEvcOpenPositionWrapperUnitTest::test_WrappedSettle_WithPreApprovedHash() (gas: 141235 → 144363 | 3128 2.215%)
↑ CowEvcCollateralSwapWrapperUnitTest::test_WrappedSettle_PreApprovedHashRevertsIfDeadlineExceeded() (gas: 86960 → 89013 | 2053 2.361%)
↑ CowEvcOpenPositionWrapperUnitTest::test_WrappedSettle_PreApprovedHashRevertsIfDeadlineExceeded() (gas: 108507 → 111228 | 2721 2.508%)
↑ CowEvcOpenPositionWrapperUnitTest::test_GetApprovalHash_DifferentForDifferentParams() (gas: 12059 → 12362 | 303 2.513%)
↑ CowEvcOpenPositionWrapperUnitTest::test_EvcInternalSettle_OnlyEVC() (gas: 9736 → 9993 | 257 2.640%)
↑ CowEvcOpenPositionWrapperUnitTest::test_EvcInternalSettle_CanBeCalledByEVC() (gas: 26013 → 26711 | 698 2.683%)
↑ CowEvcClosePositionWrapperUnitTest::test_GetApprovalHash_DifferentForDifferentParams() (gas: 17412 → 18048 | 636 3.653%)
↑ CowEvcOpenPositionWrapperUnitTest::test_WrappedSettle_WithPermitSignature() (gas: 55570 → 58475 | 2905 5.228%)

New tests:
  + CowEvcCollateralSwapWrapperUnitTest::test_EvcInternalSettle_CanBeCalledByEVC()
  + CowEvcCollateralSwapWrapperUnitTest::test_EvcInternalSettle_OnlyEVC()
  + CowEvcCollateralSwapWrapperUnitTest::test_EvcInternalSettle_RequiresCorrectCalldata()
  + CowEvcCollateralSwapWrapperUnitTest::test_EvcInternalSettle_SameOwnerAndAccount()
  + CowEvcCollateralSwapWrapperUnitTest::test_EvcInternalSettle_SubaccountMustBeControlledByOwner()
  + CowEvcCollateralSwapWrapperUnitTest::test_EvcInternalSettle_WithRemainingWrapperData()
  + CowEvcCollateralSwapWrapperUnitTest::test_EvcInternalSettle_WithSubaccount_KindBuy()
  + CowEvcCollateralSwapWrapperUnitTest::test_EvcInternalSettle_WithSubaccount_KindSell()

--------------------------------------------------------------------------------
Total tests: 116, ↑ 81, ↓ 5, ━ 30
Overall gas change: 96864 (0.348%)
```

</details>

The gas impact is visible (especially for opening a position, since it
adds a function parameter to an external function) but overall limited
(max ~3k gas), which I consider a reasonable tradeoff.
- Add validateWrapperData implementation to CowEvcCollateralSwapWrapper
- Remove deprecated parseWrapperData function
- Update tests to use validateWrapperData
- All tests passing
@kaze-cow kaze-cow requested a review from fedgiac December 4, 2025 09:46
kaze-cow and others added 2 commits December 11, 2025 20:26
Replicate changes from the open position wrapper's upstream PR to the
collateral swap wrapper for consistency:

- Add COLLATERAL_SWAP_PARAMS_TYPE_HASH constant for EIP-712 type hashing
- Update constructor to set MAX_BATCH_OPERATIONS in body instead of parameter
- Change _parseCollateralSwapParams to pure and remove remainingWrapperData
- Update getApprovalHash and _wrap to pass type hash parameter
- Update validateWrapperData from view to pure
- Update getSignedCalldata to append approval hash
- Rename needsPermit parameter to needsPermission
- Fix GPv2Wrapper comment references to CowWrapper
- Add test for invalid signature rejection
- Remove test_GetApprovalHash_MatchesEIP712 unit test
- Add tests for hash not pre-approved and tampered signature scenarios

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
IERC20(WBTC).approve(vaultRelayer, type(uint256).max);
}

struct SettlementData {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets consolidate the settlement data structure into 1 that is shared by all 3 wrappers. Currently its kind of split between them which is bad. related to #14 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants