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

Copy link
Contributor

@fedgiac fedgiac left a comment

Choose a reason for hiding this comment

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

First round of comments. My only major concern is about the function _findRatePrices: the price that it computes can be easily manipulated by solvers and may cause things to break even if the solver isn't malicious.

Comment on lines 54 to 55
/// protection mixed in so that signed orders are only valid for specific
/// this contract.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// protection mixed in so that signed orders are only valid for specific
/// this contract.
/// protection mixed in so that signed orders are only valid for this
/// contract at this address and on this one chain.

Comment on lines 298 to 300
require(msg.sender == address(EVC), Unauthorized(msg.sender));
(address onBehalfOfAccount,) = EVC.getCurrentOnBehalfOfAccount(address(0));
require(onBehalfOfAccount == address(this), Unauthorized(onBehalfOfAccount));
Copy link
Contributor

Choose a reason for hiding this comment

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

There are no further checks, meaning that if the EVC is in any way insecure or has some unexpected code path then someone would be able to execute arbitrary settlements. I don't think we want to risk that, the peace of mind we get for being able to sleep soundly if the EVC gets hacked is worth the extra checks.

We cam do this by hashing all input parameters and validate them here.
In the current code it wouldn't even be that hard: it's already writing abi.encodeCall(this.evcInternalSwap, (settleData, wrapperData, remainingWrapperData) to memory, so it's just about hashing that and comparing that with the hash of message.data as stored in transient storage.
It would need to account for the possibility of being called multiple times or out of order though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

great suggestion! implemented 362e61a

its worth noting that since we are validating the calldata is correct and verifying its only called once (by clearing after the call is complete), we can now remove the onBehalfOfAccount check, which saves us a external contract call! We could hypothetically also remove the msg.sender == address(EVC) check as well but decided to leave it for now since its cheap.

prevents errors and code now explains itself, in theory.
also switch to computed params size

it seems like its going to be too unweildly to do the calculation from a security and complexity perspective. Unfortunately it is another `approve()`
Comment on lines +321 to +326
uint256 balanceAfter = IERC20(params.fromVault).balanceOf(params.owner);

if (balanceAfter > balanceBefore) {
SafeERC20Lib.safeTransferFrom(
IERC20(params.fromVault), params.owner, params.account, balanceAfter - balanceBefore, address(0)
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Using balances is risky if users have any other order or logic changing the balance of their address. Will think a bit more about this tomorrow if there's another solution.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yep that is a possibility, but it seems infitesimably rare and even when it were to happen--the worst case I can think of is maybe in the exact same transaction there is another order to add collateral to the user's account for a loan they have on their main/owner wallet, and then the collateral gets transferred to their subaccount instead of remaining the main account. the user would end up with funds in a place they are not expecting them, and perhaps they get liquidated?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

also yea I think the only way a user could change the balance in their own account is through some CoW means (either they have a separate order or they specify some other wrapper as part of this order)

in either case the user has to do this to themselves somehow

@kaze-cow kaze-cow requested a review from fedgiac December 3, 2025 08:43
bytes32 separator = DOMAIN_SEPARATOR;
uint256 paramsSize = PARAMS_SIZE;
assembly ("memory-safe") {
structHash := keccak256(params, paramsSize)
Copy link
Contributor

@fedgiac fedgiac Dec 3, 2025

Choose a reason for hiding this comment

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

I missed it at first, but this isn't a valid struct hash for EIP712 because the type hash is missing (see here).

Copy link
Contributor

@fedgiac fedgiac Dec 3, 2025

Choose a reason for hiding this comment

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

We should add a test that checks the hash matches the expected EIP712 hash as computed by Foundry. We could also consider using the dedicated OpenZeppelin library.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

in this case having a EIP712 for the hash used by approved hashes isnt strictly required because they wont be signed by a user wallet and will not have any relation to the EVC.permit signing flow. but it is indeed good for standardizability I suppose.

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
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