Skip to content
Draft
Show file tree
Hide file tree
Changes from 68 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
ee6eb57
feat: pull in from PoC and minify and adjust for easy integratoin
Sep 11, 2025
0172171
remove unnecessary console logs
Sep 11, 2025
3e72e99
fix: some security lockdowns
Sep 11, 2025
453c007
it builds
Sep 11, 2025
0f635c7
fix: new strategy
Sep 11, 2025
5ebd847
compiles
kaze-cow Sep 29, 2025
b168d45
fix tests
kaze-cow Sep 29, 2025
e56eac3
forge fmt
kaze-cow Sep 29, 2025
2e7f051
enable optimizer
kaze-cow Sep 29, 2025
de12e99
setup secrets and fix build failure
kaze-cow Sep 29, 2025
c3dc655
remove sizes for now
kaze-cow Sep 29, 2025
217af04
Revert "forge fmt"
kaze-cow Sep 29, 2025
4f6c81e
use a settle state instead of original sender
kaze-cow Sep 29, 2025
2c9704a
feat: pull in required vendor dependencies
kaze-cow Sep 30, 2025
8982db0
feat: pull in from PoC and minify and adjust for easy integratoin
kaze-cow Sep 30, 2025
e9afd40
remove unnecessary console logs
Sep 11, 2025
35cf23b
fix: some security lockdowns
Sep 11, 2025
938b8e9
remove cannon things
kaze-cow Sep 29, 2025
5ca3ae5
remove unnecessary file
kaze-cow Sep 29, 2025
ec9d67d
update all pragmas
kaze-cow Sep 29, 2025
9b7a904
undo undos
kaze-cow Sep 30, 2025
164efff
fix license
kaze-cow Sep 30, 2025
2d7989b
Merge pull request #3 from cowprotocol/feat/new-wrapper
kaze-cow Sep 30, 2025
413d162
Merge branch 'feat/initial' into feat/security
kaze-cow Sep 30, 2025
3ff8dc0
Merge pull request #2 from cowprotocol/feat/security
kaze-cow Sep 30, 2025
ca207e7
cleanup SwapVerifier
kaze-cow Oct 1, 2025
b2a9cae
begin adding test for closing position theoretical impl
kaze-cow Oct 2, 2025
d65ca1a
add close loan test/poc concept
kaze-cow Oct 2, 2025
dad08de
significant improvement--no more excess on swapping out
kaze-cow Oct 2, 2025
093c0f1
general flow works in e2e
kaze-cow Oct 13, 2025
39f24df
update base test file
kaze-cow Oct 13, 2025
30deecf
impl subaccounts handling
kaze-cow Oct 14, 2025
c138c72
rename to more standard euler format
kaze-cow Oct 15, 2025
47ad1cb
remove file
kaze-cow Oct 17, 2025
7f9d8df
pre approved hashes (needed for single 7702 submission
kaze-cow Oct 17, 2025
9de833a
Merge branch 'feat/wrapper-helpers' into feat/specialized-wrappers
kaze-cow Oct 17, 2025
9842d70
minor cleanups and organization
kaze-cow Oct 20, 2025
3a1a2d1
fix tests and ensure approval hashes case works
kaze-cow Oct 20, 2025
bdaa880
more tests working
kaze-cow Oct 20, 2025
49f11bb
all tests passing
kaze-cow Oct 20, 2025
08dc6b3
update the tests to make it more clear what is going
kaze-cow Oct 21, 2025
6f63f1b
Merge remote-tracking branch 'origin/feat/wrapper-helpers' into feat/…
kaze-cow Oct 23, 2025
6de1cc7
clean up warnings
kaze-cow Oct 23, 2025
6a0a1c4
fixes from ai feedback
kaze-cow Oct 23, 2025
eb3654a
fixes from ai feedback
kaze-cow Oct 23, 2025
c579571
remove unnecessary depth and settleCall
kaze-cow Oct 23, 2025
5782561
update claude md to try and clean up the review text
kaze-cow Oct 24, 2025
74514cc
update documentation again to try and clean up claude suggestions
kaze-cow Oct 24, 2025
2abfed2
claude!
kaze-cow Oct 24, 2025
c4350c5
chore: apply formatting
kaze-cow Oct 27, 2025
8f6e579
Merge branch 'feat/wrapper-helpers' into feat/specialized-wrappers
kaze-cow Oct 27, 2025
6cad6ee
code cleanup, preapproved hash gas optimization
kaze-cow Oct 28, 2025
33e992c
initial tests (mostly AI generated)
kaze-cow Oct 28, 2025
46f6263
more misc fixes; update claude.md to try and prevent false security r…
kaze-cow Oct 28, 2025
b0f7d8f
Merge branch 'feat/wrapper-helpers' into feat/specialized-wrappers
kaze-cow Oct 28, 2025
bbe733c
fix forge fmt
kaze-cow Oct 28, 2025
76cdeb0
Merge branch 'feat/specialized-wrappers' of https://github.com/cowpro…
kaze-cow Oct 28, 2025
ee83ec1
patch up more potential security holes
kaze-cow Oct 28, 2025
7bd929e
forge fmt
kaze-cow Oct 28, 2025
1c9bc2e
changes to support exactIn/exactOut order types
kaze-cow Oct 29, 2025
b363e7a
fix compiler failures
kaze-cow Oct 30, 2025
49fbed9
fix tests
kaze-cow Oct 30, 2025
780556f
pull in downstream changes, fix compiler warnings
kaze-cow Nov 3, 2025
8cada64
Merge branch 'feat/wrapper-helpers' into feat/specialized-wrappers
kaze-cow Nov 5, 2025
f9a5071
forge fmt
kaze-cow Nov 5, 2025
5d9e085
Merge branch 'feat/wrapper-helpers' into feat/specialized-wrappers
kaze-cow Nov 5, 2025
156a830
remove close position wrapper from these tests
kaze-cow Nov 6, 2025
7780c3f
remove unnecessary tests
kaze-cow Nov 6, 2025
a4cdeae
Merge branch 'feat/wrapper-helpers' into feat/open-position-wrapper
kaze-cow Nov 6, 2025
f91e31f
Merge branch 'chore/remove-stuff' into feat/open-position-wrapper
kaze-cow Nov 6, 2025
a1a3ee1
refactor: improve test suite DRYness and remove redundant tests
kaze-cow Nov 7, 2025
c8989f8
Merge branch 'feat/open-position-wrapper' of https://github.com/cowpr…
kaze-cow Nov 7, 2025
96cda14
add test for verifying CoW order
kaze-cow Nov 7, 2025
d70bf35
fix buffers and increase buffer sizes, fix test bug
kaze-cow Nov 8, 2025
1831334
refactor `_createLeveragedPosition`
kaze-cow Nov 9, 2025
86a9671
refactor more duplicated functions, clean out warnings/notes
kaze-cow Nov 9, 2025
04e8c5f
refactor: use CowBaseTest helper functions in OpenPositionWrapper
kaze-cow Nov 9, 2025
3892978
remove unneeded function
kaze-cow Nov 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ This repository contains **Euler-CoW Protocol integration contracts** that enabl

### Build
```bash
forge build
forge build --deny notes
```

### Test
Expand Down Expand Up @@ -97,6 +97,20 @@ forge snapshot

## Important Implementation Details

### Security Considerations

- It is generally assumed that the `solvers` (aka, an address for which `CowAuthentication.isSolver()` returns true) is a trusted actor within the system. Only in the case that a solver could steal an entire user's deposit or funds, or steal funds beyond what the user specified as their minimum out/minimum buy amount, assume there is incentive for a solver to provide the best rate/user outcome possible. To be clear, a solver cannot steal funds simply by setting arbitrary `clearingPrices` (as documented a bit later).
- For a solver to be able to steal an entire user's deposit or funds, they must be able to withdraw the users token to an address of their choosing or otherwise in their control (therefore, a "nuisance" transfer between two wallets that the user effectively owns does not count).
- If a user takes on debt, that debt position must be sufficiently collateralized above a set collateralization ratio higher than liquidation ratio before the EVC batch transaction concludes. If it is not, the transaction reverts and nothing can happen. Therefore, there is no risk of undercollateralization to the system due to a user opening a position because the transaction would revert.
- anyone can call the `EVC.batch()` function to initialize a batched call through the EVC. This call is allowed to be reentrant. Therefore, simply checking that a caller is the `address(EVC)` doesn't really offer any added security benefit.
- The parameters supplied by a solver to the settlement contract are all indirectly bounded from within the settlement contract by ceratin restrictions:
- `tokens` -- this is a mapping used by the settlement contract to save on gas. If a token used by an order is missing, it will fail to pass signature checks.
- `clearingPrices` -- these define prices to go with the previously defined `tokens`. These clearing prices are set by the solver and determine exactly how many tokens come out of a trade. **However, if a clearingPrice is lower than any of a user's limit price in `trades`, the transaction will revert. Therefore, it is not possible for a user to steal a users funds simply by setting clearingPrices to an arbitrary value.** There is incentive to provide the best clearingPrice because an auction is held off-chain by CoW Protocol and only the best overall rate outcome is selected.
- `trades` -- List of orders to fulfill. All of the data inside this structure is effectively signed by the user and cannot be altered by solvers, other than adding or removing signed orders.
- `interactions` -- Solvers use this to specify operations that should be executed from within the settlement contract. This could include swaps, pre-hooks, post-hooks, etc. This is completely controlled by the solver.

- Please consider any potential security vulnerabilities resulting from potential flawed assumptions of the above from any contracts outside this repo, including the Ethereum Vault Connector (EVC), Settlement Contract, or Euler Vaults, out of scope.

### Wrapper Data Format
Wrapper data is passed as a calldata slice with format:
```
Expand Down
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ src = "src"
out = "out"
libs = ["lib"]
optimizer = true
via_ir = true

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
293 changes: 293 additions & 0 deletions src/CowEvcOpenPositionWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8;

import {IEVC} from "evc/EthereumVaultConnector.sol";

import {CowWrapper, ICowSettlement} from "./CowWrapper.sol";
import {IERC4626, IBorrowing} from "euler-vault-kit/src/EVault/IEVault.sol";
import {PreApprovedHashes} from "./PreApprovedHashes.sol";

/// @title CowEvcOpenPositionWrapper
/// @notice A specialized wrapper for opening leveraged positions with EVC
/// @dev This wrapper hardcodes the EVC operations needed to open a position:
/// 1. Enable collateral vault
/// 2. Enable controller (borrow vault)
/// 3. Deposit collateral
/// 4. Borrow assets
/// @dev The settle call by this order should be performing the necessary swap
/// from IERC20(borrowVault.asset()) -> collateralVault. The recipient of the
/// swap should be the `owner` (not this contract). Furthermore, the buyAmountIn should
/// be the same as `maxRepayAmount`.
contract CowEvcOpenPositionWrapper is CowWrapper, PreApprovedHashes {
IEVC public immutable EVC;

/// @dev The EIP-712 domain type hash used for computing the domain
/// separator.
bytes32 private constant DOMAIN_TYPE_HASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");

/// @dev The EIP-712 domain name used for computing the domain separator.
bytes32 private constant DOMAIN_NAME = keccak256("CowEvcOpenPositionWrapper");

/// @dev The EIP-712 domain version used for computing the domain separator.
bytes32 private constant DOMAIN_VERSION = keccak256("1");

/// @dev The domain separator used for signing orders that gets mixed in
/// making signatures for different domains incompatible. This domain
/// separator is computed following the EIP-712 standard and has replay
/// protection mixed in so that signed orders are only valid for specific
/// this contract.
bytes32 public immutable DOMAIN_SEPARATOR;

//// @dev The EVC nonce namespace to use when calling `EVC.permit` to authorize this contract.
uint256 public immutable NONCE_NAMESPACE;

/// @dev A descriptive label for this contract, as required by CowWrapper
string public override name = "Euler EVC - Open Position";

/// @dev Indicates that the current operation cannot be completed with the given msgSender
error Unauthorized(address msgSender);

/// @dev Indicates that the pre-approved hash is no longer able to be executed because the block timestamp is too old
error OperationDeadlineExceeded(uint256 validToTimestamp, uint256 currentTimestamp);

/// @dev Emitted when a position is opened via this wrapper
event CowEvcPositionOpened(
address indexed owner,
address account,
address indexed collateralVault,
address indexed borrowVault,
uint256 collateralAmount,
uint256 borrowAmount
);

constructor(address _evc, ICowSettlement _settlement) CowWrapper(_settlement) {
Copy link

Choose a reason for hiding this comment

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

Security: Missing zero address validation

The constructor doesn't validate that _evc is not the zero address. If deployed with address(0), all evc.batch() calls would fail, making the wrapper completely non-functional.

Consider adding:

require(_evc != address(0), "Invalid EVC address");

EVC = IEVC(_evc);
NONCE_NAMESPACE = uint256(uint160(address(this)));

DOMAIN_SEPARATOR =
keccak256(abi.encode(DOMAIN_TYPE_HASH, DOMAIN_NAME, DOMAIN_VERSION, block.chainid, address(this)));
}

/**
* @notice A command to open a debt position against an euler vault using collateral as backing.
* @dev This structure is used, combined with domain separator, to indicate a pre-approved hash.
* the `deadline` is used for deduplication checking, so be careful to ensure this value is unique.
*/
struct OpenPositionParams {
/**
* @dev The ethereum address that has permission to operate upon the account
*/
address owner;

/**
* @dev The subaccount to open the position on. Learn more about Euler subaccounts https://evc.wtf/docs/concepts/internals/sub-accounts
*/
address account;

/**
* @dev A date by which this operation must be completed
*/
uint256 deadline;

/**
* @dev The Euler vault to use as collateral
*/
address collateralVault;

/**
* @dev The Euler vault to use as leverage
*/
address borrowVault;

/**
* @dev The amount of collateral to import as margin. Set this to `0` if the vault already has margin collateral.
*/
uint256 collateralAmount;

/**
* @dev The amount of debt to take out. The borrowed tokens will be converted to `collateralVault` tokens and deposited into the account.
*/
uint256 borrowAmount;
}

function _parseOpenPositionParams(bytes calldata wrapperData)
internal
pure
returns (OpenPositionParams memory params, bytes memory signature, bytes calldata remainingWrapperData)
{
(params, signature) = abi.decode(wrapperData, (OpenPositionParams, bytes));

// Calculate consumed bytes for abi.encode(OpenPositionParams, bytes)
// Structure:
// - 32 bytes: offset to params (0x40)
// - 32 bytes: offset to signature
// - 224 bytes: params data (7 fields × 32 bytes)
// - 32 bytes: signature length
// - N bytes: signature data (padded to 32-byte boundary)
// We can just math this out
uint256 consumed = 224 + 64 + ((signature.length + 31) & ~uint256(31));
Copy link

Choose a reason for hiding this comment

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

Code Quality: Hardcoded magic number

The value 224 represents the size of OpenPositionParams (7 fields × 32 bytes). This is fragile and error-prone if the struct changes.

Consider using a constant or documenting this more clearly:

// OpenPositionParams size: 7 fields × 32 bytes = 224 bytes
uint256 constant OPEN_POSITION_PARAMS_SIZE = 224;
uint256 consumed = OPEN_POSITION_PARAMS_SIZE + 64 + ((signature.length + 31) & ~uint256(31));

The same issue exists on line 145 in the assembly block.

Copy link

Choose a reason for hiding this comment

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

Maintainability: Magic number should be a named constant

The value 224 represents OpenPositionParams size (7 fields × 32 bytes). This should be a named constant to prevent bugs if the struct changes:

Suggested change
uint256 consumed = 224 + 64 + ((signature.length + 31) & ~uint256(31));
// OpenPositionParams size: 7 fields × 32 bytes = 224 bytes
uint256 constant OPEN_POSITION_PARAMS_SIZE = 224;
uint256 consumed = OPEN_POSITION_PARAMS_SIZE + 64 + ((signature.length + 31) & ~uint256(31));

The same constant should replace the hardcoded 224 on line 145.


remainingWrapperData = wrapperData[consumed:];
}

/// @notice Helper function to compute the hash that would be approved
/// @param params The OpenPositionParams to hash
/// @return The hash of the signed calldata for these params
function getApprovalHash(OpenPositionParams memory params) external view returns (bytes32) {
return _getApprovalHash(params);
}

function _getApprovalHash(OpenPositionParams memory params) internal view returns (bytes32 digest) {
bytes32 structHash;
bytes32 separator = DOMAIN_SEPARATOR;
assembly ("memory-safe") {
structHash := keccak256(params, 224)
let ptr := mload(0x40)
mstore(ptr, "\x19\x01")
mstore(add(ptr, 0x02), separator)
mstore(add(ptr, 0x22), structHash)
digest := keccak256(ptr, 0x42)
}
}

function parseWrapperData(bytes calldata wrapperData)
external
pure
override
returns (bytes calldata remainingWrapperData)
{
(,, remainingWrapperData) = _parseOpenPositionParams(wrapperData);
}

/// @notice Implementation of GPv2Wrapper._wrap - executes EVC operations to open a position
/// @param settleData Data which will be used for the parameters in a call to `CowSettlement.settle`
/// @param wrapperData Additional data containing OpenPositionParams
function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData)
Copy link

Choose a reason for hiding this comment

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

Security: No validation of critical parameters

The function doesn't validate that:

  • params.collateralVault != address(0)
  • params.borrowVault != address(0)
  • params.collateralVault != params.borrowVault (using same vault for both could cause issues)
  • params.deadline >= block.timestamp when using permit signature path (only checked in pre-approved path)

While some of these would fail later in EVC calls, explicit validation provides clearer error messages and fails faster.

internal
override
{
// Decode wrapper data into OpenPositionParams
OpenPositionParams memory params;
bytes memory signature;
(params, signature,) = _parseOpenPositionParams(wrapperData);
Copy link

Choose a reason for hiding this comment

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

Code Quality: Missing parameter validation

Consider validating critical parameters before proceeding:

Suggested change
(params, signature,) = _parseOpenPositionParams(wrapperData);
(params, signature,) = _parseOpenPositionParams(wrapperData);
// Validate critical parameters
require(params.collateralVault != address(0), "Invalid collateral vault");
require(params.borrowVault != address(0), "Invalid borrow vault");
require(params.collateralVault != params.borrowVault, "Vaults cannot be the same");

While these would fail later in EVC calls, explicit validation provides clearer error messages and fails faster.


// Check if the signed calldata hash is pre-approved
IEVC.BatchItem[] memory signedItems = _getSignedCalldata(params);
bool isPreApproved = signature.length == 0 && _consumePreApprovedHash(params.owner, _getApprovalHash(params));

// Build the EVC batch items for opening a position
IEVC.BatchItem[] memory items = new IEVC.BatchItem[](isPreApproved ? signedItems.length + 1 : 2);

uint256 itemIndex = 0;

// 1. There are two ways this contract can be executed: either the user approves this contract as
// and operator and supplies a pre-approved hash for the operation to take, or they submit a permit hash
// for this specific instance
if (!isPreApproved) {
Copy link

Choose a reason for hiding this comment

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

Code Quality: Inconsistent deadline validation

The pre-approved hash path validates params.deadline >= block.timestamp (line 207), but the permit signature path doesn't. While the permit call will handle this, consider adding explicit validation for consistency:

Suggested change
if (!isPreApproved) {
if (!isPreApproved) {
require(params.deadline >= block.timestamp, OperationDeadlineExceeded(params.deadline, block.timestamp));
items[itemIndex++] = IEVC.BatchItem({

items[itemIndex++] = IEVC.BatchItem({
onBehalfOfAccount: address(0),
targetContract: address(EVC),
value: 0,
data: abi.encodeCall(
IEVC.permit,
(
params.owner,
address(this),
uint256(NONCE_NAMESPACE),
EVC.getNonce(bytes19(bytes20(params.owner)), NONCE_NAMESPACE),
params.deadline,
0,
abi.encodeCall(EVC.batch, signedItems),
signature
)
)
});
} else {
require(params.deadline >= block.timestamp, OperationDeadlineExceeded(params.deadline, block.timestamp));
// copy the operations to execute. we can operate on behalf of the user directly
for (; itemIndex < signedItems.length; itemIndex++) {
items[itemIndex] = signedItems[itemIndex];
}
}

// 2. Settlement call
items[itemIndex] = IEVC.BatchItem({
onBehalfOfAccount: address(this),
targetContract: address(this),
value: 0,
data: abi.encodeCall(this.evcInternalSettle, (settleData, remainingWrapperData))
});

// 3. Account status check (automatically done by EVC at end of batch)
// For more info, see: https://evc.wtf/docs/concepts/internals/account-status-checks
// No explicit item needed - EVC handles this

// Execute all items in a single batch
EVC.batch(items);

emit CowEvcPositionOpened(
params.owner,
params.account,
params.collateralVault,
params.borrowVault,
params.collateralAmount,
params.borrowAmount
);
}

function getSignedCalldata(OpenPositionParams memory params) external view returns (bytes memory) {
return abi.encodeCall(IEVC.batch, _getSignedCalldata(params));
}

function _getSignedCalldata(OpenPositionParams memory params)
internal
view
returns (IEVC.BatchItem[] memory items)
{
items = new IEVC.BatchItem[](4);

// 1. Enable collateral
items[0] = IEVC.BatchItem({
onBehalfOfAccount: address(0),
targetContract: address(EVC),
value: 0,
data: abi.encodeCall(IEVC.enableCollateral, (params.account, params.collateralVault))
});

// 2. Enable controller (borrow vault)
items[1] = IEVC.BatchItem({
onBehalfOfAccount: address(0),
targetContract: address(EVC),
value: 0,
data: abi.encodeCall(IEVC.enableController, (params.account, params.borrowVault))
});

// 3. Deposit collateral
items[2] = IEVC.BatchItem({
onBehalfOfAccount: params.owner,
targetContract: params.collateralVault,
value: 0,
data: abi.encodeCall(IERC4626.deposit, (params.collateralAmount, params.account))
});

// 4. Borrow assets
items[3] = IEVC.BatchItem({
onBehalfOfAccount: params.account,
targetContract: params.borrowVault,
value: 0,
data: abi.encodeCall(IBorrowing.borrow, (params.borrowAmount, params.owner))
});
}

/// @notice Internal settlement function called by EVC
function evcInternalSettle(bytes calldata settleData, bytes calldata remainingWrapperData) external payable {
Copy link

Choose a reason for hiding this comment

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

Security: Potential reentrancy vulnerability

While the EVC batch ensures atomicity, there's no reentrancy protection on this external function. An attacker could potentially call evcInternalSettle from within a malicious vault contract during the EVC batch execution.

The checks msg.sender == address(EVC) and onBehalfOfAccount == address(this) provide some protection, but consider whether this is sufficient given the complex call flow through EVC.

require(msg.sender == address(EVC), Unauthorized(msg.sender));
(address onBehalfOfAccount,) = EVC.getCurrentOnBehalfOfAccount(address(0));
require(onBehalfOfAccount == address(this), Unauthorized(onBehalfOfAccount));

// Use GPv2Wrapper's _internalSettle to call the settlement contract
// wrapperData is empty since we've already processed it in _wrap
_next(settleData, remainingWrapperData);
}
}
Loading