|
| 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