diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..89f2351 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,161 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This repository contains **Euler-CoW Protocol integration contracts** that enable leveraged position management (opening/closing) through CoW Protocol settlements combined with Ethereum Vault Connector (EVC) operations. The contracts act as "wrappers" that coordinate complex multi-step DeFi operations atomically. + +### Core Architecture + +**Wrapper Pattern**: The codebase uses a chaining wrapper pattern where solvers can execute wrapped settlements that perform custom logic before/during/after CoW Protocol settlements. + +- `CowWrapper.sol`: Base abstract contract providing the wrapper framework + - Validates callers are authenticated solvers + - Implements `wrappedSettle()` entry point + - Provides `_internalSettle()` for continuing the settlement chain + - Wrappers can be chained: Wrapper1 → Wrapper2 → Settlement + +- `CowWrapperHelpers.sol`: Helper utilities for wrapper data parsing and validation +- The CowWrapper is designed to support reentrancy. Additionally, the CowWrapper is designed with gas efficiency in mind, so we only check if the previous contract was part of the trusted wrapper chain. Furthermore, any wrappers that will ever be approved exist will use `CowWrapper.sol` as a base, so its not possible to inject a unauthorized wrapper into the chain without it getting ultimately rejected by the time the settlement contract is reached. + +**Specialized Wrappers**: Two production wrappers implement specific EVC + CoW Protocol workflows: + +1. **`CowEvcOpenPositionWrapper.sol`**: Opens leveraged positions + - Enables collateral vault + - Enables controller (borrow vault) + - Deposits collateral + - Borrows assets + - Executes CoW settlement to swap borrowed assets → collateral + - All operations are atomic within EVC batch + +2. **`CowEvcClosePositionWrapper.sol`**: Closes leveraged positions + - Executes CoW settlement to swap collateral → repayment assets + - Repays debt to borrow vault + - Returns excess assets to user + - Disables collateral if full repayment + - All operations are atomic within EVC batch + +**Authorization Mechanisms**: Both wrappers support two authorization modes: +- **EVC Permit**: One-time permit signature for specific operation +- **Pre-Approved Hashes** (`PreApprovedHashes.sol`): Users pre-approve operation hashes on-chain (useful for EIP-7702 wallet interactions) + +### Key Dependencies + +- **Euler Vault Kit** (`lib/euler-vault-kit`): ERC4626 vault implementation with borrowing +- **Ethereum Vault Connector (EVC)** (`lib/evc`): Batch transaction coordinator with account checking +- **CoW Protocol** (`lib/cow`): DEX aggregator settlement contracts and order libraries + +## Development Commands + +### Build +```bash +forge build +``` + +### Test +```bash +# Run all tests (requires FORK_RPC_URL environment variable) +forge test + +# Run specific test file +forge test --match-path test/CowEvcOpenPositionWrapper.t.sol + +# Run specific test function +forge test --match-test test_OpenPosition + +# Run with verbose output +forge test -vvv +``` + +**Important**: Tests require mainnet fork. Set `FORK_RPC_URL` environment variable to a mainnet RPC endpoint. + +### Format +```bash +forge fmt +``` + +### Gas Snapshots +```bash +forge snapshot +``` + +## Testing Architecture + +**Base Test Contract**: `test/helpers/CowBaseTest.sol` +- Sets up mainnet fork at block 22546006 +- Configures CoW Protocol settlement and authenticator +- Deploys test solver contract +- Sets up test vaults (eSUSDS, eWETH) and tokens +- Provides helper functions for creating settlement data structures + +**Test Helpers**: +- `MilkSwap.sol`: Simple test DEX for simulating swaps in settlements +- `GPv2OrderHelper.sol`: Utilities for constructing CoW Protocol orders +- `SignerECDSA.sol`: ECDSA signature utilities for tests +- `EmptyWrapper.sol`: Minimal wrapper for testing wrapper chaining + +## Important Implementation Details + +### Wrapper Data Format +Wrapper data is passed as a calldata slice with format: +``` +[wrapper-specific-params][signature][next-wrapper-address (20 bytes)][remaining-wrapper-data] +``` + +The `parseWrapperData()` function must consume its portion and return the remainder. + +### EVC Integration +Both wrappers execute operations within EVC batches to ensure atomicity and proper account health checks. The flow is: +1. Wrapper validates authorization (permit or pre-approved hash) +2. Build EVC.BatchItem[] array with all operations +3. Call `EVC.batch()` - EVC ensures account is healthy at end + +### Settlement Execution Context +- Wrappers use `evcInternalSettle()` as internal callback from EVC batch +- This function can only be called by EVC during batch execution +- Uses transient storage (`depth`, `settleCalls`) to prevent reentrancy + +### Authentication +- Only authenticated CoW Protocol solvers can call `wrappedSettle()` +- Authentication checked via `AUTHENTICATOR.isSolver(msg.sender)` +- Wrappers themselves can be added to solver allowlist for testing + +## Foundry Configuration + +- Compiler optimization: enabled +- IR compilation: enabled (`via_ir = true`) +- Source directory: `src/` +- Test directory: `test/` +- Library dependencies managed via git submodules + +## Coding Style + +### Error Handling +**Always use `require()` with custom errors instead of `if () { revert }`**. This pattern is used consistently throughout the codebase: + +```solidity +// ✅ Preferred +require(msg.sender == address(EVC), Unauthorized(msg.sender)); +require(depth > 0 && settleCalls == 0, Unauthorized(address(0))); + +// ❌ Avoid +if (msg.sender != address(EVC)) { + revert Unauthorized(msg.sender); +} +``` + +This approach is more concise and maintains consistency with the existing codebase style. + +## Remappings + +Key import remappings: +- `cow/` → CoW Protocol contracts (`lib/cow/src/contracts`) +- `evc/` → Ethereum Vault Connector (`lib/euler-vault-kit/lib/ethereum-vault-connector/src/`) +- `euler-vault-kit/` → Euler vault implementation +- `openzeppelin/` → OpenZeppelin contracts (via EVC dependency) + + +## When Giving PR feedback +* do not re-suggest or address feedback after it has already been given, either by you or other contributors who have commented. +* be careful not to use too many inline comments. If there are already inline comments on the same line that you want to comment on, or if the inline comment is about something that has already been suggested, don't comment. diff --git a/src/CowWrapper.sol b/src/CowWrapper.sol new file mode 100644 index 0000000..5828272 --- /dev/null +++ b/src/CowWrapper.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0 <0.9.0; + +/** + * CoW Wrapper all-in-one integration file + * CoW Protocol Developers + * This file is completely self-contained (ie no dependencies) and can be portably copied to whatever projects it is needed. + * It contains: + * * CowWrapper -- an abstract base contract which should be inherited by all wrappers + * * ICowWrapper -- the required interface for all wrappers + * * ICowSettlement -- A minimized interface and base structures for CoW Protocol settlement contract. From https://github.com/cowprotocol/contracts/blob/main/src/contracts/GPv2Settlement.sol + * * ICowAuthentication -- The authentication interface used by ICowSettlement. From https://github.com/cowprotocol/contracts/blob/main/src/contracts/interfaces/GPv2Authentication.sol + */ + +/// @title CoW Protocol Authentication Interface +/// @author CoW DAO developers +interface ICowAuthentication { + /// @dev determines whether the provided address is an authenticated solver. + /// @param prospectiveSolver the address of prospective solver. + /// @return true when prospectiveSolver is an authenticated solver, otherwise false. + function isSolver(address prospectiveSolver) external view returns (bool); +} + +/// @title CoW Protocol Settlement Interface +/// @notice Minimal interface for CoW Protocol's settlement contract +/// @dev Used for type-safe calls to the settlement contract's settle function +interface ICowSettlement { + /// @notice Trade data structure matching GPv2Settlement + struct Trade { + uint256 sellTokenIndex; + uint256 buyTokenIndex; + address receiver; + uint256 sellAmount; + uint256 buyAmount; + uint32 validTo; + bytes32 appData; + uint256 feeAmount; + uint256 flags; + uint256 executedAmount; + bytes signature; + } + + /// @notice Interaction data structure for pre/intra/post-settlement actions which are supplied by the solver to complete the user request + struct Interaction { + address target; + uint256 value; + bytes callData; + } + + /// @notice Returns the authentication contract used by the settlement contract. + function authenticator() external view returns (ICowAuthentication); + + /// @notice Returns the address of the vaultRelayer, the target for approvals for funds entering the settlement contract. + function vaultRelayer() external view returns (address); + + /// @notice Returns the domain separator for EIP-712 signing + function domainSeparator() external view returns (bytes32); + + /// @notice Allows for approval of orders by submitting an authorized hash on-chain prior to order execution. + function setPreSignature(bytes calldata orderUid, bool signed) external; + + /// @notice Settles a batch of trades atomically + /// @param tokens Array of token addresses involved in the settlement + /// @param clearingPrices Array of clearing prices for each token + /// @param trades Array of trades to execute + /// @param interactions Array of three interaction arrays (pre, intra, post-settlement) + function settle( + address[] calldata tokens, + uint256[] calldata clearingPrices, + Trade[] calldata trades, + Interaction[][3] calldata interactions + ) external; +} + +/// @title CoW Protocol Wrapper Interface +/// @notice Interface for wrapper contracts that add custom logic around CoW settlements +/// @dev Wrappers can be chained together to compose multiple settlement operations +interface ICowWrapper { + /// @notice A human readable label for this wrapper. Used for display in explorer/analysis UIs + function name() external view returns (string memory); + + /// @notice The settlement contract used by this wrapper + /// @return The CowSettlement contract address + function SETTLEMENT() external view returns (ICowSettlement); + + /// @notice Initiates a wrapped settlement call + /// @dev This is the entry point for wrapped settlements. The wrapper will execute custom logic + /// before calling the next wrapper or settlement contract in the chain. + /// @param settleData ABI-encoded call to ICowSettlement.settle() containing trade data + /// @dev SECURITY: `settleData` is NOT guaranteed to remain unchanged through the wrapper chain. + /// Intermediate wrappers could modify it before passing it along. Do not rely on + /// `settleData` validation for security-critical checks. + /// @param wrapperData Encoded wrapper chain with the following format: + /// Structure: [uint16 len1][bytes data1][address wrapper2][uint16 len2][bytes data2][address wrapper3]... + /// + /// Each wrapper in the chain consists of: + /// - 2 bytes: uint16 length of wrapper-specific data + /// - `length` bytes: wrapper-specific data for this wrapper + /// - 20 bytes: address of next wrapper (omitted for the final wrapper) + /// + /// The final wrapper in the chain omits the next wrapper address and calls SETTLEMENT directly. + /// + /// Example: [0x0005][0xAABBCCDDEE][0x1234...ABCD][0x0003][0x112233] + /// ↑len ↑data ↑next wrapper ↑len ↑data (final, no next address) + /// + function wrappedSettle(bytes calldata settleData, bytes calldata wrapperData) external; +} + +/// @title CoW Protocol Wrapper Base Contract +/// @notice Abstract base contract for creating wrapper contracts around CoW Protocol settlements +/// @dev A wrapper enables custom pre/post-settlement and context-setting logic and can be chained with other wrappers. +/// Wrappers must: +/// - Be approved by the ICowAuthentication contract +/// - Verify the caller is an authenticated solver +/// - Eventually call settle() on the approved ICowSettlement contract +/// - Implement _wrap() for custom logic +abstract contract CowWrapper is ICowWrapper { + /// @notice Thrown when the caller is not an authenticated solver + /// @param unauthorized The address that attempted to call wrappedSettle + error NotASolver(address unauthorized); + + /// @notice Thrown when settle data doesn't contain the correct function selector + /// @param invalidSettleData The invalid settle data that was provided + error InvalidSettleData(bytes invalidSettleData); + + /// @notice The settlement contract + ICowSettlement public immutable SETTLEMENT; + + /// @notice The authentication contract used to verify solvers + /// @dev This is derived from `SETTLEMENT.authenticator()`. + ICowAuthentication public immutable AUTHENTICATOR; + + /// @notice Constructs a new CowWrapper + /// @param settlement_ The ICowSettlement contract to use at the end of the wrapper chain. Also used for wrapper authentication. + constructor(ICowSettlement settlement_) { + SETTLEMENT = settlement_; + AUTHENTICATOR = settlement_.authenticator(); + } + + /// @inheritdoc ICowWrapper + function wrappedSettle(bytes calldata settleData, bytes calldata wrapperData) external { + // Revert if not a valid solver + require(AUTHENTICATOR.isSolver(msg.sender), NotASolver(msg.sender)); + + // Find out how long the next wrapper data is supposed to be + // We use 2 bytes to decode the length of the wrapper data because it allows for up to 64KB of data for each wrapper. + // This should be plenty of length for all identified use-cases of wrappers in the forseeable future. + uint256 nextWrapperDataLen = uint16(bytes2(wrapperData[0:2])); + + // Delegate to the wrapper's custom logic + uint256 remainingWrapperDataStart = 2 + nextWrapperDataLen; + _wrap(settleData, wrapperData[2:remainingWrapperDataStart], wrapperData[remainingWrapperDataStart:]); + } + + /// @notice Internal function containing the wrapper's custom logic + /// @dev Must be implemented by concrete wrapper contracts. Should execute custom logic + /// then eventually call _next() to continue the wrapped settlement chain. + /// @param settleData ABI-encoded call to ICowSettlement.settle() + /// @param wrapperData The wrapper data which should be consumed by this wrapper + /// @param remainingWrapperData Additional wrapper data. It is the reminder bytes resulting from consuming the current's wrapper data from the original `wrapperData` in the `wrappedSettle` call. This should be passed unaltered to `_next` that will call the settlement function if this remainder is empty, or delegate the settlement to the next wrapper + function _wrap(bytes calldata settleData, bytes calldata wrapperData, bytes calldata remainingWrapperData) + internal + virtual; + + /// @notice Continues the wrapped settlement chain by calling the next wrapper or settlement contract + /// @dev Extracts the next target address from wrapperData and either: + /// - Calls ICowSettlement.settle() directly if no more wrappers remain, or + /// - Calls the next CowWrapper.wrappedSettle() to continue the chain + /// @param settleData ABI-encoded call to ICowSettlement.settle() + /// @param remainingWrapperData Remaining wrapper data starting with the next target address (20 bytes) + function _next(bytes calldata settleData, bytes calldata remainingWrapperData) internal { + if (remainingWrapperData.length == 0) { + // No more wrapper data - we're calling the final settlement contract + // Verify the settle data has the correct function selector + require(bytes4(settleData[:4]) == ICowSettlement.settle.selector, InvalidSettleData(settleData)); + + // Call the settlement contract directly with the settle data + (bool success, bytes memory returnData) = address(SETTLEMENT).call(settleData); + + if (!success) { + // Bubble up the revert reason from the settlement contract + assembly ("memory-safe") { + revert(add(returnData, 0x20), mload(returnData)) + } + } + } else { + // Extract the next wrapper address from the first 20 bytes of wrapperData + address nextWrapper = address(bytes20(remainingWrapperData[:20])); + + // Skip past the address we just read + remainingWrapperData = remainingWrapperData[20:]; + + // More wrapper data remains - call the next wrapper in the chain + CowWrapper(nextWrapper).wrappedSettle(settleData, remainingWrapperData); + } + } +} diff --git a/src/vendor/interfaces/IGPv2Settlement.sol b/src/vendor/interfaces/IGPv2Settlement.sol deleted file mode 100644 index 85d0f7c..0000000 --- a/src/vendor/interfaces/IGPv2Settlement.sol +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// !! THIS FILE WAS AUTOGENERATED BY abi-to-sol v0.8.0. SEE SOURCE BELOW. !! -pragma solidity ^0.8; -pragma experimental ABIEncoderV2; - -interface IGPv2Settlement { - receive() external payable; - function authenticator() external view returns (address); - function domainSeparator() external view returns (bytes32); - function filledAmount(bytes memory) external view returns (uint256); - function freeFilledAmountStorage(bytes[] memory orderUids) external; - function freePreSignatureStorage(bytes[] memory orderUids) external; - function getStorageAt(uint256 offset, uint256 length) external view returns (bytes memory); - function invalidateOrder(bytes memory orderUid) external; - function preSignature(bytes memory) external view returns (uint256); - function setPreSignature(bytes memory orderUid, bool signed) external; - function settle( - address[] memory tokens, - uint256[] memory clearingPrices, - GPv2Trade.Data[] memory trades, - GPv2Interaction.Data[][3] memory interactions - ) external; - function simulateDelegatecall(address targetContract, bytes memory calldataPayload) - external - returns (bytes memory response); - function simulateDelegatecallInternal(address targetContract, bytes memory calldataPayload) - external - returns (bytes memory response); - function swap(IVault.BatchSwapStep[] memory swaps, address[] memory tokens, GPv2Trade.Data memory trade) external; - function vault() external view returns (address); - function vaultRelayer() external view returns (address); - - event Interaction(address indexed target, uint256 value, bytes4 selector); - event OrderInvalidated(address indexed owner, bytes orderUid); - event PreSignature(address indexed owner, bytes orderUid, bool signed); - event Settlement(address indexed solver); - event Trade( - address indexed owner, - address sellToken, - address buyToken, - uint256 sellAmount, - uint256 buyAmount, - uint256 feeAmount, - bytes orderUid - ); -} - -interface GPv2Trade { - struct Data { - uint256 sellTokenIndex; - uint256 buyTokenIndex; - address receiver; - uint256 sellAmount; - uint256 buyAmount; - uint32 validTo; - bytes32 appData; - uint256 feeAmount; - uint256 flags; - uint256 executedAmount; - bytes signature; - } -} - -interface GPv2Interaction { - struct Data { - address target; - uint256 value; - bytes callData; - } -} - -interface IVault { - struct BatchSwapStep { - bytes32 poolId; - uint256 assetInIndex; - uint256 assetOutIndex; - uint256 amount; - bytes userData; - } -} - -// THIS FILE WAS AUTOGENERATED FROM THE FOLLOWING ABI JSON: -/* -[{"type":"constructor","inputs":[{"name":"authenticator_","type":"address","internalType":"contract GPv2Authentication"},{"name":"vault_","type":"address","internalType":"contract IVault"}],"stateMutability":"nonpayable"},{"type":"receive","stateMutability":"payable"},{"type":"function","name":"authenticator","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract GPv2Authentication"}],"stateMutability":"view"},{"type":"function","name":"domainSeparator","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"filledAmount","inputs":[{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"freeFilledAmountStorage","inputs":[{"name":"orderUids","type":"bytes[]","internalType":"bytes[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"freePreSignatureStorage","inputs":[{"name":"orderUids","type":"bytes[]","internalType":"bytes[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getStorageAt","inputs":[{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"length","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"invalidateOrder","inputs":[{"name":"orderUid","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"preSignature","inputs":[{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"setPreSignature","inputs":[{"name":"orderUid","type":"bytes","internalType":"bytes"},{"name":"signed","type":"bool","internalType":"bool"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settle","inputs":[{"name":"tokens","type":"address[]","internalType":"contract IERC20[]"},{"name":"clearingPrices","type":"uint256[]","internalType":"uint256[]"},{"name":"trades","type":"tuple[]","internalType":"struct GPv2Trade.Data[]","components":[{"name":"sellTokenIndex","type":"uint256","internalType":"uint256"},{"name":"buyTokenIndex","type":"uint256","internalType":"uint256"},{"name":"receiver","type":"address","internalType":"address"},{"name":"sellAmount","type":"uint256","internalType":"uint256"},{"name":"buyAmount","type":"uint256","internalType":"uint256"},{"name":"validTo","type":"uint32","internalType":"uint32"},{"name":"appData","type":"bytes32","internalType":"bytes32"},{"name":"feeAmount","type":"uint256","internalType":"uint256"},{"name":"flags","type":"uint256","internalType":"uint256"},{"name":"executedAmount","type":"uint256","internalType":"uint256"},{"name":"signature","type":"bytes","internalType":"bytes"}]},{"name":"interactions","type":"tuple[][3]","internalType":"struct GPv2Interaction.Data[][3]","components":[{"name":"target","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"},{"name":"callData","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"simulateDelegatecall","inputs":[{"name":"targetContract","type":"address","internalType":"address"},{"name":"calldataPayload","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"response","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},{"type":"function","name":"simulateDelegatecallInternal","inputs":[{"name":"targetContract","type":"address","internalType":"address"},{"name":"calldataPayload","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"response","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},{"type":"function","name":"swap","inputs":[{"name":"swaps","type":"tuple[]","internalType":"struct IVault.BatchSwapStep[]","components":[{"name":"poolId","type":"bytes32","internalType":"bytes32"},{"name":"assetInIndex","type":"uint256","internalType":"uint256"},{"name":"assetOutIndex","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"userData","type":"bytes","internalType":"bytes"}]},{"name":"tokens","type":"address[]","internalType":"contract IERC20[]"},{"name":"trade","type":"tuple","internalType":"struct GPv2Trade.Data","components":[{"name":"sellTokenIndex","type":"uint256","internalType":"uint256"},{"name":"buyTokenIndex","type":"uint256","internalType":"uint256"},{"name":"receiver","type":"address","internalType":"address"},{"name":"sellAmount","type":"uint256","internalType":"uint256"},{"name":"buyAmount","type":"uint256","internalType":"uint256"},{"name":"validTo","type":"uint32","internalType":"uint32"},{"name":"appData","type":"bytes32","internalType":"bytes32"},{"name":"feeAmount","type":"uint256","internalType":"uint256"},{"name":"flags","type":"uint256","internalType":"uint256"},{"name":"executedAmount","type":"uint256","internalType":"uint256"},{"name":"signature","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"vault","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IVault"}],"stateMutability":"view"},{"type":"function","name":"vaultRelayer","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract GPv2VaultRelayer"}],"stateMutability":"view"},{"type":"event","name":"Interaction","inputs":[{"name":"target","type":"address","indexed":true,"internalType":"address"},{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"selector","type":"bytes4","indexed":false,"internalType":"bytes4"}],"anonymous":false},{"type":"event","name":"OrderInvalidated","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"orderUid","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"PreSignature","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"orderUid","type":"bytes","indexed":false,"internalType":"bytes"},{"name":"signed","type":"bool","indexed":false,"internalType":"bool"}],"anonymous":false},{"type":"event","name":"Settlement","inputs":[{"name":"solver","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Trade","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"sellToken","type":"address","indexed":false,"internalType":"contract IERC20"},{"name":"buyToken","type":"address","indexed":false,"internalType":"contract IERC20"},{"name":"sellAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"buyAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"feeAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"orderUid","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false}] -*/ diff --git a/test/EmptyWrapper.sol b/test/EmptyWrapper.sol new file mode 100644 index 0000000..325535c --- /dev/null +++ b/test/EmptyWrapper.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8; + +import {ICowSettlement, CowWrapper} from "../src/CowWrapper.sol"; + +contract EmptyWrapper is CowWrapper { + string public override name = "Empty Wrapper"; + + constructor(ICowSettlement settlement_) CowWrapper(settlement_) {} + + function _wrap(bytes calldata settleData, bytes calldata, bytes calldata remainingWrapperData) internal override { + _next(settleData, remainingWrapperData); + } +} diff --git a/test/helpers/CowWrapperHelpers.sol b/test/helpers/CowWrapperHelpers.sol new file mode 100644 index 0000000..a830af9 --- /dev/null +++ b/test/helpers/CowWrapperHelpers.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import {ICowSettlement, ICowAuthentication, ICowWrapper} from "src/CowWrapper.sol"; + +library CowWrapperHelpers { + struct SettleCall { + address[] tokens; + uint256[] clearingPrices; + ICowSettlement.Trade[] trades; + ICowSettlement.Interaction[][3] interactions; + } + + /** + * @dev This function is intended for testing purposes and is not memory efficient. + * @param wrappers Array of wrapper addresses to chain together + * @param wrapperDatas Array of wrapper-specific data for each wrapper + * @param settlement The settlement call parameters + */ + function encodeWrapperCall( + address[] calldata wrappers, + bytes[] calldata wrapperDatas, + address, + SettleCall calldata settlement + ) external pure returns (address target, bytes memory fullCalldata) { + // Build the wrapper data chain + bytes memory wrapperData; + for (uint256 i = 0; i < wrappers.length; i++) { + wrapperData = abi.encodePacked(wrapperData, uint16(wrapperDatas[i].length), wrapperDatas[i]); + // Include the next wrapper address if there is one + if (wrappers.length > i + 1) { + wrapperData = abi.encodePacked(wrapperData, wrappers[i + 1]); + } + // For the last wrapper, don't add anything - the static SETTLEMENT will be called automatically + } + + // Build the settle calldata + bytes memory settleData = abi.encodeWithSelector( + ICowSettlement.settle.selector, + settlement.tokens, + settlement.clearingPrices, + settlement.trades, + settlement.interactions + ); + + // Encode the wrappedSettle call + fullCalldata = abi.encodeWithSelector(ICowWrapper.wrappedSettle.selector, settleData, wrapperData); + + return (wrappers[0], fullCalldata); + } +} diff --git a/test/unit/CowWrapper.t.sol b/test/unit/CowWrapper.t.sol new file mode 100644 index 0000000..e950b21 --- /dev/null +++ b/test/unit/CowWrapper.t.sol @@ -0,0 +1,281 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8; + +import {Test} from "forge-std/Test.sol"; +import {CowWrapper, ICowWrapper, ICowSettlement, ICowAuthentication} from "../../src/CowWrapper.sol"; +import {EmptyWrapper} from "../EmptyWrapper.sol"; + +import {MockWrapper, MockCowSettlement, MockCowAuthentication} from "./mocks/MockCowProtocol.sol"; + +import {CowWrapperHelpers} from "../helpers/CowWrapperHelpers.sol"; + +contract CowWrapperTest is Test { + MockCowAuthentication public authenticator; + MockCowSettlement public mockSettlement; + address public solver; + + MockWrapper private wrapper1; + MockWrapper private wrapper2; + MockWrapper private wrapper3; + + function setUp() public { + // Deploy mock contracts + authenticator = new MockCowAuthentication(); + mockSettlement = new MockCowSettlement(address(authenticator)); + + solver = makeAddr("solver"); + // Add solver to the authenticator + authenticator.setSolver(solver, true); + + // Create test wrapper and three EmptyWrapper instances with the settlement contract + // (use type(uint16).max because it will force consuming all the wrapper data, which is + // most useful for these tests) + wrapper1 = new MockWrapper(ICowSettlement(address(mockSettlement)), type(uint16).max); + wrapper2 = new MockWrapper(ICowSettlement(address(mockSettlement)), type(uint16).max); + wrapper3 = new MockWrapper(ICowSettlement(address(mockSettlement)), type(uint16).max); + + // Add all wrappers as solvers + authenticator.setSolver(address(wrapper1), true); + authenticator.setSolver(address(wrapper2), true); + authenticator.setSolver(address(wrapper3), true); + } + + function _emptyInteractions() private pure returns (ICowSettlement.Interaction[][3] memory) { + return + [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ]; + } + + function _createSimpleSettleData(uint256 tokenCount) private returns (bytes memory) { + address[] memory tokens = new address[](tokenCount); + uint256[] memory clearingPrices = new uint256[](tokenCount); + for (uint256 i = 0; i < tokenCount; i++) { + tokens[i] = makeAddr(string.concat("Settle Token #", vm.toString(i + 1))); + clearingPrices[i] = 100 * (i + 1); + } + return abi.encodeCall( + ICowSettlement.settle, (tokens, clearingPrices, new ICowSettlement.Trade[](0), _emptyInteractions()) + ); + } + + function test_verifyInitialState() public { + assertEq( + address(wrapper1.SETTLEMENT()), + address(mockSettlement), + "Settlement contract should be initialized correctly" + ); + assertEq( + address(wrapper1.AUTHENTICATOR()), + address(authenticator), + "Authenticator contract should be initialized from the settlement contract" + ); + } + + function test_next_CallsWrapperAndThenNextSettlement() public { + bytes memory settleData = abi.encodePacked(_createSimpleSettleData(1), hex"123456"); + // here we encode [2-byte len] followed by the actual wrapper data (which is 3 bytes, 6 chars hex) + bytes memory secondCallWrapperData = abi.encodePacked(uint16(3), hex"098765"); + // here we encode [2-byte len] followed by the actual wrapper data (which is 2 bytes, 4 chars hex), and build the chain + bytes memory wrapperData = abi.encodePacked(uint16(2), hex"1234", address(wrapper1), secondCallWrapperData); + + // verify the outside wrapper call data + vm.expectCall(address(wrapper1), abi.encodeCall(ICowWrapper.wrappedSettle, (settleData, wrapperData))); + + // verify the internal wrapper call data + vm.expectCall(address(wrapper1), abi.encodeCall(ICowWrapper.wrappedSettle, (settleData, secondCallWrapperData))); + + // the settlement contract gets called once after wrappers (including the surplus data at the end) + vm.expectCall(address(mockSettlement), 0, settleData, 1); + + vm.prank(solver); + wrapper1.wrappedSettle(settleData, wrapperData); + } + + function test_wrappedSettle_RevertsWithNotASolver() public { + bytes memory settleData = _createSimpleSettleData(0); + address notASolver = makeAddr("notASolver"); + + // Should revert when called by non-solver + vm.prank(notASolver); + vm.expectRevert(abi.encodeWithSelector(CowWrapper.NotASolver.selector, notASolver)); + wrapper1.wrappedSettle(settleData, hex""); + } + + function test_wrappedSettle_RevertsOnInvalidSettleSelector() public { + bytes memory settleData = abi.encodePacked(bytes4(0xdeadbeef), hex"1234"); + bytes memory wrapperData = hex"0000"; // Empty wrapper data, goes straight to settlement + vm.prank(solver); + vm.expectRevert(abi.encodeWithSelector(CowWrapper.InvalidSettleData.selector, settleData)); + wrapper1.wrappedSettle(settleData, wrapperData); + } + + function test_integration_ThreeWrappersChained() public { + // Set up a more sophisticated settlement call to make sure it all gets through as expected. + CowWrapperHelpers.SettleCall memory settlement; + settlement.tokens = new address[](2); + settlement.tokens[0] = address(0x1); + settlement.tokens[1] = address(0x2); + settlement.clearingPrices = new uint256[](2); + settlement.clearingPrices[0] = 100; + settlement.clearingPrices[1] = 200; + + settlement.trades = new ICowSettlement.Trade[](10); + for (uint256 i = 0; i < 10; i++) { + settlement.trades[i] = ICowSettlement.Trade({ + sellTokenIndex: 0, + buyTokenIndex: 1, + receiver: address(0x123), + sellAmount: 1000 * i, + buyAmount: 900 * i, + validTo: uint32(block.timestamp + 1000), + appData: bytes32(uint256(1)), + feeAmount: 10, + flags: 0, + executedAmount: 0, + signature: hex"aabbccddee" + }); + } + + settlement.interactions = [ + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0), + new ICowSettlement.Interaction[](0) + ]; + + // Build the chained wrapper data: + // solver -> wrapper1 -> wrapper2 -> wrapper1 -> wrapper3 -> mockSettlement + address[] memory wrappers = new address[](4); + wrappers[0] = address(wrapper1); + wrappers[1] = address(wrapper2); + wrappers[2] = address(wrapper1); + wrappers[3] = address(wrapper3); + + bytes[] memory datas = new bytes[](4); + + datas[2] = hex"828348"; + + (address target, bytes memory fullCalldata) = + CowWrapperHelpers.encodeWrapperCall(wrappers, datas, address(mockSettlement), settlement); + + // all the wrappers get called, with wrapper 1 called twice + + // we only want to verify that wrappedSettle was called. (not the specific data passed to wrappedSettle) + bytes memory wrappedSettleSelector = abi.encodePacked(ICowWrapper.wrappedSettle.selector); + vm.expectCall(address(wrapper1), 0, wrappedSettleSelector, 2); + vm.expectCall(address(wrapper2), 0, wrappedSettleSelector, 1); + vm.expectCall(address(wrapper3), 0, wrappedSettleSelector, 1); + + // the settlement gets called with the full data + vm.expectCall(address(mockSettlement), new bytes(0)); + + // Call wrapper1 as the solver + vm.prank(solver); + (bool success,) = target.call(fullCalldata); + assertTrue(success, "Chained wrapper call should succeed"); + } + + function test_wrappedSettle_RevertsOnZeroLengthWrapperData() public { + bytes memory settleData = _createSimpleSettleData(0); + bytes memory wrapperData = hex""; // Completely empty wrapper data + + vm.prank(solver); + vm.expectRevert(); // Should revert with out-of-bounds array access + wrapper1.wrappedSettle(settleData, wrapperData); + } + + function test_wrappedSettle_RevertsOnOneByteWrapperData() public { + bytes memory settleData = _createSimpleSettleData(0); + bytes memory wrapperData = hex"01"; // Only 1 byte - not enough to read the 2-byte length + + vm.prank(solver); + vm.expectRevert(); // Should revert with out-of-bounds array access + wrapper1.wrappedSettle(settleData, wrapperData); + } + + function test_wrappedSettle_SucceedsWithZeroLengthIndicator() public { + bytes memory settleData = _createSimpleSettleData(0); + bytes memory wrapperData = hex"0000"; // 2 bytes indicating 0-length wrapper data + + // Should call settlement directly with no wrapper-specific data + vm.expectCall(address(mockSettlement), 0, settleData, 1); + + vm.prank(solver); + wrapper1.wrappedSettle(settleData, wrapperData); + } + + function test_wrappedSettle_SucceedsWithMaximumLengthWrapperData() public { + bytes memory settleData = _createSimpleSettleData(0); + + // Create maximum length wrapper data (65535 bytes) + // Format: [2-byte length = 0xFFFF][65535 bytes of data] + bytes memory maxData = new bytes(65535); + for (uint256 i = 0; i < 65535; i++) { + maxData[i] = bytes1(uint8(i % 256)); + } + + bytes memory wrapperData = abi.encodePacked(uint16(65535), maxData); + + // Should successfully parse the maximum length data and call settlement + vm.expectCall(address(mockSettlement), 0, settleData, 1); + + vm.prank(solver); + wrapper1.wrappedSettle(settleData, wrapperData); + } + + function test_wrappedSettle_RevertsWhenDataShorterThanIndicated() public { + bytes memory settleData = _createSimpleSettleData(0); + + // Wrapper data claims to be 100 bytes but only provides 50 + bytes memory shortData = new bytes(50); + bytes memory wrapperData = abi.encodePacked(uint16(100), shortData); + + vm.prank(solver); + vm.expectRevert(); // Should revert with out-of-bounds array access + wrapper1.wrappedSettle(settleData, wrapperData); + } + + function test_wrappedSettle_SucceedsWithMaxLengthAndNextWrapper() public { + bytes memory settleData = _createSimpleSettleData(0); + + // Create maximum length wrapper data followed by next wrapper address + bytes memory maxData = new bytes(65535); + for (uint256 i = 0; i < 65535; i++) { + maxData[i] = bytes1(uint8(i % 256)); + } + + // Format: [2-byte length = 0xFFFF][65535 bytes of data][20-byte next wrapper address][remaining data] + bytes memory nextWrapperData = hex"00030000FF"; // 3 bytes of data for next wrapper + bytes memory wrapperData = abi.encodePacked(type(uint16).max, maxData, address(wrapper2), nextWrapperData); + + // Should call wrapper2 with the remaining data + vm.expectCall(address(wrapper2), 0, abi.encodePacked(ICowWrapper.wrappedSettle.selector), 1); + + vm.prank(solver); + wrapper1.wrappedSettle(settleData, wrapperData); + } + + function test_wrappedSettle_RevertsWithInsufficientLengthData() public { + bytes memory settleData = _createSimpleSettleData(0); + + // Format: [1-byte length = 1 (insufficient)] + bytes memory wrapperData = hex"01"; + + vm.expectRevert(new bytes(0)); + vm.prank(solver); + wrapper1.wrappedSettle(settleData, wrapperData); + } + + function test_wrappedSettle_RevertsWithInsufficientCallData() public { + bytes memory settleData = _createSimpleSettleData(0); + + // Format: [2-byte length = 0xa][9 bytes of data (insufficient)] + bytes memory wrapperData = hex"000A123412341234123412"; + + vm.expectRevert(new bytes(0)); + vm.prank(solver); + wrapper1.wrappedSettle(settleData, wrapperData); + } +} diff --git a/test/unit/mocks/MockCowProtocol.sol b/test/unit/mocks/MockCowProtocol.sol new file mode 100644 index 0000000..5d774f2 --- /dev/null +++ b/test/unit/mocks/MockCowProtocol.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8; + +import {ICowSettlement, ICowAuthentication, CowWrapper} from "../../../src/CowWrapper.sol"; + +/// @title MockCowAuthentication +/// @notice Mock implementation of CoW Protocol authenticator for unit testing +contract MockCowAuthentication is ICowAuthentication { + mapping(address => bool) public solvers; + + function setSolver(address solver, bool authorized) external { + solvers[solver] = authorized; + } + + function isSolver(address prospectiveSolver) external view override returns (bool) { + return solvers[prospectiveSolver]; + } +} + +/// @title MockCowSettlement +/// @notice Mock implementation of CoW Protocol settlement contract for unit testing +contract MockCowSettlement is ICowSettlement { + ICowAuthentication public immutable AUTH; + bool public shouldSucceed = true; + + constructor(address _auth) { + AUTH = ICowAuthentication(_auth); + } + + function authenticator() external view override returns (ICowAuthentication) { + return AUTH; + } + + function vaultRelayer() external pure override returns (address) { + return address(0x7777); + } + + function domainSeparator() external pure override returns (bytes32) { + return keccak256("MockDomainSeparator"); + } + + function setPreSignature(bytes calldata, bool) external pure override {} + + function settle(address[] calldata, uint256[] calldata, Trade[] calldata, Interaction[][3] calldata) + external + view + override + { + require(shouldSucceed, "MockCowSettlement: settle failed"); + } + + function setSuccessfulSettle(bool success) external { + shouldSucceed = success; + } +} + +contract MockWrapper is CowWrapper { + string public override name = "Mock Wrapper"; + uint256 public consumeBytes; + + constructor(ICowSettlement settlement_, uint256 consumeBytes_) CowWrapper(settlement_) { + consumeBytes = consumeBytes_; + } + + function _wrap(bytes calldata settleData, bytes calldata, bytes calldata remainingWrapperData) internal override { + _next(settleData, remainingWrapperData); + } +}