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