Skip to content

Conversation

@kaze-cow
Copy link
Collaborator

@kaze-cow kaze-cow commented Nov 6, 2025

Description

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

Context

Read up on notion

Considerations

  • While we call this flow "open positoin wrapper", it could also be used to grow an existing position, or take on a position without buying any Euler tokens (by setting collateralAmount to 0)
  • this should be compatible ewith CoW (if two users open opposite positions, for example)

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-65/initial-merge-contract-of-cowevcopenpositionwrapper

Kaze and others added 30 commits September 11, 2025 21:44
* check solver on internalSettle function
* check only callable through EVC
* prevent reentrancy
make settlement the gatekeeper

why did I not do this before
This reverts commit e56eac3.
best way to ensure the expected flow is followed exactly
* check solver on internalSettle function
* check only callable through EVC
* prevent reentrancy
they shouldn't have been in the repository
feat: use new wrapper from upstream
we use the settlement contract, so it shouldn't be needed anymore

also soljson.latest is still here
kaze-cow and others added 21 commits December 9, 2025 16:08
Co-authored-by: Federico Giacon <[email protected]>
Co-authored-by: Federico Giacon <[email protected]>
Co-authored-by: Federico Giacon <[email protected]>
Co-authored-by: Federico Giacon <[email protected]>
Co-authored-by: Federico Giacon <[email protected]>
Co-authored-by: Federico Giacon <[email protected]>
ensures that all params are included in the signed data (this should already be the case, but now we are extra sure)
…d signatures work as expected

the `_consumePreApprovedHash` method was not actually reverting--instead returning a boolean, which broke the security guarentee of consume pre approved hash. And there was no test validating the negative case for pre approved hashes
Comment on lines +145 to +151
function getSignedCalldata(OpenPositionParams memory params) external view returns (bytes memory) {
(IEVC.BatchItem[] memory items,) = _encodeBatchItemsBefore(memoryLocation(params));
return abi.encodePacked(
abi.encodeCall(IEVC.batch, items), _getApprovalHash(OPEN_POSITION_PARAMS_TYPE_HASH, memoryLocation(params))
);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Since (1) this logic needs to be repeated for each wrapper and (2) the same packing is also done in the base wrapper, I'd suggest moving this to the base wrapper.
Doing something like the following would also make immediately clear the link between this function and permit:

Suggested change
function getSignedCalldata(OpenPositionParams memory params) external view returns (bytes memory) {
(IEVC.BatchItem[] memory items,) = _encodeBatchItemsBefore(memoryLocation(params));
return abi.encodePacked(
abi.encodeCall(IEVC.batch, items), _getApprovalHash(OPEN_POSITION_PARAMS_TYPE_HASH, memoryLocation(params))
);
}
function getSignedCalldata(OpenPositionParams memory params) external view returns (bytes memory) {
(IEVC.BatchItem[] memory items,) = _encodeBatchItemsBefore(memoryLocation(params));
return _encodePermitData(items, params)
);
}

under the assumption that encodePermitData is also used in _addEvcBatchItems's permit encoding.

Then in a comment of _encodePermitData we can explain why this is done (right now this comes out of nowhere).

(Yeah this doesn't work as-is because of OPEN_POSITION_PARAMS_TYPE_HASH. I'd suggest trying to make it into an immutable so that the base contract can use it, if this makes things nicer.)

);
}

function getSignedCalldata(OpenPositionParams memory params) external view returns (bytes memory) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This external function is quite important to frontends since the easiest implementation is to call it to build the permit call that needs to be executed. My suggestions are:

  1. Adding devdocs explaining what this is for (populating permit.data)
  2. Renaming this function to something clearer. Maybe encodePermitData or preparePermitData?

returns (IEVC.BatchItem[] memory items, bool needsPermission)
{
OpenPositionParams memory params = paramsFromMemory(paramsLocation);
items = new IEVC.BatchItem[](4);
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's make the constant relationship explicit:

Suggested change
items = new IEVC.BatchItem[](4);
items = new IEVC.BatchItem[](MAX_BATCH_OPERATIONS - 1);

Ideally there would be a test that makes sure the output of this function is indeed an array of length MAX_BATCH_OPERATIONS - 1 and not more.

deal(SUSDS, user, 10000e18);
}

struct SettlementData {
Copy link
Contributor

Choose a reason for hiding this comment

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

There's a lot of code I expect will be repeating in tests for other wrappers, like SettlementData, _setupUserSusdsApproval, part of setUp, _setupUserPreApprovedFlow, _verifyPositionOpened, getOpenPositionSettlement, part of _createPermitSignature, and probably more.
I don't see this as a problem for merging this PR, but it would be a blocker for merging the next ones. 😛
Something nice for reviewability would be an intermediate PR: main -> open position -> (new) make tests more modular but add nothing new -> close position -> collateral swap. The assumption here is that the helpers won't change much in this code review, otherwise since there are so many tests to backport that it would make it significantly hard to integrate changes.

function test_OpenPositionWrapper_Success() external {
vm.skip(bytes(forkRpcUrl).length == 0);

address account = address(uint160(user) ^ 1);
Copy link
Contributor

Choose a reason for hiding this comment

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

(Not a code change, just a discussion point.)
Are we guaranteed that the account the order uses is "reserved" for our trade? And if so, how? Is it the Euler frontend managing which accounts to use?

Comment on lines +241 to +245
address[] memory targets = new address[](1);
bytes[] memory datas = new bytes[](1);
targets[0] = wrapper;
datas[0] = abi.encodeWithSignature("wrappedSettle(bytes,bytes)", settleData, wrapperData);
solver.runBatch(targets, datas);
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand it, why the need to run this batching setup instead of just pranking an EOA solver and directly execute wrappedSettle? Do we ever need to execute more than one operation from the solvers?

Comment on lines +208 to +209
assertEq(debtBefore, 0, "User should start with no debt");
assertEq(susdsBalanceBefore, 0, "User should start with no eSUSDS");
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe this should be moved to where debtBefore and susdsBalanceBefore are defined, and then there should be checks on the updated balances.

Comment on lines +112 to +124
function _verifyPositionOpened(
address account,
uint256 expectedCollateral,
uint256 expectedDebt,
uint256 allowedDelta
) internal view {
assertApproxEqAbs(
IEVault(ESUSDS).convertToAssets(IERC20(ESUSDS).balanceOf(account)),
expectedCollateral,
allowedDelta,
"User should have collateral deposited"
);
assertEq(IEVault(EWETH).debtOf(account), expectedDebt, "User should have debt");
Copy link
Contributor

Choose a reason for hiding this comment

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

This function hardcodes the vaults to ESUSDS and EWETH but many other don't. The most important thing to me is consistency, but in general I think it makes more sense to make the vault generic since it's one of the tokens and we may want to change them in other tests.

executeWrappedSettlement(address(openPositionWrapper), settleData, wrapperData);

// Verify position was created successfully
_verifyPositionOpened(account, DEFAULT_BUY_AMOUNT + SUSDS_MARGIN, DEFAULT_BORROW_AMOUNT, 1 ether);
Copy link
Contributor

Choose a reason for hiding this comment

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

This function is hard to read, mostly because it has too many similar parameters. Here it makes sense to make the parameters explicit.

Suggested change
_verifyPositionOpened(account, DEFAULT_BUY_AMOUNT + SUSDS_MARGIN, DEFAULT_BORROW_AMOUNT, 1 ether);
_verifyPositionOpened({
account: account,
expectedCollateral: DEFAULT_BUY_AMOUNT + SUSDS_MARGIN,
expectedDebt: DEFAULT_BORROW_AMOUNT,
allowedDelta: 1 ether
});

executeWrappedSettlement(address(openPositionWrapper), settleData, wrapperData);

// Verify position was created successfully
_verifyPositionOpened(account, DEFAULT_BUY_AMOUNT + SUSDS_MARGIN, DEFAULT_BORROW_AMOUNT, 1 ether);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need approximate equality in the first place? Can't we make this an exact equality?
Also, it's unclear what the delta is for, is it the collateral or the debt?
Isn't there a way for us to estimate the error exactly? What creates confusion in the amounts?

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