Skip to content

Commit f312246

Browse files
committed
feat: close position wrapper
1 parent 7780c3f commit f312246

File tree

4 files changed

+1952
-1
lines changed

4 files changed

+1952
-1
lines changed

src/CowEvcClosePositionWrapper.sol

Lines changed: 395 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,395 @@
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, ICowSettlement} from "./CowWrapper.sol";
7+
import {IERC4626, IBorrowing, 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 CowEvcClosePositionWrapper
12+
/// @notice A specialized wrapper for closing leveraged positions with EVC
13+
/// @dev This wrapper hardcodes the EVC operations needed to close a position:
14+
/// 1. Execute settlement to acquire repayment assets
15+
/// 2. Repay debt and return remaining assets to user
16+
/// @dev The settle call by this order should be performing the necessary swap
17+
/// from collateralVault -> IERC20(borrowVault.asset()). The recipient of the
18+
/// swap should *THIS* contract so that it can repay on behalf of the owner. Furthermore,
19+
/// the order should be of type GPv2Order.KIND_BUY to prevent excess from being sent to the contract.
20+
/// If a full close is being performed, leave a small buffer for intrest accumultation, and the dust will
21+
/// be returned to the owner's wallet.
22+
contract CowEvcClosePositionWrapper is CowWrapper, PreApprovedHashes {
23+
IEVC public immutable EVC;
24+
25+
/// @dev The EIP-712 domain type hash used for computing the domain
26+
/// separator.
27+
bytes32 private constant DOMAIN_TYPE_HASH =
28+
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
29+
30+
/// @dev The EIP-712 domain name used for computing the domain separator.
31+
bytes32 private constant DOMAIN_NAME = keccak256("CowEvcClosePositionWrapper");
32+
33+
/// @dev The EIP-712 domain version used for computing the domain separator.
34+
bytes32 private constant DOMAIN_VERSION = keccak256("1");
35+
36+
/// @dev The marker value for a sell order for computing the order struct
37+
/// hash. This allows the EIP-712 compatible wallets to display a
38+
/// descriptive string for the order kind (instead of 0 or 1).
39+
///
40+
/// This value is pre-computed from the following expression:
41+
/// ```
42+
/// keccak256("sell")
43+
/// ```
44+
bytes32 private constant KIND_SELL = hex"f3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775";
45+
46+
/// @dev The OrderKind marker value for a buy order for computing the order
47+
/// struct hash.
48+
///
49+
/// This value is pre-computed from the following expression:
50+
/// ```
51+
/// keccak256("buy")
52+
/// ```
53+
bytes32 private constant KIND_BUY = hex"6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc";
54+
55+
/// @dev The domain separator used for signing orders that gets mixed in
56+
/// making signatures for different domains incompatible. This domain
57+
/// separator is computed following the EIP-712 standard and has replay
58+
/// protection mixed in so that signed orders are only valid for specific
59+
/// this contract.
60+
bytes32 public immutable DOMAIN_SEPARATOR;
61+
62+
//// @dev The EVC nonce namespace to use when calling `EVC.permit` to authorize this contract.
63+
uint256 public immutable NONCE_NAMESPACE;
64+
65+
/// @dev A descriptive label for this contract, as required by CowWrapper
66+
string public override name = "Euler EVC - Close Position";
67+
68+
/// @dev Indicates that the current operation cannot be completed with the given msgSender
69+
error Unauthorized(address msgSender);
70+
71+
/// @dev Indicates that the pre-approved hash is no longer able to be executed because the block timestamp is too old
72+
error OperationDeadlineExceeded(uint256 validToTimestamp, uint256 currentTimestamp);
73+
74+
/// @dev Indicates that this contract did not receive enough repayment assets from the settlement contract in order to cover all user's orders
75+
error InsufficientRepaymentAsset(address vault, uint256 balanceAmount, uint256 repayAmount);
76+
77+
/// @dev Indicates that the close order cannot be executed becuase the necessary pricing data is not present in the `tokens`/`clearingPrices` variable
78+
error PricesNotFoundInSettlement(address collateralVaultToken, address borrowToken);
79+
80+
/// @dev Indicates that a user attempted to interact with an account that is not their own
81+
error SubaccountMustBeControlledByOwner(address subaccount, address owner);
82+
83+
/// @dev Emitted when a position is closed via this wrapper
84+
event CowEvcPositionClosed(
85+
address indexed owner,
86+
address account,
87+
address indexed borrowVault,
88+
address indexed collateralVault,
89+
uint256 collateralAmount,
90+
uint256 repayAmount,
91+
bytes32 kind
92+
);
93+
94+
constructor(address _evc, ICowSettlement _settlement) CowWrapper(_settlement) {
95+
EVC = IEVC(_evc);
96+
NONCE_NAMESPACE = uint256(uint160(address(this)));
97+
98+
DOMAIN_SEPARATOR =
99+
keccak256(abi.encode(DOMAIN_TYPE_HASH, DOMAIN_NAME, DOMAIN_VERSION, block.chainid, address(this)));
100+
}
101+
102+
/**
103+
* @notice A command to close a debt position against an euler vault by repaying debt and returning collateral.
104+
* @dev This structure is used, combined with domain separator, to indicate a pre-approved hash.
105+
* the `deadline` is used for deduplication checking, so be careful to ensure this value is unique.
106+
*/
107+
108+
struct ClosePositionParams {
109+
/**
110+
* @dev The ethereum address that has permission to operate upon the account
111+
*/
112+
address owner;
113+
114+
/**
115+
* @dev The subaccount to close the position on. Learn more about Euler subaccounts https://evc.wtf/docs/concepts/internals/sub-accounts
116+
*/
117+
address account;
118+
119+
/**
120+
* @dev A date by which this operation must be completed
121+
*/
122+
uint256 deadline;
123+
124+
/**
125+
* @dev The Euler vault from which debt was borrowed
126+
*/
127+
address borrowVault;
128+
129+
/**
130+
* @dev The Euler vault used as collateral
131+
*/
132+
address collateralVault;
133+
134+
/**
135+
* @dev
136+
*/
137+
uint256 collateralAmount;
138+
139+
/**
140+
* @dev The amount of debt to repay. If greater than the actual debt, the full debt is repaid
141+
*/
142+
uint256 repayAmount;
143+
144+
/**
145+
* @dev Whether the `collateralAmount` or `repayAmount` is the exact amount. Either `GPv2Order.KIND_BUY` or `GPv2Order.KIND_SELL`
146+
*/
147+
bytes32 kind;
148+
}
149+
150+
function _parseClosePositionParams(bytes calldata wrapperData)
151+
internal
152+
pure
153+
returns (ClosePositionParams memory params, bytes memory signature, bytes calldata remainingWrapperData)
154+
{
155+
(params, signature) = abi.decode(wrapperData, (ClosePositionParams, bytes));
156+
157+
// Calculate consumed bytes for abi.encode(ClosePositionParams, bytes)
158+
// Structure:
159+
// - 32 bytes: offset to params (0x40)
160+
// - 32 bytes: offset to signature
161+
// - 256 bytes: params data (8 fields × 32 bytes)
162+
// - 32 bytes: signature length
163+
// - N bytes: signature data (padded to 32-byte boundary)
164+
// We can just math this out
165+
uint256 consumed = 256 + 64 + ((signature.length + 31) & ~uint256(31));
166+
167+
remainingWrapperData = wrapperData[consumed:];
168+
}
169+
170+
/// @notice Helper function to compute the hash that would be approved
171+
/// @param params The ClosePositionParams to hash
172+
/// @return The hash of the signed calldata for these params
173+
function getApprovalHash(ClosePositionParams memory params) external view returns (bytes32) {
174+
return _getApprovalHash(params);
175+
}
176+
177+
function _getApprovalHash(ClosePositionParams memory params) internal view returns (bytes32 digest) {
178+
bytes32 structHash;
179+
bytes32 separator = DOMAIN_SEPARATOR;
180+
assembly ("memory-safe") {
181+
structHash := keccak256(params, 256)
182+
let ptr := mload(0x40)
183+
mstore(ptr, "\x19\x01")
184+
mstore(add(ptr, 0x02), separator)
185+
mstore(add(ptr, 0x22), structHash)
186+
digest := keccak256(ptr, 0x42)
187+
}
188+
}
189+
190+
function parseWrapperData(bytes calldata wrapperData)
191+
external
192+
pure
193+
override
194+
returns (bytes calldata remainingWrapperData)
195+
{
196+
(,, remainingWrapperData) = _parseClosePositionParams(wrapperData);
197+
}
198+
199+
function getSignedCalldata(ClosePositionParams memory params) external view returns (bytes memory) {
200+
return abi.encodeCall(IEVC.batch, _getSignedCalldata(params));
201+
}
202+
203+
function _getSignedCalldata(ClosePositionParams memory params)
204+
internal
205+
view
206+
returns (IEVC.BatchItem[] memory items)
207+
{
208+
items = new IEVC.BatchItem[](1);
209+
210+
// 1. Repay debt and return remaining assets
211+
items[0] = IEVC.BatchItem({
212+
onBehalfOfAccount: params.account,
213+
targetContract: address(this),
214+
value: 0,
215+
data: abi.encodeCall(this.helperRepay, (params.borrowVault, params.owner, params.account))
216+
});
217+
}
218+
219+
/// @notice Called by the EVC after a CoW swap is completed to repay the user's debt. Will use all available collateral in the user's account to do so.
220+
/// @param vault The Euler vault in which the repayment should be made
221+
/// @param owner The address that should be receiving any surplus dust that may exist after the repayment is complete
222+
/// @param account The subaccount that should be receiving the repayment of debt
223+
function helperRepay(address vault, address owner, address account) external {
224+
require(msg.sender == address(EVC), Unauthorized(msg.sender));
225+
(address onBehalfOfAccount,) = EVC.getCurrentOnBehalfOfAccount(address(0));
226+
require(onBehalfOfAccount == account, Unauthorized(onBehalfOfAccount));
227+
228+
IERC20 asset = IERC20(IERC4626(vault).asset());
229+
230+
uint256 debtAmount = IBorrowing(vault).debtOf(account);
231+
232+
// repay as much debt as we can
233+
uint256 repayAmount = asset.balanceOf(owner);
234+
if (repayAmount > debtAmount) {
235+
// the user intends to repay all their debt. we will revert if their balance is not sufficient.
236+
repayAmount = debtAmount;
237+
}
238+
239+
// pull funds from the user (they should have approved spending by this contract)
240+
SafeERC20Lib.safeTransferFrom(asset, owner, address(this), repayAmount, address(0));
241+
242+
// repay what was requested on the vault
243+
asset.approve(vault, repayAmount);
244+
IBorrowing(vault).repay(repayAmount, account);
245+
}
246+
247+
/// @notice Implementation of GPv2Wrapper._wrap - executes EVC operations to close a position
248+
/// @param settleData Data which will be used for the parameters in a call to `CowSettlement.settle`
249+
/// @param wrapperData Additional data containing ClosePositionParams
250+
function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData)
251+
internal
252+
override
253+
{
254+
// Decode wrapper data into ClosePositionParams
255+
ClosePositionParams memory params;
256+
bytes memory signature;
257+
(params, signature,) = _parseClosePositionParams(wrapperData);
258+
259+
// Check if the signed calldata hash is pre-approved
260+
IEVC.BatchItem[] memory signedItems = _getSignedCalldata(params);
261+
bool isPreApproved = signature.length == 0 && _consumePreApprovedHash(params.owner, _getApprovalHash(params));
262+
263+
// Calculate the number of items needed
264+
uint256 baseItemCount = 2;
265+
uint256 itemCount = isPreApproved ? baseItemCount - 1 + signedItems.length : baseItemCount;
266+
267+
IEVC.BatchItem[] memory items = new IEVC.BatchItem[](itemCount);
268+
uint256 itemIndex = 0;
269+
270+
// Build the EVC batch items for closing a position
271+
// 1. Settlement call
272+
items[itemIndex++] = IEVC.BatchItem({
273+
onBehalfOfAccount: address(this),
274+
targetContract: address(this),
275+
value: 0,
276+
data: abi.encodeCall(this.evcInternalSettle, (settleData, wrapperData, remainingWrapperData))
277+
});
278+
279+
// 2. There are two ways this contract can be executed: either the user approves this contract as
280+
// an operator and supplies a pre-approved hash for the operation to take, or they submit a permit hash
281+
// for this specific instance
282+
if (!isPreApproved) {
283+
items[itemIndex] = IEVC.BatchItem({
284+
onBehalfOfAccount: address(0),
285+
targetContract: address(EVC),
286+
value: 0,
287+
data: abi.encodeCall(
288+
IEVC.permit,
289+
(
290+
params.owner,
291+
address(this),
292+
uint256(NONCE_NAMESPACE),
293+
EVC.getNonce(bytes19(bytes20(params.owner)), NONCE_NAMESPACE),
294+
params.deadline,
295+
0,
296+
abi.encodeCall(EVC.batch, signedItems),
297+
signature
298+
)
299+
)
300+
});
301+
} else {
302+
require(params.deadline >= block.timestamp, OperationDeadlineExceeded(params.deadline, block.timestamp));
303+
// copy the operations to execute. we can operate on behalf of the user directly
304+
uint256 signedItemIndex = 0;
305+
for (; itemIndex < itemCount; itemIndex++) {
306+
items[itemIndex] = signedItems[signedItemIndex++];
307+
}
308+
}
309+
310+
// 3. Account status check (automatically done by EVC at end of batch)
311+
// For more info, see: https://evc.wtf/docs/concepts/internals/account-status-checks
312+
// No explicit item needed - EVC handles this
313+
314+
// Execute all items in a single batch
315+
EVC.batch(items);
316+
317+
emit CowEvcPositionClosed(
318+
params.owner,
319+
params.account,
320+
params.borrowVault,
321+
params.collateralVault,
322+
params.collateralAmount,
323+
params.repayAmount,
324+
params.kind
325+
);
326+
}
327+
328+
function _findRatePrices(bytes calldata settleData, address collateralVault, address borrowVault)
329+
internal
330+
view
331+
returns (uint256 collateralVaultPrice, uint256 borrowPrice)
332+
{
333+
address borrowAsset = IERC4626(borrowVault).asset();
334+
(address[] memory tokens, uint256[] memory clearingPrices,,) =
335+
abi.decode(settleData[4:], (address[], uint256[], ICowSettlement.Trade[], ICowSettlement.Interaction[][3]));
336+
for (uint256 i = 0; i < tokens.length; i++) {
337+
if (tokens[i] == collateralVault) {
338+
collateralVaultPrice = clearingPrices[i];
339+
} else if (tokens[i] == borrowAsset) {
340+
borrowPrice = clearingPrices[i];
341+
}
342+
}
343+
require(collateralVaultPrice != 0 && borrowPrice != 0, PricesNotFoundInSettlement(collateralVault, borrowAsset));
344+
}
345+
346+
/// @notice Internal settlement function called by EVC
347+
function evcInternalSettle(
348+
bytes calldata settleData,
349+
bytes calldata wrapperData,
350+
bytes calldata remainingWrapperData
351+
) external payable {
352+
require(msg.sender == address(EVC), Unauthorized(msg.sender));
353+
(address onBehalfOfAccount,) = EVC.getCurrentOnBehalfOfAccount(address(0));
354+
require(onBehalfOfAccount == address(this), Unauthorized(onBehalfOfAccount));
355+
356+
ClosePositionParams memory params;
357+
(params,,) = _parseClosePositionParams(wrapperData);
358+
_evcInternalSettle(settleData, remainingWrapperData, params);
359+
}
360+
361+
function _evcInternalSettle(
362+
bytes calldata settleData,
363+
bytes calldata remainingWrapperData,
364+
ClosePositionParams memory params
365+
) internal {
366+
// If a subaccount is being used, we need to transfer the required amount of collateral for the trade into the owner's wallet.
367+
// This is required becuase the settlement contract can only pull funds from the wallet that signed the transaction.
368+
// Since its not possible for a subaccount to sign a transaction due to the private key not existing and their being no
369+
// contract deployed to the subaccount address, transferring to the owner's account is the only option.
370+
// Additionally, we don't transfer this collateral directly to the settlement contract because the settlement contract
371+
// requires receiving of funds from the user's wallet, and cannot be put in the contract in advance.
372+
if (params.owner != params.account) {
373+
require(
374+
bytes19(bytes20(params.owner)) == bytes19(bytes20(params.account)),
375+
SubaccountMustBeControlledByOwner(params.account, params.owner)
376+
);
377+
378+
uint256 transferAmount = params.collateralAmount;
379+
380+
if (params.kind == KIND_BUY) {
381+
(uint256 collateralVaultPrice, uint256 borrowPrice) =
382+
_findRatePrices(settleData, params.collateralVault, params.borrowVault);
383+
transferAmount = params.repayAmount * borrowPrice / collateralVaultPrice;
384+
}
385+
386+
SafeERC20Lib.safeTransferFrom(
387+
IERC20(params.collateralVault), params.account, params.owner, transferAmount, address(0)
388+
);
389+
}
390+
391+
// Use GPv2Wrapper's _internalSettle to call the settlement contract
392+
// wrapperData is empty since we've already processed it in _wrap
393+
_next(settleData, remainingWrapperData);
394+
}
395+
}

0 commit comments

Comments
 (0)