Skip to content

Commit 3d247fb

Browse files
committed
Merge branch 'feat/specialized-wrappers' into feat/collateral-swap-wrapper
2 parents 9563fbe + 49fbed9 commit 3d247fb

26 files changed

+2776
-691
lines changed

.github/pull_request_template.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
## Description
2+
Summarize what this PR does and why in a single, clear sentence.
3+
A reader should understand the purpose and scope of the change just from this line.
4+
5+
## Context
6+
Explain the motivation or problem this PR addresses.
7+
Include links to any relevant documentation, tickets, or discussions.
8+
9+
Provide background that helps reviewers understand *why* the change is needed.
10+
Highlight any important design decisions, dependencies, or related components.
11+
12+
## Out of Scope
13+
Specify what is *not* covered in this PR.
14+
This helps reviewers focus on the intended scope and prevents scope creep.
15+
16+
## Testing Instructions
17+
Provide clear, step-by-step instructions for verifying this change locally.
18+
Assume the reviewer has no prior context about your setup.
19+
20+
This section reinforces the scope of the PR by outlining what should be tested and what the expected outcomes are.

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ jobs:
2020

2121
- name: Install Foundry
2222
uses: foundry-rs/foundry-toolchain@v1
23+
with:
24+
version: v1.4.3
2325

2426
- name: Show Forge version
2527
run: |

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ This repository contains **Euler-CoW Protocol integration contracts** that enabl
5050

5151
### Build
5252
```bash
53-
forge build
53+
forge build --deny notes
5454
```
5555

5656
### Test
@@ -100,6 +100,7 @@ forge snapshot
100100
### Security Considerations
101101

102102
- 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).
103+
- 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).
103104
- 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.
104105
- 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.
105106
- The parameters supplied by a solver to the settlement contract are all indirectly bounded from within the settlement contract by ceratin restrictions:

foundry.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
src = "src"
33
out = "out"
44
libs = ["lib"]
5-
optimize = true
5+
optimizer = true
66
via_ir = true
77

88
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

src/CowEvcClosePositionWrapper.sol

Lines changed: 129 additions & 87 deletions
Large diffs are not rendered by default.

src/CowEvcOpenPositionWrapper.sol

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ contract CowEvcOpenPositionWrapper is CowWrapper, PreApprovedHashes {
2424
/// @dev The EIP-712 domain type hash used for computing the domain
2525
/// separator.
2626
bytes32 private constant DOMAIN_TYPE_HASH =
27-
keccak256(
28-
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
29-
);
27+
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
3028

3129
/// @dev The EIP-712 domain name used for computing the domain separator.
3230
bytes32 private constant DOMAIN_NAME = keccak256("CowEvcOpenPositionWrapper");
@@ -39,28 +37,26 @@ contract CowEvcOpenPositionWrapper is CowWrapper, PreApprovedHashes {
3937
/// separator is computed following the EIP-712 standard and has replay
4038
/// protection mixed in so that signed orders are only valid for specific
4139
/// this contract.
42-
bytes32 public immutable domainSeparator;
40+
bytes32 public immutable DOMAIN_SEPARATOR;
4341

44-
string public constant name = "Euler EVC - Open Position";
42+
//// @dev The EVC nonce namespace to use when calling `EVC.permit` to authorize this contract.
43+
uint256 public immutable NONCE_NAMESPACE;
4544

46-
uint256 public immutable nonceNamespace;
45+
/// @dev A descriptive label for this contract, as required by CowWrapper
46+
string public override name = "Euler EVC - Open Position";
4747

48+
/// @dev Indicates that the current operation cannot be completed with the given msgSender
4849
error Unauthorized(address msgSender);
50+
51+
/// @dev Indicates that the pre-approved hash is no longer able to be executed because the block timestamp is too old
4952
error OperationDeadlineExceeded(uint256 validToTimestamp, uint256 currentTimestamp);
5053

5154
constructor(address _evc, CowSettlement _settlement) CowWrapper(_settlement) {
5255
EVC = IEVC(_evc);
53-
nonceNamespace = uint256(uint160(address(this)));
54-
55-
domainSeparator = keccak256(
56-
abi.encode(
57-
DOMAIN_TYPE_HASH,
58-
DOMAIN_NAME,
59-
DOMAIN_VERSION,
60-
block.chainid,
61-
address(this)
62-
)
63-
);
56+
NONCE_NAMESPACE = uint256(uint160(address(this)));
57+
58+
DOMAIN_SEPARATOR =
59+
keccak256(abi.encode(DOMAIN_TYPE_HASH, DOMAIN_NAME, DOMAIN_VERSION, block.chainid, address(this)));
6460
}
6561

6662
/**
@@ -73,7 +69,7 @@ contract CowEvcOpenPositionWrapper is CowWrapper, PreApprovedHashes {
7369
* @dev The ethereum address that has permission to operate upon the account
7470
*/
7571
address owner;
76-
72+
7773
/**
7874
* @dev The subaccount to open the position on. Learn more about Euler subaccounts https://evc.wtf/docs/concepts/internals/sub-accounts
7975
*/
@@ -133,9 +129,10 @@ contract CowEvcOpenPositionWrapper is CowWrapper, PreApprovedHashes {
133129
}
134130

135131
function _getApprovalHash(OpenPositionParams memory params) internal view returns (bytes32 digest) {
136-
bytes32 structHash = keccak256(abi.encode(params));
137-
bytes32 separator = domainSeparator;
132+
bytes32 structHash;
133+
bytes32 separator = DOMAIN_SEPARATOR;
138134
assembly ("memory-safe") {
135+
structHash := keccak256(params, 224)
139136
let ptr := mload(0x40)
140137
mstore(ptr, "\x19\x01")
141138
mstore(add(ptr, 0x02), separator)
@@ -156,15 +153,18 @@ contract CowEvcOpenPositionWrapper is CowWrapper, PreApprovedHashes {
156153
/// @notice Implementation of GPv2Wrapper._wrap - executes EVC operations to open a position
157154
/// @param settleData Data which will be used for the parameters in a call to `CowSettlement.settle`
158155
/// @param wrapperData Additional data containing OpenPositionParams
159-
function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData) internal override {
156+
function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData)
157+
internal
158+
override
159+
{
160160
// Decode wrapper data into OpenPositionParams
161161
OpenPositionParams memory params;
162162
bytes memory signature;
163-
(params, signature, ) = _parseOpenPositionParams(wrapperData);
163+
(params, signature,) = _parseOpenPositionParams(wrapperData);
164164

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

169169
// Build the EVC batch items for opening a position
170170
IEVC.BatchItem[] memory items = new IEVC.BatchItem[](isPreApproved ? signedItems.length + 1 : 2);
@@ -184,8 +184,8 @@ contract CowEvcOpenPositionWrapper is CowWrapper, PreApprovedHashes {
184184
(
185185
params.owner,
186186
address(this),
187-
uint256(nonceNamespace),
188-
EVC.getNonce(bytes19(bytes20(params.owner)), nonceNamespace),
187+
uint256(NONCE_NAMESPACE),
188+
EVC.getNonce(bytes19(bytes20(params.owner)), NONCE_NAMESPACE),
189189
params.deadline,
190190
0,
191191
abi.encodeCall(EVC.batch, signedItems),
@@ -264,6 +264,8 @@ contract CowEvcOpenPositionWrapper is CowWrapper, PreApprovedHashes {
264264
/// @notice Internal settlement function called by EVC
265265
function evcInternalSettle(bytes calldata settleData, bytes calldata remainingWrapperData) external payable {
266266
require(msg.sender == address(EVC), Unauthorized(msg.sender));
267+
(address onBehalfOfAccount,) = EVC.getCurrentOnBehalfOfAccount(address(0));
268+
require(onBehalfOfAccount == address(this), Unauthorized(onBehalfOfAccount));
267269

268270
// Use GPv2Wrapper's _internalSettle to call the settlement contract
269271
// wrapperData is empty since we've already processed it in _wrap

src/PreApprovedHashes.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ abstract contract PreApprovedHashes {
2727
/// @param hash The keccak256 hash of the signed calldata
2828
/// @param approved True to approve the hash, false to revoke approval
2929
function setPreApprovedHash(bytes32 hash, bool approved) external {
30-
require(preApprovedHashes[msg.sender][hash] != CONSUMED_PRE_APPROVED, AlreadyConsumed(msg.sender, hash));
30+
require(preApprovedHashes[msg.sender][hash] != CONSUMED_PRE_APPROVED, AlreadyConsumed(msg.sender, hash));
3131

3232
if (approved) {
3333
preApprovedHashes[msg.sender][hash] = PRE_APPROVED;

src/vendor/CowWrapper.sol

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ interface CowSettlement {
4343
/// @notice Returns the address of the vaultRelayer, the target for approvals for funds entering the settlement contract.
4444
function vaultRelayer() external view returns (address);
4545

46-
/// @notice Returns the domain separator for EIP-712 signing
46+
/// @notice Returns the domain separator for EIP-712 signing
4747
function domainSeparator() external view returns (bytes32);
4848

4949
/// @notice Allows for approval of orders by submitting an authorized hash on-chain prior to order execution.
@@ -135,23 +135,29 @@ abstract contract CowWrapper is ICowWrapper {
135135
uint16 nextWrapperDataLen = uint16(bytes2(wrapperData[0:2]));
136136

137137
// Delegate to the wrapper's custom logic
138-
_wrap(settleData, wrapperData[2:2+nextWrapperDataLen], wrapperData[2+nextWrapperDataLen:]);
138+
_wrap(settleData, wrapperData[2:2 + nextWrapperDataLen], wrapperData[2 + nextWrapperDataLen:]);
139139
}
140140

141141
/// @notice Parses and validates wrapper-specific data
142142
/// @dev Must be implemented by concrete wrapper contracts. Used for pre-execution validation.
143143
/// The implementation should consume its wrapper-specific data and return the remainder.
144144
/// @param wrapperData The full wrapper data to parse
145145
/// @return remainingWrapperData The portion of wrapper data not consumed by this wrapper
146-
function parseWrapperData(bytes calldata wrapperData) external virtual view returns (bytes calldata remainingWrapperData);
146+
function parseWrapperData(bytes calldata wrapperData)
147+
external
148+
view
149+
virtual
150+
returns (bytes calldata remainingWrapperData);
147151

148152
/// @notice Internal function containing the wrapper's custom logic
149153
/// @dev Must be implemented by concrete wrapper contracts. Should execute custom logic
150154
/// then eventually call _internalSettle() to continue the settlement chain.
151155
/// @param settleData ABI-encoded call to CowSettlement.settle()
152156
/// @param wrapperData The wrapper data which should be consumed by this wrapper
153157
/// @param remainingWrapperData Additional wrapper data needed by future wrappers. This should be passed unaltered to _internalSettle
154-
function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData) internal virtual;
158+
function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData)
159+
internal
160+
virtual;
155161

156162
/// @notice Continues the settlement chain by calling the next wrapper or settlement contract
157163
/// @dev Extracts the next target address from wrapperData and either:

src/vendor/CowWrapperHelpers.sol

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ contract CowWrapperHelpers {
2424
/// @param wrapperError The error returned by the wrapper's parseWrapperData
2525
error WrapperDataMalformed(uint256 wrapperIndex, bytes wrapperError);
2626

27+
/// @notice Thrown when the data for the wrapper is too long. Its limited to 65535 bytes.
28+
/// @param wrapperIndex The index of the wrapper with data that is too long
29+
/// @param exceedingLength The observed length of the data
30+
error WrapperDataTooLong(uint256 wrapperIndex, uint256 exceedingLength);
31+
2732
/// @notice Thrown when the settlement contract is authenticated as a solver
2833
/// @dev The settlement contract should not be a solver to prevent direct settlement calls bypassing wrappers
2934
/// @param settlementContract The settlement contract address
@@ -71,31 +76,38 @@ contract CowWrapperHelpers {
7176
/// Note: No settlement address is appended as wrappers now use a static SETTLEMENT.
7277
/// @param wrapperCalls Array of calls in execution order
7378
/// @return wrapperData The encoded wrapper data ready to be passed to the first wrapper's wrappedSettle
74-
function verifyAndBuildWrapperData(WrapperCall[] memory wrapperCalls) external view returns (bytes memory wrapperData) {
79+
function verifyAndBuildWrapperData(WrapperCall[] memory wrapperCalls)
80+
external
81+
view
82+
returns (bytes memory wrapperData)
83+
{
7584
if (wrapperCalls.length == 0) {
7685
return wrapperData;
7786
}
7887

7988
// First pass: verify all wrappers are authenticated
80-
for (uint256 i = 0;i < wrapperCalls.length;i++) {
81-
if (!WRAPPER_AUTHENTICATOR.isSolver(wrapperCalls[i].target)) {
82-
revert NotAWrapper(i, wrapperCalls[i].target, address(WRAPPER_AUTHENTICATOR));
83-
}
89+
for (uint256 i = 0; i < wrapperCalls.length; i++) {
90+
require(
91+
WRAPPER_AUTHENTICATOR.isSolver(wrapperCalls[i].target),
92+
NotAWrapper(i, wrapperCalls[i].target, address(WRAPPER_AUTHENTICATOR))
93+
);
8494
}
8595

8696
// Get the expected settlement from the first wrapper
8797
address expectedSettlement = address(ICowWrapper(wrapperCalls[0].target).SETTLEMENT());
8898

89-
for (uint256 i = 0;i < wrapperCalls.length;i++) {
90-
99+
for (uint256 i = 0; i < wrapperCalls.length; i++) {
91100
// All wrappers must use the same settlement contract
92101
address wrapperSettlement = address(ICowWrapper(wrapperCalls[i].target).SETTLEMENT());
93-
if (wrapperSettlement != expectedSettlement) {
94-
revert SettlementMismatch(i, expectedSettlement, wrapperSettlement);
95-
}
102+
103+
require(
104+
wrapperSettlement == expectedSettlement, SettlementMismatch(i, expectedSettlement, wrapperSettlement)
105+
);
96106

97107
// The wrapper data must be parsable and fully consumed
98-
try ICowWrapper(wrapperCalls[i].target).parseWrapperData(wrapperCalls[i].data) returns (bytes memory remainingWrapperData) {
108+
try ICowWrapper(wrapperCalls[i].target).parseWrapperData(wrapperCalls[i].data) returns (
109+
bytes memory remainingWrapperData
110+
) {
99111
if (remainingWrapperData.length > 0) {
100112
revert WrapperDataNotFullyConsumed(i, remainingWrapperData);
101113
}
@@ -109,11 +121,14 @@ contract CowWrapperHelpers {
109121
revert SettlementContractShouldNotBeSolver(expectedSettlement, address(SOLVER_AUTHENTICATOR));
110122
}
111123

112-
// Build wrapper data without settlement address at the end
113-
wrapperData = abi.encodePacked(uint16(wrapperCalls[0].data.length), wrapperCalls[0].data);
124+
// Build wrapper data
125+
for (uint256 i = 0; i < wrapperCalls.length; i++) {
126+
if (i > 0) {
127+
wrapperData = abi.encodePacked(wrapperData, wrapperCalls[i].target);
128+
}
114129

115-
for (uint256 i = 1;i < wrapperCalls.length;i++) {
116-
wrapperData = abi.encodePacked(wrapperData, wrapperCalls[i].target, uint16(wrapperCalls[i].data.length), wrapperCalls[i].data);
130+
require(wrapperCalls[i].data.length < 65536, WrapperDataTooLong(i, wrapperCalls[i].data.length));
131+
wrapperData = abi.encodePacked(wrapperData, uint16(wrapperCalls[i].data.length), wrapperCalls[i].data);
117132
}
118133

119134
return wrapperData;

src/vendor/interfaces/IERC20.sol

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,7 @@ interface IERC20 {
4444
*
4545
* Emits a {Transfer} event.
4646
*/
47-
function transfer(
48-
address recipient,
49-
uint256 amount
50-
) external returns (bool);
47+
function transfer(address recipient, uint256 amount) external returns (bool);
5148

5249
/**
5350
* @dev Returns the remaining number of tokens that `spender` will be
@@ -56,10 +53,7 @@ interface IERC20 {
5653
*
5754
* This value changes when {approve} or {transferFrom} are called.
5855
*/
59-
function allowance(
60-
address owner,
61-
address spender
62-
) external view returns (uint256);
56+
function allowance(address owner, address spender) external view returns (uint256);
6357

6458
/**
6559
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
@@ -86,11 +80,7 @@ interface IERC20 {
8680
*
8781
* Emits a {Transfer} event.
8882
*/
89-
function transferFrom(
90-
address sender,
91-
address recipient,
92-
uint256 amount
93-
) external returns (bool);
83+
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
9484

9585
/**
9686
* @dev Emitted when `value` tokens are moved from one account (`from`) to
@@ -104,9 +94,5 @@ interface IERC20 {
10494
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
10595
* a call to {approve}. `value` is the new allowance.
10696
*/
107-
event Approval(
108-
address indexed owner,
109-
address indexed spender,
110-
uint256 value
111-
);
97+
event Approval(address indexed owner, address indexed spender, uint256 value);
11298
}

0 commit comments

Comments
 (0)