Skip to content

Commit 5e8e2ad

Browse files
committed
feat: collateral swap router
1 parent f312246 commit 5e8e2ad

File tree

4 files changed

+1974
-23
lines changed

4 files changed

+1974
-23
lines changed
Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
pragma solidity ^0.8;
3+
4+
import {IEVC} from "evc/EthereumVaultConnector.sol";
5+
6+
import {CowWrapper, CowSettlement} from "./vendor/CowWrapper.sol";
7+
import {IERC20} from "euler-vault-kit/src/EVault/IEVault.sol";
8+
import {SafeERC20Lib} from "euler-vault-kit/src/EVault/shared/lib/SafeERC20Lib.sol";
9+
import {PreApprovedHashes} from "./PreApprovedHashes.sol";
10+
11+
/// @title CowEvcCollateralSwapWrapper
12+
/// @notice A specialized wrapper for swapping collateral between vaults with EVC
13+
/// @dev This wrapper enables atomic collateral swaps:
14+
/// 1. Enable new collateral vault
15+
/// 2. Transfer collateral from EVC subaccount to main account (if using subaccount)
16+
/// 3. Execute settlement to swap collateral (new collateral is deposited directly into user's account)
17+
/// All operations are atomic within EVC batch
18+
contract CowEvcCollateralSwapWrapper is CowWrapper, PreApprovedHashes {
19+
IEVC public immutable EVC;
20+
21+
/// @dev The EIP-712 domain type hash used for computing the domain
22+
/// separator.
23+
bytes32 private constant DOMAIN_TYPE_HASH =
24+
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
25+
26+
/// @dev The EIP-712 domain name used for computing the domain separator.
27+
bytes32 private constant DOMAIN_NAME = keccak256("CowEvcCollateralSwapWrapper");
28+
29+
/// @dev The EIP-712 domain version used for computing the domain separator.
30+
bytes32 private constant DOMAIN_VERSION = keccak256("1");
31+
32+
/// @dev The marker value for a sell order for computing the order struct
33+
/// hash. This allows the EIP-712 compatible wallets to display a
34+
/// descriptive string for the order kind (instead of 0 or 1).
35+
///
36+
/// This value is pre-computed from the following expression:
37+
/// ```
38+
/// keccak256("sell")
39+
/// ```
40+
bytes32 private constant KIND_SELL = hex"f3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775";
41+
42+
/// @dev The OrderKind marker value for a buy order for computing the order
43+
/// struct hash.
44+
///
45+
/// This value is pre-computed from the following expression:
46+
/// ```
47+
/// keccak256("buy")
48+
/// ```
49+
bytes32 private constant KIND_BUY = hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc";
50+
51+
/// @dev The domain separator used for signing orders that gets mixed in
52+
/// making signatures for different domains incompatible. This domain
53+
/// separator is computed following the EIP-712 standard and has replay
54+
/// protection mixed in so that signed orders are only valid for specific
55+
/// this contract.
56+
bytes32 public immutable DOMAIN_SEPARATOR;
57+
58+
//// @dev The EVC nonce namespace to use when calling `EVC.permit` to authorize this contract.
59+
uint256 public immutable NONCE_NAMESPACE;
60+
61+
/// @dev A descriptive label for this contract, as required by CowWrapper
62+
string public override name = "Euler EVC - Collateral Swap";
63+
64+
/// @dev Indicates that the current operation cannot be completed with the given msgSender
65+
error Unauthorized(address msgSender);
66+
67+
/// @dev Indicates that the pre-approved hash is no longer able to be executed because the block timestamp is too old
68+
error OperationDeadlineExceeded(uint256 validToTimestamp, uint256 currentTimestamp);
69+
70+
/// @dev Indicates that the collateral swap cannot be executed because the necessary pricing data is not present in the `tokens`/`clearingPrices` variable
71+
error PricesNotFoundInSettlement(address fromVault, address toVault);
72+
73+
/// @dev Indicates that a user attempted to interact with an account that is not their own
74+
error SubaccountMustBeControlledByOwner(address subaccount, address owner);
75+
76+
/// @dev Emitted when collateral is swapped via this wrapper
77+
event CowEvcCollateralSwapped(
78+
address indexed owner,
79+
address account,
80+
address indexed fromVault,
81+
address indexed toVault,
82+
uint256 swapAmount,
83+
bytes32 kind
84+
);
85+
86+
constructor(address _evc, CowSettlement _settlement) CowWrapper(_settlement) {
87+
EVC = IEVC(_evc);
88+
NONCE_NAMESPACE = uint256(uint160(address(this)));
89+
90+
DOMAIN_SEPARATOR =
91+
keccak256(abi.encode(DOMAIN_TYPE_HASH, DOMAIN_NAME, DOMAIN_VERSION, block.chainid, address(this)));
92+
}
93+
94+
/**
95+
* @notice A command to swap collateral between vaults
96+
* @dev This structure is used, combined with domain separator, to indicate a pre-approved hash.
97+
* the `deadline` is used for deduplication checking, so be careful to ensure this value is unique.
98+
*/
99+
struct CollateralSwapParams {
100+
/**
101+
* @dev The ethereum address that has permission to operate upon the account
102+
*/
103+
address owner;
104+
105+
/**
106+
* @dev The subaccount to swap collateral from. Learn more about Euler subaccounts https://evc.wtf/docs/concepts/internals/sub-accounts
107+
*/
108+
address account;
109+
110+
/**
111+
* @dev A date by which this operation must be completed
112+
*/
113+
uint256 deadline;
114+
115+
/**
116+
* @dev The source collateral vault (what we're swapping from)
117+
*/
118+
address fromVault;
119+
120+
/**
121+
* @dev The destination collateral vault (what we're swapping to)
122+
*/
123+
address toVault;
124+
125+
/**
126+
* @dev The amount of collateral to swap from the source vault
127+
*/
128+
uint256 swapAmount;
129+
130+
/**
131+
* @dev Effectively determines whether this is an exactIn or exactOut order. Must be either KIND_BUY or KIND_SELL as defined in GPv2Order. Should be the same as whats in the actual order.
132+
*/
133+
bytes32 kind;
134+
}
135+
136+
function _parseCollateralSwapParams(bytes calldata wrapperData)
137+
internal
138+
pure
139+
returns (CollateralSwapParams memory params, bytes memory signature, bytes calldata remainingWrapperData)
140+
{
141+
(params, signature) = abi.decode(wrapperData, (CollateralSwapParams, bytes));
142+
143+
// Calculate consumed bytes for abi.encode(CollateralSwapParams, bytes)
144+
// Structure:
145+
// - 32 bytes: offset to params (0x40)
146+
// - 32 bytes: offset to signature
147+
// - 224 bytes: params data (7 fields × 32 bytes)
148+
// - 32 bytes: signature length
149+
// - N bytes: signature data (padded to 32-byte boundary)
150+
uint256 consumed = 224 + 64 + ((signature.length + 31) & ~uint256(31));
151+
152+
remainingWrapperData = wrapperData[consumed:];
153+
}
154+
155+
/// @notice Helper function to compute the hash that would be approved
156+
/// @param params The CollateralSwapParams to hash
157+
/// @return The hash of the signed calldata for these params
158+
function getApprovalHash(CollateralSwapParams memory params) external view returns (bytes32) {
159+
return _getApprovalHash(params);
160+
}
161+
162+
function _getApprovalHash(CollateralSwapParams memory params) internal view returns (bytes32 digest) {
163+
bytes32 structHash;
164+
bytes32 separator = DOMAIN_SEPARATOR;
165+
assembly ("memory-safe") {
166+
structHash := keccak256(params, 224)
167+
let ptr := mload(0x40)
168+
mstore(ptr, "\x19\x01")
169+
mstore(add(ptr, 0x02), separator)
170+
mstore(add(ptr, 0x22), structHash)
171+
digest := keccak256(ptr, 0x42)
172+
}
173+
}
174+
175+
function parseWrapperData(bytes calldata wrapperData)
176+
external
177+
pure
178+
override
179+
returns (bytes calldata remainingWrapperData)
180+
{
181+
(,, remainingWrapperData) = _parseCollateralSwapParams(wrapperData);
182+
}
183+
184+
function getSignedCalldata(CollateralSwapParams memory params) external view returns (bytes memory) {
185+
return abi.encodeCall(IEVC.batch, _getSignedCalldata(params));
186+
}
187+
188+
function _getSignedCalldata(CollateralSwapParams memory params)
189+
internal
190+
view
191+
returns (IEVC.BatchItem[] memory items)
192+
{
193+
items = new IEVC.BatchItem[](1);
194+
195+
// Enable the destination collateral vault for the account
196+
items[0] = IEVC.BatchItem({
197+
onBehalfOfAccount: address(0),
198+
targetContract: address(EVC),
199+
value: 0,
200+
data: abi.encodeCall(IEVC.enableCollateral, (params.account, params.toVault))
201+
});
202+
}
203+
204+
/// @notice Implementation of GPv2Wrapper._wrap - executes EVC operations to swap collateral
205+
/// @param settleData Data which will be used for the parameters in a call to `CowSettlement.settle`
206+
/// @param wrapperData Additional data containing CollateralSwapParams
207+
function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData)
208+
internal
209+
override
210+
{
211+
// Decode wrapper data into CollateralSwapParams
212+
CollateralSwapParams memory params;
213+
bytes memory signature;
214+
(params, signature,) = _parseCollateralSwapParams(wrapperData);
215+
216+
// Check if the signed calldata hash is pre-approved
217+
IEVC.BatchItem[] memory signedItems = _getSignedCalldata(params);
218+
bool isPreApproved = signature.length == 0 && _consumePreApprovedHash(params.owner, _getApprovalHash(params));
219+
220+
// Build the EVC batch items for swapping collateral
221+
IEVC.BatchItem[] memory items = new IEVC.BatchItem[](isPreApproved ? signedItems.length + 1 : 2);
222+
223+
uint256 itemIndex = 0;
224+
225+
// 1. There are two ways this contract can be executed: either the user approves this contract as
226+
// an operator and supplies a pre-approved hash for the operation to take, or they submit a permit hash
227+
// for this specific instance
228+
if (!isPreApproved) {
229+
items[itemIndex++] = IEVC.BatchItem({
230+
onBehalfOfAccount: address(0),
231+
targetContract: address(EVC),
232+
value: 0,
233+
data: abi.encodeCall(
234+
IEVC.permit,
235+
(
236+
params.owner,
237+
address(this),
238+
uint256(NONCE_NAMESPACE),
239+
EVC.getNonce(bytes19(bytes20(params.owner)), NONCE_NAMESPACE),
240+
params.deadline,
241+
0,
242+
abi.encodeCall(EVC.batch, signedItems),
243+
signature
244+
)
245+
)
246+
});
247+
} else {
248+
require(params.deadline >= block.timestamp, OperationDeadlineExceeded(params.deadline, block.timestamp));
249+
// copy the operations to execute. we can operate on behalf of the user directly
250+
for (; itemIndex < signedItems.length; itemIndex++) {
251+
items[itemIndex] = signedItems[itemIndex];
252+
}
253+
}
254+
255+
// 2. Settlement call
256+
items[itemIndex] = IEVC.BatchItem({
257+
onBehalfOfAccount: address(this),
258+
targetContract: address(this),
259+
value: 0,
260+
data: abi.encodeCall(this.evcInternalSwap, (settleData, wrapperData, remainingWrapperData))
261+
});
262+
263+
// 3. Account status check (automatically done by EVC at end of batch)
264+
// For more info, see: https://evc.wtf/docs/concepts/internals/account-status-checks
265+
// No explicit item needed - EVC handles this
266+
267+
// Execute all items in a single batch
268+
EVC.batch(items);
269+
270+
emit CowEvcCollateralSwapped(
271+
params.owner, params.account, params.fromVault, params.toVault, params.swapAmount, params.kind
272+
);
273+
}
274+
275+
function _findRatePrices(bytes calldata settleData, address fromVault, address toVault)
276+
internal
277+
pure
278+
returns (uint256 fromVaultPrice, uint256 toVaultPrice)
279+
{
280+
(address[] memory tokens, uint256[] memory clearingPrices,,) = abi.decode(
281+
settleData[4:], (address[], uint256[], CowSettlement.CowTradeData[], CowSettlement.CowInteractionData[][3])
282+
);
283+
for (uint256 i = 0; i < tokens.length; i++) {
284+
if (tokens[i] == fromVault) {
285+
fromVaultPrice = clearingPrices[i];
286+
} else if (tokens[i] == toVault) {
287+
toVaultPrice = clearingPrices[i];
288+
}
289+
}
290+
require(fromVaultPrice != 0 && toVaultPrice != 0, PricesNotFoundInSettlement(fromVault, toVault));
291+
}
292+
293+
/// @notice Internal swap function called by EVC
294+
function evcInternalSwap(
295+
bytes calldata settleData,
296+
bytes calldata wrapperData,
297+
bytes calldata remainingWrapperData
298+
) external payable {
299+
require(msg.sender == address(EVC), Unauthorized(msg.sender));
300+
(address onBehalfOfAccount,) = EVC.getCurrentOnBehalfOfAccount(address(0));
301+
require(onBehalfOfAccount == address(this), Unauthorized(onBehalfOfAccount));
302+
303+
CollateralSwapParams memory params;
304+
(params,,) = _parseCollateralSwapParams(wrapperData);
305+
_evcInternalSwap(settleData, remainingWrapperData, params);
306+
}
307+
308+
function _evcInternalSwap(
309+
bytes calldata settleData,
310+
bytes calldata remainingWrapperData,
311+
CollateralSwapParams memory params
312+
) internal {
313+
// If a subaccount is being used, we need to transfer the required amount of collateral for the trade into the owner's wallet.
314+
// This is required becuase the settlement contract can only pull funds from the wallet that signed the transaction.
315+
// Since its not possible for a subaccount to sign a transaction due to the private key not existing and their being no
316+
// contract deployed to the subaccount address, transferring to the owner's account is the only option.
317+
// Additionally, we don't transfer this collateral directly to the settlement contract because the settlement contract
318+
// requires receiving of funds from the user's wallet, and cannot be put in the contract in advance.
319+
if (params.owner != params.account) {
320+
require(
321+
bytes19(bytes20(params.owner)) == bytes19(bytes20(params.account)),
322+
SubaccountMustBeControlledByOwner(params.account, params.owner)
323+
);
324+
325+
uint256 transferAmount = params.swapAmount;
326+
327+
if (params.kind == KIND_BUY) {
328+
(uint256 fromVaultPrice, uint256 toVaultPrice) =
329+
_findRatePrices(settleData, params.fromVault, params.toVault);
330+
transferAmount = params.swapAmount * toVaultPrice / fromVaultPrice;
331+
}
332+
333+
SafeERC20Lib.safeTransferFrom(
334+
IERC20(params.fromVault), params.account, params.owner, transferAmount, address(0)
335+
);
336+
}
337+
338+
// Use GPv2Wrapper's _internalSettle to call the settlement contract
339+
// wrapperData is empty since we've already processed it in _wrap
340+
_internalSettle(settleData, remainingWrapperData);
341+
}
342+
}

0 commit comments

Comments
 (0)