-
Notifications
You must be signed in to change notification settings - Fork 0
feat: collateral swap wrapper #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/close-position-wrapper
Are you sure you want to change the base?
feat: collateral swap wrapper #13
Conversation
|
Claude encountered an error —— View job Claude encountered an error —— View job Code Review in Progress
|
| if (params.kind == KIND_BUY) { | ||
| (uint256 fromVaultPrice, uint256 toVaultPrice) = | ||
| _findRatePrices(settleData, params.fromVault, params.toVault); | ||
| transferAmount = params.swapAmount * toVaultPrice / fromVaultPrice; |
There was a problem hiding this comment.
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:
- Tokens have different decimal places
- Prices have significant magnitude differences
- 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), |
There was a problem hiding this comment.
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:206CowEvcClosePositionWrapper.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 { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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:
- WETH price as exactly 2500 sUSDS
- 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)); |
There was a problem hiding this comment.
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:
- Go to the
account - Be transferred to the
accountafter borrowing - 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; |
There was a problem hiding this comment.
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:
- Using a named constant:
uint256 constant TEST_SLIPPAGE = 0.98 ether; - Adding a comment explaining why 2% slippage is applied
- 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 |
There was a problem hiding this comment.
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:
- Enable new collateral vault
- Transfer collateral from subaccount
- Execute settlement
Actual execution order:
- Enable vault (line 190)
- Execute settlement (line 246-251)
- 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; |
There was a problem hiding this comment.
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:
- Tokens have different decimal places (e.g., WBTC has 8 decimals, sUSDS has 18)
- Prices have significant magnitude differences
- 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); |
There was a problem hiding this comment.
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:
- WETH price as exactly 2500 sUSDS
- 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)); |
There was a problem hiding this comment.
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:
- Borrow to
accountif that's the intended behavior - Transfer the borrowed assets to
accountafter borrowing - 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; |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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:
- Enable new collateral vault
- Transfer collateral from EVC subaccount to main account (if using subaccount)
- Execute settlement to swap collateral
However, the actual execution order in the code is:
- Enable vault (line 190)
- Execute settlement (line 246-251)
- 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), |
There was a problem hiding this comment.
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:206usesonBehalfOfAccount: address(this)CowEvcClosePositionWrapper.sol:262usesonBehalfOfAccount: address(this)
Using address(0) may cause issues with EVC context tracking and doesn't maintain the correct operation context.
| 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; |
There was a problem hiding this comment.
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:
- Tokens have different decimal places (e.g., WBTC=8, sUSDS=18)
- Prices have significant magnitude differences
- 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); |
There was a problem hiding this comment.
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:
- WETH price as exactly 2500 sUSDS
- 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)); |
There was a problem hiding this comment.
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:
- Borrow to
accountif that's the intended behavior - Transfer borrowed assets to
accountafter borrowing - 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; |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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:
- Enable new collateral vault
- Transfer collateral from subaccount
- Execute settlement
Actual execution order:
- Enable vault (line 196)
- Execute settlement (line 256)
- 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 batching12c61f8 to
5e8e2ad
Compare
Code Review SummaryI'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 Issues1. Precision Loss in Price Calculation (Line 329) The calculation Recommendation: Consider rounding up or adding a small buffer to ensure sufficient tokens are transferred. High Priority Issues2. Test Coverage Gaps in E2E Tests While the unit tests are comprehensive, the E2E test suite only covers happy paths. Missing scenarios include:
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:
Recommendation: Use milkSwap.prices() for consistency and add comments explaining the slippage factors. Medium Priority Issues4. 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. Documentation5. 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
The implementation is solid overall. Addressing the precision loss issue and expanding E2E test coverage would significantly strengthen the codebase. |
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

Description
Implements the
CowEvcCollateralSwapWrapperin order to satisfy a usecase for the Euler integration.Context
Read up on notion
Considerations
CowEvcClosePositionWrapper. It has to enable the destination collateral, however.KIND_BUYandKIND_SELLfrom 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:
fixes https://linear.app/cowswap/issue/COW-93/initial-merge-contract-of-cowevccollateralswapwrapper