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

r.interactions[1][0] = getWithdrawInteraction(sellVaultToken, sellAmount);

// Swap underlying assets
uint256 swapAmount = sellAmount * 0.999 ether / 1 ether;
Copy link

Choose a reason for hiding this comment

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

Medium Priority: Another undocumented magic number

uint256 swapAmount = sellAmount * 0.999 ether / 1 ether;

The 0.999 ether (0.1% reduction) isn't explained. Is this:

  • Simulating a swap fee?
  • Accounting for rounding?
  • Testing slippage protection?

Recommendation: Add a comment or use a named constant explaining what this represents.

}

/// @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.

High Priority: Missing comprehensive test coverage

This E2E test suite only covers happy paths. Critical missing test scenarios:

Security Tests:

  • Unauthorized users attempting to swap another user's collateral
  • Replay attacks with consumed pre-approved hashes
  • Invalid signatures
  • Expired permits (beyond what's in unit tests)

Edge Cases:

  • Insufficient collateral in account
  • Invalid/non-existent vault addresses
  • Zero swap amounts
  • Maximum swap amounts
  • KIND_BUY orders (all current E2E tests use KIND_SELL)

Health Factor Tests:

  • Swaps that would violate collateralization requirements
  • Swaps during unhealthy account states
  • Minimum collateral after swap

Note: The unit test file is excellent and comprehensive. Please apply similar rigor to E2E tests.

Recommendation: Add at least one test for each category above, similar to the security tests at lines 351-392 (which are good but insufficient).

/// @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.

Low Priority: Documentation doesn't match execution order

The NatSpec documentation states:

  1. Enable new collateral vault
  2. Transfer collateral from EVC subaccount to main account (if using subaccount)
  3. Execute settlement to swap collateral

However, the actual execution order in the code is:

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

Recommendation: Either:

  • Update the documentation to reflect actual execution order
  • Add a note clarifying this describes the logical workflow rather than execution order

Example:

/// @dev This wrapper enables atomic collateral swaps through the following logical steps:
///      1. Enable new collateral vault
///      2. Transfer collateral from EVC subaccount to main account (if using subaccount)
///      3. Execute settlement to swap collateral (new collateral deposited directly into user's account)
///      Note: Actual execution order may differ due to EVC batching


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

Critical: Incorrect onBehalfOfAccount parameter

This should be address(this) not address(0) to match the pattern in other wrappers:

  • CowEvcOpenPositionWrapper.sol:206 uses onBehalfOfAccount: address(this)
  • CowEvcClosePositionWrapper.sol:262 uses onBehalfOfAccount: address(this)

Using address(0) may cause issues with EVC context tracking and doesn't maintain the correct operation context.

Suggested change
onBehalfOfAccount: address(this),
onBehalfOfAccount: address(this),

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

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

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

Recommendation: Consider adding a small buffer or rounding up:

transferAmount = (params.swapAmount * toVaultPrice / fromVaultPrice) + 1;

Or document the precision assumptions and decimal requirements clearly.

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.

Medium Priority: Hardcoded price instead of actual prices

This calculation hardcodes:

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

This doesn't use milkSwap.prices(WETH) which could lead to test flakiness if prices don't match reality.

Recommendation:

uint256 wethPrice = milkSwap.prices(WETH);
uint256 susdsPrice = milkSwap.prices(SUSDS);
uint256 approximateSwapOutput = borrowAmount * wethPrice / susdsPrice * 99 / 100;
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.

Medium Priority: Suspicious borrow receiver

The borrowed WETH goes to address(this) (the test contract) instead of account. This doesn't match the stated goal of "setting up a leveraged position" for the user's account.

Recommendation: Either:

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


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.

Low Priority: Undocumented magic number

The * 1 ether / 0.98 ether multiplier (2% slippage) lacks explanation.

Recommendation:

uint256 constant TEST_SLIPPAGE_BPS = 200; // 2% slippage for test settlement
r.clearingPrices[1] = milkSwap.prices(IERC4626(buyVaultToken).asset()) * 10000 / (10000 - TEST_SLIPPAGE_BPS);

Or at minimum add a comment explaining the 2% slippage simulation.

}

/// @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.

High Priority: Missing comprehensive E2E test coverage

This test suite only covers happy paths. Critical missing scenarios:

Security Tests:

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

Edge Cases:

  • ❌ Insufficient collateral in account
  • ❌ Invalid/non-existent vault addresses
  • ❌ Zero swap amounts
  • ❌ Maximum swap amounts
  • ❌ KIND_BUY orders (all current E2E tests use KIND_SELL)

Health Factor Tests:

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

Note: The unit test file (CowEvcCollateralSwapWrapper.unit.t.sol) is excellent and comprehensive. Consider applying similar rigor to E2E tests.

/// @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.

Low Priority: Documentation doesn't match execution order

The documented order states:

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

Actual execution order:

  1. Enable vault (line 196)
  2. Execute settlement (line 256)
  3. Transfer happens during settlement callback (line 333)

Recommendation: Add a note clarifying this describes the logical workflow:

/// @dev This wrapper enables atomic collateral swaps through the following logical steps:
///      1. Enable new collateral vault
///      2. Transfer collateral from EVC subaccount to main account (if using subaccount)
///      3. Execute settlement to swap collateral (new collateral deposited directly into user's account)
///      Note: Actual execution order may differ due to EVC batching

@kaze-cow kaze-cow marked this pull request as ready for review November 3, 2025 09:25
@kaze-cow kaze-cow force-pushed the feat/collateral-swap-wrapper branch from 12c61f8 to 5e8e2ad Compare November 6, 2025 00:47
@kaze-cow kaze-cow changed the base branch from feat/specialized-wrappers to feat/close-position-wrapper November 6, 2025 00:48
@claude
Copy link

claude bot commented Nov 6, 2025

Code Review Summary

I've completed a comprehensive review of the CowEvcCollateralSwapWrapper implementation. Overall, the code follows the established patterns well and the unit tests are excellent. However, I've identified some important issues that should be addressed:

Critical Issues

1. Precision Loss in Price Calculation (Line 329)

The calculation params.swapAmount * toVaultPrice / fromVaultPrice can suffer from precision loss when tokens have different decimals or when prices have magnitude differences. This could cause transactions to fail if the calculated transferAmount is slightly insufficient.

Recommendation: Consider rounding up or adding a small buffer to ensure sufficient tokens are transferred.

High Priority Issues

2. Test Coverage Gaps in E2E Tests

While the unit tests are comprehensive, the E2E test suite only covers happy paths. Missing scenarios include:

  • KIND_BUY orders (all E2E tests use KIND_SELL)
  • Insufficient collateral scenarios
  • Health factor violations
  • Invalid vault addresses
  • Zero/maximum swap amounts

Recommendation: Add E2E tests for edge cases and failure scenarios, similar to the excellent unit test coverage.

3. Hardcoded Test Values (Lines 73, 135, 148)

Tests use hardcoded prices and slippage values instead of deriving from actual test setup:

  • Line 73: borrowAmount * 2500e18 / 0.99e18 hardcodes WETH price
  • Line 135: * 1 ether / 0.98 ether applies 2% slippage without explanation
  • Line 148: * 0.999 ether / 1 ether applies 0.1% without explanation

Recommendation: Use milkSwap.prices() for consistency and add comments explaining the slippage factors.

Medium Priority Issues

4. Suspicious Borrow Pattern (Line 79)

In _setupLeveragedPosition, borrowed WETH goes to address(this) instead of account. This doesn't match the stated goal of setting up a leveraged position for the account.

Recommendation: Either borrow to account or add a comment explaining why the test contract receives the funds.

Documentation

5. NatSpec Execution Order (Line 14)

The documented order doesn't match actual execution flow. The documentation states operations happen in a specific sequence, but the actual execution interleaves settlement and transfers.

Recommendation: Add a note clarifying this describes the logical workflow rather than exact execution order.


Positive Observations

  • Excellent consistency with existing wrapper patterns (Open/Close Position)
  • Comprehensive unit tests with good coverage of edge cases
  • Proper EIP-712 implementation for signature verification
  • Clear code structure that follows established patterns
  • Good use of assembly for gas optimization in hash calculations

The implementation is solid overall. Addressing the precision loss issue and expanding E2E test coverage would significantly strengthen the codebase.

@kaze-cow kaze-cow mentioned this pull request Nov 6, 2025
kaze-cow and others added 12 commits November 8, 2025 05:44
Refactor CowEvcCollateralSwapWrapper test files to reduce code duplication
and improve maintainability:

Integration tests (CowEvcCollateralSwapWrapper.t.sol):
- Add helper functions for common test setup patterns
  - _createDefaultParams(): standardized param creation
  - _createPermitSignature(): permit signature creation
  - _encodeWrapperData(): wrapper data encoding
  - _executeWrappedSettlement(): settlement execution
  - _setupSubaccountApprovals(): comprehensive subaccount setup
- Add DEFAULT_SWAP_AMOUNT and DEFAULT_BUY_AMOUNT constants
- Reduce test function size by ~40% while maintaining clarity

Unit tests (CowEvcCollateralSwapWrapper.unit.t.sol):
- Add helper functions for common patterns
  - _getDefaultParams(): returns default params (tests modify as needed)
  - _getEmptySettleData(): creates empty settlement data
  - _encodeWrapperData(): wrapper data encoding
  - _setupPreApprovedHash(): pre-approved hash setup
- Add DEFAULT_SWAP_AMOUNT constant
- Remove 3 redundant tests with no coverage impact:
  - test_Constructor_SetsName (trivial getter)
  - test_GetApprovalHash_Consistency (deterministic function)
  - test_ParseWrapperData_WithSignature (duplicate code path)
- Reduce from 33 to 30 unit tests with no loss in coverage

All tests pass: 6/6 integration tests, 30/30 unit tests

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

Co-Authored-By: Claude <[email protected]>
also found a bug which caused another test to succeed when it shouldn't (afaict)

will have to finish this tomorrow
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.

2 participants