-
Notifications
You must be signed in to change notification settings - Fork 0
feat: open position wrapper #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: chore/remove-stuff
Are you sure you want to change the base?
Changes from 68 commits
ee6eb57
0172171
3e72e99
453c007
0f635c7
5ebd847
b168d45
e56eac3
2e7f051
de12e99
c3dc655
217af04
4f6c81e
2c9704a
8982db0
e9afd40
35cf23b
938b8e9
5ca3ae5
ec9d67d
9b7a904
164efff
2d7989b
413d162
3ff8dc0
ca207e7
b2a9cae
d65ca1a
dad08de
093c0f1
39f24df
30deecf
c138c72
47ad1cb
7f9d8df
9de833a
9842d70
3a1a2d1
bdaa880
49f11bb
08dc6b3
6f63f1b
6de1cc7
6a0a1c4
eb3654a
c579571
5782561
74514cc
2abfed2
c4350c5
8f6e579
6cad6ee
33e992c
46f6263
b0f7d8f
bbe733c
76cdeb0
ee83ec1
7bd929e
1c9bc2e
b363e7a
49fbed9
780556f
8cada64
f9a5071
5d9e085
156a830
7780c3f
a4cdeae
f91e31f
a1a3ee1
c8989f8
96cda14
d70bf35
1831334
86a9671
04e8c5f
3892978
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,293 @@ | ||||||||||||||||
| // SPDX-License-Identifier: MIT OR Apache-2.0 | ||||||||||||||||
| pragma solidity ^0.8; | ||||||||||||||||
|
|
||||||||||||||||
| import {IEVC} from "evc/EthereumVaultConnector.sol"; | ||||||||||||||||
|
|
||||||||||||||||
| import {CowWrapper, ICowSettlement} from "./CowWrapper.sol"; | ||||||||||||||||
| import {IERC4626, IBorrowing} from "euler-vault-kit/src/EVault/IEVault.sol"; | ||||||||||||||||
| import {PreApprovedHashes} from "./PreApprovedHashes.sol"; | ||||||||||||||||
|
|
||||||||||||||||
| /// @title CowEvcOpenPositionWrapper | ||||||||||||||||
| /// @notice A specialized wrapper for opening leveraged positions with EVC | ||||||||||||||||
| /// @dev This wrapper hardcodes the EVC operations needed to open a position: | ||||||||||||||||
| /// 1. Enable collateral vault | ||||||||||||||||
| /// 2. Enable controller (borrow vault) | ||||||||||||||||
| /// 3. Deposit collateral | ||||||||||||||||
| /// 4. Borrow assets | ||||||||||||||||
| /// @dev The settle call by this order should be performing the necessary swap | ||||||||||||||||
| /// from IERC20(borrowVault.asset()) -> collateralVault. The recipient of the | ||||||||||||||||
| /// swap should be the `owner` (not this contract). Furthermore, the buyAmountIn should | ||||||||||||||||
| /// be the same as `maxRepayAmount`. | ||||||||||||||||
| contract CowEvcOpenPositionWrapper is CowWrapper, PreApprovedHashes { | ||||||||||||||||
| IEVC public immutable EVC; | ||||||||||||||||
|
|
||||||||||||||||
| /// @dev The EIP-712 domain type hash used for computing the domain | ||||||||||||||||
| /// separator. | ||||||||||||||||
| bytes32 private constant DOMAIN_TYPE_HASH = | ||||||||||||||||
| keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); | ||||||||||||||||
|
|
||||||||||||||||
| /// @dev The EIP-712 domain name used for computing the domain separator. | ||||||||||||||||
| bytes32 private constant DOMAIN_NAME = keccak256("CowEvcOpenPositionWrapper"); | ||||||||||||||||
|
|
||||||||||||||||
| /// @dev The EIP-712 domain version used for computing the domain separator. | ||||||||||||||||
| bytes32 private constant DOMAIN_VERSION = keccak256("1"); | ||||||||||||||||
|
|
||||||||||||||||
| /// @dev The domain separator used for signing orders that gets mixed in | ||||||||||||||||
| /// making signatures for different domains incompatible. This domain | ||||||||||||||||
| /// separator is computed following the EIP-712 standard and has replay | ||||||||||||||||
| /// protection mixed in so that signed orders are only valid for specific | ||||||||||||||||
| /// this contract. | ||||||||||||||||
| bytes32 public immutable DOMAIN_SEPARATOR; | ||||||||||||||||
|
|
||||||||||||||||
| //// @dev The EVC nonce namespace to use when calling `EVC.permit` to authorize this contract. | ||||||||||||||||
| uint256 public immutable NONCE_NAMESPACE; | ||||||||||||||||
|
|
||||||||||||||||
| /// @dev A descriptive label for this contract, as required by CowWrapper | ||||||||||||||||
| string public override name = "Euler EVC - Open Position"; | ||||||||||||||||
|
|
||||||||||||||||
| /// @dev Indicates that the current operation cannot be completed with the given msgSender | ||||||||||||||||
| error Unauthorized(address msgSender); | ||||||||||||||||
|
|
||||||||||||||||
| /// @dev Indicates that the pre-approved hash is no longer able to be executed because the block timestamp is too old | ||||||||||||||||
| error OperationDeadlineExceeded(uint256 validToTimestamp, uint256 currentTimestamp); | ||||||||||||||||
|
|
||||||||||||||||
| /// @dev Emitted when a position is opened via this wrapper | ||||||||||||||||
| event CowEvcPositionOpened( | ||||||||||||||||
| address indexed owner, | ||||||||||||||||
| address account, | ||||||||||||||||
| address indexed collateralVault, | ||||||||||||||||
| address indexed borrowVault, | ||||||||||||||||
| uint256 collateralAmount, | ||||||||||||||||
| uint256 borrowAmount | ||||||||||||||||
| ); | ||||||||||||||||
|
|
||||||||||||||||
| constructor(address _evc, ICowSettlement _settlement) CowWrapper(_settlement) { | ||||||||||||||||
| EVC = IEVC(_evc); | ||||||||||||||||
| NONCE_NAMESPACE = uint256(uint160(address(this))); | ||||||||||||||||
|
|
||||||||||||||||
| DOMAIN_SEPARATOR = | ||||||||||||||||
| keccak256(abi.encode(DOMAIN_TYPE_HASH, DOMAIN_NAME, DOMAIN_VERSION, block.chainid, address(this))); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * @notice A command to open a debt position against an euler vault using collateral as backing. | ||||||||||||||||
| * @dev This structure is used, combined with domain separator, to indicate a pre-approved hash. | ||||||||||||||||
| * the `deadline` is used for deduplication checking, so be careful to ensure this value is unique. | ||||||||||||||||
| */ | ||||||||||||||||
| struct OpenPositionParams { | ||||||||||||||||
| /** | ||||||||||||||||
| * @dev The ethereum address that has permission to operate upon the account | ||||||||||||||||
| */ | ||||||||||||||||
| address owner; | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * @dev The subaccount to open the position on. Learn more about Euler subaccounts https://evc.wtf/docs/concepts/internals/sub-accounts | ||||||||||||||||
| */ | ||||||||||||||||
| address account; | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * @dev A date by which this operation must be completed | ||||||||||||||||
| */ | ||||||||||||||||
| uint256 deadline; | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * @dev The Euler vault to use as collateral | ||||||||||||||||
| */ | ||||||||||||||||
| address collateralVault; | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * @dev The Euler vault to use as leverage | ||||||||||||||||
| */ | ||||||||||||||||
| address borrowVault; | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * @dev The amount of collateral to import as margin. Set this to `0` if the vault already has margin collateral. | ||||||||||||||||
| */ | ||||||||||||||||
| uint256 collateralAmount; | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * @dev The amount of debt to take out. The borrowed tokens will be converted to `collateralVault` tokens and deposited into the account. | ||||||||||||||||
| */ | ||||||||||||||||
| uint256 borrowAmount; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| function _parseOpenPositionParams(bytes calldata wrapperData) | ||||||||||||||||
| internal | ||||||||||||||||
| pure | ||||||||||||||||
| returns (OpenPositionParams memory params, bytes memory signature, bytes calldata remainingWrapperData) | ||||||||||||||||
| { | ||||||||||||||||
| (params, signature) = abi.decode(wrapperData, (OpenPositionParams, bytes)); | ||||||||||||||||
|
|
||||||||||||||||
| // Calculate consumed bytes for abi.encode(OpenPositionParams, bytes) | ||||||||||||||||
| // Structure: | ||||||||||||||||
| // - 32 bytes: offset to params (0x40) | ||||||||||||||||
| // - 32 bytes: offset to signature | ||||||||||||||||
| // - 224 bytes: params data (7 fields × 32 bytes) | ||||||||||||||||
| // - 32 bytes: signature length | ||||||||||||||||
| // - N bytes: signature data (padded to 32-byte boundary) | ||||||||||||||||
| // We can just math this out | ||||||||||||||||
| uint256 consumed = 224 + 64 + ((signature.length + 31) & ~uint256(31)); | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Code Quality: Hardcoded magic number The value Consider using a constant or documenting this more clearly: // OpenPositionParams size: 7 fields × 32 bytes = 224 bytes
uint256 constant OPEN_POSITION_PARAMS_SIZE = 224;
uint256 consumed = OPEN_POSITION_PARAMS_SIZE + 64 + ((signature.length + 31) & ~uint256(31));The same issue exists on line 145 in the assembly block. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maintainability: Magic number should be a named constant The value
Suggested change
The same constant should replace the hardcoded |
||||||||||||||||
|
|
||||||||||||||||
| remainingWrapperData = wrapperData[consumed:]; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /// @notice Helper function to compute the hash that would be approved | ||||||||||||||||
| /// @param params The OpenPositionParams to hash | ||||||||||||||||
| /// @return The hash of the signed calldata for these params | ||||||||||||||||
| function getApprovalHash(OpenPositionParams memory params) external view returns (bytes32) { | ||||||||||||||||
| return _getApprovalHash(params); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| function _getApprovalHash(OpenPositionParams memory params) internal view returns (bytes32 digest) { | ||||||||||||||||
| bytes32 structHash; | ||||||||||||||||
| bytes32 separator = DOMAIN_SEPARATOR; | ||||||||||||||||
| assembly ("memory-safe") { | ||||||||||||||||
| structHash := keccak256(params, 224) | ||||||||||||||||
| let ptr := mload(0x40) | ||||||||||||||||
| mstore(ptr, "\x19\x01") | ||||||||||||||||
| mstore(add(ptr, 0x02), separator) | ||||||||||||||||
| mstore(add(ptr, 0x22), structHash) | ||||||||||||||||
| digest := keccak256(ptr, 0x42) | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| function parseWrapperData(bytes calldata wrapperData) | ||||||||||||||||
| external | ||||||||||||||||
| pure | ||||||||||||||||
| override | ||||||||||||||||
| returns (bytes calldata remainingWrapperData) | ||||||||||||||||
| { | ||||||||||||||||
| (,, remainingWrapperData) = _parseOpenPositionParams(wrapperData); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /// @notice Implementation of GPv2Wrapper._wrap - executes EVC operations to open a position | ||||||||||||||||
| /// @param settleData Data which will be used for the parameters in a call to `CowSettlement.settle` | ||||||||||||||||
| /// @param wrapperData Additional data containing OpenPositionParams | ||||||||||||||||
| function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData) | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Security: No validation of critical parameters The function doesn't validate that:
While some of these would fail later in EVC calls, explicit validation provides clearer error messages and fails faster. |
||||||||||||||||
| internal | ||||||||||||||||
| override | ||||||||||||||||
| { | ||||||||||||||||
| // Decode wrapper data into OpenPositionParams | ||||||||||||||||
| OpenPositionParams memory params; | ||||||||||||||||
| bytes memory signature; | ||||||||||||||||
| (params, signature,) = _parseOpenPositionParams(wrapperData); | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Code Quality: Missing parameter validation Consider validating critical parameters before proceeding:
Suggested change
While these would fail later in EVC calls, explicit validation provides clearer error messages and fails faster. |
||||||||||||||||
|
|
||||||||||||||||
| // Check if the signed calldata hash is pre-approved | ||||||||||||||||
| IEVC.BatchItem[] memory signedItems = _getSignedCalldata(params); | ||||||||||||||||
| bool isPreApproved = signature.length == 0 && _consumePreApprovedHash(params.owner, _getApprovalHash(params)); | ||||||||||||||||
|
|
||||||||||||||||
| // Build the EVC batch items for opening a position | ||||||||||||||||
| IEVC.BatchItem[] memory items = new IEVC.BatchItem[](isPreApproved ? signedItems.length + 1 : 2); | ||||||||||||||||
|
|
||||||||||||||||
| uint256 itemIndex = 0; | ||||||||||||||||
|
|
||||||||||||||||
| // 1. There are two ways this contract can be executed: either the user approves this contract as | ||||||||||||||||
| // and operator and supplies a pre-approved hash for the operation to take, or they submit a permit hash | ||||||||||||||||
| // for this specific instance | ||||||||||||||||
| if (!isPreApproved) { | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Code Quality: Inconsistent deadline validation The pre-approved hash path validates
Suggested change
|
||||||||||||||||
| items[itemIndex++] = IEVC.BatchItem({ | ||||||||||||||||
| onBehalfOfAccount: address(0), | ||||||||||||||||
| targetContract: address(EVC), | ||||||||||||||||
| value: 0, | ||||||||||||||||
| data: abi.encodeCall( | ||||||||||||||||
| IEVC.permit, | ||||||||||||||||
| ( | ||||||||||||||||
| params.owner, | ||||||||||||||||
| address(this), | ||||||||||||||||
| uint256(NONCE_NAMESPACE), | ||||||||||||||||
| EVC.getNonce(bytes19(bytes20(params.owner)), NONCE_NAMESPACE), | ||||||||||||||||
| params.deadline, | ||||||||||||||||
| 0, | ||||||||||||||||
| abi.encodeCall(EVC.batch, signedItems), | ||||||||||||||||
| signature | ||||||||||||||||
| ) | ||||||||||||||||
| ) | ||||||||||||||||
| }); | ||||||||||||||||
| } else { | ||||||||||||||||
| require(params.deadline >= block.timestamp, OperationDeadlineExceeded(params.deadline, block.timestamp)); | ||||||||||||||||
| // copy the operations to execute. we can operate on behalf of the user directly | ||||||||||||||||
| for (; itemIndex < signedItems.length; itemIndex++) { | ||||||||||||||||
| items[itemIndex] = signedItems[itemIndex]; | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // 2. Settlement call | ||||||||||||||||
| items[itemIndex] = IEVC.BatchItem({ | ||||||||||||||||
| onBehalfOfAccount: address(this), | ||||||||||||||||
| targetContract: address(this), | ||||||||||||||||
| value: 0, | ||||||||||||||||
| data: abi.encodeCall(this.evcInternalSettle, (settleData, remainingWrapperData)) | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| // 3. Account status check (automatically done by EVC at end of batch) | ||||||||||||||||
| // For more info, see: https://evc.wtf/docs/concepts/internals/account-status-checks | ||||||||||||||||
| // No explicit item needed - EVC handles this | ||||||||||||||||
|
|
||||||||||||||||
| // Execute all items in a single batch | ||||||||||||||||
| EVC.batch(items); | ||||||||||||||||
|
|
||||||||||||||||
| emit CowEvcPositionOpened( | ||||||||||||||||
| params.owner, | ||||||||||||||||
| params.account, | ||||||||||||||||
| params.collateralVault, | ||||||||||||||||
| params.borrowVault, | ||||||||||||||||
| params.collateralAmount, | ||||||||||||||||
| params.borrowAmount | ||||||||||||||||
| ); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| function getSignedCalldata(OpenPositionParams memory params) external view returns (bytes memory) { | ||||||||||||||||
| return abi.encodeCall(IEVC.batch, _getSignedCalldata(params)); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| function _getSignedCalldata(OpenPositionParams memory params) | ||||||||||||||||
| internal | ||||||||||||||||
| view | ||||||||||||||||
| returns (IEVC.BatchItem[] memory items) | ||||||||||||||||
| { | ||||||||||||||||
| items = new IEVC.BatchItem[](4); | ||||||||||||||||
|
|
||||||||||||||||
| // 1. Enable collateral | ||||||||||||||||
| items[0] = IEVC.BatchItem({ | ||||||||||||||||
| onBehalfOfAccount: address(0), | ||||||||||||||||
| targetContract: address(EVC), | ||||||||||||||||
| value: 0, | ||||||||||||||||
| data: abi.encodeCall(IEVC.enableCollateral, (params.account, params.collateralVault)) | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| // 2. Enable controller (borrow vault) | ||||||||||||||||
| items[1] = IEVC.BatchItem({ | ||||||||||||||||
| onBehalfOfAccount: address(0), | ||||||||||||||||
| targetContract: address(EVC), | ||||||||||||||||
| value: 0, | ||||||||||||||||
| data: abi.encodeCall(IEVC.enableController, (params.account, params.borrowVault)) | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| // 3. Deposit collateral | ||||||||||||||||
| items[2] = IEVC.BatchItem({ | ||||||||||||||||
| onBehalfOfAccount: params.owner, | ||||||||||||||||
| targetContract: params.collateralVault, | ||||||||||||||||
| value: 0, | ||||||||||||||||
| data: abi.encodeCall(IERC4626.deposit, (params.collateralAmount, params.account)) | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| // 4. Borrow assets | ||||||||||||||||
| items[3] = IEVC.BatchItem({ | ||||||||||||||||
| onBehalfOfAccount: params.account, | ||||||||||||||||
| targetContract: params.borrowVault, | ||||||||||||||||
| value: 0, | ||||||||||||||||
| data: abi.encodeCall(IBorrowing.borrow, (params.borrowAmount, params.owner)) | ||||||||||||||||
| }); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /// @notice Internal settlement function called by EVC | ||||||||||||||||
| function evcInternalSettle(bytes calldata settleData, bytes calldata remainingWrapperData) external payable { | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Security: Potential reentrancy vulnerability While the EVC batch ensures atomicity, there's no reentrancy protection on this external function. An attacker could potentially call The checks |
||||||||||||||||
| require(msg.sender == address(EVC), Unauthorized(msg.sender)); | ||||||||||||||||
| (address onBehalfOfAccount,) = EVC.getCurrentOnBehalfOfAccount(address(0)); | ||||||||||||||||
| require(onBehalfOfAccount == address(this), Unauthorized(onBehalfOfAccount)); | ||||||||||||||||
|
|
||||||||||||||||
| // Use GPv2Wrapper's _internalSettle to call the settlement contract | ||||||||||||||||
| // wrapperData is empty since we've already processed it in _wrap | ||||||||||||||||
| _next(settleData, remainingWrapperData); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security: Missing zero address validation
The constructor doesn't validate that
_evcis not the zero address. If deployed with address(0), allevc.batch()calls would fail, making the wrapper completely non-functional.Consider adding: