From 720896bb019d4c64949792ae5371eaa361695cef Mon Sep 17 00:00:00 2001 From: vimageDE Date: Fri, 10 Jan 2025 12:41:03 +0100 Subject: [PATCH 01/35] SimpleWitnessAllocator --- src/allocators/SimpleWitnessAllocator.sol | 75 ++++++++++++++++++++++ src/interfaces/ISimpleWitnessAllocator.sol | 29 +++++++++ 2 files changed, 104 insertions(+) create mode 100644 src/allocators/SimpleWitnessAllocator.sol create mode 100644 src/interfaces/ISimpleWitnessAllocator.sol diff --git a/src/allocators/SimpleWitnessAllocator.sol b/src/allocators/SimpleWitnessAllocator.sol new file mode 100644 index 0000000..5d2c2d0 --- /dev/null +++ b/src/allocators/SimpleWitnessAllocator.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import { ERC6909 } from "@solady/tokens/ERC6909.sol"; +import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import { ITheCompact } from "@uniswap/the-compact/interfaces/ITheCompact.sol"; +import { Compact } from "@uniswap/the-compact/types/EIP712Types.sol"; +import { SimpleAllocator } from "./SimpleAllocator.sol"; +import { IAllocator } from "../interfaces/IAllocator.sol"; +import { ISimpleWitnessAllocator } from "../interfaces/ISimpleWitnessAllocator.sol"; + +contract SimpleWitnessAllocator is SimpleAllocator, ISimpleWitnessAllocator { + // abi.decode(bytes("Compact(address arbiter,address "), (bytes32)) + bytes32 constant COMPACT_TYPESTRING_FRAGMENT_ONE = 0x436f6d70616374286164647265737320617262697465722c6164647265737320; + // abi.decode(bytes("sponsor,uint256 nonce,uint256 ex"), (bytes32)) + bytes32 constant COMPACT_TYPESTRING_FRAGMENT_TWO = 0x73706f6e736f722c75696e74323536206e6f6e63652c75696e74323536206578; + // abi.decode(bytes("pires,uint256 id,uint256 amount)"), (bytes32)) + bytes32 constant COMPACT_TYPESTRING_FRAGMENT_THREE = 0x70697265732c75696e743235362069642c75696e7432353620616d6f756e7429; + // uint200(abi.decode(bytes(",Witness witness)Witness("), (bytes25))) + uint200 constant WITNESS_TYPESTRING = 0x2C5769746E657373207769746E657373295769746E65737328; + + constructor(address compactContract_, address arbiter_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) + SimpleAllocator(compactContract_, arbiter_, minWithdrawalDelay_, maxWithdrawalDelay_) {} + + /// @inheritdoc ISimpleWitnessAllocator + function lockWithWitness(Compact calldata compact_, bytes32 typestringHash_, bytes32 witnessHash_) external { + bytes32 tokenHash = _checkAllocation(compact_); + + bytes32 digest = keccak256( + abi.encodePacked( + bytes2(0x1901), + ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + typestringHash_, // keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness)Witness(uint256 witnessArgument)") + compact_.arbiter, + compact_.sponsor, + compact_.nonce, + compact_.expires, + compact_.id, + compact_.amount, + witnessHash_ + ) + ) + ) + ); + + _claim[tokenHash] = compact_.expires; + _amount[tokenHash] = compact_.amount; + _nonce[tokenHash] = compact_.nonce; + _sponsor[digest] = tokenHash; + + emit Locked(compact_.sponsor, compact_.id, compact_.amount, compact_.expires); + } + + /// @inheritdoc ISimpleWitnessAllocator + function getTypestringHashForWitness(string calldata witness_) external pure returns (bytes32 typestringHash_) { + assembly ("memory-safe") { + let memoryOffset := mload(0x40) + mstore(memoryOffset, COMPACT_TYPESTRING_FRAGMENT_ONE) + mstore(add(memoryOffset, 0x20), COMPACT_TYPESTRING_FRAGMENT_TWO) + mstore(add(memoryOffset, 0x40), COMPACT_TYPESTRING_FRAGMENT_THREE) + mstore(add(memoryOffset, sub(0x60, 0x01)), shl(56, WITNESS_TYPESTRING)) + let witnessPointer := add(memoryOffset, add(sub(0x60, 0x01), 0x19)) + calldatacopy(witnessPointer, witness_.offset, witness_.length) + let witnessEnd := add(witnessPointer, witness_.length) + mstore8(witnessEnd, 0x29) + typestringHash_ := keccak256(memoryOffset, sub(add(witnessEnd, 0x01), memoryOffset)) + + mstore(0x40, add(or(witnessEnd, 0x1f), 0x20)) + } + return typestringHash_; + } +} diff --git a/src/interfaces/ISimpleWitnessAllocator.sol b/src/interfaces/ISimpleWitnessAllocator.sol new file mode 100644 index 0000000..a6ab26c --- /dev/null +++ b/src/interfaces/ISimpleWitnessAllocator.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import { ISimpleAllocator } from "./ISimpleAllocator.sol"; +import { Compact } from "@uniswap/the-compact/types/EIP712Types.sol"; + +interface ISimpleWitnessAllocator is ISimpleAllocator { + + /// @notice Locks the tokens of an id for a claim with a witness + /// @dev Locks all tokens of a sponsor for an id with a witness + /// @dev example for the typeHash: + /// keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness)Witness(uint256 witnessArgument)") + /// + /// @param compact_ The compact that contains the data about the lock + /// @param typeHash_ The type hash of the full compact, including the witness + /// @param witnessHash_ The witness hash of the witness + function lockWithWitness(Compact calldata compact_, bytes32 typeHash_,bytes32 witnessHash_) external; + + /// @notice Returns the witness typestring hash including a given witness argument + /// @dev example of a witness type string input: + /// "uint256 witnessArgument" + /// @dev full typestring: + /// Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness)Witness(uint256 witnessArgument) + /// + /// @param witness_ The witness typestring argument + /// @return typestringHash_ The full compact typestring hash, including the witness + function getTypestringHashForWitness(string calldata witness_) external pure returns (bytes32 typestringHash_); +} From d19b528647caf590305a5391799ebe094bae5700 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Fri, 10 Jan 2025 13:06:17 +0100 Subject: [PATCH 02/35] Simple ERC7683 Allocator --- src/allocators/SimpleERC7683Allocator.sol | 440 ++++++++++++++++++++++ src/allocators/types/TribunalStructs.sol | 28 ++ src/interfaces/ERC7683/IOriginSettler.sol | 130 +++++++ 3 files changed, 598 insertions(+) create mode 100644 src/allocators/SimpleERC7683Allocator.sol create mode 100644 src/allocators/types/TribunalStructs.sol create mode 100644 src/interfaces/ERC7683/IOriginSettler.sol diff --git a/src/allocators/SimpleERC7683Allocator.sol b/src/allocators/SimpleERC7683Allocator.sol new file mode 100644 index 0000000..8e8cdf1 --- /dev/null +++ b/src/allocators/SimpleERC7683Allocator.sol @@ -0,0 +1,440 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { Compact, BatchCompact, BATCH_COMPACT_TYPEHASH, COMPACT_TYPEHASH } from "@uniswap/the-compact/types/EIP712Types.sol"; +import { ITheCompact } from "@uniswap/the-compact/interfaces/ITheCompact.sol"; +import { SimpleAllocator } from "./SimpleAllocator.sol"; +import { Claim, Mandate } from "./types/TribunalStructs.sol"; +import { IOriginSettler } from "../interfaces/ERC7683/IOriginSettler.sol"; + +contract SimpleERC7683Allocator is SimpleAllocator, IOriginSettler { + + struct OrderData { + address sponsor; + uint96 nonce; + uint32 claimDeadline; + Settlement[] settlements; + address arbiter; + bytes sponsorSignature; + } + + struct OrderDataGasless { + uint32 claimDeadline; + Settlement[] settlements; + address arbiter; + } + + struct Settlement { + Output input; + Output output; + bytes32 destinationSettler; // tribunal + } + + struct Witness { // for single / non-batch compact + uint256 originChainId; + uint256 targetChainId; + bytes32 targetTokenAddress; + uint256 targetMinAmount; + bytes32 recipient; + bytes32 destinationSettler; + uint32 fillDeadline; + } + + error InvalidOriginSettler(address originSettler, address expectedOriginSettler); + error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); + error InvalidChainId(uint256 chainId, uint256 expectedChainId); + error InvalidRecipient(address recipient, address expectedRecipient); + error InvalidNonce(uint256 nonce); + error NonceAlreadyInUse(uint96 nonce); + error InvalidSignature(address signer, address expectedSigner); + + // The typehash of the OrderData struct + // keccak256("OrderData(address sponsor,uint96 nonce,uint32 claimDeadline,Settlement[] settlements,address arbiter,bytes sponsorSignature)Output(bytes32 token,uint256 amount,bytes32 recipient,uint256 chainId)Settlement(Output input,Output output,bytes32 destinationSettler)") + bytes32 constant ORDERDATA_TYPEHASH = 0xe8225b67751f9ff0d865fdc55742ea54087b20406855f954bd737ab200819ab8; + + // The typehash of the OrderDataGasless struct + // keccak256("OrderDataGasless(uint32 claimDeadline,Settlement[] settlements,address arbiter)Output(bytes32 token,uint256 amount,bytes32 recipient,uint256 chainId)Settlement(Output input,Output output,bytes32 destinationSettler)") + bytes32 constant ORDERDATA_GASLESS_TYPEHASH = 0x4679e16e516a2f88beb96ee00964e5f28b9e2ed596592f8c3d92dd70411611a9; + + // keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness) + // Witness(uint256 originChainId,uint256 targetChainId,bytes32 targetTokenAddress,uint256 targetMinAmount,bytes32 recipient,bytes32 destinationSettler,uint32 fillDeadline)") + bytes32 constant COMPACT_WITNESS_TYPEHASH = 0x2f0f51aa07316f3d8860366b556177042fafb2edffc93c799091bae7c194d9a6; + + /// FOR SINGLE COMPACT WE HAVE: + /// - arbiter + /// - sponsor + /// - nonce + /// - expires + /// - id + /// - amount + /// WHAT ADDITIONAL DATA NEEDS TO BE SIGNED: + // Witness( + // uint256 originChainId, + // uint256 targetChainId, + // bytes32 targetTokenAddress, + // uint256 targetMinAmount, + // bytes32 recipient, + // bytes32 destinationSettler, + // uint32 fillDeadline + // ) + + // keccak256("BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts,Witness witness) + // Witness(uint256[] originChainId,uint256[] targetChainId,bytes32[] targetTokenAddress,uint256[] targetMinAmount,bytes32[] recipient,bytes32[] destinationSettler,uint32 fillDeadline)") + bytes32 constant BATCH_COMPACT_WITNESS_TYPEHASH = 0x3158fb17880387b9302c66a40dd45893126fd532c2abb7deee6e53149c53646b; + + /// FOR BATCH COMPACT WE HAVE: + /// - arbiter + /// - sponsor + /// - nonce + /// - expires + /// - idsAndAmounts + /// WHAT ADDITIONAL DATA NEEDS TO BE SIGNED: + // Witness( + // uint256[] originChainId, + // uint256[] targetChainId, + // bytes32[] targetTokenAddress, + // uint256[] targetMinAmount, + // bytes32[] recipient, + // bytes32[] destinationSettler, + // uint32 fillDeadline + // ) + + /// TODO: batch compacts witness + + // The block interval on the deployed chain + uint32 immutable BLOCK_INTERVAL; + + // The nonce of the allocator + mapping(uint256 identifier => bool nonceUsed) private _userNonce; + + constructor(address compactContract_, address arbiter_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_, uint32 blockInterval_) + SimpleAllocator(compactContract_, arbiter_, minWithdrawalDelay_, maxWithdrawalDelay_) { + BLOCK_INTERVAL = blockInterval_; + } + + /// @notice Opens a gasless cross-chain order on behalf of a user. + /// @dev To be called by the filler. + /// @dev This method must emit the Open event + /// @param order The GaslessCrossChainOrder definition + /// @param signature The user's signature over the order + function openFor(GaslessCrossChainOrder calldata order, bytes calldata signature, bytes calldata) external{ + // since we have the users signature, we can create locks in the name of the user + + // Check if orderDataType is the one expected by the allocator + if (order.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { + revert InvalidOrderDataType(order.orderDataType, ORDERDATA_GASLESS_TYPEHASH); + } + // Check the allocator is the chosen origin settler + // This check is not strictly necessary, since the user does not sign a allocator. The filler will not get payed if he maliciously choses a different allocator. + if (order.originSettler != address(this)) { + revert InvalidOriginSettler(order.originSettler, address(this)); + } + + // Decode the orderData + OrderDataGasless memory orderDataGasless = abi.decode(order.orderData, (OrderDataGasless)); + // Enforce a uint96 nonce since the contract handles the nonce management by combining the provided nonce with the user address + uint96 nonce = uint96(order.nonce); + if (order.nonce != nonce) { + revert InvalidNonce(order.nonce); + } + + OrderData memory orderData = _convertGaslessOrderData(order.user, nonce, orderDataGasless, signature); + + _open(orderData, order.fillDeadline, false); + } + + /// @notice Opens a cross-chain order + /// @dev To be called by the user + /// @dev This method must emit the Open event + /// @dev This locks the users tokens + /// @param order The OnchainCrossChainOrder definition + function open(OnchainCrossChainOrder calldata order) external{ + // Check if orderDataType is the one expected by the allocator + if (order.orderDataType != ORDERDATA_TYPEHASH) { + revert InvalidOrderDataType(order.orderDataType, ORDERDATA_TYPEHASH); + } + + // Decode the orderData + OrderData memory orderData = abi.decode(order.orderData, (OrderData)); + _open(orderData, order.fillDeadline, false); + } + + /// @notice Resolves a specific GaslessCrossChainOrder into a generic ResolvedCrossChainOrder + /// @dev Intended to improve standardized integration of various order types and settlement contracts + /// @param order The GaslessCrossChainOrder definition + /// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order + function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata) external pure returns (ResolvedCrossChainOrder memory){ + OrderDataGasless memory orderDataGasless = abi.decode(order.orderData, (OrderDataGasless)); + + // Enforce a uint96 nonce since the contract handles the nonce management by combining the provided nonce with the user address + uint96 nonce = uint96(order.nonce); + if (order.nonce != nonce) { + revert InvalidNonce(order.nonce); + } + OrderData memory orderData = _convertGaslessOrderData(order.user, nonce, orderDataGasless, ""); + return _resolveOrder(order.fillDeadline, orderData); + } + + /// @notice Resolves a specific OnchainCrossChainOrder into a generic ResolvedCrossChainOrder + /// @dev Intended to improve standardized integration of various order types and settlement contracts + /// @param order The OnchainCrossChainOrder definition + /// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order + function resolve(OnchainCrossChainOrder calldata order) external pure returns (ResolvedCrossChainOrder memory){ + OrderData memory orderData = abi.decode(order.orderData, (OrderData)); + return _resolveOrder(order.fillDeadline, orderData); + } + + function getCompactWitnessString() external pure returns (string memory) { + return "Witness(uint256 originChainId, uint256 targetChainId, bytes32 targetTokenAddress, uint256 targetMinAmount, bytes32 recipient, bytes32 destinationSettler, uint32 fillDeadline)"; + } + + function getBatchCompactWitnessString() external pure returns (string memory) { + return "Witness(uint256[] originChainId,uint256[] targetChainId,bytes32[] targetTokenAddress,uint256[] targetMinAmount,bytes32[] recipient,bytes32[] destinationSettler,uint32 fillDeadline)"; + } + + function _open(OrderData memory orderData_, uint32 fillDeadline_, bool delegated_) internal { + // Check the user + if(!delegated_ && orderData_.sponsor != msg.sender) { + revert InvalidCaller(msg.sender, orderData_.sponsor); + } + // Check the arbiter + if (orderData_.arbiter != ARBITER) { + revert InvalidArbiter(orderData_.arbiter); + } + + uint256 identifier = _createIdentifier(orderData_.sponsor, orderData_.nonce); + // Check the nonce + if (_userNonce[identifier]) { + revert NonceAlreadyInUse(orderData_.nonce); + } + _userNonce[identifier] = true; + + // We do not enforce a specific tribunal, so we do not check the address. This will allow to support new tribunals after the deployment of the allocator + // Going with an immutable tribunal would limit support for new chains with a fully decentralized allocator + /// TODO: THINK ABOUT IF THE ARBITER MUST BE ENFORCED OR NOT + + uint256 settlementsLength = orderData_.settlements.length; + + bytes32 digest; + bytes32 tokenHash; + if(settlementsLength > 1) { + uint256[2][] memory idsAndAmounts = new uint256[2][](settlementsLength); + + uint256[] memory originChainIds = new uint256[](settlementsLength); + uint256[] memory targetChainIds = new uint256[](settlementsLength); + bytes32[] memory targetTokenAddresses = new bytes32[](settlementsLength); + uint256[] memory targetMinAmounts = new uint256[](settlementsLength); + bytes32[] memory recipients = new bytes32[](settlementsLength); + bytes32[] memory destinationSettlers = new bytes32[](settlementsLength); + + // Iterate over the inputs and lock the tokens + for(uint256 i = 0; i < settlementsLength; ++i) { + if (orderData_.settlements[i].input.chainId != block.chainid) { + // MultiChainCompact not supported + revert InvalidChainId(orderData_.settlements[i].input.chainId, block.chainid); + } + if (_castToAddress(orderData_.settlements[i].input.recipient) != orderData_.sponsor) { + // Sponsor must be the same throughout all settlements + revert InvalidRecipient(_castToAddress(orderData_.settlements[i].input.recipient), orderData_.sponsor); + } + tokenHash = _lockTokens(orderData_, identifier, i); + idsAndAmounts[i] = [uint256(orderData_.settlements[i].input.token), orderData_.settlements[i].input.amount]; + originChainIds[i] = block.chainid; + targetChainIds[i] = orderData_.settlements[i].output.chainId; + targetTokenAddresses[i] = orderData_.settlements[i].output.token; + targetMinAmounts[i] = orderData_.settlements[i].output.amount; + recipients[i] = orderData_.settlements[i].output.recipient; + destinationSettlers[i] = orderData_.settlements[i].destinationSettler; + } + + // Work with a BatchCompact digest + digest = keccak256( + abi.encodePacked( + bytes2(0x1901), + ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + BATCH_COMPACT_WITNESS_TYPEHASH, + orderData_.arbiter, + orderData_.sponsor, + identifier, // TODO: IS THE CAST REQUIRED? + orderData_.claimDeadline, + idsAndAmounts, + keccak256( + abi.encode( + // Skip struct, because it would also need to be named 'Witness' (to be consistent with theCompact) + originChainIds, + targetChainIds, + targetTokenAddresses, + targetMinAmounts, + recipients, + destinationSettlers, + fillDeadline_ + ) + ) + ) + ) + ) + ); + } else { + if (orderData_.settlements[0].input.chainId != block.chainid) { + revert InvalidChainId(orderData_.settlements[0].input.chainId, block.chainid); + } + if (_castToAddress(orderData_.settlements[0].input.recipient) != orderData_.sponsor) { + // Sponsor must be the same throughout in the settlements + revert InvalidRecipient(_castToAddress(orderData_.settlements[0].input.recipient), orderData_.sponsor); + } + + tokenHash = _lockTokens(orderData_, identifier, 0); + + // Work with a Compact digest + digest = keccak256( + abi.encodePacked( + bytes2(0x1901), + ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + COMPACT_WITNESS_TYPEHASH, + orderData_.arbiter, + orderData_.sponsor, + identifier, + orderData_.claimDeadline, + uint256(orderData_.settlements[0].input.token), // TODO: IS THE CAST REQUIRED HERE? + orderData_.settlements[0].input.amount, + keccak256( + abi.encode( + Witness({ + originChainId: orderData_.settlements[0].input.chainId, + targetChainId: orderData_.settlements[0].output.chainId, + targetTokenAddress: orderData_.settlements[0].output.token, + targetMinAmount: orderData_.settlements[0].output.amount, + recipient: orderData_.settlements[0].output.recipient, + destinationSettler: orderData_.settlements[0].destinationSettler, + fillDeadline: fillDeadline_ + }) + ) + ) + ) + ) + ) + ); + } + + /// TODO: SHOULD WE SKIP THE REQUIREMENT OF THE SPONSOR SIGNATURE FOR A NON DELEGATED OPEN FUNCTION? + // confirm the signature matches the digest + address signer = ECDSA.recover(digest, orderData_.sponsorSignature); + if (orderData_.sponsor != signer) { + revert InvalidSignature(orderData_.sponsor, signer); + } + + // The stored tokenHash will only be used to check for the expiration of the order in the isValidSignature function. + // Since the expiration is the same for all allocations, it does not matter which of the tokenHashes is stored for a batch compact. + _sponsor[digest] = tokenHash; + + // Emit an open event + emit Open(bytes32(identifier), _resolveOrder(fillDeadline_, orderData_)); + } + + function _lockTokens(OrderData memory orderData_, uint256 identifier, uint256 index_) internal returns (bytes32 tokenHash_) { + return _lockTokens(orderData_.arbiter, orderData_.sponsor, identifier, orderData_.claimDeadline, uint256(orderData_.settlements[index_].input.token), orderData_.settlements[index_].input.amount); + } + + function _lockTokens(address arbiter, address sponsor, uint256 identifier, uint32 expires, uint256 id, uint256 amount) internal returns (bytes32 tokenHash_) { + tokenHash_ = _checkAllocation(Compact({ + arbiter: arbiter, + sponsor: sponsor, + nonce: identifier, + expires: expires, + id: id, + amount: amount + })); + _claim[tokenHash_] = expires; + _amount[tokenHash_] = amount; + _nonce[tokenHash_] = identifier; + + return tokenHash_; + + } + + function _resolveOrder(uint32 fillDeadline, OrderData memory orderData) internal pure returns (ResolvedCrossChainOrder memory) { + uint256 settlementLength = orderData.settlements.length; + FillInstruction[] memory fillInstructions = new FillInstruction[](settlementLength); + + Output[] memory outputs = new Output[](settlementLength); + Output[] memory inputs = new Output[](settlementLength); + + for(uint256 i = 0; i < settlementLength; ++i) { + inputs[i] = orderData.settlements[i].input; + outputs[i] = orderData.settlements[i].output; + + /// TODO: FILL THE MANDATE + Mandate memory mandate = Mandate({ + recipient: _castToAddress(orderData.settlements[i].output.recipient), + expires: fillDeadline, // TODO: is this correct? Or do we ignore the fill deadline and only care about the claim deadline? + token: _castToAddress(orderData.settlements[i].output.token), + minimumAmount: orderData.settlements[i].output.amount, + baselinePriorityFee: 0, // TODO: check whats happening here + scalingFactor: 0, // TODO: check whats happening here + salt: 0 // TODO: whats difference between salt and nonce? This is still sponsor signed data + }); + Claim memory claim = Claim({ + chainId: orderData.settlements[i].input.chainId, // TODO: IS THIS TARGET OR ORIGIN CHAIN ID? IT APPARENTLY SHOULD BE THE ORIGIN CHAIN ID, BUT WHERE IS THE TARGET CHAIN ID ADDED? + compact: Compact({ + arbiter: orderData.arbiter, + sponsor: orderData.sponsor, + nonce: orderData.nonce, + expires: orderData.claimDeadline, + id: uint256(orderData.settlements[i].input.token), // TODO: make it clear in doc that outputs and inputs are always connected via the index + amount: orderData.settlements[i].input.amount + }), + sponsorSignature: orderData.sponsorSignature, + allocatorSignature: "" // No signature required from this allocator, it will verify the claim on chain. + }); + + fillInstructions[i] = FillInstruction({ + destinationChainId: uint64(orderData.settlements[i].output.chainId), // TODO: WHY SUDDENLY A UINT64 INSTEAD OF UINT256? + destinationSettler: orderData.settlements[i].destinationSettler, + originData: abi.encode(claim, mandate) // TODO: FILL WITH THE ORIGIN DATA REQUIRED BY THE TRIBUNAL + }); + } + + ResolvedCrossChainOrder memory resolvedOrder = ResolvedCrossChainOrder({ + user: orderData.sponsor, + originChainId: orderData.settlements[0].input.chainId, // must be same for every input + openDeadline: orderData.claimDeadline, /// TODO: CAN THE OPEN DEADLINE BE THE CLAIM DEADLINE? + fillDeadline: fillDeadline, + orderId: bytes32(_createIdentifier(orderData.sponsor, orderData.nonce)), + maxSpent: inputs, + minReceived: outputs, + fillInstructions: fillInstructions + }); + return resolvedOrder; + } + + function _createIdentifier(address sponsor_, uint96 nonce_) internal pure returns (uint256 identifier_) { + assembly ("memory-safe") { + identifier_ := or(shl(160, nonce_), shr(96, shl(96,sponsor_))) + } + return identifier_; + } + + function _castToAddress(bytes32 address_) internal pure returns (address output_) { + assembly ("memory-safe") { + output_ := shr(96, shl(96, address_)) + } + } + + function _convertGaslessOrderData(address sponsor_, uint96 nonce_, OrderDataGasless memory orderDataGasless_, bytes memory signature_) internal pure returns (OrderData memory orderData_) { + orderData_ = OrderData({ + sponsor: sponsor_, + nonce: nonce_, + claimDeadline: orderDataGasless_.claimDeadline, + settlements: orderDataGasless_.settlements, + arbiter: orderDataGasless_.arbiter, + sponsorSignature: signature_ + }); + return orderData_; + } +} diff --git a/src/allocators/types/TribunalStructs.sol b/src/allocators/types/TribunalStructs.sol new file mode 100644 index 0000000..00cc0bf --- /dev/null +++ b/src/allocators/types/TribunalStructs.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import { Compact } from "@uniswap/the-compact/types/EIP712Types.sol"; + + + struct Claim { + uint256 chainId; // Claim processing chain ID + Compact compact; + bytes sponsorSignature; // Authorization from the sponsor + bytes allocatorSignature; // Authorization from the allocator + } + + struct Mandate { + address recipient; // Recipient of settled tokens + uint256 expires; // Mandate expiration timestamp + address token; // Settlement token (address(0) for native) + uint256 minimumAmount; // Minimum settlement amount + uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in + uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) + bytes32 salt; // Replay protection parameter + } + + struct Directive { + address claimant; // Recipient of claimed tokens + uint256 dispensation; // Cross-chain message layer payment + } \ No newline at end of file diff --git a/src/interfaces/ERC7683/IOriginSettler.sol b/src/interfaces/ERC7683/IOriginSettler.sol new file mode 100644 index 0000000..eeb685c --- /dev/null +++ b/src/interfaces/ERC7683/IOriginSettler.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @title IOriginSettler +/// @notice Standard interface for settlement contracts on the origin chain +interface IOriginSettler { + /// @title GaslessCrossChainOrder CrossChainOrder type + /// @notice Standard order struct to be signed by users, disseminated to fillers, and submitted to origin settler contracts + struct GaslessCrossChainOrder { + /// @dev The contract address that the order is meant to be settled by. + /// Fillers send this order to this contract address on the origin chain + address originSettler; + /// @dev The address of the user who is initiating the swap, + /// whose input tokens will be taken and escrowed + address user; + /// @dev Nonce to be used as replay protection for the order + uint256 nonce; + /// @dev The chainId of the origin chain + uint256 originChainId; + /// @dev The timestamp by which the order must be opened + uint32 openDeadline; + /// @dev The timestamp by which the order must be filled on the destination chain + uint32 fillDeadline; + /// @dev Type identifier for the order data. This is an EIP-712 typehash. + bytes32 orderDataType; + /// @dev Arbitrary implementation-specific data + /// Can be used to define tokens, amounts, destination chains, fees, settlement parameters, + /// or any other order-type specific information + bytes orderData; + } + + /// @title OnchainCrossChainOrder CrossChainOrder type + /// @notice Standard order struct for user-opened orders, where the user is the msg.sender. + struct OnchainCrossChainOrder { + /// @dev The timestamp by which the order must be filled on the destination chain + uint32 fillDeadline; + /// @dev Type identifier for the order data. This is an EIP-712 typehash. + bytes32 orderDataType; + /// @dev Arbitrary implementation-specific data + /// Can be used to define tokens, amounts, destination chains, fees, settlement parameters, + /// or any other order-type specific information + bytes orderData; + } + /// @title ResolvedCrossChainOrder type + /// @notice An implementation-generic representation of an order intended for filler consumption + /// @dev Defines all requirements for filling an order by unbundling the implementation-specific orderData. + /// @dev Intended to improve integration generalization by allowing fillers to compute the exact input and output information of any order + struct ResolvedCrossChainOrder { + /// @dev The address of the user who is initiating the transfer + address user; + /// @dev The chainId of the origin chain + uint256 originChainId; + /// @dev The timestamp by which the order must be opened + uint32 openDeadline; + /// @dev The timestamp by which the order must be filled on the destination chain(s) + uint32 fillDeadline; + /// @dev The unique identifier for this order within this settlement system + bytes32 orderId; + + /// @dev The max outputs that the filler will send. It's possible the actual amount depends on the state of the destination + /// chain (destination dutch auction, for instance), so these outputs should be considered a cap on filler liabilities. + Output[] maxSpent; + /// @dev The minimum outputs that must be given to the filler as part of order settlement. Similar to maxSpent, it's possible + /// that special order types may not be able to guarantee the exact amount at open time, so this should be considered + /// a floor on filler receipts. + Output[] minReceived; + /// @dev Each instruction in this array is parameterizes a single leg of the fill. This provides the filler with the information + /// necessary to perform the fill on the destination(s). + FillInstruction[] fillInstructions; + } + + /// @notice Tokens that must be received for a valid order fulfillment + struct Output { + /// @dev The address of the ERC20 token on the destination chain + /// @dev address(0) used as a sentinel for the native token + bytes32 token; + /// @dev The amount of the token to be sent + uint256 amount; + /// @dev The address to receive the output tokens + bytes32 recipient; + /// @dev The destination chain for this output + uint256 chainId; + } + + /// @title FillInstruction type + /// @notice Instructions to parameterize each leg of the fill + /// @dev Provides all the origin-generated information required to produce a valid fill leg + struct FillInstruction { + /// @dev The contract address that the order is meant to be settled by + uint64 destinationChainId; + /// @dev The contract address that the order is meant to be filled on + bytes32 destinationSettler; + /// @dev The data generated on the origin chain needed by the destinationSettler to process the fill + bytes originData; + } + + /// @notice Signals that an order has been opened + /// @param orderId a unique order identifier within this settlement system + /// @param resolvedOrder resolved order that would be returned by resolve if called instead of Open + event Open(bytes32 indexed orderId, ResolvedCrossChainOrder resolvedOrder); + + + /// @notice Opens a gasless cross-chain order on behalf of a user. + /// @dev To be called by the filler. + /// @dev This method must emit the Open event + /// @param order The GaslessCrossChainOrder definition + /// @param signature The user's signature over the order + /// @param originFillerData Any filler-defined data required by the settler + function openFor(GaslessCrossChainOrder calldata order, bytes calldata signature, bytes calldata originFillerData) external; + + /// @notice Opens a cross-chain order + /// @dev To be called by the user + /// @dev This method must emit the Open event + /// @param order The OnchainCrossChainOrder definition + function open(OnchainCrossChainOrder calldata order) external; + + /// @notice Resolves a specific GaslessCrossChainOrder into a generic ResolvedCrossChainOrder + /// @dev Intended to improve standardized integration of various order types and settlement contracts + /// @param order The GaslessCrossChainOrder definition + /// @param originFillerData Any filler-defined data required by the settler + /// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order + function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata originFillerData) external view returns (ResolvedCrossChainOrder memory); + + /// @notice Resolves a specific OnchainCrossChainOrder into a generic ResolvedCrossChainOrder + /// @dev Intended to improve standardized integration of various order types and settlement contracts + /// @param order The OnchainCrossChainOrder definition + /// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order + function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory); +} \ No newline at end of file From 872891be75735974b50c825826d0de36cfdd4bfe Mon Sep 17 00:00:00 2001 From: vimageDE Date: Fri, 10 Jan 2025 13:09:38 +0100 Subject: [PATCH 03/35] readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a2e3b17..00cf797 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ The allocators are designed to be used with the [The Compact](https://github.com - [ServerAllocator](src/allocators/ServerAllocator.sol): The ServerAllocator stands as an on chain verification contract for a server based allocator. It is ready for the callbacks of the [The Compact](https://github.com/uniswap/the-compact) during a claim and verifies the allocator signatures have been signed by an authorized address. It does not keep track of any locked down tokens, but instead relies on the server to do so. - [SimpleAllocator](src/allocators/SimpleAllocator.sol): A simple, fully decentralized allocator that allows for a single claim per token. This means the contract will lock down all tokens of a sponsor for an id for a single claim, so it is not possible to start multiple claims for the same sponsor and id at the same time. The contract does though keep track of the amount of locked tokens and so it will faithfully attest for a transfer of those, even during an ongoing claim. The contract is a good starting point when learning about allocators and it is kept very simple on purpose to learn about the concept of an allocator or use this contract as a template. To be used in production, the contract would require the ability to work with witness data, since a real cross chain swap will always require a witness besides the Compact. An example implementation of a witness allocator can be found [here](src/allocators/SimpleWitnessAllocator.sol). - [SimpleWitnessAllocator](src/allocators/SimpleWitnessAllocator.sol): This contract enhances the [SimpleAllocator](src/allocators/SimpleAllocator.sol) with the ability of processing witness data besides the Compact. This makes it a much more production ready allocator. + - [SimpleERC7683Allocator](src/allocators/SimpleERC7683Allocator.sol): This contract enhances the [SimpleAllocator](src/allocators/SimpleAllocator.sol) and making it compatible with the [ERC7683](https://eips.ethereum.org/EIPS/eip-7683) standard. The Allocator therefor also becomes a [IOriginSettler](src/interfaces/ERC7683/IOriginSettler.sol) and converts a OnchainCrossChainOrder to a `Compact`/`BatchCompact` and a `Claim` / `Mendate` as required by the tribunal on the target chain. ## Deployment From c7c5e60584065937e56a784a1d49102b05f39189 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Wed, 15 Jan 2025 12:01:41 +0100 Subject: [PATCH 04/35] SimpleERC7683 updates --- src/allocators/SimpleERC7683Allocator.sol | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/allocators/SimpleERC7683Allocator.sol b/src/allocators/SimpleERC7683Allocator.sol index 8e8cdf1..b6d8350 100644 --- a/src/allocators/SimpleERC7683Allocator.sol +++ b/src/allocators/SimpleERC7683Allocator.sol @@ -18,6 +18,9 @@ contract SimpleERC7683Allocator is SimpleAllocator, IOriginSettler { Settlement[] settlements; address arbiter; bytes sponsorSignature; + /// TODO: add baselinePriorityFee? + /// TODO: add scalingFactor? + /// TODO: add salt? } struct OrderDataGasless { @@ -103,15 +106,11 @@ contract SimpleERC7683Allocator is SimpleAllocator, IOriginSettler { /// TODO: batch compacts witness - // The block interval on the deployed chain - uint32 immutable BLOCK_INTERVAL; - // The nonce of the allocator mapping(uint256 identifier => bool nonceUsed) private _userNonce; - constructor(address compactContract_, address arbiter_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_, uint32 blockInterval_) + constructor(address compactContract_, address arbiter_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) SimpleAllocator(compactContract_, arbiter_, minWithdrawalDelay_, maxWithdrawalDelay_) { - BLOCK_INTERVAL = blockInterval_; } /// @notice Opens a gasless cross-chain order on behalf of a user. From 217a8c4bab5b11629734f077f0b2361f4d23ce62 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Wed, 15 Jan 2025 12:01:49 +0100 Subject: [PATCH 05/35] RoutingAllocator --- src/allocators/RoutingAllocatorERC7683.sol | 547 +++++++++++++++++++++ 1 file changed, 547 insertions(+) create mode 100644 src/allocators/RoutingAllocatorERC7683.sol diff --git a/src/allocators/RoutingAllocatorERC7683.sol b/src/allocators/RoutingAllocatorERC7683.sol new file mode 100644 index 0000000..5f83d59 --- /dev/null +++ b/src/allocators/RoutingAllocatorERC7683.sol @@ -0,0 +1,547 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import { ERC6909 } from "@solady/tokens/ERC6909.sol"; +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; +import { Compact, BatchCompact, BATCH_COMPACT_TYPEHASH, COMPACT_TYPEHASH } from "@uniswap/the-compact/types/EIP712Types.sol"; +import { IdLib } from "@uniswap/the-compact/lib/IdLib.sol"; +import { Lock } from "@uniswap/the-compact/types/Lock.sol"; +import { ResetPeriod } from "@uniswap/the-compact/types/ResetPeriod.sol"; +import { Scope } from "@uniswap/the-compact/types/Scope.sol"; +import { ITheCompact } from "@uniswap/the-compact/interfaces/ITheCompact.sol"; +import { SimpleAllocator } from "./SimpleAllocator.sol"; +import { Claim, Mandate } from "./types/TribunalStructs.sol"; +import { IOriginSettler } from "../interfaces/ERC7683/IOriginSettler.sol"; + +contract RoutingAllocatorERC7683 is SimpleAllocator, IOriginSettler { + using SafeTransferLib for address; + + struct OrderData { + address sponsor; + uint96 nonce; + uint32 claimDeadline; + Settlement[] settlements; + address arbiter; + bytes sponsorSignature; + /// TODO: add baselinePriorityFee? + /// TODO: add scalingFactor? + /// TODO: add salt? + } + + struct OrderDataGasless { + uint32 claimDeadline; + Settlement[] settlements; + address arbiter; + } + + struct Settlement { + Output input; + Output output; + bytes32 destinationSettler; // tribunal + } + + struct Witness { // for single / non-batch compact + uint256 originChainId; + uint256 targetChainId; + bytes32 targetTokenAddress; + uint256 targetMinAmount; + bytes32 recipient; + bytes32 destinationSettler; + uint32 fillDeadline; + } + + error InvalidOriginSettler(address originSettler, address expectedOriginSettler); + error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); + error InvalidChainId(uint256 chainId, uint256 expectedChainId); + error InvalidRecipient(address recipient, address expectedRecipient); + error InvalidNonce(uint256 nonce); + error NonceAlreadyInUse(uint96 nonce); + error InvalidSignature(address signer, address expectedSigner); + error InvalidAmount(uint256 amount, uint256 expectedAmount); + error InvalidTransferAmount(); + error FailedToRefund(address recipient, uint256 amount); + error InvalidCall(); + error InvalidId(uint256 actualId, uint256 expectedId); + error InvalidTokenId(uint256 tokenId, bytes32 expectedTokenPrefix); + + // The typehash of the OrderData struct + // keccak256("OrderData(address sponsor,uint96 nonce,uint32 claimDeadline,Settlement[] settlements,address arbiter,bytes sponsorSignature)Output(bytes32 token,uint256 amount,bytes32 recipient,uint256 chainId)Settlement(Output input,Output output,bytes32 destinationSettler)") + bytes32 constant ORDERDATA_TYPEHASH = 0xe8225b67751f9ff0d865fdc55742ea54087b20406855f954bd737ab200819ab8; + + // The typehash of the OrderDataGasless struct + // keccak256("OrderDataGasless(uint32 claimDeadline,Settlement[] settlements,address arbiter)Output(bytes32 token,uint256 amount,bytes32 recipient,uint256 chainId)Settlement(Output input,Output output,bytes32 destinationSettler)") + bytes32 constant ORDERDATA_GASLESS_TYPEHASH = 0x4679e16e516a2f88beb96ee00964e5f28b9e2ed596592f8c3d92dd70411611a9; + + // keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness) + // Witness(uint256 originChainId,uint256 targetChainId,bytes32 targetTokenAddress,uint256 targetMinAmount,bytes32 recipient,bytes32 destinationSettler,uint32 fillDeadline)") + bytes32 constant COMPACT_WITNESS_TYPEHASH = 0x2f0f51aa07316f3d8860366b556177042fafb2edffc93c799091bae7c194d9a6; + + // keccak256("BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts,Witness witness) + // Witness(uint256[] originChainId,uint256[] targetChainId,bytes32[] targetTokenAddress,uint256[] targetMinAmount,bytes32[] recipient,bytes32[] destinationSettler,uint32 fillDeadline)") + bytes32 constant BATCH_COMPACT_WITNESS_TYPEHASH = 0x3158fb17880387b9302c66a40dd45893126fd532c2abb7deee6e53149c53646b; + + ResetPeriod constant DEFAULT_RESET_PERIOD = ResetPeriod.TenMinutes; + Scope constant DEFAULT_SCOPE = Scope.Multichain; + bytes32 immutable ALLOCATOR_ID_PREFIX; + + mapping(uint256 identifier => bool nonceUsed) private _userNonce; + + constructor(address compactContract_, address arbiter_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) + SimpleAllocator(compactContract_, arbiter_, minWithdrawalDelay_, maxWithdrawalDelay_) { + ALLOCATOR_ID_PREFIX = bytes32(IdLib.toId(Lock({ + token: address(0), + allocator: address(this), + resetPeriod: DEFAULT_RESET_PERIOD, + scope: DEFAULT_SCOPE + }))); + } + + /// @notice Opens a gasless cross-chain order on behalf of a user. + /// @dev To be called by the filler. + /// @dev This method must emit the Open event + /// @dev This deposits remaining tokens to fulfill the order in the compact contract + /// @dev This locks the users tokens + /// @param order The GaslessCrossChainOrder definition + /// @param signature The user's signature over the order + function openFor(GaslessCrossChainOrder calldata order, bytes calldata signature, bytes calldata) external { + // since we have the users signature, we can create locks in the name of the user + + // Check if orderDataType is the one expected by the allocator + if (order.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { + revert InvalidOrderDataType(order.orderDataType, ORDERDATA_GASLESS_TYPEHASH); + } + // Check the allocator is the chosen origin settler + // This check is not strictly necessary, since the user does not sign a allocator. The filler will not get payed if he maliciously choses a different allocator. + if (order.originSettler != address(this)) { + revert InvalidOriginSettler(order.originSettler, address(this)); + } + + // Decode the orderData + OrderDataGasless memory orderDataGasless = abi.decode(order.orderData, (OrderDataGasless)); + // Enforce a uint96 nonce since the contract handles the nonce management by combining the provided nonce with the user address + uint96 nonce = uint96(order.nonce); + if (order.nonce != nonce) { + revert InvalidNonce(order.nonce); + } + + OrderData memory orderData = _convertGaslessOrderData(order.user, nonce, orderDataGasless, signature); + + _open(orderData, order.fillDeadline, false); + } + + /// @notice Opens a cross-chain order + /// @dev To be called by the user + /// @dev This method must emit the Open event + /// @dev This deposits remaining tokens to fulfill the order in the compact contract + /// @dev This locks the users tokens + /// @param order The OnchainCrossChainOrder definition + function open(OnchainCrossChainOrder calldata order) external { + // Check if orderDataType is the one expected by the allocator + if (order.orderDataType != ORDERDATA_TYPEHASH) { + revert InvalidOrderDataType(order.orderDataType, ORDERDATA_TYPEHASH); + } + + // Decode the orderData + OrderData memory orderData = abi.decode(order.orderData, (OrderData)); + _open(orderData, order.fillDeadline, false); + } + + /// @notice Opens a cross-chain order for native tokens + /// @notice This function also works for ERC20 tokens, but refunding the remaining balance to the user is not necessary for ERC20 tokens + /// @dev To be called by the user + /// @dev This method must emit the Open event + /// @dev This deposits remaining tokens to fulfill the order in the compact contract + /// @dev This locks the users tokens + /// @dev This refunds the remaining balance to the user + /// @param order The OnchainCrossChainOrder definition + function openNativeTokenOrder(OnchainCrossChainOrder calldata order) external payable { + // Check if orderDataType is the one expected by the allocator + if (order.orderDataType != ORDERDATA_TYPEHASH) { + revert InvalidOrderDataType(order.orderDataType, ORDERDATA_TYPEHASH); + } + + // Decode the orderData + OrderData memory orderData = abi.decode(order.orderData, (OrderData)); + _open(orderData, order.fillDeadline, false); + // Refund the remaining native token balance to the user + _refundBalance(msg.sender); + } + + receive() external payable { + revert InvalidCall(); + } + + fallback() external payable { + revert InvalidCall(); + } + + /// @notice Resolves a specific GaslessCrossChainOrder into a generic ResolvedCrossChainOrder + /// @dev Intended to improve standardized integration of various order types and settlement contracts + /// @param order The GaslessCrossChainOrder definition + /// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order + function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata) external pure returns (ResolvedCrossChainOrder memory){ + OrderDataGasless memory orderDataGasless = abi.decode(order.orderData, (OrderDataGasless)); + + // Enforce a uint96 nonce since the contract handles the nonce management by combining the provided nonce with the user address + uint96 nonce = uint96(order.nonce); + if (order.nonce != nonce) { + revert InvalidNonce(order.nonce); + } + OrderData memory orderData = _convertGaslessOrderData(order.user, nonce, orderDataGasless, ""); + return _resolveOrder(order.fillDeadline, orderData); + } + + /// @notice Resolves a specific OnchainCrossChainOrder into a generic ResolvedCrossChainOrder + /// @dev Intended to improve standardized integration of various order types and settlement contracts + /// @param order The OnchainCrossChainOrder definition + /// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order + function resolve(OnchainCrossChainOrder calldata order) external pure returns (ResolvedCrossChainOrder memory){ + OrderData memory orderData = abi.decode(order.orderData, (OrderData)); + return _resolveOrder(order.fillDeadline, orderData); + } + + function getCompactWitnessString() external pure returns (string memory) { + return "Witness(uint256 originChainId, uint256 targetChainId, bytes32 targetTokenAddress, uint256 targetMinAmount, bytes32 recipient, bytes32 destinationSettler, uint32 fillDeadline)"; + } + + function getBatchCompactWitnessString() external pure returns (string memory) { + return "Witness(uint256[] originChainId,uint256[] targetChainId,bytes32[] targetTokenAddress,uint256[] targetMinAmount,bytes32[] recipient,bytes32[] destinationSettler,uint32 fillDeadline)"; + } + + function _deposit(address sponsor_, address token_, uint256 amount_, bool useDeposited_) internal { + uint256 id = _toId(token_); + uint256 depositAmount = amount_; + if(useDeposited_){ + // Check unlocked balance + uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(sponsor_, id); + bytes32 tokenHash = _getTokenHash(id, sponsor_); + + if (_claim[tokenHash] > block.timestamp) { + // Lock is still active, add the locked amount if the nonce has not yet been consumed. It cannot be bigger then the balance. + balance -= ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)) ? 0 : _amount[tokenHash]; + } + if( balance > amount_) { + // Cap balance to required amount + balance = amount_; + } + // Subtract the unlocked balance from the amount to deposit + depositAmount -= balance; + } + + if(depositAmount > 0) { + // Deposit the remaining amount + uint256 actualId; + if (token_ == address(0)) { + if(msg.value < depositAmount) { + revert InvalidAmount(msg.value, depositAmount); + } + // deposit native token + actualId = ITheCompact(COMPACT_CONTRACT).deposit{value: depositAmount}(address(this), DEFAULT_RESET_PERIOD, DEFAULT_SCOPE, sponsor_); + } else { + uint256 prevAmount = IERC20(token_).balanceOf(address(this)); + // Requires previous approval + token_.safeTransferFrom(sponsor_, address(this), depositAmount); + uint256 newAmount = IERC20(token_).balanceOf(address(this)); + + if(newAmount <= prevAmount) { + revert InvalidTransferAmount(); + } + // NOTE: settling fee-on-transfer tokens will result in fewer tokens + // being received. Be sure to accommodate for this when + // providing the desired deposit amount. + depositAmount = newAmount - prevAmount; + // Depositing the actual received amount after the transfer + actualId = ITheCompact(COMPACT_CONTRACT).deposit(token_, address(this), DEFAULT_RESET_PERIOD, DEFAULT_SCOPE, depositAmount, sponsor_); + } + if(actualId != id) { + revert InvalidId(actualId, id); + } + } + } + + function _open(OrderData memory orderData_, uint32 fillDeadline_, bool delegated_) internal { + // Check the user + if(!delegated_ && orderData_.sponsor != msg.sender) { + revert InvalidCaller(msg.sender, orderData_.sponsor); + } + // Check the arbiter + if (orderData_.arbiter != ARBITER) { + revert InvalidArbiter(orderData_.arbiter); + } + + uint256 identifier = _createIdentifier(orderData_.sponsor, orderData_.nonce); + // Check the nonce + if (_userNonce[identifier]) { + revert NonceAlreadyInUse(orderData_.nonce); + } + _userNonce[identifier] = true; + + // NOTE: We do not enforce a specific tribunal, so we do not check the address. + // This will allow to support new tribunals after the deployment of the allocator + // Going with an immutable tribunal would limit support for new chains with a fully decentralized allocator + /// TODO: THINK ABOUT IF THE ARBITER, TRIBUNAL MUST BE ENFORCED OR NOT + + uint256 settlementsLength = orderData_.settlements.length; + + bytes32 digest; + bytes32 tokenHash; + if(settlementsLength > 1) { + uint256[2][] memory idsAndAmounts = new uint256[2][](settlementsLength); + + uint256[] memory originChainIds = new uint256[](settlementsLength); + uint256[] memory targetChainIds = new uint256[](settlementsLength); + bytes32[] memory targetTokenAddresses = new bytes32[](settlementsLength); + uint256[] memory targetMinAmounts = new uint256[](settlementsLength); + bytes32[] memory recipients = new bytes32[](settlementsLength); + bytes32[] memory destinationSettlers = new bytes32[](settlementsLength); + + // Iterate over the inputs and lock the tokens + for(uint256 i = 0; i < settlementsLength; ++i) { + if (orderData_.settlements[i].input.chainId != block.chainid) { + // MultiChainCompact not supported + revert InvalidChainId(orderData_.settlements[i].input.chainId, block.chainid); + } + if (_castToAddress(orderData_.settlements[i].input.recipient) != orderData_.sponsor) { + // Sponsor must be the same throughout all settlements + revert InvalidRecipient(_castToAddress(orderData_.settlements[i].input.recipient), orderData_.sponsor); + } + if (!_checkIdPrefix(uint256(orderData_.settlements[i].input.token))) { + revert InvalidTokenId(uint256(orderData_.settlements[i].input.token), ALLOCATOR_ID_PREFIX); + } + // Deposit the amount of tokens that is not already available and unlocked + _deposit(orderData_.sponsor, _castToAddress(orderData_.settlements[i].input.token), orderData_.settlements[i].input.amount, true); + // lock the tokens + tokenHash = _lockTokens(orderData_, identifier, i); + + idsAndAmounts[i] = [uint256(orderData_.settlements[i].input.token), orderData_.settlements[i].input.amount]; + originChainIds[i] = block.chainid; + targetChainIds[i] = orderData_.settlements[i].output.chainId; + targetTokenAddresses[i] = orderData_.settlements[i].output.token; + targetMinAmounts[i] = orderData_.settlements[i].output.amount; + recipients[i] = orderData_.settlements[i].output.recipient; + destinationSettlers[i] = orderData_.settlements[i].destinationSettler; + } + + // Work with a BatchCompact digest + digest = keccak256( + abi.encodePacked( + bytes2(0x1901), + ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + BATCH_COMPACT_WITNESS_TYPEHASH, + orderData_.arbiter, + orderData_.sponsor, + identifier, // TODO: IS THE CAST REQUIRED? + orderData_.claimDeadline, + idsAndAmounts, + keccak256( + abi.encode( + // Skips usage of a struct and supplies data directly + originChainIds, + targetChainIds, + targetTokenAddresses, + targetMinAmounts, + recipients, + destinationSettlers, + fillDeadline_ + ) + ) + ) + ) + ) + ); + } else { + if (orderData_.settlements[0].input.chainId != block.chainid) { + revert InvalidChainId(orderData_.settlements[0].input.chainId, block.chainid); + } + if (_castToAddress(orderData_.settlements[0].input.recipient) != orderData_.sponsor) { + // Sponsor must be the same throughout in the settlements + revert InvalidRecipient(_castToAddress(orderData_.settlements[0].input.recipient), orderData_.sponsor); + } + if (!_checkIdPrefix(uint256(orderData_.settlements[0].input.token))) { + revert InvalidTokenId(uint256(orderData_.settlements[0].input.token), ALLOCATOR_ID_PREFIX); + } + + // Deposit the amount of tokens that is not already available and unlocked + _deposit(orderData_.sponsor, _castToAddress(orderData_.settlements[0].input.token), orderData_.settlements[0].input.amount, true); + // lock the tokens + tokenHash = _lockTokens(orderData_, identifier, 0); + + // Work with a Compact digest + digest = keccak256( + abi.encodePacked( + bytes2(0x1901), + ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + COMPACT_WITNESS_TYPEHASH, + orderData_.arbiter, + orderData_.sponsor, + identifier, + orderData_.claimDeadline, + uint256(orderData_.settlements[0].input.token), // TODO: IS THE CAST REQUIRED HERE? + orderData_.settlements[0].input.amount, + keccak256( + abi.encode( + Witness({ + originChainId: orderData_.settlements[0].input.chainId, + targetChainId: orderData_.settlements[0].output.chainId, + targetTokenAddress: orderData_.settlements[0].output.token, + targetMinAmount: orderData_.settlements[0].output.amount, + recipient: orderData_.settlements[0].output.recipient, + destinationSettler: orderData_.settlements[0].destinationSettler, + fillDeadline: fillDeadline_ + }) + ) + ) + ) + ) + ) + ); + } + + // confirm the signature matches the sponsor to verify the users intent (deposit, lock and open order intents) + address signer = ECDSA.recover(digest, orderData_.sponsorSignature); + if (orderData_.sponsor != signer) { + revert InvalidSignature(orderData_.sponsor, signer); + } + + // Finalize the lock by storing the tokenHash based on the digest + _sponsor[digest] = tokenHash; + // The stored tokenHash will only be used to check for the expiration of the order in the isValidSignature function. + // Since the expiration is the same for all allocations, it does not matter which of the tokenHashes is stored for a batch compact. + + // Emit an open event + emit Open(bytes32(identifier), _resolveOrder(fillDeadline_, orderData_)); + } + + function _lockTokens(OrderData memory orderData_, uint256 identifier, uint256 index_) internal returns (bytes32 tokenHash_) { + return _lockTokens(orderData_.arbiter, orderData_.sponsor, identifier, orderData_.claimDeadline, uint256(orderData_.settlements[index_].input.token), orderData_.settlements[index_].input.amount); + } + + function _lockTokens(address arbiter, address sponsor, uint256 identifier, uint32 expires, uint256 id, uint256 amount) internal returns (bytes32 tokenHash_) { + tokenHash_ = _checkAllocation(Compact({ + arbiter: arbiter, + sponsor: sponsor, + nonce: identifier, + expires: expires, + id: id, + amount: amount + })); + _claim[tokenHash_] = expires; + _amount[tokenHash_] = amount; + _nonce[tokenHash_] = identifier; + + return tokenHash_; + + } + + function _resolveOrder(uint32 fillDeadline, OrderData memory orderData) internal pure returns (ResolvedCrossChainOrder memory) { + uint256 settlementLength = orderData.settlements.length; + FillInstruction[] memory fillInstructions = new FillInstruction[](settlementLength); + + Output[] memory outputs = new Output[](settlementLength); + Output[] memory inputs = new Output[](settlementLength); + + for(uint256 i = 0; i < settlementLength; ++i) { + inputs[i] = orderData.settlements[i].input; + outputs[i] = orderData.settlements[i].output; + + /// TODO: FILL THE MANDATE + Mandate memory mandate = Mandate({ + recipient: _castToAddress(orderData.settlements[i].output.recipient), + expires: fillDeadline, // TODO: is this correct? Or do we ignore the fill deadline and only care about the claim deadline? + token: _castToAddress(orderData.settlements[i].output.token), + minimumAmount: orderData.settlements[i].output.amount, + baselinePriorityFee: 0, // TODO: check whats happening here + scalingFactor: 0, // TODO: check whats happening here + salt: 0 // TODO: whats difference between salt and nonce? This is still sponsor signed data + }); + Claim memory claim = Claim({ + chainId: orderData.settlements[i].input.chainId, // TODO: IS THIS TARGET OR ORIGIN CHAIN ID? IT APPARENTLY SHOULD BE THE ORIGIN CHAIN ID, BUT WHERE IS THE TARGET CHAIN ID ADDED? + compact: Compact({ + arbiter: orderData.arbiter, + sponsor: orderData.sponsor, + nonce: orderData.nonce, + expires: orderData.claimDeadline, + id: uint256(orderData.settlements[i].input.token), // TODO: make it clear in doc that outputs and inputs are always connected via the index + amount: orderData.settlements[i].input.amount + }), + sponsorSignature: orderData.sponsorSignature, + allocatorSignature: "" // No signature required from this allocator, it will verify the claim on chain. + }); + + fillInstructions[i] = FillInstruction({ + destinationChainId: uint64(orderData.settlements[i].output.chainId), // TODO: WHY SUDDENLY A UINT64 INSTEAD OF UINT256? + destinationSettler: orderData.settlements[i].destinationSettler, + originData: abi.encode(claim, mandate) // TODO: FILL WITH THE ORIGIN DATA REQUIRED BY THE TRIBUNAL + }); + } + + ResolvedCrossChainOrder memory resolvedOrder = ResolvedCrossChainOrder({ + user: orderData.sponsor, + originChainId: orderData.settlements[0].input.chainId, // must be same for every input + openDeadline: orderData.claimDeadline, /// TODO: CAN THE OPEN DEADLINE BE THE CLAIM DEADLINE? + fillDeadline: fillDeadline, + orderId: bytes32(_createIdentifier(orderData.sponsor, orderData.nonce)), + maxSpent: inputs, + minReceived: outputs, + fillInstructions: fillInstructions + }); + return resolvedOrder; + } + + function _refundBalance(address recipient_) internal { + if (address(this).balance > 0) { + (bool success, ) = payable(recipient_).call{value: address(this).balance}(""); + if (!success) { + revert FailedToRefund(recipient_, address(this).balance); + } + } + } + + function _createIdentifier(address sponsor_, uint96 nonce_) internal pure returns (uint256 identifier_) { + assembly ("memory-safe") { + identifier_ := or(shl(160, nonce_), shr(96, shl(96,sponsor_))) + } + return identifier_; + } + + function _castToAddress(bytes32 address_) internal pure returns (address output_) { + assembly ("memory-safe") { + output_ := shr(96, shl(96, address_)) + } + } + + function _checkIdPrefix(uint256 id_) internal view returns (bool isSame_) { + bytes32 idPrefix = ALLOCATOR_ID_PREFIX; + assembly ("memory-safe") { + isSame_ := eq(idPrefix, shl(160, shr(160, id_))) + } + } + + function _toId(address token_) internal view returns (uint256 id_) { + bytes32 idPrefix = ALLOCATOR_ID_PREFIX; + assembly ("memory-safe") { + id_ := or(idPrefix, shr(96, shl(96, token_))) + } + return id_; + } + + function _convertGaslessOrderData(address sponsor_, uint96 nonce_, OrderDataGasless memory orderDataGasless_, bytes memory signature_) internal pure returns (OrderData memory orderData_) { + orderData_ = OrderData({ + sponsor: sponsor_, + nonce: nonce_, + claimDeadline: orderDataGasless_.claimDeadline, + settlements: orderDataGasless_.settlements, + arbiter: orderDataGasless_.arbiter, + sponsorSignature: signature_ + }); + return orderData_; + } +} + From e268af4055b569b7a0200bf2cc43b03a67fe137c Mon Sep 17 00:00:00 2001 From: vimageDE Date: Tue, 25 Feb 2025 12:54:01 +0100 Subject: [PATCH 06/35] ERC8683Allocator contract and tests --- foundry.toml | 1 + src/allocators/ERC7683Allocator.sol | 303 ++++++++++ src/allocators/RoutingAllocatorERC7683.sol | 6 +- src/allocators/SimpleAllocator.sol | 25 +- src/allocators/SimpleERC7683Allocator.sol | 472 +++++++--------- src/allocators/types/TribunalStructs.sol | 7 +- src/interfaces/ERC7683/IOriginSettler.sol | 2 +- src/interfaces/IERC7683Allocator.sol | 77 +++ src/interfaces/ISimpleAllocator.sol | 6 +- src/test/TheCompactMock.sol | 9 +- test/ERC7683Allocator.t.sol | 618 +++++++++++++++++++++ test/SimpleAllocator.t.sol | 49 +- test/SimpleERC7683Allocator.t.sol | 101 ++++ 13 files changed, 1362 insertions(+), 314 deletions(-) create mode 100644 src/allocators/ERC7683Allocator.sol create mode 100644 src/interfaces/IERC7683Allocator.sol create mode 100644 test/ERC7683Allocator.t.sol create mode 100644 test/SimpleERC7683Allocator.t.sol diff --git a/foundry.toml b/foundry.toml index 1009696..2b355c8 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,6 +8,7 @@ via_ir = true solc = "0.8.27" verbosity = 2 ffi = true +evm_version = "cancun" fs_permissions = [ { access = "read-write", path = ".forge-snapshots"}, { access = "read", path = "script/" } diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol new file mode 100644 index 0000000..1a620ec --- /dev/null +++ b/src/allocators/ERC7683Allocator.sol @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { Compact } from "@uniswap/the-compact/types/EIP712Types.sol"; +import { ITheCompact } from "@uniswap/the-compact/interfaces/ITheCompact.sol"; +import { SimpleAllocator } from "./SimpleAllocator.sol"; +import { Claim, Mandate } from "./types/TribunalStructs.sol"; +import { IERC7683Allocator } from "../interfaces/IERC7683Allocator.sol"; + +contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { + + // The typehash of the OrderData struct + // keccak256("OrderData(address arbiter,address sponsor,uint256 nonce,uint256 id,uint256 amount,Mandate mandate) + // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") + bytes32 public constant ORDERDATA_TYPEHASH = 0x9e0e1bdb0df35509b65bbc49d209dd42496c5a3f13998f9a74dc842d6932656b; + + // The typehash of the OrderDataGasless struct + // keccak256("OrderDataGasless(address arbiter,uint256 id,uint256 amount,Mandate mandate) + // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") + bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = 0x9ab67658b7c0f35b64fdadd7adee1e58b6399a8201f38c355d3a109a2d7081d7; + + // keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate) + // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") + bytes32 public constant COMPACT_WITNESS_TYPEHASH = 0x27f09e0bb8ce2ae63380578af7af85055d3ada248c502e2378b85bc3d05ee0b0; + + bytes32 immutable _COMPACT_DOMAIN_SEPARATOR; + + mapping(uint256 nonce => bool nonceUsed) private _userNonce; + + constructor(address compactContract_, address arbiter_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) + SimpleAllocator(compactContract_, arbiter_, minWithdrawalDelay_, maxWithdrawalDelay_) { + _COMPACT_DOMAIN_SEPARATOR = ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(); + } + + /// @inheritdoc IERC7683Allocator + function openFor(GaslessCrossChainOrder calldata order_, bytes calldata sponsorSignature_, bytes calldata) external{ + // With the users signature, we can create locks in the name of the user + + // Check if orderDataType is the one expected by the allocator + if (order_.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { + revert InvalidOrderDataType(order_.orderDataType, ORDERDATA_GASLESS_TYPEHASH); + } + if(order_.originSettler != address(this)){ + revert InvalidOriginSettler(order_.originSettler, address(this)); + } + + // Decode the orderData + OrderDataGasless memory orderDataGasless = abi.decode(order_.orderData, (OrderDataGasless)); + + OrderData memory orderData = _convertGaslessOrderData(order_.user, order_.nonce, order_.openDeadline, orderDataGasless); + + _open(orderData, order_.fillDeadline, order_.user, sponsorSignature_); + } + + /// @inheritdoc IERC7683Allocator + function open(OnchainCrossChainOrder calldata order) external{ + // Check if orderDataType is the one expected by the allocator + if (order.orderDataType != ORDERDATA_TYPEHASH) { + revert InvalidOrderDataType(order.orderDataType, ORDERDATA_TYPEHASH); + } + + // Decode the orderData + OrderData memory orderData = abi.decode(order.orderData, (OrderData)); + if(orderData.sponsor != msg.sender) { + revert InvalidSponsor(orderData.sponsor, msg.sender); + } + + _open(orderData, order.fillDeadline, msg.sender, ""); + } + + /// @inheritdoc IERC7683Allocator + function resolveFor(GaslessCrossChainOrder calldata order_, bytes calldata) external view returns (ResolvedCrossChainOrder memory){ + OrderDataGasless memory orderDataGasless = abi.decode(order_.orderData, (OrderDataGasless)); + + OrderData memory orderData = _convertGaslessOrderData(order_.user, order_.nonce, order_.openDeadline, orderDataGasless); + return _resolveOrder(order_.user, order_.fillDeadline, order_.nonce, orderData, ""); + } + + /// @inheritdoc IERC7683Allocator + function resolve(OnchainCrossChainOrder calldata order_) external view returns (ResolvedCrossChainOrder memory){ + OrderData memory orderData = abi.decode(order_.orderData, (OrderData)); + return _resolveOrder(orderData.sponsor, order_.fillDeadline, orderData.nonce, orderData, ""); + } + + /// @inheritdoc IERC7683Allocator + function getCompactWitnessTypeString() external pure returns (string memory) { + return "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt))"; + } + + /// @inheritdoc IERC7683Allocator + function checkNonce(address sponsor_, uint256 nonce_) external view returns (bool nonceFree_) { + _checkNonce(sponsor_, nonce_); + nonceFree_ = !_userNonce[nonce_]; + return nonceFree_; + } + + function _open(OrderData memory orderData_, uint32 fillDeadline_, address sponsor_, bytes memory sponsorSignature_) internal { + // Enforce a nonce where the most significant 96 bits are the nonce and the least significant 160 bits are the sponsor + _checkNonce(sponsor_, orderData_.nonce); + + // Check the nonce + if (_userNonce[orderData_.nonce]) { + revert NonceAlreadyInUse(orderData_.nonce); + } + _userNonce[orderData_.nonce] = true; + + // We do not enforce a specific tribunal or arbiter. This will allow to support new arbiters and tribunals after the deployment of the allocator + // Going with an immutable arbiter and tribunal would limit support for new chains with a fully decentralized allocator + + bytes32 tokenHash = _lockTokens(orderData_, sponsor_, orderData_.nonce); + + // Work with a Compact digest + bytes32 claimHash = keccak256( + abi.encode( + COMPACT_WITNESS_TYPEHASH, + orderData_.arbiter, + sponsor_, + orderData_.nonce, + orderData_.expires, + orderData_.id, + orderData_.amount, + keccak256( + abi.encode( + orderData_.chainId, + orderData_.tribunal, + orderData_.recipient, + fillDeadline_, + orderData_.token, + orderData_.minimumAmount, + orderData_.baselinePriorityFee, + orderData_.scalingFactor, + orderData_.salt + ) + ) + ) + ); + bytes32 digest = keccak256( + abi.encodePacked( + bytes2(0x1901), + _COMPACT_DOMAIN_SEPARATOR, + claimHash + ) + ); + + // We check for the length, which means this could also be triggered by a zero length signature provided in the openFor function. This enables relaying of orders if the claim was registered on the compact. + if(sponsorSignature_.length > 0) { + // confirm the signature matches the digest + address signer = ECDSA.recover(digest, sponsorSignature_); + if (sponsor_ != signer) { + revert InvalidSignature(sponsor_, signer); + } + } else { + // confirm the claim hash is registered on the compact + (bool isActive, uint256 registrationExpiration) = ITheCompact(COMPACT_CONTRACT).getRegistrationStatus(sponsor_, claimHash, COMPACT_WITNESS_TYPEHASH); + if (!isActive || registrationExpiration < orderData_.expires) { + revert InvalidRegistration(sponsor_, claimHash); + } + } + + _sponsor[digest] = tokenHash; + + // Emit an open event + emit Open(bytes32(orderData_.nonce), _resolveOrder(sponsor_, fillDeadline_, orderData_.nonce, orderData_, sponsorSignature_)); + } + + function _lockTokens(OrderData memory orderData_, address sponsor_, uint256 identifier) internal returns (bytes32 tokenHash_) { + return _lockTokens(orderData_.arbiter, sponsor_, identifier, orderData_.expires, orderData_.id, orderData_.amount); + } + + function _lockTokens(address arbiter, address sponsor, uint256 identifier, uint256 expires, uint256 id, uint256 amount) internal returns (bytes32 tokenHash_) { + tokenHash_ = _checkAllocation(Compact({ + arbiter: arbiter, + sponsor: sponsor, + nonce: identifier, + expires: expires, + id: id, + amount: amount + })); + _claim[tokenHash_] = expires; + _amount[tokenHash_] = amount; + _nonce[tokenHash_] = identifier; + + return tokenHash_; + + } + + function _resolveOrder(address sponsor, uint32 fillDeadline, uint256 identifier, OrderData memory orderData, bytes memory sponsorSignature) internal view returns (ResolvedCrossChainOrder memory) { + FillInstruction[] memory fillInstructions = new FillInstruction[](1); + + Mandate memory mandate = Mandate({ + chainId: orderData.chainId, + tribunal: orderData.tribunal, + recipient: orderData.recipient, + expires: fillDeadline, + token: orderData.token, + minimumAmount: orderData.minimumAmount, + baselinePriorityFee: orderData.baselinePriorityFee, + scalingFactor: orderData.scalingFactor, + salt: orderData.salt + }); + Claim memory claim = Claim({ + chainId: block.chainid, + compact: Compact({ + arbiter: orderData.arbiter, + sponsor: sponsor, + nonce: orderData.nonce, + expires: orderData.expires, + id: orderData.id, + amount: orderData.amount + }), + sponsorSignature: sponsorSignature, + allocatorSignature: "" // No signature required from this allocator, it will verify the claim on chain via ERC1271. + }); + + fillInstructions[0] = FillInstruction({ + destinationChainId: orderData.chainId, + destinationSettler: _addressToBytes32(orderData.tribunal), + originData: abi.encode(claim, mandate) + }); + + Output memory spent = Output({ + token: _addressToBytes32(orderData.token), + amount: type(uint256).max, + recipient: _addressToBytes32(orderData.recipient), + chainId: orderData.chainId + }); + Output memory received = Output({ + token: _addressToBytes32(_idToToken(orderData.id)), + amount: orderData.amount, + recipient: bytes32(0), + chainId: block.chainid + }); + + Output[] memory maxSpent = new Output[](1); + maxSpent[0] = spent; + Output[] memory minReceived = new Output[](1); + minReceived[0] = received; + + ResolvedCrossChainOrder memory resolvedOrder = ResolvedCrossChainOrder({ + user: sponsor, + originChainId: block.chainid, + openDeadline: uint32(orderData.expires), + fillDeadline: fillDeadline, + orderId: bytes32(identifier), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + return resolvedOrder; + } + + function _checkNonce(address sponsor_, uint256 nonce_) internal pure { + // Enforce a nonce where the most significant 96 bits are the nonce and the least significant 160 bits are the sponsor + // This ensures that the nonce is unique for a given sponsor + address expectedSponsor; + assembly ("memory-safe") { + expectedSponsor := shr(96, shl(96, nonce_)) + } + if(expectedSponsor != sponsor_) { + revert InvalidNonce(nonce_); + } + } + + function _castToAddress(bytes32 address_) internal pure returns (address output_) { + assembly ("memory-safe") { + output_ := shr(96, shl(96, address_)) + } + } + + function _idToToken(uint256 id_) internal pure returns (address token_) { + assembly ("memory-safe") { + token_ := shr(96, shl(96, id_)) + } + } + + function _addressToBytes32(address address_) internal pure returns (bytes32 output_) { + assembly ("memory-safe") { + output_ := shr(96, shl(96, address_)) + } + } + + function _convertGaslessOrderData(address sponsor_, uint256 nonce_, uint32 openDeadline_, OrderDataGasless memory orderDataGasless_) internal pure returns (OrderData memory orderData_) { + orderData_ = OrderData({ + arbiter: orderDataGasless_.arbiter, + sponsor: sponsor_, + nonce: nonce_, + expires: openDeadline_, + id: orderDataGasless_.id, + amount: orderDataGasless_.amount, + chainId: orderDataGasless_.chainId, + tribunal: orderDataGasless_.tribunal, + recipient: orderDataGasless_.recipient, + token: orderDataGasless_.token, + minimumAmount: orderDataGasless_.minimumAmount, + baselinePriorityFee: orderDataGasless_.baselinePriorityFee, + scalingFactor: orderDataGasless_.scalingFactor, + salt: orderDataGasless_.salt + }); + return orderData_; + } +} diff --git a/src/allocators/RoutingAllocatorERC7683.sol b/src/allocators/RoutingAllocatorERC7683.sol index 5f83d59..98b899b 100644 --- a/src/allocators/RoutingAllocatorERC7683.sol +++ b/src/allocators/RoutingAllocatorERC7683.sol @@ -267,10 +267,6 @@ contract RoutingAllocatorERC7683 is SimpleAllocator, IOriginSettler { if(!delegated_ && orderData_.sponsor != msg.sender) { revert InvalidCaller(msg.sender, orderData_.sponsor); } - // Check the arbiter - if (orderData_.arbiter != ARBITER) { - revert InvalidArbiter(orderData_.arbiter); - } uint256 identifier = _createIdentifier(orderData_.sponsor, orderData_.nonce); // Check the nonce @@ -453,6 +449,8 @@ contract RoutingAllocatorERC7683 is SimpleAllocator, IOriginSettler { /// TODO: FILL THE MANDATE Mandate memory mandate = Mandate({ + chainId: orderData.settlements[i].input.chainId, + tribunal: orderData.arbiter, recipient: _castToAddress(orderData.settlements[i].output.recipient), expires: fillDeadline, // TODO: is this correct? Or do we ignore the fill deadline and only care about the claim deadline? token: _castToAddress(orderData.settlements[i].output.token), diff --git a/src/allocators/SimpleAllocator.sol b/src/allocators/SimpleAllocator.sol index 0808515..3e16d6a 100644 --- a/src/allocators/SimpleAllocator.sol +++ b/src/allocators/SimpleAllocator.sol @@ -7,6 +7,7 @@ import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; import { ITheCompact } from "@uniswap/the-compact/interfaces/ITheCompact.sol"; import { Compact } from "@uniswap/the-compact/types/EIP712Types.sol"; import { ResetPeriod } from "@uniswap/the-compact/lib/IdLib.sol"; +import { ForcedWithdrawalStatus } from "@uniswap/the-compact/types/ForcedWithdrawalStatus.sol"; import { IAllocator } from "../interfaces/IAllocator.sol"; import { ISimpleAllocator } from "../interfaces/ISimpleAllocator.sol"; @@ -15,7 +16,6 @@ contract SimpleAllocator is ISimpleAllocator { bytes32 constant COMPACT_TYPEHASH = 0xcdca950b17b5efc016b74b912d8527dfba5e404a688cbc3dab16cb943287fec2; address public immutable COMPACT_CONTRACT; - address public immutable ARBITER; uint256 public immutable MIN_WITHDRAWAL_DELAY; uint256 public immutable MAX_WITHDRAWAL_DELAY; @@ -28,9 +28,8 @@ contract SimpleAllocator is ISimpleAllocator { /// @dev mapping of the lock digest to the tokenHash of the lock mapping(bytes32 digest => bytes32 tokenHash) internal _sponsor; - constructor(address compactContract_, address arbiter_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) { + constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) { COMPACT_CONTRACT = compactContract_; - ARBITER = arbiter_; MIN_WITHDRAWAL_DELAY = minWithdrawalDelay_; MAX_WITHDRAWAL_DELAY = maxWithdrawalDelay_; @@ -117,10 +116,6 @@ contract SimpleAllocator is ISimpleAllocator { /// @inheritdoc ISimpleAllocator function checkCompactLocked(Compact calldata compact_) external view returns (bool locked_, uint256 expires_) { - // TODO: Check the force unlock time in the compact contract and adapt expires_ if needed - if (compact_.arbiter != ARBITER) { - revert InvalidArbiter(compact_.arbiter); - } bytes32 tokenHash = _getTokenHash(compact_.id, compact_.sponsor); bytes32 digest = keccak256( abi.encodePacked( @@ -141,6 +136,13 @@ contract SimpleAllocator is ISimpleAllocator { ); uint256 expires = _claim[tokenHash]; bool active = _sponsor[digest] == tokenHash && expires > block.timestamp && !ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)); + if (active) { + (ForcedWithdrawalStatus status, uint256 forcedWithdrawalAvailableAt) = ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus(compact_.sponsor, compact_.id); + if (status == ForcedWithdrawalStatus.Enabled && forcedWithdrawalAvailableAt < expires) { + expires = forcedWithdrawalAvailableAt; + active = expires > block.timestamp; + } + } return (active, active ? expires : 0); } @@ -158,16 +160,15 @@ contract SimpleAllocator is ISimpleAllocator { if (_claim[tokenHash] > block.timestamp && !ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this))) { revert ClaimActive(compact_.sponsor); } - // Check arbiter is valid - if (compact_.arbiter != ARBITER) { - revert InvalidArbiter(compact_.arbiter); - } // Check expiration is not too soon or too late if (compact_.expires < block.timestamp + MIN_WITHDRAWAL_DELAY || compact_.expires > block.timestamp + MAX_WITHDRAWAL_DELAY) { revert InvalidExpiration(compact_.expires); } + (,address allocator, ResetPeriod resetPeriod,) = ITheCompact(COMPACT_CONTRACT).getLockDetails(compact_.id); + if (allocator != address(this)) { + revert InvalidAllocator(allocator); + } // Check expiration is not longer then the tokens forced withdrawal time - (,, ResetPeriod resetPeriod,) = ITheCompact(COMPACT_CONTRACT).getLockDetails(compact_.id); if (compact_.expires > block.timestamp + _resetPeriodToSeconds(resetPeriod)) { revert ForceWithdrawalAvailable(compact_.expires, block.timestamp + _resetPeriodToSeconds(resetPeriod)); } diff --git a/src/allocators/SimpleERC7683Allocator.sol b/src/allocators/SimpleERC7683Allocator.sol index b6d8350..371e263 100644 --- a/src/allocators/SimpleERC7683Allocator.sol +++ b/src/allocators/SimpleERC7683Allocator.sol @@ -12,37 +12,43 @@ import { IOriginSettler } from "../interfaces/ERC7683/IOriginSettler.sol"; contract SimpleERC7683Allocator is SimpleAllocator, IOriginSettler { struct OrderData { - address sponsor; - uint96 nonce; - uint32 claimDeadline; - Settlement[] settlements; - address arbiter; - bytes sponsorSignature; - /// TODO: add baselinePriorityFee? - /// TODO: add scalingFactor? - /// TODO: add salt? + // COMPACT + address arbiter; // The account tasked with verifying and submitting the claim. + address sponsor; // The account to source the tokens from. + uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. + uint256 expires; // The time at which the claim expires. + uint256 id; // The token ID of the ERC6909 token to allocate. + uint256 amount; // The amount of ERC6909 tokens to allocate. + // MANDATE + uint256 chainId; // (implicit arg, included in EIP712 payload) + address tribunal; // (implicit arg, included in EIP712 payload) + address recipient; // Recipient of settled tokens + // uint256 expires; // Mandate expiration timestamp + address token; // Settlement token (address(0) for native) + uint256 minimumAmount; // Minimum settlement amount + uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in + uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) + bytes32 salt; // Replay protection parameter } struct OrderDataGasless { - uint32 claimDeadline; - Settlement[] settlements; - address arbiter; - } - - struct Settlement { - Output input; - Output output; - bytes32 destinationSettler; // tribunal - } - - struct Witness { // for single / non-batch compact - uint256 originChainId; - uint256 targetChainId; - bytes32 targetTokenAddress; - uint256 targetMinAmount; - bytes32 recipient; - bytes32 destinationSettler; - uint32 fillDeadline; + // COMPACT + // address arbiter; // The account tasked with verifying and submitting the claim. + // address sponsor; // The account to source the tokens from. + // uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. + uint256 expires; // The time at which the claim expires. + uint256 id; // The token ID of the ERC6909 token to allocate. + uint256 amount; // The amount of ERC6909 tokens to allocate. + // MANDATE + uint256 chainId; // (implicit arg, included in EIP712 payload) + address tribunal; // (implicit arg, included in EIP712 payload) + address recipient; // Recipient of settled tokens + // uint256 expires; // Mandate expiration timestamp + address token; // Settlement token (address(0) for native) + uint256 minimumAmount; // Minimum settlement amount + uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in + uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) + bytes32 salt; // Replay protection parameter } error InvalidOriginSettler(address originSettler, address expectedOriginSettler); @@ -50,20 +56,26 @@ contract SimpleERC7683Allocator is SimpleAllocator, IOriginSettler { error InvalidChainId(uint256 chainId, uint256 expectedChainId); error InvalidRecipient(address recipient, address expectedRecipient); error InvalidNonce(uint256 nonce); - error NonceAlreadyInUse(uint96 nonce); + error NonceAlreadyInUse(uint256 nonce); error InvalidSignature(address signer, address expectedSigner); + error InvalidRegistration(address sponsor, bytes32 claimHash); + error InvalidSponsor(address sponsor, address expectedSponsor); // The typehash of the OrderData struct - // keccak256("OrderData(address sponsor,uint96 nonce,uint32 claimDeadline,Settlement[] settlements,address arbiter,bytes sponsorSignature)Output(bytes32 token,uint256 amount,bytes32 recipient,uint256 chainId)Settlement(Output input,Output output,bytes32 destinationSettler)") - bytes32 constant ORDERDATA_TYPEHASH = 0xe8225b67751f9ff0d865fdc55742ea54087b20406855f954bd737ab200819ab8; + // keccak256("OrderData(address arbiter,address sponsor,uint256 nonce,uint256 id,uint256 amount,Mandate mandate) + // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") + bytes32 constant ORDERDATA_TYPEHASH = 0x9e0e1bdb0df35509b65bbc49d209dd42496c5a3f13998f9a74dc842d6932656b; // The typehash of the OrderDataGasless struct - // keccak256("OrderDataGasless(uint32 claimDeadline,Settlement[] settlements,address arbiter)Output(bytes32 token,uint256 amount,bytes32 recipient,uint256 chainId)Settlement(Output input,Output output,bytes32 destinationSettler)") - bytes32 constant ORDERDATA_GASLESS_TYPEHASH = 0x4679e16e516a2f88beb96ee00964e5f28b9e2ed596592f8c3d92dd70411611a9; + // keccak256("OrderDataGasless(address arbiter,uint256 id,uint256 amount,Mandate mandate) + // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") + bytes32 constant ORDERDATA_GASLESS_TYPEHASH = 0x9ab67658b7c0f35b64fdadd7adee1e58b6399a8201f38c355d3a109a2d7081d7; + + // keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate) + // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") + bytes32 constant COMPACT_WITNESS_TYPEHASH = 0x27f09e0bb8ce2ae63380578af7af85055d3ada248c502e2378b85bc3d05ee0b0; - // keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness) - // Witness(uint256 originChainId,uint256 targetChainId,bytes32 targetTokenAddress,uint256 targetMinAmount,bytes32 recipient,bytes32 destinationSettler,uint32 fillDeadline)") - bytes32 constant COMPACT_WITNESS_TYPEHASH = 0x2f0f51aa07316f3d8860366b556177042fafb2edffc93c799091bae7c194d9a6; + bytes32 immutable _COMPACT_DOMAIN_SEPARATOR; /// FOR SINGLE COMPACT WE HAVE: /// - arbiter @@ -83,10 +95,6 @@ contract SimpleERC7683Allocator is SimpleAllocator, IOriginSettler { // uint32 fillDeadline // ) - // keccak256("BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts,Witness witness) - // Witness(uint256[] originChainId,uint256[] targetChainId,bytes32[] targetTokenAddress,uint256[] targetMinAmount,bytes32[] recipient,bytes32[] destinationSettler,uint32 fillDeadline)") - bytes32 constant BATCH_COMPACT_WITNESS_TYPEHASH = 0x3158fb17880387b9302c66a40dd45893126fd532c2abb7deee6e53149c53646b; - /// FOR BATCH COMPACT WE HAVE: /// - arbiter /// - sponsor @@ -107,41 +115,32 @@ contract SimpleERC7683Allocator is SimpleAllocator, IOriginSettler { /// TODO: batch compacts witness // The nonce of the allocator - mapping(uint256 identifier => bool nonceUsed) private _userNonce; + mapping(uint256 nonce => bool nonceUsed) private _userNonce; - constructor(address compactContract_, address arbiter_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) - SimpleAllocator(compactContract_, arbiter_, minWithdrawalDelay_, maxWithdrawalDelay_) { + constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) + SimpleAllocator(compactContract_, minWithdrawalDelay_, maxWithdrawalDelay_) { + _COMPACT_DOMAIN_SEPARATOR = ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(); } /// @notice Opens a gasless cross-chain order on behalf of a user. /// @dev To be called by the filler. /// @dev This method must emit the Open event - /// @param order The GaslessCrossChainOrder definition - /// @param signature The user's signature over the order - function openFor(GaslessCrossChainOrder calldata order, bytes calldata signature, bytes calldata) external{ - // since we have the users signature, we can create locks in the name of the user + /// @param order_ The GaslessCrossChainOrder definition + /// @param sponsorSignature_ The user's signature over the order + function openFor(GaslessCrossChainOrder calldata order_, bytes calldata sponsorSignature_, bytes calldata) external{ + // With the users signature, we can create locks in the name of the user // Check if orderDataType is the one expected by the allocator - if (order.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { - revert InvalidOrderDataType(order.orderDataType, ORDERDATA_GASLESS_TYPEHASH); - } - // Check the allocator is the chosen origin settler - // This check is not strictly necessary, since the user does not sign a allocator. The filler will not get payed if he maliciously choses a different allocator. - if (order.originSettler != address(this)) { - revert InvalidOriginSettler(order.originSettler, address(this)); + if (order_.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { + revert InvalidOrderDataType(order_.orderDataType, ORDERDATA_GASLESS_TYPEHASH); } // Decode the orderData - OrderDataGasless memory orderDataGasless = abi.decode(order.orderData, (OrderDataGasless)); - // Enforce a uint96 nonce since the contract handles the nonce management by combining the provided nonce with the user address - uint96 nonce = uint96(order.nonce); - if (order.nonce != nonce) { - revert InvalidNonce(order.nonce); - } + OrderDataGasless memory orderDataGasless = abi.decode(order_.orderData, (OrderDataGasless)); - OrderData memory orderData = _convertGaslessOrderData(order.user, nonce, orderDataGasless, signature); + OrderData memory orderData = _convertGaslessOrderData(order_.user, order_.nonce, order_.originSettler, orderDataGasless); - _open(orderData, order.fillDeadline, false); + _open(orderData, order_.fillDeadline, order_.user, sponsorSignature_); } /// @notice Opens a cross-chain order @@ -150,6 +149,8 @@ contract SimpleERC7683Allocator is SimpleAllocator, IOriginSettler { /// @dev This locks the users tokens /// @param order The OnchainCrossChainOrder definition function open(OnchainCrossChainOrder calldata order) external{ + // TODO: Think about if this can only be used with a registered compact? Or do we want the sponsor signature in the orderData? + // Check if orderDataType is the one expected by the allocator if (order.orderDataType != ORDERDATA_TYPEHASH) { revert InvalidOrderDataType(order.orderDataType, ORDERDATA_TYPEHASH); @@ -157,190 +158,120 @@ contract SimpleERC7683Allocator is SimpleAllocator, IOriginSettler { // Decode the orderData OrderData memory orderData = abi.decode(order.orderData, (OrderData)); - _open(orderData, order.fillDeadline, false); + if(orderData.sponsor != msg.sender) { + revert InvalidSponsor(orderData.sponsor, msg.sender); + } + + _open(orderData, order.fillDeadline, msg.sender, ""); } /// @notice Resolves a specific GaslessCrossChainOrder into a generic ResolvedCrossChainOrder /// @dev Intended to improve standardized integration of various order types and settlement contracts /// @param order The GaslessCrossChainOrder definition /// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order - function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata) external pure returns (ResolvedCrossChainOrder memory){ + function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata) external view returns (ResolvedCrossChainOrder memory){ OrderDataGasless memory orderDataGasless = abi.decode(order.orderData, (OrderDataGasless)); - // Enforce a uint96 nonce since the contract handles the nonce management by combining the provided nonce with the user address - uint96 nonce = uint96(order.nonce); - if (order.nonce != nonce) { - revert InvalidNonce(order.nonce); - } - OrderData memory orderData = _convertGaslessOrderData(order.user, nonce, orderDataGasless, ""); - return _resolveOrder(order.fillDeadline, orderData); + OrderData memory orderData = _convertGaslessOrderData(order.user, order.nonce, order.originSettler, orderDataGasless); + return _resolveOrder(order.user, order.fillDeadline, order.nonce, orderData, ""); } /// @notice Resolves a specific OnchainCrossChainOrder into a generic ResolvedCrossChainOrder /// @dev Intended to improve standardized integration of various order types and settlement contracts /// @param order The OnchainCrossChainOrder definition /// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order - function resolve(OnchainCrossChainOrder calldata order) external pure returns (ResolvedCrossChainOrder memory){ + function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory){ OrderData memory orderData = abi.decode(order.orderData, (OrderData)); - return _resolveOrder(order.fillDeadline, orderData); + return _resolveOrder(orderData.sponsor, order.fillDeadline, orderData.nonce, orderData, ""); } function getCompactWitnessString() external pure returns (string memory) { - return "Witness(uint256 originChainId, uint256 targetChainId, bytes32 targetTokenAddress, uint256 targetMinAmount, bytes32 recipient, bytes32 destinationSettler, uint32 fillDeadline)"; + return "Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)"; } - function getBatchCompactWitnessString() external pure returns (string memory) { - return "Witness(uint256[] originChainId,uint256[] targetChainId,bytes32[] targetTokenAddress,uint256[] targetMinAmount,bytes32[] recipient,bytes32[] destinationSettler,uint32 fillDeadline)"; + function checkNonce(address sponsor_, uint256 nonce_) external view returns (bool nonceUnused_) { + _checkNonce(sponsor_, nonce_); + nonceUnused_ = !_userNonce[nonce_]; + return nonceUnused_; } - function _open(OrderData memory orderData_, uint32 fillDeadline_, bool delegated_) internal { - // Check the user - if(!delegated_ && orderData_.sponsor != msg.sender) { - revert InvalidCaller(msg.sender, orderData_.sponsor); - } - // Check the arbiter - if (orderData_.arbiter != ARBITER) { - revert InvalidArbiter(orderData_.arbiter); - } + function _open(OrderData memory orderData_, uint32 fillDeadline_, address sponsor_, bytes memory sponsorSignature_) internal { + + // Enforce a nonce where the most significant 96 bits are the nonce and the least significant 160 bits are the sponsor + _checkNonce(sponsor_, orderData_.nonce); - uint256 identifier = _createIdentifier(orderData_.sponsor, orderData_.nonce); // Check the nonce - if (_userNonce[identifier]) { + if (_userNonce[orderData_.nonce]) { revert NonceAlreadyInUse(orderData_.nonce); } - _userNonce[identifier] = true; + _userNonce[orderData_.nonce] = true; // We do not enforce a specific tribunal, so we do not check the address. This will allow to support new tribunals after the deployment of the allocator // Going with an immutable tribunal would limit support for new chains with a fully decentralized allocator /// TODO: THINK ABOUT IF THE ARBITER MUST BE ENFORCED OR NOT - uint256 settlementsLength = orderData_.settlements.length; - - bytes32 digest; - bytes32 tokenHash; - if(settlementsLength > 1) { - uint256[2][] memory idsAndAmounts = new uint256[2][](settlementsLength); - - uint256[] memory originChainIds = new uint256[](settlementsLength); - uint256[] memory targetChainIds = new uint256[](settlementsLength); - bytes32[] memory targetTokenAddresses = new bytes32[](settlementsLength); - uint256[] memory targetMinAmounts = new uint256[](settlementsLength); - bytes32[] memory recipients = new bytes32[](settlementsLength); - bytes32[] memory destinationSettlers = new bytes32[](settlementsLength); - - // Iterate over the inputs and lock the tokens - for(uint256 i = 0; i < settlementsLength; ++i) { - if (orderData_.settlements[i].input.chainId != block.chainid) { - // MultiChainCompact not supported - revert InvalidChainId(orderData_.settlements[i].input.chainId, block.chainid); - } - if (_castToAddress(orderData_.settlements[i].input.recipient) != orderData_.sponsor) { - // Sponsor must be the same throughout all settlements - revert InvalidRecipient(_castToAddress(orderData_.settlements[i].input.recipient), orderData_.sponsor); - } - tokenHash = _lockTokens(orderData_, identifier, i); - idsAndAmounts[i] = [uint256(orderData_.settlements[i].input.token), orderData_.settlements[i].input.amount]; - originChainIds[i] = block.chainid; - targetChainIds[i] = orderData_.settlements[i].output.chainId; - targetTokenAddresses[i] = orderData_.settlements[i].output.token; - targetMinAmounts[i] = orderData_.settlements[i].output.amount; - recipients[i] = orderData_.settlements[i].output.recipient; - destinationSettlers[i] = orderData_.settlements[i].destinationSettler; - } - - // Work with a BatchCompact digest - digest = keccak256( - abi.encodePacked( - bytes2(0x1901), - ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - BATCH_COMPACT_WITNESS_TYPEHASH, - orderData_.arbiter, - orderData_.sponsor, - identifier, // TODO: IS THE CAST REQUIRED? - orderData_.claimDeadline, - idsAndAmounts, - keccak256( - abi.encode( - // Skip struct, because it would also need to be named 'Witness' (to be consistent with theCompact) - originChainIds, - targetChainIds, - targetTokenAddresses, - targetMinAmounts, - recipients, - destinationSettlers, - fillDeadline_ - ) - ) - ) + bytes32 tokenHash = _lockTokens(orderData_, sponsor_, orderData_.nonce); + + // Work with a Compact digest + bytes32 claimHash = keccak256( + abi.encode( + COMPACT_WITNESS_TYPEHASH, + orderData_.arbiter, + sponsor_, + orderData_.nonce, + orderData_.expires, + orderData_.id, + orderData_.amount, + keccak256( + abi.encode( + orderData_.chainId, + orderData_.tribunal, + orderData_.recipient, + fillDeadline_, + orderData_.token, + orderData_.minimumAmount, + orderData_.baselinePriorityFee, + orderData_.scalingFactor, + orderData_.salt ) ) - ); - } else { - if (orderData_.settlements[0].input.chainId != block.chainid) { - revert InvalidChainId(orderData_.settlements[0].input.chainId, block.chainid); - } - if (_castToAddress(orderData_.settlements[0].input.recipient) != orderData_.sponsor) { - // Sponsor must be the same throughout in the settlements - revert InvalidRecipient(_castToAddress(orderData_.settlements[0].input.recipient), orderData_.sponsor); - } - - tokenHash = _lockTokens(orderData_, identifier, 0); - - // Work with a Compact digest - digest = keccak256( + ) + ); + + // TODO: This means everyone can open an order for a user if they have registered the claim hash on the compact (just call openFor with an empty signature). Any issues with that? + // Do not currently see an issue, since its the same with the sponsors signature. + if(sponsorSignature_.length > 0) { + // confirm the signature matches the digest + bytes32 digest = keccak256( abi.encodePacked( bytes2(0x1901), - ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - COMPACT_WITNESS_TYPEHASH, - orderData_.arbiter, - orderData_.sponsor, - identifier, - orderData_.claimDeadline, - uint256(orderData_.settlements[0].input.token), // TODO: IS THE CAST REQUIRED HERE? - orderData_.settlements[0].input.amount, - keccak256( - abi.encode( - Witness({ - originChainId: orderData_.settlements[0].input.chainId, - targetChainId: orderData_.settlements[0].output.chainId, - targetTokenAddress: orderData_.settlements[0].output.token, - targetMinAmount: orderData_.settlements[0].output.amount, - recipient: orderData_.settlements[0].output.recipient, - destinationSettler: orderData_.settlements[0].destinationSettler, - fillDeadline: fillDeadline_ - }) - ) - ) - ) - ) + _COMPACT_DOMAIN_SEPARATOR, + claimHash ) ); + address signer = ECDSA.recover(digest, sponsorSignature_); + if (sponsor_ != signer) { + revert InvalidSignature(sponsor_, signer); + } + } else { + // confirm the claim hash is registered on the compact + (bool isActive, uint256 registrationExpiration) = ITheCompact(COMPACT_CONTRACT).getRegistrationStatus(sponsor_, claimHash, COMPACT_WITNESS_TYPEHASH); + if (!isActive || registrationExpiration < orderData_.expires) { + revert InvalidRegistration(sponsor_, claimHash); + } } - /// TODO: SHOULD WE SKIP THE REQUIREMENT OF THE SPONSOR SIGNATURE FOR A NON DELEGATED OPEN FUNCTION? - // confirm the signature matches the digest - address signer = ECDSA.recover(digest, orderData_.sponsorSignature); - if (orderData_.sponsor != signer) { - revert InvalidSignature(orderData_.sponsor, signer); - } - - // The stored tokenHash will only be used to check for the expiration of the order in the isValidSignature function. - // Since the expiration is the same for all allocations, it does not matter which of the tokenHashes is stored for a batch compact. - _sponsor[digest] = tokenHash; + _sponsor[claimHash] = tokenHash; // Emit an open event - emit Open(bytes32(identifier), _resolveOrder(fillDeadline_, orderData_)); + emit Open(bytes32(orderData_.nonce), _resolveOrder(sponsor_, fillDeadline_, orderData_.nonce, orderData_, sponsorSignature_)); } - function _lockTokens(OrderData memory orderData_, uint256 identifier, uint256 index_) internal returns (bytes32 tokenHash_) { - return _lockTokens(orderData_.arbiter, orderData_.sponsor, identifier, orderData_.claimDeadline, uint256(orderData_.settlements[index_].input.token), orderData_.settlements[index_].input.amount); + function _lockTokens(OrderData memory orderData_, address sponsor_, uint256 identifier) internal returns (bytes32 tokenHash_) { + return _lockTokens(orderData_.arbiter, sponsor_, identifier, orderData_.expires, orderData_.id, orderData_.amount); } - function _lockTokens(address arbiter, address sponsor, uint256 identifier, uint32 expires, uint256 id, uint256 amount) internal returns (bytes32 tokenHash_) { + function _lockTokens(address arbiter, address sponsor, uint256 identifier, uint256 expires, uint256 id, uint256 amount) internal returns (bytes32 tokenHash_) { tokenHash_ = _checkAllocation(Compact({ arbiter: arbiter, sponsor: sponsor, @@ -357,66 +288,81 @@ contract SimpleERC7683Allocator is SimpleAllocator, IOriginSettler { } - function _resolveOrder(uint32 fillDeadline, OrderData memory orderData) internal pure returns (ResolvedCrossChainOrder memory) { - uint256 settlementLength = orderData.settlements.length; - FillInstruction[] memory fillInstructions = new FillInstruction[](settlementLength); - - Output[] memory outputs = new Output[](settlementLength); - Output[] memory inputs = new Output[](settlementLength); - - for(uint256 i = 0; i < settlementLength; ++i) { - inputs[i] = orderData.settlements[i].input; - outputs[i] = orderData.settlements[i].output; - - /// TODO: FILL THE MANDATE - Mandate memory mandate = Mandate({ - recipient: _castToAddress(orderData.settlements[i].output.recipient), - expires: fillDeadline, // TODO: is this correct? Or do we ignore the fill deadline and only care about the claim deadline? - token: _castToAddress(orderData.settlements[i].output.token), - minimumAmount: orderData.settlements[i].output.amount, - baselinePriorityFee: 0, // TODO: check whats happening here - scalingFactor: 0, // TODO: check whats happening here - salt: 0 // TODO: whats difference between salt and nonce? This is still sponsor signed data - }); - Claim memory claim = Claim({ - chainId: orderData.settlements[i].input.chainId, // TODO: IS THIS TARGET OR ORIGIN CHAIN ID? IT APPARENTLY SHOULD BE THE ORIGIN CHAIN ID, BUT WHERE IS THE TARGET CHAIN ID ADDED? - compact: Compact({ - arbiter: orderData.arbiter, - sponsor: orderData.sponsor, - nonce: orderData.nonce, - expires: orderData.claimDeadline, - id: uint256(orderData.settlements[i].input.token), // TODO: make it clear in doc that outputs and inputs are always connected via the index - amount: orderData.settlements[i].input.amount - }), - sponsorSignature: orderData.sponsorSignature, - allocatorSignature: "" // No signature required from this allocator, it will verify the claim on chain. - }); - - fillInstructions[i] = FillInstruction({ - destinationChainId: uint64(orderData.settlements[i].output.chainId), // TODO: WHY SUDDENLY A UINT64 INSTEAD OF UINT256? - destinationSettler: orderData.settlements[i].destinationSettler, - originData: abi.encode(claim, mandate) // TODO: FILL WITH THE ORIGIN DATA REQUIRED BY THE TRIBUNAL - }); - } + function _resolveOrder(address sponsor, uint32 fillDeadline, uint256 identifier, OrderData memory orderData, bytes memory sponsorSignature) internal view returns (ResolvedCrossChainOrder memory) { + FillInstruction[] memory fillInstructions = new FillInstruction[](1); + + Mandate memory mandate = Mandate({ + chainId: orderData.chainId, + tribunal: orderData.tribunal, + recipient: orderData.recipient, + expires: fillDeadline, + token: orderData.token, + minimumAmount: orderData.minimumAmount, + baselinePriorityFee: orderData.baselinePriorityFee, + scalingFactor: orderData.scalingFactor, + salt: orderData.salt + }); + Claim memory claim = Claim({ + chainId: block.chainid, + compact: Compact({ + arbiter: orderData.arbiter, + sponsor: sponsor, + nonce: orderData.nonce, + expires: orderData.expires, + id: orderData.id, + amount: orderData.amount + }), + sponsorSignature: sponsorSignature, + allocatorSignature: "" // No signature required from this allocator, it will verify the claim on chain. + }); + + fillInstructions[0] = FillInstruction({ + destinationChainId: orderData.chainId, + destinationSettler: bytes20(orderData.tribunal), + originData: abi.encode(claim, mandate) // TODO: FILL WITH THE ORIGIN DATA REQUIRED BY THE TRIBUNAL + }); + + Output memory spent = Output({ + token: bytes20(orderData.token), + amount: type(uint256).max, + recipient: bytes20(orderData.recipient), + chainId: orderData.chainId + }); + Output memory received = Output({ + token: bytes20(_idToToken(orderData.id)), + amount: orderData.amount, + recipient: bytes32(0), + chainId: block.chainid + }); + + Output[] memory maxSpent = new Output[](1); + maxSpent[0] = spent; + Output[] memory minReceived = new Output[](1); + minReceived[0] = received; ResolvedCrossChainOrder memory resolvedOrder = ResolvedCrossChainOrder({ - user: orderData.sponsor, - originChainId: orderData.settlements[0].input.chainId, // must be same for every input - openDeadline: orderData.claimDeadline, /// TODO: CAN THE OPEN DEADLINE BE THE CLAIM DEADLINE? + user: sponsor, + originChainId: block.chainid, + openDeadline: uint32(orderData.expires), fillDeadline: fillDeadline, - orderId: bytes32(_createIdentifier(orderData.sponsor, orderData.nonce)), - maxSpent: inputs, - minReceived: outputs, + orderId: bytes32(identifier), + maxSpent: maxSpent, + minReceived: minReceived, fillInstructions: fillInstructions }); return resolvedOrder; } - function _createIdentifier(address sponsor_, uint96 nonce_) internal pure returns (uint256 identifier_) { + function _checkNonce(address sponsor_, uint256 nonce_) internal pure { + // Enforce a nonce where the most significant 96 bits are the nonce and the least significant 160 bits are the sponsor + // This ensures that the nonce is unique for a given sponsor + address expectedSponsor; assembly ("memory-safe") { - identifier_ := or(shl(160, nonce_), shr(96, shl(96,sponsor_))) + expectedSponsor := shr(96, shl(96, nonce_)) + } + if(expectedSponsor != sponsor_) { + revert InvalidNonce(nonce_); } - return identifier_; } function _castToAddress(bytes32 address_) internal pure returns (address output_) { @@ -425,14 +371,28 @@ contract SimpleERC7683Allocator is SimpleAllocator, IOriginSettler { } } - function _convertGaslessOrderData(address sponsor_, uint96 nonce_, OrderDataGasless memory orderDataGasless_, bytes memory signature_) internal pure returns (OrderData memory orderData_) { + function _idToToken(uint256 id_) internal pure returns (address token_) { + assembly ("memory-safe") { + token_ := shr(96, shl(96, id_)) + } + } + + function _convertGaslessOrderData(address sponsor_, uint256 nonce_, address arbiter_, OrderDataGasless memory orderDataGasless_) internal pure returns (OrderData memory orderData_) { orderData_ = OrderData({ + arbiter: arbiter_, sponsor: sponsor_, nonce: nonce_, - claimDeadline: orderDataGasless_.claimDeadline, - settlements: orderDataGasless_.settlements, - arbiter: orderDataGasless_.arbiter, - sponsorSignature: signature_ + expires: orderDataGasless_.expires, + id: orderDataGasless_.id, + amount: orderDataGasless_.amount, + chainId: orderDataGasless_.chainId, + tribunal: orderDataGasless_.tribunal, + recipient: orderDataGasless_.recipient, + token: orderDataGasless_.token, + minimumAmount: orderDataGasless_.minimumAmount, + baselinePriorityFee: orderDataGasless_.baselinePriorityFee, + scalingFactor: orderDataGasless_.scalingFactor, + salt: orderDataGasless_.salt }); return orderData_; } diff --git a/src/allocators/types/TribunalStructs.sol b/src/allocators/types/TribunalStructs.sol index 00cc0bf..b13156d 100644 --- a/src/allocators/types/TribunalStructs.sol +++ b/src/allocators/types/TribunalStructs.sol @@ -13,6 +13,8 @@ import { Compact } from "@uniswap/the-compact/types/EIP712Types.sol"; } struct Mandate { + uint256 chainId; // (implicit arg, included in EIP712 payload) + address tribunal; // (implicit arg, included in EIP712 payload) address recipient; // Recipient of settled tokens uint256 expires; // Mandate expiration timestamp address token; // Settlement token (address(0) for native) @@ -20,9 +22,4 @@ import { Compact } from "@uniswap/the-compact/types/EIP712Types.sol"; uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) bytes32 salt; // Replay protection parameter - } - - struct Directive { - address claimant; // Recipient of claimed tokens - uint256 dispensation; // Cross-chain message layer payment } \ No newline at end of file diff --git a/src/interfaces/ERC7683/IOriginSettler.sol b/src/interfaces/ERC7683/IOriginSettler.sol index eeb685c..6bacf67 100644 --- a/src/interfaces/ERC7683/IOriginSettler.sol +++ b/src/interfaces/ERC7683/IOriginSettler.sol @@ -88,7 +88,7 @@ interface IOriginSettler { /// @dev Provides all the origin-generated information required to produce a valid fill leg struct FillInstruction { /// @dev The contract address that the order is meant to be settled by - uint64 destinationChainId; + uint256 destinationChainId; /// @dev The contract address that the order is meant to be filled on bytes32 destinationSettler; /// @dev The data generated on the origin chain needed by the destinationSettler to process the fill diff --git a/src/interfaces/IERC7683Allocator.sol b/src/interfaces/IERC7683Allocator.sol new file mode 100644 index 0000000..1dc0e6e --- /dev/null +++ b/src/interfaces/IERC7683Allocator.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import { IOriginSettler } from "./ERC7683/IOriginSettler.sol"; + +interface IERC7683Allocator is IOriginSettler { + + struct OrderData { + // COMPACT + address arbiter; // The account tasked with verifying and submitting the claim. + address sponsor; // The account to source the tokens from. + uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. + uint256 expires; // The time at which the claim expires. + uint256 id; // The token ID of the ERC6909 token to allocate. + uint256 amount; // The amount of ERC6909 tokens to allocate. + // MANDATE + uint256 chainId; // (implicit arg, included in EIP712 payload) + address tribunal; // (implicit arg, included in EIP712 payload) + address recipient; // Recipient of settled tokens + // uint256 expires; // Mandate expiration timestamp + address token; // Settlement token (address(0) for native) + uint256 minimumAmount; // Minimum settlement amount + uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in + uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) + bytes32 salt; // Replay protection parameter + } + + struct OrderDataGasless { + // COMPACT + address arbiter; // The account tasked with verifying and submitting the claim. + // address sponsor; // The account to source the tokens from. + // uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. + // uint256 expires; // The time at which the claim expires. + uint256 id; // The token ID of the ERC6909 token to allocate. + uint256 amount; // The amount of ERC6909 tokens to allocate. + // MANDATE + uint256 chainId; // (implicit arg, included in EIP712 payload) + address tribunal; // (implicit arg, included in EIP712 payload) + address recipient; // Recipient of settled tokens + // uint256 expires; // Mandate expiration timestamp + address token; // Settlement token (address(0) for native) + uint256 minimumAmount; // Minimum settlement amount + uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in + uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) + bytes32 salt; // Replay protection parameter + } + + error InvalidOriginSettler(address originSettler, address expectedOriginSettler); + error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); + error InvalidNonce(uint256 nonce); + error NonceAlreadyInUse(uint256 nonce); + error InvalidSignature(address signer, address expectedSigner); + error InvalidRegistration(address sponsor, bytes32 claimHash); + error InvalidSponsor(address sponsor, address expectedSponsor); + + + /// @inheritdoc IOriginSettler + function openFor(GaslessCrossChainOrder calldata order, bytes calldata signature, bytes calldata originFillerData) external; + + /// @inheritdoc IOriginSettler + /// @dev Requires the user to have previously registered the claim hash on the compact + function open(OnchainCrossChainOrder calldata order) external; + + /// @inheritdoc IOriginSettler + function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata originFillerData) external view returns (ResolvedCrossChainOrder memory); + + /// @inheritdoc IOriginSettler + function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory); + + /// @notice Returns the type string of the compact including the witness + function getCompactWitnessTypeString() external pure returns (string memory); + + /// @notice Checks if a nonce is free to be used + /// @dev The nonce is the most significant 96 bits. The least significant 160 bits must be the sponsor address + function checkNonce(address sponsor_, uint256 nonce_) external view returns (bool nonceFree_); +} \ No newline at end of file diff --git a/src/interfaces/ISimpleAllocator.sol b/src/interfaces/ISimpleAllocator.sol index 9cbd273..55b7963 100644 --- a/src/interfaces/ISimpleAllocator.sol +++ b/src/interfaces/ISimpleAllocator.sol @@ -12,9 +12,6 @@ interface ISimpleAllocator is IAllocator { /// @notice Thrown if the caller is invalid error InvalidCaller(address caller, address expected); - /// @notice Thrown if the suggested arbiter is not the arbiter of the allocator - error InvalidArbiter(address arbiter); - /// @notice Thrown if the nonce has already been consumed on the compact contract error NonceAlreadyConsumed(uint256 nonce); @@ -27,6 +24,9 @@ interface ISimpleAllocator is IAllocator { /// @notice Thrown if the expiration is longer then the tokens forced withdrawal time error ForceWithdrawalAvailable(uint256 expires, uint256 forcedWithdrawalExpiration); + /// @notice Thrown if the allocator is not the one expected + error InvalidAllocator(address allocator); + /// @notice Thrown if the provided lock is not available or expired /// @dev The expiration will be '0' if no lock is available error InvalidLock(bytes32 digest, uint256 expiration); diff --git a/src/test/TheCompactMock.sol b/src/test/TheCompactMock.sol index 0a63f0d..a43fbf9 100644 --- a/src/test/TheCompactMock.sol +++ b/src/test/TheCompactMock.sol @@ -34,7 +34,7 @@ contract TheCompactMock is ERC6909 { return 0; } - function deposit(address token, uint256 amount, address allocator) external { + function deposit(address token, address allocator, uint256 amount) external { ERC20(token).transferFrom(msg.sender, address(this), amount); uint256 id = _getTokenId(token, allocator); tokens[id] = token; @@ -125,7 +125,10 @@ contract TheCompactMock is ERC6909 { return ""; } - function _getTokenId(address token, address allocator) internal pure returns (uint256) { - return uint256(keccak256(abi.encode(token, allocator))); + function _getTokenId(address token, address allocator) internal pure returns (uint256 tokenId) { + assembly ("memory-safe") { + tokenId := or(shl(160, allocator),shr(96,shl(96,token))) + } + return tokenId; } } diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol new file mode 100644 index 0000000..8ceb534 --- /dev/null +++ b/test/ERC7683Allocator.t.sol @@ -0,0 +1,618 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import { Test } from "forge-std/Test.sol"; +import { ERC7683Allocator } from "src/allocators/ERC7683Allocator.sol"; +import { IERC7683Allocator } from "src/interfaces/IERC7683Allocator.sol"; +import { IOriginSettler } from "src/interfaces/ERC7683/IOriginSettler.sol"; +import { Mandate, Claim } from "src/allocators/types/TribunalStructs.sol"; +import { ITheCompact } from "@uniswap/the-compact/interfaces/ITheCompact.sol"; +import { TheCompact } from "@uniswap/the-compact/TheCompact.sol"; +import { ClaimWithWitness } from "@uniswap/the-compact/types/Claims.sol"; +import { IdLib } from "@uniswap/the-compact/lib/IdLib.sol"; +import { Scope } from "@uniswap/the-compact/types/Scope.sol"; +import { ResetPeriod } from "@uniswap/the-compact/types/ResetPeriod.sol"; +import { Lock } from "@uniswap/the-compact/types/Lock.sol"; +import { Compact, COMPACT_TYPEHASH } from "@uniswap/the-compact/types/EIP712Types.sol"; +import { ForcedWithdrawalStatus } from "@uniswap/the-compact/types/ForcedWithdrawalStatus.sol"; +import { TheCompactMock } from "src/test/TheCompactMock.sol"; +import { ERC20Mock } from "src/test/ERC20Mock.sol"; +import { ERC6909 } from "@solady/tokens/ERC6909.sol"; +import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import { ISimpleAllocator } from "src/interfaces/ISimpleAllocator.sol"; +import { console } from "forge-std/console.sol"; + +abstract contract MocksSetup is Test { + address user; + uint256 userPK; + address attacker; + uint256 attackerPK; + address arbiter; + address tribunal; + ERC20Mock usdc; + TheCompact compactContract; + ERC7683Allocator erc7683Allocator; + uint256 usdcId; + + ResetPeriod defaultResetPeriod = ResetPeriod.OneMinute; + Scope defaultScope = Scope.Multichain; + uint256 defaultResetPeriodTimestamp = 60; + uint256 defaultAmount = 1000; + uint256 defaultNonce; + uint256 defaultOutputChainId = 130; + address defaultOutputToken = makeAddr("outputToken"); + uint256 defaultMinimumAmount = 1000; + uint256 defaultBaselinePriorityFee = 0; + uint256 defaultScalingFactor = 0; + bytes32 defaultSalt = bytes32(0); + + bytes32 ORDERDATA_GASLESS_TYPEHASH; + bytes32 ORDERDATA_TYPEHASH; + + function setUp() public virtual { + (user, userPK) = makeAddrAndKey("user"); + arbiter = makeAddr("arbiter"); + tribunal = makeAddr("tribunal"); + usdc = new ERC20Mock("USDC", "USDC"); + compactContract = new TheCompact(); + erc7683Allocator = new ERC7683Allocator(address(compactContract), arbiter, 5, 100); + Lock memory lock = Lock({ + token: address(usdc), + allocator: address(erc7683Allocator), + resetPeriod: defaultResetPeriod, + scope: defaultScope + }); + usdcId = IdLib.toId(lock); + (attacker, attackerPK) = makeAddrAndKey("attacker"); + defaultNonce = uint256(bytes32(abi.encodePacked(uint96(1), user))); + + ORDERDATA_GASLESS_TYPEHASH = erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(); + ORDERDATA_TYPEHASH = erc7683Allocator.ORDERDATA_TYPEHASH(); + } +} + +abstract contract CreateHash is Test { + struct Allocator { + bytes32 hash; + } + + // stringified types + string EIP712_DOMAIN_TYPE = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; // Hashed inside the function + // EIP712 domain type + string name = "The Compact"; + string version = "0"; + + string compactWitnessTypeString = "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)"; + string witnessTypeString = "Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)"; + + function _hashCompact(Compact memory data, Mandate memory mandate, address verifyingContract) internal view returns (bytes32) { + bytes32 compactHash = _hashCompact(data, mandate); + // hash typed data + return keccak256( + abi.encodePacked( + "\x19\x01", // backslash is needed to escape the character + _domainSeparator(verifyingContract), + compactHash + ) + ); + } + + function _hashCompact(Compact memory data, Mandate memory mandate) internal view returns (bytes32 compactHash) { + return keccak256(abi.encode(keccak256(bytes(compactWitnessTypeString)), data.arbiter, data.sponsor, data.nonce, data.expires, data.id, data.amount, keccak256(abi.encode(mandate)))); + } + + function _getTypeHash() internal view returns (bytes32) { + return keccak256(bytes(compactWitnessTypeString)); + } + + function _domainSeparator(address verifyingContract) internal view returns (bytes32) { + return keccak256(abi.encode(keccak256(bytes(EIP712_DOMAIN_TYPE)), keccak256(bytes(name)), keccak256(bytes(version)), block.chainid, verifyingContract)); + } + + function _signMessage(bytes32 hash_, uint256 signerPK_) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPK_, hash_); + return abi.encodePacked(r, s, v); + } + + function _hashAndSign(Compact memory data, Mandate memory mandate, address verifyingContract, uint256 signerPK) internal view returns (bytes memory) { + bytes32 hash = _hashCompact(data, mandate, verifyingContract); + bytes memory signature = _signMessage(hash, signerPK); + return signature; + } +} + +abstract contract CompactData is MocksSetup { + Compact private compact; + Mandate private mandate; + + function setUp() public virtual override { + super.setUp(); + + compact = Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + expires: _getClaimExpiration(), + id: usdcId, + amount: defaultAmount + }); + + mandate = Mandate({ + chainId: defaultOutputChainId, + tribunal: tribunal, + recipient: user, + expires: _getFillExpiration(), + token: defaultOutputToken, + minimumAmount: defaultMinimumAmount, + baselinePriorityFee: defaultBaselinePriorityFee, + scalingFactor: defaultScalingFactor, + salt: defaultSalt + }); + } + + function _getCompact() internal returns (Compact memory) { + compact.expires = _getClaimExpiration(); + return compact; + } + + function _getMandate() internal returns (Mandate memory) { + mandate.expires = _getFillExpiration(); + return mandate; + } + + function _getFillExpiration() internal view returns (uint256) { + return vm.getBlockTimestamp() + defaultResetPeriodTimestamp - 1; + } + + function _getClaimExpiration() internal view returns (uint256) { + return vm.getBlockTimestamp() + defaultResetPeriodTimestamp; + } +} + +abstract contract GaslessCrossChainOrderData is CompactData, CreateHash { + IOriginSettler.GaslessCrossChainOrder private gaslessCrossChainOrder; + + function setUp() public virtual override { + super.setUp(); + + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + gaslessCrossChainOrder = IOriginSettler.GaslessCrossChainOrder({ + originSettler: address(erc7683Allocator), + user: compact_.sponsor, + nonce: compact_.nonce, + originChainId: block.chainid, + openDeadline: uint32(_getClaimExpiration()), + fillDeadline: uint32(_getFillExpiration()), + orderDataType: erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(), + orderData: abi.encode(compact_.arbiter, compact_.id, compact_.amount, mandate_.chainId, mandate_.tribunal, mandate_.recipient, mandate_.token, mandate_.minimumAmount, mandate_.baselinePriorityFee, mandate_.scalingFactor, mandate_.salt) + }); + } + + function _getGaslessCrossChainOrder(address allocator, Compact memory compact_, Mandate memory mandate_, uint256 chainId_, bytes32 orderDataGaslessTypeHash_, address verifyingContract, uint256 signerPK) internal view returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) { + IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = IOriginSettler.GaslessCrossChainOrder({ + originSettler: allocator, + user: compact_.sponsor, + nonce: compact_.nonce, + originChainId: chainId_, + openDeadline: uint32(compact_.expires), + fillDeadline: uint32(mandate_.expires), + orderDataType: orderDataGaslessTypeHash_, + orderData: abi.encode(compact_.arbiter, compact_.id, compact_.amount, mandate_.chainId, mandate_.tribunal, mandate_.recipient, mandate_.token, mandate_.minimumAmount, mandate_.baselinePriorityFee, mandate_.scalingFactor, mandate_.salt) + }); + + (bytes memory signature_) = _hashAndSign(compact_, mandate_, verifyingContract, signerPK); + return (gaslessCrossChainOrder_, signature_); + } + + function _getGaslessCrossChainOrder() internal returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) { + (bytes memory signature_) = _hashAndSign(_getCompact(), _getMandate(), address(compactContract), userPK); + return (gaslessCrossChainOrder, signature_); + } +} + +abstract contract OnChainCrossChainOrderData is CompactData { + IOriginSettler.OnchainCrossChainOrder private onchainCrossChainOrder; + + function setUp() public virtual override { + super.setUp(); + + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + onchainCrossChainOrder = IOriginSettler.OnchainCrossChainOrder({ + fillDeadline: uint32(_getFillExpiration()), + orderDataType: erc7683Allocator.ORDERDATA_TYPEHASH(), + orderData: abi.encode(compact_.arbiter, compact_.sponsor, compact_.nonce, compact_.expires, compact_.id, compact_.amount, mandate_.chainId, mandate_.tribunal, mandate_.recipient, mandate_.token, mandate_.minimumAmount, mandate_.baselinePriorityFee, mandate_.scalingFactor, mandate_.salt) + }); + } + + function _getOnChainCrossChainOrder() internal view returns (IOriginSettler.OnchainCrossChainOrder memory) { + return onchainCrossChainOrder; + } + + function _getOnChainCrossChainOrder(Compact memory compact_, Mandate memory mandate_, bytes32 orderDataType_) internal pure returns (IOriginSettler.OnchainCrossChainOrder memory) { + IOriginSettler.OnchainCrossChainOrder memory onchainCrossChainOrder_ = IOriginSettler.OnchainCrossChainOrder({ + fillDeadline: uint32(mandate_.expires), + orderDataType: orderDataType_, + orderData: abi.encode(compact_.arbiter, compact_.sponsor, compact_.nonce, compact_.expires, compact_.id, compact_.amount, mandate_.chainId, mandate_.tribunal, mandate_.recipient, mandate_.token, mandate_.minimumAmount, mandate_.baselinePriorityFee, mandate_.scalingFactor, mandate_.salt) + }); + return onchainCrossChainOrder_; + } +} + +abstract contract Deposited is MocksSetup { + function setUp() public virtual override { + super.setUp(); + + vm.startPrank(user); + + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit(address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user); + + vm.stopPrank(); + } +} + +contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { + function test_revert_InvalidOrderDataType() public { + // Order data type is invalid + bytes32 falseOrderDataType = keccak256("false"); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidOrderDataType.selector, falseOrderDataType, erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH())); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = _getGaslessCrossChainOrder(); + falseGaslessCrossChainOrder.orderDataType = falseOrderDataType; + erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ""); + } + + function test_revert_InvalidDecoding() public { + // Decoding fails because of additional data + vm.prank(user); + vm.expectRevert(); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = _getGaslessCrossChainOrder(); + falseGaslessCrossChainOrder.orderData = abi.encode(falseGaslessCrossChainOrder.orderData, uint8(1)); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ""); + } + + function test_revert_InvalidOriginSettler() public { + // Origin settler is not the allocator + address falseOriginSettler = makeAddr("falseOriginSettler"); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidOriginSettler.selector, falseOriginSettler, address(erc7683Allocator))); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = + _getGaslessCrossChainOrder(falseOriginSettler, _getCompact(), _getMandate(), block.chainid, ORDERDATA_GASLESS_TYPEHASH, address(erc7683Allocator), userPK); + vm.prank(user); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ""); + } + + function test_revert_InvalidNonce() public { + // Nonce is invalid because the least significant 160 bits are not the sponsor address + Compact memory compact_ = _getCompact(); + compact_.nonce = uint256(bytes32(abi.encodePacked(uint96(1), attacker))); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, compact_.nonce)); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = + _getGaslessCrossChainOrder(address(erc7683Allocator), compact_, _getMandate(), block.chainid, ORDERDATA_GASLESS_TYPEHASH, address(erc7683Allocator), userPK); + vm.prank(user); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ""); + } + + function test_revert_InvalidSponsorSignature() public { + // Sponsor signature is invalid + + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit(address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user); + vm.stopPrank(); + + // Create a malicious signature + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder(address(erc7683Allocator), _getCompact(), _getMandate(), block.chainid, ORDERDATA_GASLESS_TYPEHASH, address(compactContract), attackerPK); + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidSignature.selector, user, attacker)); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ""); + } + + function test_successful() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit(address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user); + vm.stopPrank(); + + + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = _getGaslessCrossChainOrder(); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultAmount, + recipient: "", + chainId: block.chainid + }); + Claim memory claim = Claim({ + chainId: block.chainid, + compact: _getCompact(), + sponsorSignature: sponsorSignature, + allocatorSignature: "" + }); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, _getMandate()) + }); + + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(_getClaimExpiration()), + fillDeadline: uint32(_getFillExpiration()), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + vm.prank(user); + vm.expectEmit(true, false, false, true, address(erc7683Allocator)); + emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ""); + } + + function test_revert_NonceAlreadyInUse() public { + // Nonce is already used + + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit(address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user); + vm.stopPrank(); + + // use the nonce once + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = _getGaslessCrossChainOrder(); + vm.prank(user); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ""); + + // try to use the nonce again + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder2, bytes memory sponsorSignature2) = _getGaslessCrossChainOrder(); + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.NonceAlreadyInUse.selector, defaultNonce)); + erc7683Allocator.openFor(gaslessCrossChainOrder2, sponsorSignature2, ""); + + } +} + +contract ERC7683Allocator_open is OnChainCrossChainOrderData, CreateHash { + function test_revert_InvalidOrderDataType() public { + // Order data type is invalid + bytes32 falseOrderDataType = keccak256("false"); + IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); + onChainCrossChainOrder_.orderDataType = falseOrderDataType; + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidOrderDataType.selector, falseOrderDataType, erc7683Allocator.ORDERDATA_TYPEHASH())); + erc7683Allocator.open(onChainCrossChainOrder_); + } + + function test_revert_InvalidSponsor() public { + IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); + + vm.prank(attacker); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidSponsor.selector, user, attacker)); + erc7683Allocator.open(onChainCrossChainOrder_); + } + + function test_revert_InvalidRegistration_Unavailable() public { + // we deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit(address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user); + + // we do NOT register a claim + + vm.stopPrank(); + + + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + bytes32 claimHash = _hashCompact(compact_, mandate_); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidRegistration.selector, user, claimHash)); + erc7683Allocator.open(onChainCrossChainOrder_); + } + + function test_revert_InvalidRegistration_Expired() public { + // we deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit(address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user); + + // we register a claim with a expiration that is too short + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp - 1); + + vm.stopPrank(); + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = + _getOnChainCrossChainOrder(); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidRegistration.selector, user, claimHash)); + erc7683Allocator.open(onChainCrossChainOrder_); + } + + function test_successful() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit(address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user); + + // register a claim + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); + + vm.stopPrank(); + + + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultAmount, + recipient: "", + chainId: block.chainid + }); + Claim memory claim = Claim({ + chainId: block.chainid, + compact: _getCompact(), + sponsorSignature: "", + allocatorSignature: "" + }); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, _getMandate()) + }); + + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(_getClaimExpiration()), + fillDeadline: uint32(_getFillExpiration()), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + vm.prank(user); + vm.expectEmit(true, false, false, true, address(erc7683Allocator)); + emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); + erc7683Allocator.open(onChainCrossChainOrder_); + } +} + +contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, CreateHash { + function test_revert_InvalidLock() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit(address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user); + + // register a claim + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); + + address filler = makeAddr("filler"); + vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); + vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); + + vm.stopPrank(); + + // we do NOT open the order or lock the tokens + + // claim should be fail, because we mess with the nonce + ClaimWithWitness memory claim = ClaimWithWitness({ + allocatorSignature: "", + sponsorSignature: "", + sponsor: user, + nonce: defaultNonce, + expires: compact_.expires, + witness: keccak256(abi.encode(mandate_)), + witnessTypestring: witnessTypeString, + id: usdcId, + allocatedAmount: defaultAmount, + claimant: filler, + amount: defaultAmount + }); + vm.prank(arbiter); + vm.expectRevert(abi.encodeWithSelector(0x8baa579f)); // check for the InvalidSignature() error in the Compact contract + compactContract.claim(claim); + + vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); + vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); + } + + function test_isValidSignature_successful() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit(address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user); + + // register a claim + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); + + address filler = makeAddr("filler"); + vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); + vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); + + + // we open the order and lock the tokens + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + erc7683Allocator.open(onChainCrossChainOrder_); + vm.stopPrank(); + + // claim should be successful + ClaimWithWitness memory claim = ClaimWithWitness({ + allocatorSignature: "", + sponsorSignature: "", + sponsor: user, + nonce: defaultNonce, + expires: compact_.expires, + witness: keccak256(abi.encode(mandate_)), + witnessTypestring: witnessTypeString, + id: usdcId, + allocatedAmount: defaultAmount, + claimant: filler, + amount: defaultAmount + }); + vm.prank(arbiter); + compactContract.claim(claim); + + vm.assertEq(compactContract.balanceOf(user, usdcId), 0); + vm.assertEq(compactContract.balanceOf(filler, usdcId), defaultAmount); + } +} \ No newline at end of file diff --git a/test/SimpleAllocator.t.sol b/test/SimpleAllocator.t.sol index 7a963e0..1472dda 100644 --- a/test/SimpleAllocator.t.sol +++ b/test/SimpleAllocator.t.sol @@ -34,7 +34,7 @@ abstract contract MocksSetup is Test { arbiter = makeAddr("arbiter"); usdc = new ERC20Mock("USDC", "USDC"); compactContract = new TheCompactMock(); - simpleAllocator = new SimpleAllocator(address(compactContract), arbiter, 5, 100); + simpleAllocator = new SimpleAllocator(address(compactContract), 5, 100); usdcId = compactContract.getTokenId(address(usdc), address(simpleAllocator)); (user, userPK) = makeAddrAndKey("user"); (attacker, attackerPK) = makeAddrAndKey("attacker"); @@ -81,7 +81,7 @@ abstract contract Deposited is MocksSetup { usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit(address(usdc), defaultAmount, address(simpleAllocator)); + compactContract.deposit(address(usdc), address(simpleAllocator), defaultAmount); vm.stopPrank(); } @@ -113,7 +113,7 @@ contract SimpleAllocator_Lock is MocksSetup { // Mint, approve and deposit usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit(address(usdc), defaultAmount, address(simpleAllocator)); + compactContract.deposit(address(usdc), address(simpleAllocator), defaultAmount); // Successfully locked simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: block.timestamp + defaultResetPeriod, amount: defaultAmount })); @@ -124,13 +124,6 @@ contract SimpleAllocator_Lock is MocksSetup { simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce + 1, id: usdcId, expires: block.timestamp + defaultResetPeriod, amount: defaultAmount })); } - function test_revert_InvalidArbiter(address falseArbiter_) public { - vm.assume(falseArbiter_ != arbiter); - vm.prank(user); - vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidArbiter.selector, falseArbiter_)); - simpleAllocator.lock(Compact({ arbiter: falseArbiter_, sponsor: user, nonce: 1, id: usdcId, expires: block.timestamp + 1, amount: 1000 })); - } - function test_revert_InvalidExpiration_tooShort(uint128 delay_) public { vm.assume(delay_ < simpleAllocator.MIN_WITHDRAWAL_DELAY()); uint256 expiration = vm.getBlockTimestamp() + delay_; @@ -215,7 +208,7 @@ contract SimpleAllocator_Lock is MocksSetup { // Mint, approve and deposit usdc.mint(user, balance_); usdc.approve(address(compactContract), balance_); - compactContract.deposit(address(usdc), balance_, address(simpleAllocator)); + compactContract.deposit(address(usdc), address(simpleAllocator), balance_); // Check balance assertEq(compactContract.balanceOf(user, usdcId), balance_); @@ -234,7 +227,7 @@ contract SimpleAllocator_Lock is MocksSetup { // Mint, approve and deposit usdc.mint(user, amount_); usdc.approve(address(compactContract), amount_); - compactContract.deposit(address(usdc), amount_, address(simpleAllocator)); + compactContract.deposit(address(usdc), address(simpleAllocator), amount_); // Check no lock exists (uint256 amountBefore, uint256 expiresBefore) = simpleAllocator.checkTokensLocked(usdcId, user); @@ -265,7 +258,7 @@ contract SimpleAllocator_Lock is MocksSetup { // Mint, approve and deposit usdc.mint(user, amount_); usdc.approve(address(compactContract), amount_); - compactContract.deposit(address(usdc), amount_, address(simpleAllocator)); + compactContract.deposit(address(usdc), address(simpleAllocator), amount_); // Create a previous lock uint256 expirationPrev = vm.getBlockTimestamp() + delay_; @@ -330,8 +323,8 @@ contract SimpleAllocator_Attest is Deposited { vm.startPrank(user); // Lock a single token - uint256 defaultExpiration = vm.getBlockTimestamp() + defaultResetPeriod; - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration, amount: 1 })); + uint256 defaultExpiration_ = vm.getBlockTimestamp() + defaultResetPeriod; + simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration_, amount: 1 })); // At this point, the deposited defaultAmount is not fully available anymore, because one of the tokens was locked @@ -347,8 +340,8 @@ contract SimpleAllocator_Attest is Deposited { vm.startPrank(user); // Lock tokens - uint256 defaultExpiration = vm.getBlockTimestamp() + defaultResetPeriod; - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration, amount: lockedAmount_ })); + uint256 defaultExpiration_ = vm.getBlockTimestamp() + defaultResetPeriod; + simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration_, amount: lockedAmount_ })); vm.expectEmit(true, true, true, true); emit ERC6909.Transfer(address(0), user, otherUser, usdcId, transferAmount_); @@ -373,15 +366,15 @@ contract SimpleAllocator_IsValidSignature is Deposited, CreateHash { vm.startPrank(user); // Lock tokens - uint256 defaultExpiration = vm.getBlockTimestamp() + defaultResetPeriod; - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration, amount: defaultAmount })); + uint256 defaultExpiration_ = vm.getBlockTimestamp() + defaultResetPeriod; + simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration_, amount: defaultAmount })); // Move time forward so lock has expired vm.warp(block.timestamp + defaultResetPeriod); - bytes32 digest = _hashCompact(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration, amount: defaultAmount }), address(compactContract)); + bytes32 digest = _hashCompact(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration_, amount: defaultAmount }), address(compactContract)); - vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidLock.selector, digest, defaultExpiration)); + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidLock.selector, digest, defaultExpiration_)); simpleAllocator.isValidSignature(digest, ""); } @@ -389,13 +382,13 @@ contract SimpleAllocator_IsValidSignature is Deposited, CreateHash { vm.startPrank(user); // Lock tokens - uint256 defaultExpiration = vm.getBlockTimestamp() + defaultResetPeriod; - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration, amount: defaultAmount })); + uint256 defaultExpiration_ = vm.getBlockTimestamp() + defaultResetPeriod; + simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration_, amount: defaultAmount })); // Move time forward so lock has expired vm.warp(block.timestamp + defaultResetPeriod - 1); - bytes32 digest = _hashCompact(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration, amount: defaultAmount }), address(compactContract)); + bytes32 digest = _hashCompact(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration_, amount: defaultAmount }), address(compactContract)); bytes4 selector = simpleAllocator.isValidSignature(digest, ""); assertEq(selector, IERC1271.isValidSignature.selector); @@ -445,12 +438,6 @@ contract SimpleAllocator_CheckTokensLocked is Locked { assertEq(expires, defaultExpiration); } - function test_checkCompactLocked_revert_InvalidArbiter() public { - address otherArbiter = makeAddr("otherArbiter"); - vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidArbiter.selector, otherArbiter)); - simpleAllocator.checkCompactLocked(Compact({ arbiter: otherArbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration, amount: defaultAmount })); - } - function test_checkCompactLocked_NoActiveLock() public { address otherUser = makeAddr("otherUser"); (bool locked, uint256 expires) = simpleAllocator.checkCompactLocked(Compact({ arbiter: arbiter, sponsor: otherUser, nonce: defaultNonce, id: usdcId, expires: defaultExpiration, amount: defaultAmount })); @@ -500,4 +487,6 @@ contract SimpleAllocator_CheckTokensLocked is Locked { assertEq(locked, true); assertEq(expires, defaultExpiration); } + + // Check a force withdrawal will impact the expiration } diff --git a/test/SimpleERC7683Allocator.t.sol b/test/SimpleERC7683Allocator.t.sol new file mode 100644 index 0000000..8d0ba8c --- /dev/null +++ b/test/SimpleERC7683Allocator.t.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import { Test } from "forge-std/Test.sol"; +import { SimpleERC7683Allocator } from "src/allocators/SimpleERC7683Allocator.sol"; +import { IOriginSettler } from "src/interfaces/ERC7683/IOriginSettler.sol"; +import { ITheCompact } from "@uniswap/the-compact/interfaces/ITheCompact.sol"; +import { Compact, COMPACT_TYPEHASH } from "@uniswap/the-compact/types/EIP712Types.sol"; +import { ForcedWithdrawalStatus } from "@uniswap/the-compact/types/ForcedWithdrawalStatus.sol"; +import { TheCompactMock } from "src/test/TheCompactMock.sol"; +import { ERC20Mock } from "src/test/ERC20Mock.sol"; +import { ERC6909 } from "@solady/tokens/ERC6909.sol"; +import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import { console } from "forge-std/console.sol"; + +abstract contract MocksSetup is Test { + address user; + uint256 userPK; + address attacker; + uint256 attackerPK; + address arbiter; + ERC20Mock usdc; + TheCompactMock compactContract; + SimpleERC7683Allocator simpleERC7683Allocator; + uint256 usdcId; + + uint256 defaultResetPeriod = 60; + uint256 defaultAmount = 1000; + uint256 defaultNonce = 1; + uint256 defaultExpiration; + + function setUp() public virtual { + arbiter = makeAddr("arbiter"); + usdc = new ERC20Mock("USDC", "USDC"); + compactContract = new TheCompactMock(); + simpleERC7683Allocator = new SimpleERC7683Allocator(address(compactContract), 5, 100); + usdcId = compactContract.getTokenId(address(usdc), address(simpleERC7683Allocator)); + (user, userPK) = makeAddrAndKey("user"); + (attacker, attackerPK) = makeAddrAndKey("attacker"); + } +} + +abstract contract CreateHash is Test { + struct Allocator { + bytes32 hash; + } + + // stringified types + string EIP712_DOMAIN_TYPE = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; // Hashed inside the function + // EIP712 domain type + string name = "The Compact"; + string version = "0"; + + function _hashCompact(Compact memory data, address verifyingContract) internal view returns (bytes32) { + // hash typed data + return keccak256( + abi.encodePacked( + "\x19\x01", // backslash is needed to escape the character + _domainSeparator(verifyingContract), + keccak256(abi.encode(COMPACT_TYPEHASH, data.arbiter, data.sponsor, data.nonce, data.expires, data.id, data.amount)) + ) + ); + } + + function _domainSeparator(address verifyingContract) internal view returns (bytes32) { + return keccak256(abi.encode(keccak256(bytes(EIP712_DOMAIN_TYPE)), keccak256(bytes(name)), keccak256(bytes(version)), block.chainid, verifyingContract)); + } + + function _signMessage(bytes32 hash_, uint256 signerPK_) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPK_, hash_); + return abi.encodePacked(r, s, v); + } +} + +abstract contract Deposited is MocksSetup { + function setUp() public virtual override { + super.setUp(); + + vm.startPrank(user); + + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit(address(usdc), address(simpleERC7683Allocator), defaultAmount); + + vm.stopPrank(); + } +} + +abstract contract Locked is Deposited { + function setUp() public virtual override { + super.setUp(); + + vm.startPrank(user); + + defaultExpiration = vm.getBlockTimestamp() + defaultResetPeriod; + simpleERC7683Allocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration, amount: defaultAmount })); + + vm.stopPrank(); + } +} \ No newline at end of file From 6f8dff7099eb864a9bde1c37b233754020510b31 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Tue, 25 Feb 2025 12:56:19 +0100 Subject: [PATCH 07/35] routing allocator fixed --- src/allocators/RoutingAllocatorERC7683.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/allocators/RoutingAllocatorERC7683.sol b/src/allocators/RoutingAllocatorERC7683.sol index 98b899b..381997d 100644 --- a/src/allocators/RoutingAllocatorERC7683.sol +++ b/src/allocators/RoutingAllocatorERC7683.sol @@ -89,8 +89,8 @@ contract RoutingAllocatorERC7683 is SimpleAllocator, IOriginSettler { mapping(uint256 identifier => bool nonceUsed) private _userNonce; - constructor(address compactContract_, address arbiter_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) - SimpleAllocator(compactContract_, arbiter_, minWithdrawalDelay_, maxWithdrawalDelay_) { + constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) + SimpleAllocator(compactContract_, minWithdrawalDelay_, maxWithdrawalDelay_) { ALLOCATOR_ID_PREFIX = bytes32(IdLib.toId(Lock({ token: address(0), allocator: address(this), From b524752ae7bde9c16bcbca094b7e9bafff402df8 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Tue, 25 Feb 2025 13:06:26 +0100 Subject: [PATCH 08/35] Removed Arbiter from constructor --- src/allocators/ERC7683Allocator.sol | 4 ++-- src/allocators/SimpleWitnessAllocator.sol | 4 ++-- test/ERC7683Allocator.t.sol | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index 1a620ec..b22d28d 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -29,8 +29,8 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { mapping(uint256 nonce => bool nonceUsed) private _userNonce; - constructor(address compactContract_, address arbiter_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) - SimpleAllocator(compactContract_, arbiter_, minWithdrawalDelay_, maxWithdrawalDelay_) { + constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) + SimpleAllocator(compactContract_, minWithdrawalDelay_, maxWithdrawalDelay_) { _COMPACT_DOMAIN_SEPARATOR = ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(); } diff --git a/src/allocators/SimpleWitnessAllocator.sol b/src/allocators/SimpleWitnessAllocator.sol index 5d2c2d0..eaaa1d1 100644 --- a/src/allocators/SimpleWitnessAllocator.sol +++ b/src/allocators/SimpleWitnessAllocator.sol @@ -20,8 +20,8 @@ contract SimpleWitnessAllocator is SimpleAllocator, ISimpleWitnessAllocator { // uint200(abi.decode(bytes(",Witness witness)Witness("), (bytes25))) uint200 constant WITNESS_TYPESTRING = 0x2C5769746E657373207769746E657373295769746E65737328; - constructor(address compactContract_, address arbiter_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) - SimpleAllocator(compactContract_, arbiter_, minWithdrawalDelay_, maxWithdrawalDelay_) {} + constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) + SimpleAllocator(compactContract_, minWithdrawalDelay_, maxWithdrawalDelay_) {} /// @inheritdoc ISimpleWitnessAllocator function lockWithWitness(Compact calldata compact_, bytes32 typestringHash_, bytes32 witnessHash_) external { diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 8ceb534..b4422d6 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -56,7 +56,7 @@ abstract contract MocksSetup is Test { tribunal = makeAddr("tribunal"); usdc = new ERC20Mock("USDC", "USDC"); compactContract = new TheCompact(); - erc7683Allocator = new ERC7683Allocator(address(compactContract), arbiter, 5, 100); + erc7683Allocator = new ERC7683Allocator(address(compactContract), 5, 100); Lock memory lock = Lock({ token: address(usdc), allocator: address(erc7683Allocator), From 9ed5e4075fd62f9a21db740bb341bb81ab61a75e Mon Sep 17 00:00:00 2001 From: vimageDE Date: Wed, 26 Feb 2025 12:43:36 +0100 Subject: [PATCH 09/35] resolve tests --- test/ERC7683Allocator.t.sol | 134 ++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index b4422d6..da4318d 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -615,4 +615,138 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Create vm.assertEq(compactContract.balanceOf(user, usdcId), 0); vm.assertEq(compactContract.balanceOf(filler, usdcId), defaultAmount); } +} + +contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { + function test_resolve_successful() public { + + // WITH THE CURRENT ERC7683 DESIGN, THE SPONSOR SIGNATURE IS NOT PROVIDED TO THE RESOLVE FUNCTION + // WHILE THE ResolvedCrossChainOrder WITHOUT THE SIGNATURE COULD STILL BE USED TO SIMULATE THE FILL, + // ACTUALLY USING THIS DATA WOULD RESULT IN A LOSS OF THE REWARD TOKENS FOR THE FILLER. + // THIS FEELS RISKY. + // THE CURRENT ALTERNATIVE WOULD BE HAVE THE INPUT SIGNATURE BEING LEFT EMPTY AND INSTEAD BE PROVIDED IN THE THE orderData OF THE GaslessCrossChainOrderData. + // THIS IS BOTH NOT IDEAL, SO CURRENTLY CHECKING FOR A SOLUTION. + + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/) = _getGaslessCrossChainOrder(); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultAmount, + recipient: "", + chainId: block.chainid + }); + Claim memory claim = Claim({ + chainId: block.chainid, + compact: _getCompact(), + sponsorSignature: "", // sponsorSignature, // THE SIGNATURE MUST BE ADDED MANUALLY BY THE FILLER WITH THE CURRENT SYSTEM, BEFORE FILLING THE ORDER ON THE TARGET CHAIN + allocatorSignature: "" + }); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, _getMandate()) + }); + + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(_getClaimExpiration()), + fillDeadline: uint32(_getFillExpiration()), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + IOriginSettler.ResolvedCrossChainOrder memory resolved = erc7683Allocator.resolveFor(gaslessCrossChainOrder_, ""); + assertEq(resolved.user, resolvedCrossChainOrder.user); + assertEq(resolved.originChainId, resolvedCrossChainOrder.originChainId); + assertEq(resolved.openDeadline, resolvedCrossChainOrder.openDeadline); + assertEq(resolved.fillDeadline, resolvedCrossChainOrder.fillDeadline); + assertEq(resolved.orderId, resolvedCrossChainOrder.orderId); + assertEq(resolved.maxSpent.length, resolvedCrossChainOrder.maxSpent.length); + assertEq(resolved.maxSpent[0].token, resolvedCrossChainOrder.maxSpent[0].token); + assertEq(resolved.maxSpent[0].amount, resolvedCrossChainOrder.maxSpent[0].amount); + assertEq(resolved.maxSpent[0].recipient, resolvedCrossChainOrder.maxSpent[0].recipient); + assertEq(resolved.maxSpent[0].chainId, resolvedCrossChainOrder.maxSpent[0].chainId); + assertEq(resolved.minReceived.length, resolvedCrossChainOrder.minReceived.length); + assertEq(resolved.minReceived[0].token, resolvedCrossChainOrder.minReceived[0].token); + assertEq(resolved.minReceived[0].amount, resolvedCrossChainOrder.minReceived[0].amount); + assertEq(resolved.minReceived[0].recipient, resolvedCrossChainOrder.minReceived[0].recipient); + assertEq(resolved.minReceived[0].chainId, resolvedCrossChainOrder.minReceived[0].chainId); + assertEq(resolved.fillInstructions.length, resolvedCrossChainOrder.fillInstructions.length); + assertEq(resolved.fillInstructions[0].destinationChainId, resolvedCrossChainOrder.fillInstructions[0].destinationChainId); + assertEq(resolved.fillInstructions[0].destinationSettler, resolvedCrossChainOrder.fillInstructions[0].destinationSettler); + assertEq(resolved.fillInstructions[0].originData, resolvedCrossChainOrder.fillInstructions[0].originData); + } +} + +contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { + function test_resolve_successful() public { + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultAmount, + recipient: "", + chainId: block.chainid + }); + Claim memory claim = Claim({ + chainId: block.chainid, + compact: _getCompact(), + sponsorSignature: "", + allocatorSignature: "" + }); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, _getMandate()) + }); + + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(_getClaimExpiration()), + fillDeadline: uint32(_getFillExpiration()), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + IOriginSettler.ResolvedCrossChainOrder memory resolved = erc7683Allocator.resolve(onChainCrossChainOrder_); + assertEq(resolved.user, resolvedCrossChainOrder.user); + assertEq(resolved.originChainId, resolvedCrossChainOrder.originChainId); + assertEq(resolved.openDeadline, resolvedCrossChainOrder.openDeadline); + assertEq(resolved.fillDeadline, resolvedCrossChainOrder.fillDeadline); + assertEq(resolved.orderId, resolvedCrossChainOrder.orderId); + assertEq(resolved.maxSpent.length, resolvedCrossChainOrder.maxSpent.length); + assertEq(resolved.maxSpent[0].token, resolvedCrossChainOrder.maxSpent[0].token); + assertEq(resolved.maxSpent[0].amount, resolvedCrossChainOrder.maxSpent[0].amount); + assertEq(resolved.maxSpent[0].recipient, resolvedCrossChainOrder.maxSpent[0].recipient); + assertEq(resolved.maxSpent[0].chainId, resolvedCrossChainOrder.maxSpent[0].chainId); + assertEq(resolved.minReceived.length, resolvedCrossChainOrder.minReceived.length); + assertEq(resolved.minReceived[0].token, resolvedCrossChainOrder.minReceived[0].token); + assertEq(resolved.minReceived[0].amount, resolvedCrossChainOrder.minReceived[0].amount); + assertEq(resolved.minReceived[0].recipient, resolvedCrossChainOrder.minReceived[0].recipient); + assertEq(resolved.minReceived[0].chainId, resolvedCrossChainOrder.minReceived[0].chainId); + assertEq(resolved.fillInstructions.length, resolvedCrossChainOrder.fillInstructions.length); + assertEq(resolved.fillInstructions[0].destinationChainId, resolvedCrossChainOrder.fillInstructions[0].destinationChainId); + assertEq(resolved.fillInstructions[0].destinationSettler, resolvedCrossChainOrder.fillInstructions[0].destinationSettler); + assertEq(resolved.fillInstructions[0].originData, resolvedCrossChainOrder.fillInstructions[0].originData); + } } \ No newline at end of file From d3d3cf151ddaaba0d3afa87ddc756431ef254ee6 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Thu, 27 Feb 2025 11:47:25 +0100 Subject: [PATCH 10/35] forge fmt --- src/allocators/ERC7683Allocator.sol | 129 ++++--- src/allocators/ServerAllocator.sol | 64 +++- src/allocators/SimpleAllocator.sol | 60 ++- src/allocators/SimpleWitnessAllocator.sol | 26 +- src/allocators/types/TribunalStructs.sol | 37 +- src/interfaces/ERC7683/IOriginSettler.sol | 231 ++++++------ src/interfaces/IAllocator.sol | 2 +- src/interfaces/IERC7683Allocator.sol | 20 +- src/interfaces/IServerAllocator.sol | 10 +- src/interfaces/ISimpleAllocator.sol | 9 +- src/interfaces/ISimpleWitnessAllocator.sol | 7 +- src/test/ERC20Mock.sol | 4 +- src/test/TheCompactMock.sol | 38 +- test/ERC7683Allocator.t.sol | 412 ++++++++++++++------- test/ServerAllocator.t.sol | 149 +++++--- test/SimpleAllocator.t.sol | 378 +++++++++++++++---- 16 files changed, 1073 insertions(+), 503 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index b22d28d..ccae1e2 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -2,15 +2,14 @@ pragma solidity ^0.8.27; -import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import { Compact } from "@uniswap/the-compact/types/EIP712Types.sol"; -import { ITheCompact } from "@uniswap/the-compact/interfaces/ITheCompact.sol"; -import { SimpleAllocator } from "./SimpleAllocator.sol"; -import { Claim, Mandate } from "./types/TribunalStructs.sol"; -import { IERC7683Allocator } from "../interfaces/IERC7683Allocator.sol"; +import {IERC7683Allocator} from '../interfaces/IERC7683Allocator.sol'; +import {SimpleAllocator} from './SimpleAllocator.sol'; +import {Claim, Mandate} from './types/TribunalStructs.sol'; +import {ECDSA} from '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; +import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { - // The typehash of the OrderData struct // keccak256("OrderData(address arbiter,address sponsor,uint256 nonce,uint256 id,uint256 amount,Mandate mandate) // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") @@ -19,43 +18,49 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { // The typehash of the OrderDataGasless struct // keccak256("OrderDataGasless(address arbiter,uint256 id,uint256 amount,Mandate mandate) // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") - bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = 0x9ab67658b7c0f35b64fdadd7adee1e58b6399a8201f38c355d3a109a2d7081d7; + bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = + 0x9ab67658b7c0f35b64fdadd7adee1e58b6399a8201f38c355d3a109a2d7081d7; // keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate) // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") - bytes32 public constant COMPACT_WITNESS_TYPEHASH = 0x27f09e0bb8ce2ae63380578af7af85055d3ada248c502e2378b85bc3d05ee0b0; + bytes32 public constant COMPACT_WITNESS_TYPEHASH = + 0x27f09e0bb8ce2ae63380578af7af85055d3ada248c502e2378b85bc3d05ee0b0; bytes32 immutable _COMPACT_DOMAIN_SEPARATOR; - - mapping(uint256 nonce => bool nonceUsed) private _userNonce; + + mapping(uint256 nonce => bool nonceUsed) private _userNonce; constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) - SimpleAllocator(compactContract_, minWithdrawalDelay_, maxWithdrawalDelay_) { + SimpleAllocator(compactContract_, minWithdrawalDelay_, maxWithdrawalDelay_) + { _COMPACT_DOMAIN_SEPARATOR = ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(); } /// @inheritdoc IERC7683Allocator - function openFor(GaslessCrossChainOrder calldata order_, bytes calldata sponsorSignature_, bytes calldata) external{ + function openFor(GaslessCrossChainOrder calldata order_, bytes calldata sponsorSignature_, bytes calldata) + external + { // With the users signature, we can create locks in the name of the user // Check if orderDataType is the one expected by the allocator if (order_.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { revert InvalidOrderDataType(order_.orderDataType, ORDERDATA_GASLESS_TYPEHASH); } - if(order_.originSettler != address(this)){ + if (order_.originSettler != address(this)) { revert InvalidOriginSettler(order_.originSettler, address(this)); } // Decode the orderData OrderDataGasless memory orderDataGasless = abi.decode(order_.orderData, (OrderDataGasless)); - OrderData memory orderData = _convertGaslessOrderData(order_.user, order_.nonce, order_.openDeadline, orderDataGasless); + OrderData memory orderData = + _convertGaslessOrderData(order_.user, order_.nonce, order_.openDeadline, orderDataGasless); _open(orderData, order_.fillDeadline, order_.user, sponsorSignature_); } /// @inheritdoc IERC7683Allocator - function open(OnchainCrossChainOrder calldata order) external{ + function open(OnchainCrossChainOrder calldata order) external { // Check if orderDataType is the one expected by the allocator if (order.orderDataType != ORDERDATA_TYPEHASH) { revert InvalidOrderDataType(order.orderDataType, ORDERDATA_TYPEHASH); @@ -63,30 +68,36 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { // Decode the orderData OrderData memory orderData = abi.decode(order.orderData, (OrderData)); - if(orderData.sponsor != msg.sender) { + if (orderData.sponsor != msg.sender) { revert InvalidSponsor(orderData.sponsor, msg.sender); } - _open(orderData, order.fillDeadline, msg.sender, ""); + _open(orderData, order.fillDeadline, msg.sender, ''); } /// @inheritdoc IERC7683Allocator - function resolveFor(GaslessCrossChainOrder calldata order_, bytes calldata) external view returns (ResolvedCrossChainOrder memory){ + function resolveFor(GaslessCrossChainOrder calldata order_, bytes calldata) + external + view + returns (ResolvedCrossChainOrder memory) + { OrderDataGasless memory orderDataGasless = abi.decode(order_.orderData, (OrderDataGasless)); - OrderData memory orderData = _convertGaslessOrderData(order_.user, order_.nonce, order_.openDeadline, orderDataGasless); - return _resolveOrder(order_.user, order_.fillDeadline, order_.nonce, orderData, ""); + OrderData memory orderData = + _convertGaslessOrderData(order_.user, order_.nonce, order_.openDeadline, orderDataGasless); + return _resolveOrder(order_.user, order_.fillDeadline, order_.nonce, orderData, ''); } /// @inheritdoc IERC7683Allocator - function resolve(OnchainCrossChainOrder calldata order_) external view returns (ResolvedCrossChainOrder memory){ + function resolve(OnchainCrossChainOrder calldata order_) external view returns (ResolvedCrossChainOrder memory) { OrderData memory orderData = abi.decode(order_.orderData, (OrderData)); - return _resolveOrder(orderData.sponsor, order_.fillDeadline, orderData.nonce, orderData, ""); + return _resolveOrder(orderData.sponsor, order_.fillDeadline, orderData.nonce, orderData, ''); } /// @inheritdoc IERC7683Allocator function getCompactWitnessTypeString() external pure returns (string memory) { - return "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt))"; + return + 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt))'; } /// @inheritdoc IERC7683Allocator @@ -96,7 +107,9 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { return nonceFree_; } - function _open(OrderData memory orderData_, uint32 fillDeadline_, address sponsor_, bytes memory sponsorSignature_) internal { + function _open(OrderData memory orderData_, uint32 fillDeadline_, address sponsor_, bytes memory sponsorSignature_) + internal + { // Enforce a nonce where the most significant 96 bits are the nonce and the least significant 160 bits are the sponsor _checkNonce(sponsor_, orderData_.nonce); @@ -136,16 +149,10 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { ) ) ); - bytes32 digest = keccak256( - abi.encodePacked( - bytes2(0x1901), - _COMPACT_DOMAIN_SEPARATOR, - claimHash - ) - ); + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT_DOMAIN_SEPARATOR, claimHash)); // We check for the length, which means this could also be triggered by a zero length signature provided in the openFor function. This enables relaying of orders if the claim was registered on the compact. - if(sponsorSignature_.length > 0) { + if (sponsorSignature_.length > 0) { // confirm the signature matches the digest address signer = ECDSA.recover(digest, sponsorSignature_); if (sponsor_ != signer) { @@ -153,7 +160,8 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { } } else { // confirm the claim hash is registered on the compact - (bool isActive, uint256 registrationExpiration) = ITheCompact(COMPACT_CONTRACT).getRegistrationStatus(sponsor_, claimHash, COMPACT_WITNESS_TYPEHASH); + (bool isActive, uint256 registrationExpiration) = + ITheCompact(COMPACT_CONTRACT).getRegistrationStatus(sponsor_, claimHash, COMPACT_WITNESS_TYPEHASH); if (!isActive || registrationExpiration < orderData_.expires) { revert InvalidRegistration(sponsor_, claimHash); } @@ -162,31 +170,45 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { _sponsor[digest] = tokenHash; // Emit an open event - emit Open(bytes32(orderData_.nonce), _resolveOrder(sponsor_, fillDeadline_, orderData_.nonce, orderData_, sponsorSignature_)); + emit Open( + bytes32(orderData_.nonce), + _resolveOrder(sponsor_, fillDeadline_, orderData_.nonce, orderData_, sponsorSignature_) + ); } - function _lockTokens(OrderData memory orderData_, address sponsor_, uint256 identifier) internal returns (bytes32 tokenHash_) { - return _lockTokens(orderData_.arbiter, sponsor_, identifier, orderData_.expires, orderData_.id, orderData_.amount); + function _lockTokens(OrderData memory orderData_, address sponsor_, uint256 identifier) + internal + returns (bytes32 tokenHash_) + { + return + _lockTokens(orderData_.arbiter, sponsor_, identifier, orderData_.expires, orderData_.id, orderData_.amount); } - function _lockTokens(address arbiter, address sponsor, uint256 identifier, uint256 expires, uint256 id, uint256 amount) internal returns (bytes32 tokenHash_) { - tokenHash_ = _checkAllocation(Compact({ - arbiter: arbiter, - sponsor: sponsor, - nonce: identifier, - expires: expires, - id: id, - amount: amount - })); + function _lockTokens( + address arbiter, + address sponsor, + uint256 identifier, + uint256 expires, + uint256 id, + uint256 amount + ) internal returns (bytes32 tokenHash_) { + tokenHash_ = _checkAllocation( + Compact({arbiter: arbiter, sponsor: sponsor, nonce: identifier, expires: expires, id: id, amount: amount}) + ); _claim[tokenHash_] = expires; _amount[tokenHash_] = amount; _nonce[tokenHash_] = identifier; return tokenHash_; - } - function _resolveOrder(address sponsor, uint32 fillDeadline, uint256 identifier, OrderData memory orderData, bytes memory sponsorSignature) internal view returns (ResolvedCrossChainOrder memory) { + function _resolveOrder( + address sponsor, + uint32 fillDeadline, + uint256 identifier, + OrderData memory orderData, + bytes memory sponsorSignature + ) internal view returns (ResolvedCrossChainOrder memory) { FillInstruction[] memory fillInstructions = new FillInstruction[](1); Mandate memory mandate = Mandate({ @@ -211,7 +233,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { amount: orderData.amount }), sponsorSignature: sponsorSignature, - allocatorSignature: "" // No signature required from this allocator, it will verify the claim on chain via ERC1271. + allocatorSignature: '' // No signature required from this allocator, it will verify the claim on chain via ERC1271. }); fillInstructions[0] = FillInstruction({ @@ -258,7 +280,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { assembly ("memory-safe") { expectedSponsor := shr(96, shl(96, nonce_)) } - if(expectedSponsor != sponsor_) { + if (expectedSponsor != sponsor_) { revert InvalidNonce(nonce_); } } @@ -281,7 +303,12 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { } } - function _convertGaslessOrderData(address sponsor_, uint256 nonce_, uint32 openDeadline_, OrderDataGasless memory orderDataGasless_) internal pure returns (OrderData memory orderData_) { + function _convertGaslessOrderData( + address sponsor_, + uint256 nonce_, + uint32 openDeadline_, + OrderDataGasless memory orderDataGasless_ + ) internal pure returns (OrderData memory orderData_) { orderData_ = OrderData({ arbiter: orderDataGasless_.arbiter, sponsor: sponsor_, diff --git a/src/allocators/ServerAllocator.sol b/src/allocators/ServerAllocator.sol index 152a462..0f586f9 100644 --- a/src/allocators/ServerAllocator.sol +++ b/src/allocators/ServerAllocator.sol @@ -2,14 +2,14 @@ pragma solidity ^0.8.27; -import { ITheCompact } from "@uniswap/the-compact/interfaces/ITheCompact.sol"; -import { Compact, COMPACT_TYPEHASH } from "@uniswap/the-compact/types/EIP712Types.sol"; -import { Ownable, Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol"; -import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; -import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; -import { IAllocator } from "../interfaces/IAllocator.sol"; -import { IServerAllocator } from "../interfaces/IServerAllocator.sol"; +import {IAllocator} from '../interfaces/IAllocator.sol'; +import {IServerAllocator} from '../interfaces/IServerAllocator.sol'; +import {Ownable, Ownable2Step} from '@openzeppelin/contracts/access/Ownable2Step.sol'; +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; +import {ECDSA} from '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; +import {EIP712} from '@openzeppelin/contracts/utils/cryptography/EIP712.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; +import {COMPACT_TYPEHASH, Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; contract ServerAllocator is Ownable2Step, EIP712, IServerAllocator { using ECDSA for bytes32; @@ -21,7 +21,8 @@ contract ServerAllocator is Ownable2Step, EIP712, IServerAllocator { bytes32 private constant _ATTESTATION_TYPE_HASH = 0x6017ed71e505719876ff40d1e87ed2a0a078883c87bd2902ea9988c117f7ca7f; // keccak256("NonceConsumption(address signer,uint256[] nonces,bytes32[] attestations)") - bytes32 private constant _NONCE_CONSUMPTION_TYPE_HASH = 0x626e2c6c331510cafaa5cc323e6ac1e87f32c48cba2a61d81c86b50534f7cc91; + bytes32 private constant _NONCE_CONSUMPTION_TYPE_HASH = + 0x626e2c6c331510cafaa5cc323e6ac1e87f32c48cba2a61d81c86b50534f7cc91; address private immutable _COMPACT_CONTRACT; @@ -40,9 +41,9 @@ contract ServerAllocator is Ownable2Step, EIP712, IServerAllocator { _; } - constructor(address owner_, address compactContract_) Ownable(owner_) EIP712("Allocator", "1") { + constructor(address owner_, address compactContract_) Ownable(owner_) EIP712('Allocator', '1') { _COMPACT_CONTRACT = compactContract_; - ITheCompact(_COMPACT_CONTRACT).__registerAllocator(address(this), ""); + ITheCompact(_COMPACT_CONTRACT).__registerAllocator(address(this), ''); } /// @inheritdoc IServerAllocator @@ -78,12 +79,17 @@ contract ServerAllocator is Ownable2Step, EIP712, IServerAllocator { } /// @inheritdoc IServerAllocator - function registerAttestationViaSignature(RegisterAttestation calldata attestation_, bytes calldata signature_) external { - bytes32 _attestationWithNonce = keccak256(abi.encode(attestation_.attestationHash, attestation_.expiration, attestation_.nonce)); + function registerAttestationViaSignature(RegisterAttestation calldata attestation_, bytes calldata signature_) + external + { + bytes32 _attestationWithNonce = + keccak256(abi.encode(attestation_.attestationHash, attestation_.expiration, attestation_.nonce)); if (_attestationSignatures[_attestationWithNonce]) { revert AlreadyUsedSig(attestation_.attestationHash, attestation_.nonce); } - address signer = _validateSignedAttestation(attestation_.signer, attestation_.attestationHash, attestation_.expiration, attestation_.nonce, signature_); + address signer = _validateSignedAttestation( + attestation_.signer, attestation_.attestationHash, attestation_.expiration, attestation_.nonce, signature_ + ); if (signer != attestation_.signer || !_containsSigner(signer)) { revert InvalidSignature(signature_, signer); } @@ -179,7 +185,11 @@ contract ServerAllocator is Ownable2Step, EIP712, IServerAllocator { } /// @inheritdoc IServerAllocator - function checkAttestationExpirations(address sponsor_, uint256 id_, uint256 amount_) external view returns (uint256[] memory) { + function checkAttestationExpirations(address sponsor_, uint256 id_, uint256 amount_) + external + view + returns (uint256[] memory) + { return _checkAttestationExpirations(keccak256(abi.encode(sponsor_, id_, amount_))); } @@ -220,12 +230,22 @@ contract ServerAllocator is Ownable2Step, EIP712, IServerAllocator { emit NoncesConsumed(nonces_); } - function _validateSignedAttestation(address signer_, bytes32 hash_, uint256 expiration_, uint256 nonce, bytes calldata signature_) internal view returns (address) { + function _validateSignedAttestation( + address signer_, + bytes32 hash_, + uint256 expiration_, + uint256 nonce, + bytes calldata signature_ + ) internal view returns (address) { bytes32 message = _hashAttestation(signer_, hash_, expiration_, nonce); return message.recover(signature_); } - function _hashAttestation(address signer_, bytes32 hash_, uint256 expiration_, uint256 nonce_) internal view returns (bytes32) { + function _hashAttestation(address signer_, bytes32 hash_, uint256 expiration_, uint256 nonce_) + internal + view + returns (bytes32) + { return _hashTypedDataV4(keccak256(abi.encode(_ATTESTATION_TYPE_HASH, signer_, hash_, expiration_, nonce_))); } @@ -233,13 +253,19 @@ contract ServerAllocator is Ownable2Step, EIP712, IServerAllocator { return digest_.recover(signature_); } - function _validateNonceConsumption(NonceConsumption calldata data_, bytes calldata signature_) internal view returns (address) { + function _validateNonceConsumption(NonceConsumption calldata data_, bytes calldata signature_) + internal + view + returns (address) + { bytes32 message = _hashNonceConsumption(data_); return message.recover(signature_); } function _hashNonceConsumption(NonceConsumption calldata data_) internal view returns (bytes32) { - return _hashTypedDataV4(keccak256(abi.encode(_NONCE_CONSUMPTION_TYPE_HASH, data_.signer, data_.nonces, data_.attestations))); + return _hashTypedDataV4( + keccak256(abi.encode(_NONCE_CONSUMPTION_TYPE_HASH, data_.signer, data_.nonces, data_.attestations)) + ); } function _containsSigner(address signer_) internal view returns (bool) { diff --git a/src/allocators/SimpleAllocator.sol b/src/allocators/SimpleAllocator.sol index 3e16d6a..5b3222d 100644 --- a/src/allocators/SimpleAllocator.sol +++ b/src/allocators/SimpleAllocator.sol @@ -2,14 +2,14 @@ pragma solidity ^0.8.27; -import { ERC6909 } from "@solady/tokens/ERC6909.sol"; -import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; -import { ITheCompact } from "@uniswap/the-compact/interfaces/ITheCompact.sol"; -import { Compact } from "@uniswap/the-compact/types/EIP712Types.sol"; -import { ResetPeriod } from "@uniswap/the-compact/lib/IdLib.sol"; -import { ForcedWithdrawalStatus } from "@uniswap/the-compact/types/ForcedWithdrawalStatus.sol"; -import { IAllocator } from "../interfaces/IAllocator.sol"; -import { ISimpleAllocator } from "../interfaces/ISimpleAllocator.sol"; +import {IAllocator} from '../interfaces/IAllocator.sol'; +import {ISimpleAllocator} from '../interfaces/ISimpleAllocator.sol'; +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; +import {ERC6909} from '@solady/tokens/ERC6909.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; +import {ResetPeriod} from '@uniswap/the-compact/lib/IdLib.sol'; +import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {ForcedWithdrawalStatus} from '@uniswap/the-compact/types/ForcedWithdrawalStatus.sol'; contract SimpleAllocator is ISimpleAllocator { // keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)") @@ -33,7 +33,7 @@ contract SimpleAllocator is ISimpleAllocator { MIN_WITHDRAWAL_DELAY = minWithdrawalDelay_; MAX_WITHDRAWAL_DELAY = maxWithdrawalDelay_; - ITheCompact(COMPACT_CONTRACT).__registerAllocator(address(this), ""); + ITheCompact(COMPACT_CONTRACT).__registerAllocator(address(this), ''); } /// @inheritdoc ISimpleAllocator @@ -67,7 +67,11 @@ contract SimpleAllocator is ISimpleAllocator { } /// @inheritdoc IAllocator - function attest(address operator_, address from_, address, uint256 id_, uint256 amount_) external view returns (bytes4) { + function attest(address operator_, address from_, address, uint256 id_, uint256 amount_) + external + view + returns (bytes4) + { if (msg.sender != COMPACT_CONTRACT) { revert InvalidCaller(msg.sender, COMPACT_CONTRACT); } @@ -82,7 +86,9 @@ contract SimpleAllocator is ISimpleAllocator { uint256 fullAmount = amount_; if (_claim[tokenHash] > block.timestamp) { // Lock is still active, add the locked amount if the nonce has not yet been consumed - fullAmount += ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)) ? 0 : _amount[tokenHash]; + fullAmount += ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)) + ? 0 + : _amount[tokenHash]; } if (balance < fullAmount) { revert InsufficientBalance(from_, id_, balance, fullAmount); @@ -104,10 +110,17 @@ contract SimpleAllocator is ISimpleAllocator { } /// @inheritdoc ISimpleAllocator - function checkTokensLocked(uint256 id_, address sponsor_) external view returns (uint256 amount_, uint256 expires_) { + function checkTokensLocked(uint256 id_, address sponsor_) + external + view + returns (uint256 amount_, uint256 expires_) + { bytes32 tokenHash = _getTokenHash(id_, sponsor_); uint256 expires = _claim[tokenHash]; - if (expires <= block.timestamp || ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this))) { + if ( + expires <= block.timestamp + || ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)) + ) { return (0, 0); } @@ -135,9 +148,11 @@ contract SimpleAllocator is ISimpleAllocator { ) ); uint256 expires = _claim[tokenHash]; - bool active = _sponsor[digest] == tokenHash && expires > block.timestamp && !ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)); + bool active = _sponsor[digest] == tokenHash && expires > block.timestamp + && !ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)); if (active) { - (ForcedWithdrawalStatus status, uint256 forcedWithdrawalAvailableAt) = ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus(compact_.sponsor, compact_.id); + (ForcedWithdrawalStatus status, uint256 forcedWithdrawalAvailableAt) = + ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus(compact_.sponsor, compact_.id); if (status == ForcedWithdrawalStatus.Enabled && forcedWithdrawalAvailableAt < expires) { expires = forcedWithdrawalAvailableAt; active = expires > block.timestamp; @@ -157,14 +172,20 @@ contract SimpleAllocator is ISimpleAllocator { } bytes32 tokenHash = _getTokenHash(compact_.id, msg.sender); // Check no lock is already active for this sponsor - if (_claim[tokenHash] > block.timestamp && !ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this))) { + if ( + _claim[tokenHash] > block.timestamp + && !ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)) + ) { revert ClaimActive(compact_.sponsor); } // Check expiration is not too soon or too late - if (compact_.expires < block.timestamp + MIN_WITHDRAWAL_DELAY || compact_.expires > block.timestamp + MAX_WITHDRAWAL_DELAY) { + if ( + compact_.expires < block.timestamp + MIN_WITHDRAWAL_DELAY + || compact_.expires > block.timestamp + MAX_WITHDRAWAL_DELAY + ) { revert InvalidExpiration(compact_.expires); } - (,address allocator, ResetPeriod resetPeriod,) = ITheCompact(COMPACT_CONTRACT).getLockDetails(compact_.id); + (, address allocator, ResetPeriod resetPeriod,) = ITheCompact(COMPACT_CONTRACT).getLockDetails(compact_.id); if (allocator != address(this)) { revert InvalidAllocator(allocator); } @@ -173,7 +194,8 @@ contract SimpleAllocator is ISimpleAllocator { revert ForceWithdrawalAvailable(compact_.expires, block.timestamp + _resetPeriodToSeconds(resetPeriod)); } // Check expiration is not past an active force withdrawal - (, uint256 forcedWithdrawalExpiration) = ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus(compact_.sponsor, compact_.id); + (, uint256 forcedWithdrawalExpiration) = + ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus(compact_.sponsor, compact_.id); if (forcedWithdrawalExpiration != 0 && forcedWithdrawalExpiration < compact_.expires) { revert ForceWithdrawalAvailable(compact_.expires, forcedWithdrawalExpiration); } diff --git a/src/allocators/SimpleWitnessAllocator.sol b/src/allocators/SimpleWitnessAllocator.sol index eaaa1d1..c6fc3c5 100644 --- a/src/allocators/SimpleWitnessAllocator.sol +++ b/src/allocators/SimpleWitnessAllocator.sol @@ -2,26 +2,30 @@ pragma solidity ^0.8.27; -import { ERC6909 } from "@solady/tokens/ERC6909.sol"; -import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; -import { ITheCompact } from "@uniswap/the-compact/interfaces/ITheCompact.sol"; -import { Compact } from "@uniswap/the-compact/types/EIP712Types.sol"; -import { SimpleAllocator } from "./SimpleAllocator.sol"; -import { IAllocator } from "../interfaces/IAllocator.sol"; -import { ISimpleWitnessAllocator } from "../interfaces/ISimpleWitnessAllocator.sol"; +import {IAllocator} from '../interfaces/IAllocator.sol'; +import {ISimpleWitnessAllocator} from '../interfaces/ISimpleWitnessAllocator.sol'; +import {SimpleAllocator} from './SimpleAllocator.sol'; +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; +import {ERC6909} from '@solady/tokens/ERC6909.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; +import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; contract SimpleWitnessAllocator is SimpleAllocator, ISimpleWitnessAllocator { // abi.decode(bytes("Compact(address arbiter,address "), (bytes32)) - bytes32 constant COMPACT_TYPESTRING_FRAGMENT_ONE = 0x436f6d70616374286164647265737320617262697465722c6164647265737320; + bytes32 constant COMPACT_TYPESTRING_FRAGMENT_ONE = + 0x436f6d70616374286164647265737320617262697465722c6164647265737320; // abi.decode(bytes("sponsor,uint256 nonce,uint256 ex"), (bytes32)) - bytes32 constant COMPACT_TYPESTRING_FRAGMENT_TWO = 0x73706f6e736f722c75696e74323536206e6f6e63652c75696e74323536206578; + bytes32 constant COMPACT_TYPESTRING_FRAGMENT_TWO = + 0x73706f6e736f722c75696e74323536206e6f6e63652c75696e74323536206578; // abi.decode(bytes("pires,uint256 id,uint256 amount)"), (bytes32)) - bytes32 constant COMPACT_TYPESTRING_FRAGMENT_THREE = 0x70697265732c75696e743235362069642c75696e7432353620616d6f756e7429; + bytes32 constant COMPACT_TYPESTRING_FRAGMENT_THREE = + 0x70697265732c75696e743235362069642c75696e7432353620616d6f756e7429; // uint200(abi.decode(bytes(",Witness witness)Witness("), (bytes25))) uint200 constant WITNESS_TYPESTRING = 0x2C5769746E657373207769746E657373295769746E65737328; constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) - SimpleAllocator(compactContract_, minWithdrawalDelay_, maxWithdrawalDelay_) {} + SimpleAllocator(compactContract_, minWithdrawalDelay_, maxWithdrawalDelay_) + {} /// @inheritdoc ISimpleWitnessAllocator function lockWithWitness(Compact calldata compact_, bytes32 typestringHash_, bytes32 witnessHash_) external { diff --git a/src/allocators/types/TribunalStructs.sol b/src/allocators/types/TribunalStructs.sol index b13156d..f1d1dfb 100644 --- a/src/allocators/types/TribunalStructs.sol +++ b/src/allocators/types/TribunalStructs.sol @@ -2,24 +2,23 @@ pragma solidity ^0.8.27; -import { Compact } from "@uniswap/the-compact/types/EIP712Types.sol"; +import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; +struct Claim { + uint256 chainId; // Claim processing chain ID + Compact compact; + bytes sponsorSignature; // Authorization from the sponsor + bytes allocatorSignature; // Authorization from the allocator +} - struct Claim { - uint256 chainId; // Claim processing chain ID - Compact compact; - bytes sponsorSignature; // Authorization from the sponsor - bytes allocatorSignature; // Authorization from the allocator - } - - struct Mandate { - uint256 chainId; // (implicit arg, included in EIP712 payload) - address tribunal; // (implicit arg, included in EIP712 payload) - address recipient; // Recipient of settled tokens - uint256 expires; // Mandate expiration timestamp - address token; // Settlement token (address(0) for native) - uint256 minimumAmount; // Minimum settlement amount - uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in - uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) - bytes32 salt; // Replay protection parameter - } \ No newline at end of file +struct Mandate { + uint256 chainId; // (implicit arg, included in EIP712 payload) + address tribunal; // (implicit arg, included in EIP712 payload) + address recipient; // Recipient of settled tokens + uint256 expires; // Mandate expiration timestamp + address token; // Settlement token (address(0) for native) + uint256 minimumAmount; // Minimum settlement amount + uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in + uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) + bytes32 salt; // Replay protection parameter +} diff --git a/src/interfaces/ERC7683/IOriginSettler.sol b/src/interfaces/ERC7683/IOriginSettler.sol index 6bacf67..026ae91 100644 --- a/src/interfaces/ERC7683/IOriginSettler.sol +++ b/src/interfaces/ERC7683/IOriginSettler.sol @@ -5,126 +5,129 @@ pragma solidity ^0.8.0; /// @title IOriginSettler /// @notice Standard interface for settlement contracts on the origin chain interface IOriginSettler { - /// @title GaslessCrossChainOrder CrossChainOrder type - /// @notice Standard order struct to be signed by users, disseminated to fillers, and submitted to origin settler contracts - struct GaslessCrossChainOrder { - /// @dev The contract address that the order is meant to be settled by. - /// Fillers send this order to this contract address on the origin chain - address originSettler; - /// @dev The address of the user who is initiating the swap, - /// whose input tokens will be taken and escrowed - address user; - /// @dev Nonce to be used as replay protection for the order - uint256 nonce; - /// @dev The chainId of the origin chain - uint256 originChainId; - /// @dev The timestamp by which the order must be opened - uint32 openDeadline; - /// @dev The timestamp by which the order must be filled on the destination chain - uint32 fillDeadline; - /// @dev Type identifier for the order data. This is an EIP-712 typehash. - bytes32 orderDataType; - /// @dev Arbitrary implementation-specific data - /// Can be used to define tokens, amounts, destination chains, fees, settlement parameters, - /// or any other order-type specific information - bytes orderData; - } + /// @title GaslessCrossChainOrder CrossChainOrder type + /// @notice Standard order struct to be signed by users, disseminated to fillers, and submitted to origin settler contracts + struct GaslessCrossChainOrder { + /// @dev The contract address that the order is meant to be settled by. + /// Fillers send this order to this contract address on the origin chain + address originSettler; + /// @dev The address of the user who is initiating the swap, + /// whose input tokens will be taken and escrowed + address user; + /// @dev Nonce to be used as replay protection for the order + uint256 nonce; + /// @dev The chainId of the origin chain + uint256 originChainId; + /// @dev The timestamp by which the order must be opened + uint32 openDeadline; + /// @dev The timestamp by which the order must be filled on the destination chain + uint32 fillDeadline; + /// @dev Type identifier for the order data. This is an EIP-712 typehash. + bytes32 orderDataType; + /// @dev Arbitrary implementation-specific data + /// Can be used to define tokens, amounts, destination chains, fees, settlement parameters, + /// or any other order-type specific information + bytes orderData; + } - /// @title OnchainCrossChainOrder CrossChainOrder type - /// @notice Standard order struct for user-opened orders, where the user is the msg.sender. - struct OnchainCrossChainOrder { - /// @dev The timestamp by which the order must be filled on the destination chain - uint32 fillDeadline; - /// @dev Type identifier for the order data. This is an EIP-712 typehash. - bytes32 orderDataType; - /// @dev Arbitrary implementation-specific data - /// Can be used to define tokens, amounts, destination chains, fees, settlement parameters, - /// or any other order-type specific information - bytes orderData; - } - /// @title ResolvedCrossChainOrder type - /// @notice An implementation-generic representation of an order intended for filler consumption - /// @dev Defines all requirements for filling an order by unbundling the implementation-specific orderData. - /// @dev Intended to improve integration generalization by allowing fillers to compute the exact input and output information of any order - struct ResolvedCrossChainOrder { - /// @dev The address of the user who is initiating the transfer - address user; - /// @dev The chainId of the origin chain - uint256 originChainId; - /// @dev The timestamp by which the order must be opened - uint32 openDeadline; - /// @dev The timestamp by which the order must be filled on the destination chain(s) - uint32 fillDeadline; - /// @dev The unique identifier for this order within this settlement system - bytes32 orderId; + /// @title OnchainCrossChainOrder CrossChainOrder type + /// @notice Standard order struct for user-opened orders, where the user is the msg.sender. + struct OnchainCrossChainOrder { + /// @dev The timestamp by which the order must be filled on the destination chain + uint32 fillDeadline; + /// @dev Type identifier for the order data. This is an EIP-712 typehash. + bytes32 orderDataType; + /// @dev Arbitrary implementation-specific data + /// Can be used to define tokens, amounts, destination chains, fees, settlement parameters, + /// or any other order-type specific information + bytes orderData; + } + /// @title ResolvedCrossChainOrder type + /// @notice An implementation-generic representation of an order intended for filler consumption + /// @dev Defines all requirements for filling an order by unbundling the implementation-specific orderData. + /// @dev Intended to improve integration generalization by allowing fillers to compute the exact input and output information of any order - /// @dev The max outputs that the filler will send. It's possible the actual amount depends on the state of the destination - /// chain (destination dutch auction, for instance), so these outputs should be considered a cap on filler liabilities. - Output[] maxSpent; - /// @dev The minimum outputs that must be given to the filler as part of order settlement. Similar to maxSpent, it's possible - /// that special order types may not be able to guarantee the exact amount at open time, so this should be considered - /// a floor on filler receipts. - Output[] minReceived; - /// @dev Each instruction in this array is parameterizes a single leg of the fill. This provides the filler with the information - /// necessary to perform the fill on the destination(s). - FillInstruction[] fillInstructions; - } + struct ResolvedCrossChainOrder { + /// @dev The address of the user who is initiating the transfer + address user; + /// @dev The chainId of the origin chain + uint256 originChainId; + /// @dev The timestamp by which the order must be opened + uint32 openDeadline; + /// @dev The timestamp by which the order must be filled on the destination chain(s) + uint32 fillDeadline; + /// @dev The unique identifier for this order within this settlement system + bytes32 orderId; + /// @dev The max outputs that the filler will send. It's possible the actual amount depends on the state of the destination + /// chain (destination dutch auction, for instance), so these outputs should be considered a cap on filler liabilities. + Output[] maxSpent; + /// @dev The minimum outputs that must be given to the filler as part of order settlement. Similar to maxSpent, it's possible + /// that special order types may not be able to guarantee the exact amount at open time, so this should be considered + /// a floor on filler receipts. + Output[] minReceived; + /// @dev Each instruction in this array is parameterizes a single leg of the fill. This provides the filler with the information + /// necessary to perform the fill on the destination(s). + FillInstruction[] fillInstructions; + } - /// @notice Tokens that must be received for a valid order fulfillment - struct Output { - /// @dev The address of the ERC20 token on the destination chain - /// @dev address(0) used as a sentinel for the native token - bytes32 token; - /// @dev The amount of the token to be sent - uint256 amount; - /// @dev The address to receive the output tokens - bytes32 recipient; - /// @dev The destination chain for this output - uint256 chainId; - } + /// @notice Tokens that must be received for a valid order fulfillment + struct Output { + /// @dev The address of the ERC20 token on the destination chain + /// @dev address(0) used as a sentinel for the native token + bytes32 token; + /// @dev The amount of the token to be sent + uint256 amount; + /// @dev The address to receive the output tokens + bytes32 recipient; + /// @dev The destination chain for this output + uint256 chainId; + } - /// @title FillInstruction type - /// @notice Instructions to parameterize each leg of the fill - /// @dev Provides all the origin-generated information required to produce a valid fill leg - struct FillInstruction { - /// @dev The contract address that the order is meant to be settled by - uint256 destinationChainId; - /// @dev The contract address that the order is meant to be filled on - bytes32 destinationSettler; - /// @dev The data generated on the origin chain needed by the destinationSettler to process the fill - bytes originData; - } + /// @title FillInstruction type + /// @notice Instructions to parameterize each leg of the fill + /// @dev Provides all the origin-generated information required to produce a valid fill leg + struct FillInstruction { + /// @dev The contract address that the order is meant to be settled by + uint256 destinationChainId; + /// @dev The contract address that the order is meant to be filled on + bytes32 destinationSettler; + /// @dev The data generated on the origin chain needed by the destinationSettler to process the fill + bytes originData; + } - /// @notice Signals that an order has been opened - /// @param orderId a unique order identifier within this settlement system - /// @param resolvedOrder resolved order that would be returned by resolve if called instead of Open - event Open(bytes32 indexed orderId, ResolvedCrossChainOrder resolvedOrder); + /// @notice Signals that an order has been opened + /// @param orderId a unique order identifier within this settlement system + /// @param resolvedOrder resolved order that would be returned by resolve if called instead of Open + event Open(bytes32 indexed orderId, ResolvedCrossChainOrder resolvedOrder); + /// @notice Opens a gasless cross-chain order on behalf of a user. + /// @dev To be called by the filler. + /// @dev This method must emit the Open event + /// @param order The GaslessCrossChainOrder definition + /// @param signature The user's signature over the order + /// @param originFillerData Any filler-defined data required by the settler + function openFor(GaslessCrossChainOrder calldata order, bytes calldata signature, bytes calldata originFillerData) + external; - /// @notice Opens a gasless cross-chain order on behalf of a user. - /// @dev To be called by the filler. - /// @dev This method must emit the Open event - /// @param order The GaslessCrossChainOrder definition - /// @param signature The user's signature over the order - /// @param originFillerData Any filler-defined data required by the settler - function openFor(GaslessCrossChainOrder calldata order, bytes calldata signature, bytes calldata originFillerData) external; + /// @notice Opens a cross-chain order + /// @dev To be called by the user + /// @dev This method must emit the Open event + /// @param order The OnchainCrossChainOrder definition + function open(OnchainCrossChainOrder calldata order) external; - /// @notice Opens a cross-chain order - /// @dev To be called by the user - /// @dev This method must emit the Open event - /// @param order The OnchainCrossChainOrder definition - function open(OnchainCrossChainOrder calldata order) external; + /// @notice Resolves a specific GaslessCrossChainOrder into a generic ResolvedCrossChainOrder + /// @dev Intended to improve standardized integration of various order types and settlement contracts + /// @param order The GaslessCrossChainOrder definition + /// @param originFillerData Any filler-defined data required by the settler + /// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order + function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata originFillerData) + external + view + returns (ResolvedCrossChainOrder memory); - /// @notice Resolves a specific GaslessCrossChainOrder into a generic ResolvedCrossChainOrder - /// @dev Intended to improve standardized integration of various order types and settlement contracts - /// @param order The GaslessCrossChainOrder definition - /// @param originFillerData Any filler-defined data required by the settler - /// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order - function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata originFillerData) external view returns (ResolvedCrossChainOrder memory); - - /// @notice Resolves a specific OnchainCrossChainOrder into a generic ResolvedCrossChainOrder - /// @dev Intended to improve standardized integration of various order types and settlement contracts - /// @param order The OnchainCrossChainOrder definition - /// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order - function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory); -} \ No newline at end of file + /// @notice Resolves a specific OnchainCrossChainOrder into a generic ResolvedCrossChainOrder + /// @dev Intended to improve standardized integration of various order types and settlement contracts + /// @param order The OnchainCrossChainOrder definition + /// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order + function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory); +} diff --git a/src/interfaces/IAllocator.sol b/src/interfaces/IAllocator.sol index 3f187ab..581edb6 100644 --- a/src/interfaces/IAllocator.sol +++ b/src/interfaces/IAllocator.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; -import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; interface IAllocator is IERC1271 { // Called on standard transfers; must return this function selector (0x1a808f91). diff --git a/src/interfaces/IERC7683Allocator.sol b/src/interfaces/IERC7683Allocator.sol index 1dc0e6e..81ab05f 100644 --- a/src/interfaces/IERC7683Allocator.sol +++ b/src/interfaces/IERC7683Allocator.sol @@ -2,10 +2,9 @@ pragma solidity ^0.8.27; -import { IOriginSettler } from "./ERC7683/IOriginSettler.sol"; +import {IOriginSettler} from './ERC7683/IOriginSettler.sol'; interface IERC7683Allocator is IOriginSettler { - struct OrderData { // COMPACT address arbiter; // The account tasked with verifying and submitting the claim. @@ -54,19 +53,22 @@ interface IERC7683Allocator is IOriginSettler { error InvalidRegistration(address sponsor, bytes32 claimHash); error InvalidSponsor(address sponsor, address expectedSponsor); - /// @inheritdoc IOriginSettler - function openFor(GaslessCrossChainOrder calldata order, bytes calldata signature, bytes calldata originFillerData) external; + function openFor(GaslessCrossChainOrder calldata order, bytes calldata signature, bytes calldata originFillerData) + external; /// @inheritdoc IOriginSettler - /// @dev Requires the user to have previously registered the claim hash on the compact - function open(OnchainCrossChainOrder calldata order) external; + /// @dev Requires the user to have previously registered the claim hash on the compact + function open(OnchainCrossChainOrder calldata order) external; /// @inheritdoc IOriginSettler - function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata originFillerData) external view returns (ResolvedCrossChainOrder memory); + function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata originFillerData) + external + view + returns (ResolvedCrossChainOrder memory); /// @inheritdoc IOriginSettler - function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory); + function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory); /// @notice Returns the type string of the compact including the witness function getCompactWitnessTypeString() external pure returns (string memory); @@ -74,4 +76,4 @@ interface IERC7683Allocator is IOriginSettler { /// @notice Checks if a nonce is free to be used /// @dev The nonce is the most significant 96 bits. The least significant 160 bits must be the sponsor address function checkNonce(address sponsor_, uint256 nonce_) external view returns (bool nonceFree_); -} \ No newline at end of file +} diff --git a/src/interfaces/IServerAllocator.sol b/src/interfaces/IServerAllocator.sol index 20d4773..37f28fe 100644 --- a/src/interfaces/IServerAllocator.sol +++ b/src/interfaces/IServerAllocator.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.27; -import { IAllocator } from "../interfaces/IAllocator.sol"; +import {IAllocator} from '../interfaces/IAllocator.sol'; interface IServerAllocator is IAllocator { struct RegisterAttestation { @@ -92,7 +92,8 @@ interface IServerAllocator is IAllocator { /// @dev Nonce management in the RegisterAttestation is only required for multiple registers of the same attestation with the same expiration. /// @param attestation_ The RegisterAttestation struct containing the signer, the hash of the attestation, the expiration and the nonce /// @param signature_ The signature of the signer - function registerAttestationViaSignature(RegisterAttestation calldata attestation_, bytes calldata signature_) external; + function registerAttestationViaSignature(RegisterAttestation calldata attestation_, bytes calldata signature_) + external; /// @notice Consume nonces on the compact contract and attestations on the allocator /// @dev The hashes array needs to be of the same length as the nonces array. @@ -128,7 +129,10 @@ interface IServerAllocator is IAllocator { /// @param id_ The id of the token /// @param amount_ The amount of the token /// @return The array of expiration dates for the registered attestations - function checkAttestationExpirations(address sponsor_, uint256 id_, uint256 amount_) external view returns (uint256[] memory); + function checkAttestationExpirations(address sponsor_, uint256 id_, uint256 amount_) + external + view + returns (uint256[] memory); /// @notice Get the address of the compact contract /// @dev Only the compact contract can call the attest function diff --git a/src/interfaces/ISimpleAllocator.sol b/src/interfaces/ISimpleAllocator.sol index 55b7963..0e83fd2 100644 --- a/src/interfaces/ISimpleAllocator.sol +++ b/src/interfaces/ISimpleAllocator.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.27; -import { IAllocator } from "../interfaces/IAllocator.sol"; -import { Compact } from "@uniswap/the-compact/types/EIP712Types.sol"; +import {IAllocator} from '../interfaces/IAllocator.sol'; +import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; interface ISimpleAllocator is IAllocator { /// @notice Thrown if a claim is already active @@ -48,7 +48,10 @@ interface ISimpleAllocator is IAllocator { /// @param sponsor_ The address of the sponsor /// @return amount_ The amount of the token that was available for locking (the full balance of the token will get locked) /// @return expires_ The expiration of the lock - function checkTokensLocked(uint256 id_, address sponsor_) external view returns (uint256 amount_, uint256 expires_); + function checkTokensLocked(uint256 id_, address sponsor_) + external + view + returns (uint256 amount_, uint256 expires_); /// @notice Checks if the a lock for the compact exists and is active /// @dev Also checks if the provided nonce has not yet been consumed on the compact contract diff --git a/src/interfaces/ISimpleWitnessAllocator.sol b/src/interfaces/ISimpleWitnessAllocator.sol index a6ab26c..3825f98 100644 --- a/src/interfaces/ISimpleWitnessAllocator.sol +++ b/src/interfaces/ISimpleWitnessAllocator.sol @@ -2,11 +2,10 @@ pragma solidity ^0.8.27; -import { ISimpleAllocator } from "./ISimpleAllocator.sol"; -import { Compact } from "@uniswap/the-compact/types/EIP712Types.sol"; +import {ISimpleAllocator} from './ISimpleAllocator.sol'; +import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; interface ISimpleWitnessAllocator is ISimpleAllocator { - /// @notice Locks the tokens of an id for a claim with a witness /// @dev Locks all tokens of a sponsor for an id with a witness /// @dev example for the typeHash: @@ -15,7 +14,7 @@ interface ISimpleWitnessAllocator is ISimpleAllocator { /// @param compact_ The compact that contains the data about the lock /// @param typeHash_ The type hash of the full compact, including the witness /// @param witnessHash_ The witness hash of the witness - function lockWithWitness(Compact calldata compact_, bytes32 typeHash_,bytes32 witnessHash_) external; + function lockWithWitness(Compact calldata compact_, bytes32 typeHash_, bytes32 witnessHash_) external; /// @notice Returns the witness typestring hash including a given witness argument /// @dev example of a witness type string input: diff --git a/src/test/ERC20Mock.sol b/src/test/ERC20Mock.sol index 61ea4e6..f8a0ce1 100644 --- a/src/test/ERC20Mock.sol +++ b/src/test/ERC20Mock.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; contract ERC20Mock is ERC20 { - constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) { } + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} function mint(address to, uint256 amount) external { _mint(to, amount); diff --git a/src/test/TheCompactMock.sol b/src/test/TheCompactMock.sol index a43fbf9..4dcee68 100644 --- a/src/test/TheCompactMock.sol +++ b/src/test/TheCompactMock.sol @@ -2,14 +2,14 @@ pragma solidity ^0.8.27; -import { IAllocator } from "src/interfaces/IAllocator.sol"; -import { ERC6909 } from "@solady/tokens/ERC6909.sol"; -import { ERC20 } from "@solady/tokens/ERC20.sol"; -import { IdLib } from "@uniswap/the-compact/lib/IdLib.sol"; -import { ForcedWithdrawalStatus } from "@uniswap/the-compact/types/ForcedWithdrawalStatus.sol"; -import { ResetPeriod } from "@uniswap/the-compact/types/ResetPeriod.sol"; -import { Scope } from "@uniswap/the-compact/types/Scope.sol"; -import { console2 } from "forge-std/console2.sol"; +import {ERC20} from '@solady/tokens/ERC20.sol'; +import {ERC6909} from '@solady/tokens/ERC6909.sol'; +import {IdLib} from '@uniswap/the-compact/lib/IdLib.sol'; +import {ForcedWithdrawalStatus} from '@uniswap/the-compact/types/ForcedWithdrawalStatus.sol'; +import {ResetPeriod} from '@uniswap/the-compact/types/ResetPeriod.sol'; +import {Scope} from '@uniswap/the-compact/types/Scope.sol'; +import {console2} from 'forge-std/console2.sol'; +import {IAllocator} from 'src/interfaces/IAllocator.sol'; contract TheCompactMock is ERC6909 { using IdLib for uint96; @@ -47,7 +47,9 @@ contract TheCompactMock is ERC6909 { _transfer(address(0), from, to, id, amount); } - function claim(address from, address to, address token, uint256 amount, address allocator, bytes calldata signature) external { + function claim(address from, address to, address token, uint256 amount, address allocator, bytes calldata signature) + external + { uint256 id = _getTokenId(token, allocator); IAllocator(allocator).isValidSignature(keccak256(abi.encode(from, id, amount)), signature); _transfer(address(0), from, to, id, amount); @@ -85,7 +87,11 @@ contract TheCompactMock is ERC6909 { return true; } - function getForcedWithdrawalStatus(address sponsor, uint256) external view returns (ForcedWithdrawalStatus, uint256) { + function getForcedWithdrawalStatus(address sponsor, uint256) + external + view + returns (ForcedWithdrawalStatus, uint256) + { uint256 expires = forcedWithdrawalStatus[sponsor]; return (expires == 0 ? ForcedWithdrawalStatus.Disabled : ForcedWithdrawalStatus.Enabled, expires); } @@ -99,8 +105,8 @@ contract TheCompactMock is ERC6909 { abi.encode( // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f, - keccak256("The Compact"), - keccak256("0"), + keccak256('The Compact'), + keccak256('0'), block.chainid, address(this) ) @@ -110,24 +116,24 @@ contract TheCompactMock is ERC6909 { function name( uint256 // id ) public view virtual override returns (string memory) { - return "TheCompactMock"; + return 'TheCompactMock'; } function symbol( uint256 // id ) public view virtual override returns (string memory) { - return "TCM"; + return 'TCM'; } function tokenURI( uint256 // id ) public view virtual override returns (string memory) { - return ""; + return ''; } function _getTokenId(address token, address allocator) internal pure returns (uint256 tokenId) { assembly ("memory-safe") { - tokenId := or(shl(160, allocator),shr(96,shl(96,token))) + tokenId := or(shl(160, allocator), shr(96, shl(96, token))) } return tokenId; } diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index da4318d..6310969 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -2,26 +2,30 @@ pragma solidity ^0.8.27; -import { Test } from "forge-std/Test.sol"; -import { ERC7683Allocator } from "src/allocators/ERC7683Allocator.sol"; -import { IERC7683Allocator } from "src/interfaces/IERC7683Allocator.sol"; -import { IOriginSettler } from "src/interfaces/ERC7683/IOriginSettler.sol"; -import { Mandate, Claim } from "src/allocators/types/TribunalStructs.sol"; -import { ITheCompact } from "@uniswap/the-compact/interfaces/ITheCompact.sol"; -import { TheCompact } from "@uniswap/the-compact/TheCompact.sol"; -import { ClaimWithWitness } from "@uniswap/the-compact/types/Claims.sol"; -import { IdLib } from "@uniswap/the-compact/lib/IdLib.sol"; -import { Scope } from "@uniswap/the-compact/types/Scope.sol"; -import { ResetPeriod } from "@uniswap/the-compact/types/ResetPeriod.sol"; -import { Lock } from "@uniswap/the-compact/types/Lock.sol"; -import { Compact, COMPACT_TYPEHASH } from "@uniswap/the-compact/types/EIP712Types.sol"; -import { ForcedWithdrawalStatus } from "@uniswap/the-compact/types/ForcedWithdrawalStatus.sol"; -import { TheCompactMock } from "src/test/TheCompactMock.sol"; -import { ERC20Mock } from "src/test/ERC20Mock.sol"; -import { ERC6909 } from "@solady/tokens/ERC6909.sol"; -import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; -import { ISimpleAllocator } from "src/interfaces/ISimpleAllocator.sol"; -import { console } from "forge-std/console.sol"; +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; +import {ERC6909} from '@solady/tokens/ERC6909.sol'; +import {TheCompact} from '@uniswap/the-compact/TheCompact.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; + +import {IdLib} from '@uniswap/the-compact/lib/IdLib.sol'; +import {ClaimWithWitness} from '@uniswap/the-compact/types/Claims.sol'; + +import {COMPACT_TYPEHASH, Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {ForcedWithdrawalStatus} from '@uniswap/the-compact/types/ForcedWithdrawalStatus.sol'; +import {Lock} from '@uniswap/the-compact/types/Lock.sol'; +import {ResetPeriod} from '@uniswap/the-compact/types/ResetPeriod.sol'; +import {Scope} from '@uniswap/the-compact/types/Scope.sol'; +import {Test} from 'forge-std/Test.sol'; + +import {console} from 'forge-std/console.sol'; +import {ERC7683Allocator} from 'src/allocators/ERC7683Allocator.sol'; +import {Claim, Mandate} from 'src/allocators/types/TribunalStructs.sol'; +import {IOriginSettler} from 'src/interfaces/ERC7683/IOriginSettler.sol'; +import {IERC7683Allocator} from 'src/interfaces/IERC7683Allocator.sol'; + +import {ISimpleAllocator} from 'src/interfaces/ISimpleAllocator.sol'; +import {ERC20Mock} from 'src/test/ERC20Mock.sol'; +import {TheCompactMock} from 'src/test/TheCompactMock.sol'; abstract contract MocksSetup is Test { address user; @@ -41,7 +45,7 @@ abstract contract MocksSetup is Test { uint256 defaultAmount = 1000; uint256 defaultNonce; uint256 defaultOutputChainId = 130; - address defaultOutputToken = makeAddr("outputToken"); + address defaultOutputToken = makeAddr('outputToken'); uint256 defaultMinimumAmount = 1000; uint256 defaultBaselinePriorityFee = 0; uint256 defaultScalingFactor = 0; @@ -51,10 +55,10 @@ abstract contract MocksSetup is Test { bytes32 ORDERDATA_TYPEHASH; function setUp() public virtual { - (user, userPK) = makeAddrAndKey("user"); - arbiter = makeAddr("arbiter"); - tribunal = makeAddr("tribunal"); - usdc = new ERC20Mock("USDC", "USDC"); + (user, userPK) = makeAddrAndKey('user'); + arbiter = makeAddr('arbiter'); + tribunal = makeAddr('tribunal'); + usdc = new ERC20Mock('USDC', 'USDC'); compactContract = new TheCompact(); erc7683Allocator = new ERC7683Allocator(address(compactContract), 5, 100); Lock memory lock = Lock({ @@ -64,7 +68,7 @@ abstract contract MocksSetup is Test { scope: defaultScope }); usdcId = IdLib.toId(lock); - (attacker, attackerPK) = makeAddrAndKey("attacker"); + (attacker, attackerPK) = makeAddrAndKey('attacker'); defaultNonce = uint256(bytes32(abi.encodePacked(uint96(1), user))); ORDERDATA_GASLESS_TYPEHASH = erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(); @@ -78,20 +82,26 @@ abstract contract CreateHash is Test { } // stringified types - string EIP712_DOMAIN_TYPE = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; // Hashed inside the function + string EIP712_DOMAIN_TYPE = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'; // Hashed inside the function // EIP712 domain type - string name = "The Compact"; - string version = "0"; - - string compactWitnessTypeString = "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)"; - string witnessTypeString = "Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)"; - - function _hashCompact(Compact memory data, Mandate memory mandate, address verifyingContract) internal view returns (bytes32) { + string name = 'The Compact'; + string version = '0'; + + string compactWitnessTypeString = + 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)'; + string witnessTypeString = + 'Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)'; + + function _hashCompact(Compact memory data, Mandate memory mandate, address verifyingContract) + internal + view + returns (bytes32) + { bytes32 compactHash = _hashCompact(data, mandate); // hash typed data return keccak256( abi.encodePacked( - "\x19\x01", // backslash is needed to escape the character + '\x19\x01', // backslash is needed to escape the character _domainSeparator(verifyingContract), compactHash ) @@ -99,7 +109,18 @@ abstract contract CreateHash is Test { } function _hashCompact(Compact memory data, Mandate memory mandate) internal view returns (bytes32 compactHash) { - return keccak256(abi.encode(keccak256(bytes(compactWitnessTypeString)), data.arbiter, data.sponsor, data.nonce, data.expires, data.id, data.amount, keccak256(abi.encode(mandate)))); + return keccak256( + abi.encode( + keccak256(bytes(compactWitnessTypeString)), + data.arbiter, + data.sponsor, + data.nonce, + data.expires, + data.id, + data.amount, + keccak256(abi.encode(mandate)) + ) + ); } function _getTypeHash() internal view returns (bytes32) { @@ -107,7 +128,15 @@ abstract contract CreateHash is Test { } function _domainSeparator(address verifyingContract) internal view returns (bytes32) { - return keccak256(abi.encode(keccak256(bytes(EIP712_DOMAIN_TYPE)), keccak256(bytes(name)), keccak256(bytes(version)), block.chainid, verifyingContract)); + return keccak256( + abi.encode( + keccak256(bytes(EIP712_DOMAIN_TYPE)), + keccak256(bytes(name)), + keccak256(bytes(version)), + block.chainid, + verifyingContract + ) + ); } function _signMessage(bytes32 hash_, uint256 signerPK_) internal pure returns (bytes memory) { @@ -115,7 +144,11 @@ abstract contract CreateHash is Test { return abi.encodePacked(r, s, v); } - function _hashAndSign(Compact memory data, Mandate memory mandate, address verifyingContract, uint256 signerPK) internal view returns (bytes memory) { + function _hashAndSign(Compact memory data, Mandate memory mandate, address verifyingContract, uint256 signerPK) + internal + view + returns (bytes memory) + { bytes32 hash = _hashCompact(data, mandate, verifyingContract); bytes memory signature = _signMessage(hash, signerPK); return signature; @@ -178,7 +211,7 @@ abstract contract GaslessCrossChainOrderData is CompactData, CreateHash { Compact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - + gaslessCrossChainOrder = IOriginSettler.GaslessCrossChainOrder({ originSettler: address(erc7683Allocator), user: compact_.sponsor, @@ -187,11 +220,31 @@ abstract contract GaslessCrossChainOrderData is CompactData, CreateHash { openDeadline: uint32(_getClaimExpiration()), fillDeadline: uint32(_getFillExpiration()), orderDataType: erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(), - orderData: abi.encode(compact_.arbiter, compact_.id, compact_.amount, mandate_.chainId, mandate_.tribunal, mandate_.recipient, mandate_.token, mandate_.minimumAmount, mandate_.baselinePriorityFee, mandate_.scalingFactor, mandate_.salt) + orderData: abi.encode( + compact_.arbiter, + compact_.id, + compact_.amount, + mandate_.chainId, + mandate_.tribunal, + mandate_.recipient, + mandate_.token, + mandate_.minimumAmount, + mandate_.baselinePriorityFee, + mandate_.scalingFactor, + mandate_.salt + ) }); } - function _getGaslessCrossChainOrder(address allocator, Compact memory compact_, Mandate memory mandate_, uint256 chainId_, bytes32 orderDataGaslessTypeHash_, address verifyingContract, uint256 signerPK) internal view returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) { + function _getGaslessCrossChainOrder( + address allocator, + Compact memory compact_, + Mandate memory mandate_, + uint256 chainId_, + bytes32 orderDataGaslessTypeHash_, + address verifyingContract, + uint256 signerPK + ) internal view returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) { IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_ = IOriginSettler.GaslessCrossChainOrder({ originSettler: allocator, user: compact_.sponsor, @@ -200,14 +253,29 @@ abstract contract GaslessCrossChainOrderData is CompactData, CreateHash { openDeadline: uint32(compact_.expires), fillDeadline: uint32(mandate_.expires), orderDataType: orderDataGaslessTypeHash_, - orderData: abi.encode(compact_.arbiter, compact_.id, compact_.amount, mandate_.chainId, mandate_.tribunal, mandate_.recipient, mandate_.token, mandate_.minimumAmount, mandate_.baselinePriorityFee, mandate_.scalingFactor, mandate_.salt) + orderData: abi.encode( + compact_.arbiter, + compact_.id, + compact_.amount, + mandate_.chainId, + mandate_.tribunal, + mandate_.recipient, + mandate_.token, + mandate_.minimumAmount, + mandate_.baselinePriorityFee, + mandate_.scalingFactor, + mandate_.salt + ) }); (bytes memory signature_) = _hashAndSign(compact_, mandate_, verifyingContract, signerPK); return (gaslessCrossChainOrder_, signature_); } - function _getGaslessCrossChainOrder() internal returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) { + function _getGaslessCrossChainOrder() + internal + returns (IOriginSettler.GaslessCrossChainOrder memory, bytes memory signature) + { (bytes memory signature_) = _hashAndSign(_getCompact(), _getMandate(), address(compactContract), userPK); return (gaslessCrossChainOrder, signature_); } @@ -221,11 +289,26 @@ abstract contract OnChainCrossChainOrderData is CompactData { Compact memory compact_ = _getCompact(); Mandate memory mandate_ = _getMandate(); - + onchainCrossChainOrder = IOriginSettler.OnchainCrossChainOrder({ fillDeadline: uint32(_getFillExpiration()), orderDataType: erc7683Allocator.ORDERDATA_TYPEHASH(), - orderData: abi.encode(compact_.arbiter, compact_.sponsor, compact_.nonce, compact_.expires, compact_.id, compact_.amount, mandate_.chainId, mandate_.tribunal, mandate_.recipient, mandate_.token, mandate_.minimumAmount, mandate_.baselinePriorityFee, mandate_.scalingFactor, mandate_.salt) + orderData: abi.encode( + compact_.arbiter, + compact_.sponsor, + compact_.nonce, + compact_.expires, + compact_.id, + compact_.amount, + mandate_.chainId, + mandate_.tribunal, + mandate_.recipient, + mandate_.token, + mandate_.minimumAmount, + mandate_.baselinePriorityFee, + mandate_.scalingFactor, + mandate_.salt + ) }); } @@ -233,11 +316,30 @@ abstract contract OnChainCrossChainOrderData is CompactData { return onchainCrossChainOrder; } - function _getOnChainCrossChainOrder(Compact memory compact_, Mandate memory mandate_, bytes32 orderDataType_) internal pure returns (IOriginSettler.OnchainCrossChainOrder memory) { + function _getOnChainCrossChainOrder(Compact memory compact_, Mandate memory mandate_, bytes32 orderDataType_) + internal + pure + returns (IOriginSettler.OnchainCrossChainOrder memory) + { IOriginSettler.OnchainCrossChainOrder memory onchainCrossChainOrder_ = IOriginSettler.OnchainCrossChainOrder({ fillDeadline: uint32(mandate_.expires), orderDataType: orderDataType_, - orderData: abi.encode(compact_.arbiter, compact_.sponsor, compact_.nonce, compact_.expires, compact_.id, compact_.amount, mandate_.chainId, mandate_.tribunal, mandate_.recipient, mandate_.token, mandate_.minimumAmount, mandate_.baselinePriorityFee, mandate_.scalingFactor, mandate_.salt) + orderData: abi.encode( + compact_.arbiter, + compact_.sponsor, + compact_.nonce, + compact_.expires, + compact_.id, + compact_.amount, + mandate_.chainId, + mandate_.tribunal, + mandate_.recipient, + mandate_.token, + mandate_.minimumAmount, + mandate_.baselinePriorityFee, + mandate_.scalingFactor, + mandate_.salt + ) }); return onchainCrossChainOrder_; } @@ -251,7 +353,9 @@ abstract contract Deposited is MocksSetup { usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit(address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); vm.stopPrank(); } @@ -260,32 +364,52 @@ abstract contract Deposited is MocksSetup { contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { function test_revert_InvalidOrderDataType() public { // Order data type is invalid - bytes32 falseOrderDataType = keccak256("false"); - + bytes32 falseOrderDataType = keccak256('false'); + vm.prank(user); - vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidOrderDataType.selector, falseOrderDataType, erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH())); - (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = _getGaslessCrossChainOrder(); + vm.expectRevert( + abi.encodeWithSelector( + IERC7683Allocator.InvalidOrderDataType.selector, + falseOrderDataType, + erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH() + ) + ); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = + _getGaslessCrossChainOrder(); falseGaslessCrossChainOrder.orderDataType = falseOrderDataType; - erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ""); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); } function test_revert_InvalidDecoding() public { // Decoding fails because of additional data vm.prank(user); vm.expectRevert(); - (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = _getGaslessCrossChainOrder(); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = + _getGaslessCrossChainOrder(); falseGaslessCrossChainOrder.orderData = abi.encode(falseGaslessCrossChainOrder.orderData, uint8(1)); - erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ""); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); } function test_revert_InvalidOriginSettler() public { // Origin settler is not the allocator - address falseOriginSettler = makeAddr("falseOriginSettler"); - vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidOriginSettler.selector, falseOriginSettler, address(erc7683Allocator))); - (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = - _getGaslessCrossChainOrder(falseOriginSettler, _getCompact(), _getMandate(), block.chainid, ORDERDATA_GASLESS_TYPEHASH, address(erc7683Allocator), userPK); + address falseOriginSettler = makeAddr('falseOriginSettler'); + vm.expectRevert( + abi.encodeWithSelector( + IERC7683Allocator.InvalidOriginSettler.selector, falseOriginSettler, address(erc7683Allocator) + ) + ); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = + _getGaslessCrossChainOrder( + falseOriginSettler, + _getCompact(), + _getMandate(), + block.chainid, + ORDERDATA_GASLESS_TYPEHASH, + address(erc7683Allocator), + userPK + ); vm.prank(user); - erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ""); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); } function test_revert_InvalidNonce() public { @@ -293,10 +417,18 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { Compact memory compact_ = _getCompact(); compact_.nonce = uint256(bytes32(abi.encodePacked(uint96(1), attacker))); vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, compact_.nonce)); - (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = - _getGaslessCrossChainOrder(address(erc7683Allocator), compact_, _getMandate(), block.chainid, ORDERDATA_GASLESS_TYPEHASH, address(erc7683Allocator), userPK); + (IOriginSettler.GaslessCrossChainOrder memory falseGaslessCrossChainOrder, bytes memory signature) = + _getGaslessCrossChainOrder( + address(erc7683Allocator), + compact_, + _getMandate(), + block.chainid, + ORDERDATA_GASLESS_TYPEHASH, + address(erc7683Allocator), + userPK + ); vm.prank(user); - erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ""); + erc7683Allocator.openFor(falseGaslessCrossChainOrder, signature, ''); } function test_revert_InvalidSponsorSignature() public { @@ -306,15 +438,25 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit(address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); vm.stopPrank(); // Create a malicious signature - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = - _getGaslessCrossChainOrder(address(erc7683Allocator), _getCompact(), _getMandate(), block.chainid, ORDERDATA_GASLESS_TYPEHASH, address(compactContract), attackerPK); + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder( + address(erc7683Allocator), + _getCompact(), + _getMandate(), + block.chainid, + ORDERDATA_GASLESS_TYPEHASH, + address(compactContract), + attackerPK + ); vm.prank(user); vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidSignature.selector, user, attacker)); - erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ""); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); } function test_successful() public { @@ -322,11 +464,13 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit(address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); vm.stopPrank(); - - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = _getGaslessCrossChainOrder(); + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder(); IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); @@ -339,14 +483,14 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { minReceived[0] = IOriginSettler.Output({ token: bytes32(uint256(uint160(address(usdc)))), amount: defaultAmount, - recipient: "", + recipient: '', chainId: block.chainid }); Claim memory claim = Claim({ chainId: block.chainid, compact: _getCompact(), sponsorSignature: sponsorSignature, - allocatorSignature: "" + allocatorSignature: '' }); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, @@ -367,7 +511,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { vm.prank(user); vm.expectEmit(true, false, false, true, address(erc7683Allocator)); emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); - erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ""); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); } function test_revert_NonceAlreadyInUse() public { @@ -377,32 +521,41 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit(address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); vm.stopPrank(); // use the nonce once - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = _getGaslessCrossChainOrder(); + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder(); vm.prank(user); - erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ""); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); // try to use the nonce again - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder2, bytes memory sponsorSignature2) = _getGaslessCrossChainOrder(); + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder2, bytes memory sponsorSignature2) = + _getGaslessCrossChainOrder(); vm.prank(user); vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.NonceAlreadyInUse.selector, defaultNonce)); - erc7683Allocator.openFor(gaslessCrossChainOrder2, sponsorSignature2, ""); - + erc7683Allocator.openFor(gaslessCrossChainOrder2, sponsorSignature2, ''); } } contract ERC7683Allocator_open is OnChainCrossChainOrderData, CreateHash { function test_revert_InvalidOrderDataType() public { // Order data type is invalid - bytes32 falseOrderDataType = keccak256("false"); + bytes32 falseOrderDataType = keccak256('false'); IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_ = _getOnChainCrossChainOrder(); onChainCrossChainOrder_.orderDataType = falseOrderDataType; vm.prank(user); - vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidOrderDataType.selector, falseOrderDataType, erc7683Allocator.ORDERDATA_TYPEHASH())); + vm.expectRevert( + abi.encodeWithSelector( + IERC7683Allocator.InvalidOrderDataType.selector, + falseOrderDataType, + erc7683Allocator.ORDERDATA_TYPEHASH() + ) + ); erc7683Allocator.open(onChainCrossChainOrder_); } @@ -419,13 +572,14 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData, CreateHash { vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit(address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); // we do NOT register a claim vm.stopPrank(); - (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); Compact memory compact_ = _getCompact(); @@ -442,7 +596,9 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData, CreateHash { vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit(address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); // we register a claim with a expiration that is too short Compact memory compact_ = _getCompact(); @@ -453,8 +609,7 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData, CreateHash { compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp - 1); vm.stopPrank(); - (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = - _getOnChainCrossChainOrder(); + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); vm.prank(user); vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidRegistration.selector, user, claimHash)); @@ -466,7 +621,9 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData, CreateHash { vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit(address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); // register a claim Compact memory compact_ = _getCompact(); @@ -478,7 +635,6 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData, CreateHash { vm.stopPrank(); - (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); @@ -492,15 +648,11 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData, CreateHash { minReceived[0] = IOriginSettler.Output({ token: bytes32(uint256(uint160(address(usdc)))), amount: defaultAmount, - recipient: "", + recipient: '', chainId: block.chainid }); - Claim memory claim = Claim({ - chainId: block.chainid, - compact: _getCompact(), - sponsorSignature: "", - allocatorSignature: "" - }); + Claim memory claim = + Claim({chainId: block.chainid, compact: _getCompact(), sponsorSignature: '', allocatorSignature: ''}); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), @@ -530,7 +682,9 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Create vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit(address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); // register a claim Compact memory compact_ = _getCompact(); @@ -540,7 +694,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Create bytes32 typeHash = _getTypeHash(); compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); - address filler = makeAddr("filler"); + address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); @@ -550,8 +704,8 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Create // claim should be fail, because we mess with the nonce ClaimWithWitness memory claim = ClaimWithWitness({ - allocatorSignature: "", - sponsorSignature: "", + allocatorSignature: '', + sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, @@ -575,7 +729,9 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Create vm.startPrank(user); usdc.mint(user, defaultAmount); usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit(address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); // register a claim Compact memory compact_ = _getCompact(); @@ -585,11 +741,10 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Create bytes32 typeHash = _getTypeHash(); compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); - address filler = makeAddr("filler"); + address filler = makeAddr('filler'); vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); - // we open the order and lock the tokens (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); erc7683Allocator.open(onChainCrossChainOrder_); @@ -597,8 +752,8 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Create // claim should be successful ClaimWithWitness memory claim = ClaimWithWitness({ - allocatorSignature: "", - sponsorSignature: "", + allocatorSignature: '', + sponsorSignature: '', sponsor: user, nonce: defaultNonce, expires: compact_.expires, @@ -619,7 +774,6 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Create contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { function test_resolve_successful() public { - // WITH THE CURRENT ERC7683 DESIGN, THE SPONSOR SIGNATURE IS NOT PROVIDED TO THE RESOLVE FUNCTION // WHILE THE ResolvedCrossChainOrder WITHOUT THE SIGNATURE COULD STILL BE USED TO SIMULATE THE FILL, // ACTUALLY USING THIS DATA WOULD RESULT IN A LOSS OF THE REWARD TOKENS FOR THE FILLER. @@ -627,7 +781,8 @@ contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { // THE CURRENT ALTERNATIVE WOULD BE HAVE THE INPUT SIGNATURE BEING LEFT EMPTY AND INSTEAD BE PROVIDED IN THE THE orderData OF THE GaslessCrossChainOrderData. // THIS IS BOTH NOT IDEAL, SO CURRENTLY CHECKING FOR A SOLUTION. - (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/) = _getGaslessCrossChainOrder(); + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, /*bytes memory sponsorSignature*/ ) = + _getGaslessCrossChainOrder(); IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); @@ -640,14 +795,14 @@ contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { minReceived[0] = IOriginSettler.Output({ token: bytes32(uint256(uint160(address(usdc)))), amount: defaultAmount, - recipient: "", + recipient: '', chainId: block.chainid }); Claim memory claim = Claim({ chainId: block.chainid, compact: _getCompact(), - sponsorSignature: "", // sponsorSignature, // THE SIGNATURE MUST BE ADDED MANUALLY BY THE FILLER WITH THE CURRENT SYSTEM, BEFORE FILLING THE ORDER ON THE TARGET CHAIN - allocatorSignature: "" + sponsorSignature: '', // sponsorSignature, // THE SIGNATURE MUST BE ADDED MANUALLY BY THE FILLER WITH THE CURRENT SYSTEM, BEFORE FILLING THE ORDER ON THE TARGET CHAIN + allocatorSignature: '' }); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, @@ -665,8 +820,9 @@ contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { minReceived: minReceived, fillInstructions: fillInstructions }); - IOriginSettler.ResolvedCrossChainOrder memory resolved = erc7683Allocator.resolveFor(gaslessCrossChainOrder_, ""); - assertEq(resolved.user, resolvedCrossChainOrder.user); + IOriginSettler.ResolvedCrossChainOrder memory resolved = + erc7683Allocator.resolveFor(gaslessCrossChainOrder_, ''); + assertEq(resolved.user, resolvedCrossChainOrder.user); assertEq(resolved.originChainId, resolvedCrossChainOrder.originChainId); assertEq(resolved.openDeadline, resolvedCrossChainOrder.openDeadline); assertEq(resolved.fillDeadline, resolvedCrossChainOrder.fillDeadline); @@ -682,8 +838,14 @@ contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { assertEq(resolved.minReceived[0].recipient, resolvedCrossChainOrder.minReceived[0].recipient); assertEq(resolved.minReceived[0].chainId, resolvedCrossChainOrder.minReceived[0].chainId); assertEq(resolved.fillInstructions.length, resolvedCrossChainOrder.fillInstructions.length); - assertEq(resolved.fillInstructions[0].destinationChainId, resolvedCrossChainOrder.fillInstructions[0].destinationChainId); - assertEq(resolved.fillInstructions[0].destinationSettler, resolvedCrossChainOrder.fillInstructions[0].destinationSettler); + assertEq( + resolved.fillInstructions[0].destinationChainId, + resolvedCrossChainOrder.fillInstructions[0].destinationChainId + ); + assertEq( + resolved.fillInstructions[0].destinationSettler, + resolvedCrossChainOrder.fillInstructions[0].destinationSettler + ); assertEq(resolved.fillInstructions[0].originData, resolvedCrossChainOrder.fillInstructions[0].originData); } } @@ -703,15 +865,11 @@ contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { minReceived[0] = IOriginSettler.Output({ token: bytes32(uint256(uint160(address(usdc)))), amount: defaultAmount, - recipient: "", + recipient: '', chainId: block.chainid }); - Claim memory claim = Claim({ - chainId: block.chainid, - compact: _getCompact(), - sponsorSignature: "", - allocatorSignature: "" - }); + Claim memory claim = + Claim({chainId: block.chainid, compact: _getCompact(), sponsorSignature: '', allocatorSignature: ''}); fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), @@ -729,7 +887,7 @@ contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { fillInstructions: fillInstructions }); IOriginSettler.ResolvedCrossChainOrder memory resolved = erc7683Allocator.resolve(onChainCrossChainOrder_); - assertEq(resolved.user, resolvedCrossChainOrder.user); + assertEq(resolved.user, resolvedCrossChainOrder.user); assertEq(resolved.originChainId, resolvedCrossChainOrder.originChainId); assertEq(resolved.openDeadline, resolvedCrossChainOrder.openDeadline); assertEq(resolved.fillDeadline, resolvedCrossChainOrder.fillDeadline); @@ -745,8 +903,14 @@ contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { assertEq(resolved.minReceived[0].recipient, resolvedCrossChainOrder.minReceived[0].recipient); assertEq(resolved.minReceived[0].chainId, resolvedCrossChainOrder.minReceived[0].chainId); assertEq(resolved.fillInstructions.length, resolvedCrossChainOrder.fillInstructions.length); - assertEq(resolved.fillInstructions[0].destinationChainId, resolvedCrossChainOrder.fillInstructions[0].destinationChainId); - assertEq(resolved.fillInstructions[0].destinationSettler, resolvedCrossChainOrder.fillInstructions[0].destinationSettler); + assertEq( + resolved.fillInstructions[0].destinationChainId, + resolvedCrossChainOrder.fillInstructions[0].destinationChainId + ); + assertEq( + resolved.fillInstructions[0].destinationSettler, + resolvedCrossChainOrder.fillInstructions[0].destinationSettler + ); assertEq(resolved.fillInstructions[0].originData, resolvedCrossChainOrder.fillInstructions[0].originData); } -} \ No newline at end of file +} diff --git a/test/ServerAllocator.t.sol b/test/ServerAllocator.t.sol index 250d5ba..0200441 100644 --- a/test/ServerAllocator.t.sol +++ b/test/ServerAllocator.t.sol @@ -2,18 +2,20 @@ pragma solidity ^0.8.27; -import { Test } from "forge-std/Test.sol"; -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import { ServerAllocator } from "src/allocators/ServerAllocator.sol"; -import { IServerAllocator } from "src/interfaces/IServerAllocator.sol"; -import { Compact, COMPACT_TYPEHASH } from "@uniswap/the-compact/types/EIP712Types.sol"; -import { TheCompactMock } from "src/test/TheCompactMock.sol"; -import { ERC20Mock } from "src/test/ERC20Mock.sol"; -import { console } from "forge-std/console.sol"; -import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; + +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; +import {COMPACT_TYPEHASH, Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {Test} from 'forge-std/Test.sol'; + +import {console} from 'forge-std/console.sol'; +import {ServerAllocator} from 'src/allocators/ServerAllocator.sol'; +import {IServerAllocator} from 'src/interfaces/IServerAllocator.sol'; +import {ERC20Mock} from 'src/test/ERC20Mock.sol'; +import {TheCompactMock} from 'src/test/TheCompactMock.sol'; abstract contract MocksSetup is Test { - address owner = makeAddr("owner"); + address owner = makeAddr('owner'); address signer; uint256 signerPK; address attacker; @@ -24,12 +26,12 @@ abstract contract MocksSetup is Test { uint256 usdcId; function setUp() public virtual { - usdc = new ERC20Mock("USDC", "USDC"); + usdc = new ERC20Mock('USDC', 'USDC'); compactContract = new TheCompactMock(); serverAllocator = new ServerAllocator(owner, address(compactContract)); usdcId = compactContract.getTokenId(address(usdc), address(serverAllocator)); - (signer, signerPK) = makeAddrAndKey("signer"); - (attacker, attackerPK) = makeAddrAndKey("attacker"); + (signer, signerPK) = makeAddrAndKey('signer'); + (attacker, attackerPK) = makeAddrAndKey('attacker'); } } @@ -47,13 +49,14 @@ abstract contract CreateHash is Test { } // stringified types - string EIP712_DOMAIN_TYPE = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; // Hashed inside the funcion + string EIP712_DOMAIN_TYPE = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'; // Hashed inside the funcion // string ALLOCATOR_TYPE = "Allocator(bytes32 hash)"; // Hashed inside the funcion - string REGISTER_ATTESTATION_TYPE = "RegisterAttestation(address signer,bytes32 attestationHash,uint256 expiration,uint256 nonce)"; // Hashed inside the funcion - string NONCE_CONSUMPTION_TYPE = "NonceConsumption(address signer,uint256[] nonces,bytes32[] attestations)"; // Hashed inside the funcion + string REGISTER_ATTESTATION_TYPE = + 'RegisterAttestation(address signer,bytes32 attestationHash,uint256 expiration,uint256 nonce)'; // Hashed inside the funcion + string NONCE_CONSUMPTION_TYPE = 'NonceConsumption(address signer,uint256[] nonces,bytes32[] attestations)'; // Hashed inside the funcion // EIP712 domain type - string name = "Allocator"; - string version = "1"; + string name = 'Allocator'; + string version = '1'; // function _hashAllocator(Allocator memory data, address verifyingContract) internal view returns (bytes32) { // // hash typed data @@ -70,36 +73,66 @@ abstract contract CreateHash is Test { // hash typed data return keccak256( abi.encodePacked( - "\x19\x01", // backslash is needed to escape the character + '\x19\x01', // backslash is needed to escape the character _domainSeparator(verifyingContract), - keccak256(abi.encode(COMPACT_TYPEHASH, data.arbiter, data.sponsor, data.nonce, data.expires, data.id, data.amount)) + keccak256( + abi.encode( + COMPACT_TYPEHASH, data.arbiter, data.sponsor, data.nonce, data.expires, data.id, data.amount + ) + ) ) ); } - function _hashRegisterAttest(ServerAllocator.RegisterAttestation memory data, address verifyingContract) internal view returns (bytes32) { + function _hashRegisterAttest(ServerAllocator.RegisterAttestation memory data, address verifyingContract) + internal + view + returns (bytes32) + { return keccak256( abi.encodePacked( - "\x19\x01", // backslash is needed to escape the character + '\x19\x01', // backslash is needed to escape the character _domainSeparator(verifyingContract), - keccak256(abi.encode(keccak256(bytes(REGISTER_ATTESTATION_TYPE)), data.signer, data.attestationHash, data.expiration, data.nonce)) + keccak256( + abi.encode( + keccak256(bytes(REGISTER_ATTESTATION_TYPE)), + data.signer, + data.attestationHash, + data.expiration, + data.nonce + ) + ) ) ); } - function _hashNonceConsumption(ServerAllocator.NonceConsumption memory data, address verifyingContract) internal view returns (bytes32) { + function _hashNonceConsumption(ServerAllocator.NonceConsumption memory data, address verifyingContract) + internal + view + returns (bytes32) + { // hash typed data return keccak256( abi.encodePacked( - "\x19\x01", // backslash is needed to escape the character + '\x19\x01', // backslash is needed to escape the character _domainSeparator(verifyingContract), - keccak256(abi.encode(keccak256(bytes(NONCE_CONSUMPTION_TYPE)), data.signer, data.nonces, data.attestations)) + keccak256( + abi.encode(keccak256(bytes(NONCE_CONSUMPTION_TYPE)), data.signer, data.nonces, data.attestations) + ) ) ); } function _domainSeparator(address verifyingContract) internal view returns (bytes32) { - return keccak256(abi.encode(keccak256(bytes(EIP712_DOMAIN_TYPE)), keccak256(bytes(name)), keccak256(bytes(version)), block.chainid, verifyingContract)); + return keccak256( + abi.encode( + keccak256(bytes(EIP712_DOMAIN_TYPE)), + keccak256(bytes(name)), + keccak256(bytes(version)), + block.chainid, + verifyingContract + ) + ); } function _signMessage(bytes32 hash_, uint256 signerPK_) internal pure returns (bytes memory) { @@ -233,7 +266,8 @@ contract ServerAllocator_Attest is SignerSet { bytes32 attest = createAttest(signer, usdcId, 100); uint256 expiration = vm.getBlockTimestamp() + 1 days; - IServerAllocator.RegisterAttestation memory attestData = IServerAllocator.RegisterAttestation(signer, attest, expiration, 0); + IServerAllocator.RegisterAttestation memory attestData = + IServerAllocator.RegisterAttestation(signer, attest, expiration, 0); bytes32 message = _hashRegisterAttest(attestData, address(serverAllocator)); bytes memory signature = _signMessage(message, attackerPK); @@ -246,7 +280,8 @@ contract ServerAllocator_Attest is SignerSet { bytes32 attest = createAttest(signer, usdcId, 100); uint256 expiration = vm.getBlockTimestamp() + 1 days; - IServerAllocator.RegisterAttestation memory attestData = IServerAllocator.RegisterAttestation(signer, attest, expiration, 0); + IServerAllocator.RegisterAttestation memory attestData = + IServerAllocator.RegisterAttestation(signer, attest, expiration, 0); bytes32 message = _hashRegisterAttest(attestData, address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); @@ -260,7 +295,8 @@ contract ServerAllocator_Attest is SignerSet { bytes32 attest = createAttest(signer, usdcId, 100); uint256 expiration = vm.getBlockTimestamp() + 1 days; - IServerAllocator.RegisterAttestation memory attestData = IServerAllocator.RegisterAttestation(signer, attest, expiration, 0); + IServerAllocator.RegisterAttestation memory attestData = + IServerAllocator.RegisterAttestation(signer, attest, expiration, 0); bytes32 message = _hashRegisterAttest(attestData, address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); @@ -300,13 +336,21 @@ contract ServerAllocator_Attest is SignerSet { vm.assume(caller_ != address(compactContract)); vm.prank(caller_); - vm.expectRevert(abi.encodeWithSelector(IServerAllocator.InvalidCaller.selector, caller_, address(compactContract))); + vm.expectRevert( + abi.encodeWithSelector(IServerAllocator.InvalidCaller.selector, caller_, address(compactContract)) + ); serverAllocator.attest(caller_, signer, attacker, usdcId, 100); } - function test_fuzz_attest_notRegistered(address operator_, address from_, address to_, uint256 id_, uint256 amount_) public { + function test_fuzz_attest_notRegistered(address operator_, address from_, address to_, uint256 id_, uint256 amount_) + public + { vm.prank(address(compactContract)); - vm.expectRevert(abi.encodeWithSelector(IServerAllocator.UnregisteredAttestation.selector, keccak256(abi.encode(from_, id_, amount_)))); + vm.expectRevert( + abi.encodeWithSelector( + IServerAllocator.UnregisteredAttestation.selector, keccak256(abi.encode(from_, id_, amount_)) + ) + ); serverAllocator.attest(operator_, from_, to_, id_, amount_); } @@ -325,10 +369,12 @@ contract ServerAllocator_Attest is SignerSet { // check attest vm.prank(address(compactContract)); vm.expectRevert(abi.encodeWithSelector(IServerAllocator.ExpiredAttestations.selector, attest)); - serverAllocator.attest(signer, attacker, makeAddr("to"), usdcId, amount_); + serverAllocator.attest(signer, attacker, makeAddr('to'), usdcId, amount_); } - function test_fuzz_attest_successful(address operator_, address from_, address to_, uint256 id_, uint256 amount_) public { + function test_fuzz_attest_successful(address operator_, address from_, address to_, uint256 id_, uint256 amount_) + public + { bytes32 attest = createAttest(from_, id_, amount_); uint256 expiration = vm.getBlockTimestamp(); @@ -424,21 +470,29 @@ contract ServerAllocator_Consume is SignerSet { } function test_consumeViaSignature_requiresNoncesAndAttestsToBeOfSameLength() public { - bytes32 message = _hashNonceConsumption(IServerAllocator.NonceConsumption(signer, new uint256[](0), new bytes32[](1)), address(serverAllocator)); + bytes32 message = _hashNonceConsumption( + IServerAllocator.NonceConsumption(signer, new uint256[](0), new bytes32[](1)), address(serverAllocator) + ); bytes memory signature = _signMessage(message, signerPK); vm.prank(signer); vm.expectRevert(abi.encodeWithSelector(IServerAllocator.InvalidInput.selector)); - serverAllocator.consumeViaSignature(IServerAllocator.NonceConsumption(signer, new uint256[](0), new bytes32[](1)), signature); + serverAllocator.consumeViaSignature( + IServerAllocator.NonceConsumption(signer, new uint256[](0), new bytes32[](1)), signature + ); } function test_consumeViaSignature_requireValidSignature() public { - bytes32 message = _hashNonceConsumption(IServerAllocator.NonceConsumption(signer, new uint256[](1), new bytes32[](1)), address(serverAllocator)); + bytes32 message = _hashNonceConsumption( + IServerAllocator.NonceConsumption(signer, new uint256[](1), new bytes32[](1)), address(serverAllocator) + ); bytes memory signature = _signMessage(message, attackerPK); vm.prank(signer); vm.expectRevert(abi.encodeWithSelector(IServerAllocator.InvalidSignature.selector, signature, attacker)); - serverAllocator.consumeViaSignature(IServerAllocator.NonceConsumption(signer, new uint256[](1), new bytes32[](1)), signature); + serverAllocator.consumeViaSignature( + IServerAllocator.NonceConsumption(signer, new uint256[](1), new bytes32[](1)), signature + ); } function test_consumeViaSignature_successfulWithoutAttests() public { @@ -447,13 +501,17 @@ contract ServerAllocator_Consume is SignerSet { nonces[1] = 2; nonces[2] = 3; - bytes32 message = _hashNonceConsumption(IServerAllocator.NonceConsumption(signer, nonces, new bytes32[](3)), address(serverAllocator)); + bytes32 message = _hashNonceConsumption( + IServerAllocator.NonceConsumption(signer, nonces, new bytes32[](3)), address(serverAllocator) + ); bytes memory signature = _signMessage(message, signerPK); vm.prank(attacker); vm.expectEmit(address(serverAllocator)); emit IServerAllocator.NoncesConsumed(nonces); - serverAllocator.consumeViaSignature(IServerAllocator.NonceConsumption(signer, nonces, new bytes32[](3)), signature); + serverAllocator.consumeViaSignature( + IServerAllocator.NonceConsumption(signer, nonces, new bytes32[](3)), signature + ); } function test_consumeViaSignature_successfulWithAttests() public { @@ -478,7 +536,8 @@ contract ServerAllocator_Consume is SignerSet { assertEq(serverAllocator.checkAttestationExpirations(attests[1])[0], vm.getBlockTimestamp()); assertEq(serverAllocator.checkAttestationExpirations(attests[2])[0], vm.getBlockTimestamp()); - bytes32 message = _hashNonceConsumption(IServerAllocator.NonceConsumption(signer, nonces, attests), address(serverAllocator)); + bytes32 message = + _hashNonceConsumption(IServerAllocator.NonceConsumption(signer, nonces, attests), address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); vm.prank(attacker); @@ -503,7 +562,8 @@ contract ServerAllocator_Consume is SignerSet { contract ServerAllocator_isValidSignature is SignerSet { function test_isValidSignature_revertInvalidSig() public { - bytes32 message = _hashCompact(Compact(signer, signer, 0, vm.getBlockTimestamp(), usdcId, 100), address(serverAllocator)); + bytes32 message = + _hashCompact(Compact(signer, signer, 0, vm.getBlockTimestamp(), usdcId, 100), address(serverAllocator)); bytes memory signature = _signMessage(message, attackerPK); vm.prank(attacker); @@ -512,7 +572,8 @@ contract ServerAllocator_isValidSignature is SignerSet { } function test_isValidSignature_successful() public { - bytes32 message = _hashCompact(Compact(signer, signer, 0, vm.getBlockTimestamp(), usdcId, 100), address(serverAllocator)); + bytes32 message = + _hashCompact(Compact(signer, signer, 0, vm.getBlockTimestamp(), usdcId, 100), address(serverAllocator)); bytes memory signature = _signMessage(message, signerPK); vm.prank(attacker); diff --git a/test/SimpleAllocator.t.sol b/test/SimpleAllocator.t.sol index 1472dda..1a2ff74 100644 --- a/test/SimpleAllocator.t.sol +++ b/test/SimpleAllocator.t.sol @@ -2,17 +2,18 @@ pragma solidity ^0.8.27; -import { Test } from "forge-std/Test.sol"; -import { SimpleAllocator } from "src/allocators/SimpleAllocator.sol"; -import { ISimpleAllocator } from "src/interfaces/ISimpleAllocator.sol"; -import { ITheCompact } from "@uniswap/the-compact/interfaces/ITheCompact.sol"; -import { Compact, COMPACT_TYPEHASH } from "@uniswap/the-compact/types/EIP712Types.sol"; -import { ForcedWithdrawalStatus } from "@uniswap/the-compact/types/ForcedWithdrawalStatus.sol"; -import { TheCompactMock } from "src/test/TheCompactMock.sol"; -import { ERC20Mock } from "src/test/ERC20Mock.sol"; -import { ERC6909 } from "@solady/tokens/ERC6909.sol"; -import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; -import { console } from "forge-std/console.sol"; +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; +import {ERC6909} from '@solady/tokens/ERC6909.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; +import {COMPACT_TYPEHASH, Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {ForcedWithdrawalStatus} from '@uniswap/the-compact/types/ForcedWithdrawalStatus.sol'; +import {Test} from 'forge-std/Test.sol'; + +import {console} from 'forge-std/console.sol'; +import {SimpleAllocator} from 'src/allocators/SimpleAllocator.sol'; +import {ISimpleAllocator} from 'src/interfaces/ISimpleAllocator.sol'; +import {ERC20Mock} from 'src/test/ERC20Mock.sol'; +import {TheCompactMock} from 'src/test/TheCompactMock.sol'; abstract contract MocksSetup is Test { address user; @@ -31,13 +32,13 @@ abstract contract MocksSetup is Test { uint256 defaultExpiration; function setUp() public virtual { - arbiter = makeAddr("arbiter"); - usdc = new ERC20Mock("USDC", "USDC"); + arbiter = makeAddr('arbiter'); + usdc = new ERC20Mock('USDC', 'USDC'); compactContract = new TheCompactMock(); simpleAllocator = new SimpleAllocator(address(compactContract), 5, 100); usdcId = compactContract.getTokenId(address(usdc), address(simpleAllocator)); - (user, userPK) = makeAddrAndKey("user"); - (attacker, attackerPK) = makeAddrAndKey("attacker"); + (user, userPK) = makeAddrAndKey('user'); + (attacker, attackerPK) = makeAddrAndKey('attacker'); } } @@ -47,24 +48,36 @@ abstract contract CreateHash is Test { } // stringified types - string EIP712_DOMAIN_TYPE = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; // Hashed inside the funcion + string EIP712_DOMAIN_TYPE = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'; // Hashed inside the funcion // EIP712 domain type - string name = "The Compact"; - string version = "0"; + string name = 'The Compact'; + string version = '0'; function _hashCompact(Compact memory data, address verifyingContract) internal view returns (bytes32) { // hash typed data return keccak256( abi.encodePacked( - "\x19\x01", // backslash is needed to escape the character + '\x19\x01', // backslash is needed to escape the character _domainSeparator(verifyingContract), - keccak256(abi.encode(COMPACT_TYPEHASH, data.arbiter, data.sponsor, data.nonce, data.expires, data.id, data.amount)) + keccak256( + abi.encode( + COMPACT_TYPEHASH, data.arbiter, data.sponsor, data.nonce, data.expires, data.id, data.amount + ) + ) ) ); } function _domainSeparator(address verifyingContract) internal view returns (bytes32) { - return keccak256(abi.encode(keccak256(bytes(EIP712_DOMAIN_TYPE)), keccak256(bytes(name)), keccak256(bytes(version)), block.chainid, verifyingContract)); + return keccak256( + abi.encode( + keccak256(bytes(EIP712_DOMAIN_TYPE)), + keccak256(bytes(name)), + keccak256(bytes(version)), + block.chainid, + verifyingContract + ) + ); } function _signMessage(bytes32 hash_, uint256 signerPK_) internal pure returns (bytes memory) { @@ -94,7 +107,16 @@ abstract contract Locked is Deposited { vm.startPrank(user); defaultExpiration = vm.getBlockTimestamp() + defaultResetPeriod; - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration, amount: defaultAmount })); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + }) + ); vm.stopPrank(); } @@ -104,7 +126,16 @@ contract SimpleAllocator_Lock is MocksSetup { function test_revert_InvalidCaller() public { vm.prank(user); vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidCaller.selector, user, attacker)); - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: attacker, nonce: 1, id: usdcId, expires: block.timestamp + 1, amount: 1000 })); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: attacker, + nonce: 1, + id: usdcId, + expires: block.timestamp + 1, + amount: 1000 + }) + ); } function test_revert_ClaimActive() public { @@ -116,12 +147,30 @@ contract SimpleAllocator_Lock is MocksSetup { compactContract.deposit(address(usdc), address(simpleAllocator), defaultAmount); // Successfully locked - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: block.timestamp + defaultResetPeriod, amount: defaultAmount })); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: block.timestamp + defaultResetPeriod, + amount: defaultAmount + }) + ); vm.warp(block.timestamp + defaultResetPeriod - 1); vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.ClaimActive.selector, user)); - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce + 1, id: usdcId, expires: block.timestamp + defaultResetPeriod, amount: defaultAmount })); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce + 1, + id: usdcId, + expires: block.timestamp + defaultResetPeriod, + amount: defaultAmount + }) + ); } function test_revert_InvalidExpiration_tooShort(uint128 delay_) public { @@ -129,7 +178,16 @@ contract SimpleAllocator_Lock is MocksSetup { uint256 expiration = vm.getBlockTimestamp() + delay_; vm.prank(user); vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidExpiration.selector, expiration)); - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: 1, id: usdcId, expires: vm.getBlockTimestamp() + delay_, amount: 1000 })); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: 1, + id: usdcId, + expires: vm.getBlockTimestamp() + delay_, + amount: 1000 + }) + ); } function test_revert_InvalidExpiration_tooLong(uint128 delay_) public { @@ -137,7 +195,16 @@ contract SimpleAllocator_Lock is MocksSetup { uint256 expiration = vm.getBlockTimestamp() + delay_; vm.prank(user); vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidExpiration.selector, expiration)); - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: 1, id: usdcId, expires: vm.getBlockTimestamp() + delay_, amount: 1000 })); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: 1, + id: usdcId, + expires: vm.getBlockTimestamp() + delay_, + amount: 1000 + }) + ); } function test_revert_ForceWithdrawalAvailable_ExpirationLongerThenResetPeriod(uint8 delay_) public { @@ -148,8 +215,12 @@ contract SimpleAllocator_Lock is MocksSetup { uint256 expiration = vm.getBlockTimestamp() + delay_; uint256 maxExpiration = vm.getBlockTimestamp() + defaultResetPeriod; vm.prank(user); - vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.ForceWithdrawalAvailable.selector, expiration, maxExpiration)); - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: 1, id: usdcId, expires: expiration, amount: 1000 })); + vm.expectRevert( + abi.encodeWithSelector(ISimpleAllocator.ForceWithdrawalAvailable.selector, expiration, maxExpiration) + ); + simpleAllocator.lock( + Compact({arbiter: arbiter, sponsor: user, nonce: 1, id: usdcId, expires: expiration, amount: 1000}) + ); } function test_revert_ForceWithdrawalAvailable_ScheduledForceWithdrawal() public { @@ -166,8 +237,12 @@ contract SimpleAllocator_Lock is MocksSetup { assertEq(status == ForcedWithdrawalStatus.Enabled, true); assertEq(expires, expiration - 1); - vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.ForceWithdrawalAvailable.selector, expiration, expiration - 1)); - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: 1, id: usdcId, expires: expiration, amount: 1000 })); + vm.expectRevert( + abi.encodeWithSelector(ISimpleAllocator.ForceWithdrawalAvailable.selector, expiration, expiration - 1) + ); + simpleAllocator.lock( + Compact({arbiter: arbiter, sponsor: user, nonce: 1, id: usdcId, expires: expiration, amount: 1000}) + ); } function test_revert_ForceWithdrawalAvailable_ActiveForceWithdrawal() public { @@ -185,8 +260,14 @@ contract SimpleAllocator_Lock is MocksSetup { assertEq(status == ForcedWithdrawalStatus.Enabled, true); assertEq(expires, forceWithdrawalTimestamp); - vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.ForceWithdrawalAvailable.selector, expiration, forceWithdrawalTimestamp)); - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: 1, id: usdcId, expires: expiration, amount: 1000 })); + vm.expectRevert( + abi.encodeWithSelector( + ISimpleAllocator.ForceWithdrawalAvailable.selector, expiration, forceWithdrawalTimestamp + ) + ); + simpleAllocator.lock( + Compact({arbiter: arbiter, sponsor: user, nonce: 1, id: usdcId, expires: expiration, amount: 1000}) + ); } function test_revert_NonceAlreadyConsumed(uint256 nonce_) public { @@ -197,7 +278,16 @@ contract SimpleAllocator_Lock is MocksSetup { assertEq(compactContract.hasConsumedAllocatorNonce(nonce_, address(simpleAllocator)), true); vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.NonceAlreadyConsumed.selector, nonce_)); - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: nonce_, id: usdcId, expires: block.timestamp + defaultResetPeriod, amount: 1000 })); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: nonce_, + id: usdcId, + expires: block.timestamp + defaultResetPeriod, + amount: 1000 + }) + ); } function test_revert_InsufficientBalance(uint256 balance_, uint256 amount_) public { @@ -213,8 +303,19 @@ contract SimpleAllocator_Lock is MocksSetup { // Check balance assertEq(compactContract.balanceOf(user, usdcId), balance_); - vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InsufficientBalance.selector, user, usdcId, balance_, amount_)); - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: 1, id: usdcId, expires: block.timestamp + defaultResetPeriod, amount: amount_ })); + vm.expectRevert( + abi.encodeWithSelector(ISimpleAllocator.InsufficientBalance.selector, user, usdcId, balance_, amount_) + ); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: 1, + id: usdcId, + expires: block.timestamp + defaultResetPeriod, + amount: amount_ + }) + ); } function test_successfullyLocked(uint256 nonce_, uint128 amount_, uint32 delay_) public { @@ -238,7 +339,9 @@ contract SimpleAllocator_Lock is MocksSetup { uint256 expiration = vm.getBlockTimestamp() + delay_; vm.expectEmit(true, true, false, true); emit ISimpleAllocator.Locked(user, usdcId, amount_, expiration); - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: nonce_, id: usdcId, expires: expiration, amount: amount_ })); + simpleAllocator.lock( + Compact({arbiter: arbiter, sponsor: user, nonce: nonce_, id: usdcId, expires: expiration, amount: amount_}) + ); // Check lock exists (uint256 amountAfter, uint256 expiresAfter) = simpleAllocator.checkTokensLocked(usdcId, user); @@ -247,7 +350,12 @@ contract SimpleAllocator_Lock is MocksSetup { assertEq(expiresAfter, expiration); } - function test_successfullyLocked_AfterNonceConsumption(uint256 nonce_, uint256 noncePrev_, uint128 amount_, uint32 delay_) public { + function test_successfullyLocked_AfterNonceConsumption( + uint256 nonce_, + uint256 noncePrev_, + uint128 amount_, + uint32 delay_ + ) public { vm.assume(delay_ > simpleAllocator.MIN_WITHDRAWAL_DELAY()); vm.assume(delay_ < simpleAllocator.MAX_WITHDRAWAL_DELAY()); vm.assume(delay_ <= defaultResetPeriod); @@ -264,7 +372,16 @@ contract SimpleAllocator_Lock is MocksSetup { uint256 expirationPrev = vm.getBlockTimestamp() + delay_; vm.expectEmit(true, true, false, true); emit ISimpleAllocator.Locked(user, usdcId, amount_, expirationPrev); - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: noncePrev_, id: usdcId, expires: expirationPrev, amount: amount_ })); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: noncePrev_, + id: usdcId, + expires: expirationPrev, + amount: amount_ + }) + ); // Check a previous lock exists (uint256 amountBefore, uint256 expiresBefore) = simpleAllocator.checkTokensLocked(usdcId, user); @@ -275,7 +392,9 @@ contract SimpleAllocator_Lock is MocksSetup { uint256 expiration = vm.getBlockTimestamp() + delay_; vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.ClaimActive.selector, user)); - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: nonce_, id: usdcId, expires: expiration, amount: amount_ })); + simpleAllocator.lock( + Compact({arbiter: arbiter, sponsor: user, nonce: nonce_, id: usdcId, expires: expiration, amount: amount_}) + ); // Consume previous nonce uint256[] memory nonces = new uint256[](1); @@ -288,7 +407,9 @@ contract SimpleAllocator_Lock is MocksSetup { vm.expectEmit(true, true, false, true); emit ISimpleAllocator.Locked(user, usdcId, amount_, expiration); - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: nonce_, id: usdcId, expires: expiration, amount: amount_ })); + simpleAllocator.lock( + Compact({arbiter: arbiter, sponsor: user, nonce: nonce_, id: usdcId, expires: expiration, amount: amount_}) + ); // Check lock exists (uint256 amountAfter, uint256 expiresAfter) = simpleAllocator.checkTokensLocked(usdcId, user); @@ -301,7 +422,9 @@ contract SimpleAllocator_Lock is MocksSetup { contract SimpleAllocator_Attest is Deposited { function test_revert_InvalidCaller_NotCompact() public { vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidCaller.selector, attacker, address(compactContract))); + vm.expectRevert( + abi.encodeWithSelector(ISimpleAllocator.InvalidCaller.selector, attacker, address(compactContract)) + ); simpleAllocator.attest(address(user), address(user), address(usdc), usdcId, defaultAmount); } @@ -315,7 +438,11 @@ contract SimpleAllocator_Attest is Deposited { vm.assume(falseAmount_ > defaultAmount); vm.prank(user); - vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InsufficientBalance.selector, user, usdcId, defaultAmount, falseAmount_)); + vm.expectRevert( + abi.encodeWithSelector( + ISimpleAllocator.InsufficientBalance.selector, user, usdcId, defaultAmount, falseAmount_ + ) + ); compactContract.transfer(user, attacker, falseAmount_, address(usdc), address(simpleAllocator)); } @@ -324,24 +451,46 @@ contract SimpleAllocator_Attest is Deposited { // Lock a single token uint256 defaultExpiration_ = vm.getBlockTimestamp() + defaultResetPeriod; - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration_, amount: 1 })); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration_, + amount: 1 + }) + ); // At this point, the deposited defaultAmount is not fully available anymore, because one of the tokens was locked // Revert if we try to transfer all of the deposited tokens - vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InsufficientBalance.selector, user, usdcId, defaultAmount, defaultAmount + 1)); + vm.expectRevert( + abi.encodeWithSelector( + ISimpleAllocator.InsufficientBalance.selector, user, usdcId, defaultAmount, defaultAmount + 1 + ) + ); compactContract.transfer(user, attacker, defaultAmount, address(usdc), address(simpleAllocator)); } function test_successfullyAttested(uint32 lockedAmount_, uint32 transferAmount_) public { vm.assume(uint256(transferAmount_) + uint256(lockedAmount_) <= defaultAmount); - address otherUser = makeAddr("otherUser"); + address otherUser = makeAddr('otherUser'); vm.startPrank(user); // Lock tokens uint256 defaultExpiration_ = vm.getBlockTimestamp() + defaultResetPeriod; - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration_, amount: lockedAmount_ })); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration_, + amount: lockedAmount_ + }) + ); vm.expectEmit(true, true, true, true); emit ERC6909.Transfer(address(0), user, otherUser, usdcId, transferAmount_); @@ -355,11 +504,20 @@ contract SimpleAllocator_Attest is Deposited { contract SimpleAllocator_IsValidSignature is Deposited, CreateHash { function test_revert_InvalidLock_NoActiveLock() public { - bytes32 digest = - _hashCompact(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: block.timestamp + defaultResetPeriod, amount: defaultAmount }), address(compactContract)); + bytes32 digest = _hashCompact( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: block.timestamp + defaultResetPeriod, + amount: defaultAmount + }), + address(compactContract) + ); vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidLock.selector, digest, 0)); - simpleAllocator.isValidSignature(digest, ""); + simpleAllocator.isValidSignature(digest, ''); } function test_revert_InvalidLock_ExpiredLock() public { @@ -367,15 +525,34 @@ contract SimpleAllocator_IsValidSignature is Deposited, CreateHash { // Lock tokens uint256 defaultExpiration_ = vm.getBlockTimestamp() + defaultResetPeriod; - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration_, amount: defaultAmount })); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration_, + amount: defaultAmount + }) + ); // Move time forward so lock has expired vm.warp(block.timestamp + defaultResetPeriod); - bytes32 digest = _hashCompact(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration_, amount: defaultAmount }), address(compactContract)); + bytes32 digest = _hashCompact( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration_, + amount: defaultAmount + }), + address(compactContract) + ); vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidLock.selector, digest, defaultExpiration_)); - simpleAllocator.isValidSignature(digest, ""); + simpleAllocator.isValidSignature(digest, ''); } function test_successfullyValidated() public { @@ -383,21 +560,40 @@ contract SimpleAllocator_IsValidSignature is Deposited, CreateHash { // Lock tokens uint256 defaultExpiration_ = vm.getBlockTimestamp() + defaultResetPeriod; - simpleAllocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration_, amount: defaultAmount })); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration_, + amount: defaultAmount + }) + ); // Move time forward so lock has expired vm.warp(block.timestamp + defaultResetPeriod - 1); - bytes32 digest = _hashCompact(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration_, amount: defaultAmount }), address(compactContract)); + bytes32 digest = _hashCompact( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration_, + amount: defaultAmount + }), + address(compactContract) + ); - bytes4 selector = simpleAllocator.isValidSignature(digest, ""); + bytes4 selector = simpleAllocator.isValidSignature(digest, ''); assertEq(selector, IERC1271.isValidSignature.selector); } } contract SimpleAllocator_CheckTokensLocked is Locked { function test_checkTokensLocked_NoActiveLock() public { - address otherUser = makeAddr("otherUser"); + address otherUser = makeAddr('otherUser'); (uint256 amount, uint256 expires) = simpleAllocator.checkTokensLocked(usdcId, otherUser); assertEq(amount, 0); assertEq(expires, 0); @@ -439,15 +635,33 @@ contract SimpleAllocator_CheckTokensLocked is Locked { } function test_checkCompactLocked_NoActiveLock() public { - address otherUser = makeAddr("otherUser"); - (bool locked, uint256 expires) = simpleAllocator.checkCompactLocked(Compact({ arbiter: arbiter, sponsor: otherUser, nonce: defaultNonce, id: usdcId, expires: defaultExpiration, amount: defaultAmount })); + address otherUser = makeAddr('otherUser'); + (bool locked, uint256 expires) = simpleAllocator.checkCompactLocked( + Compact({ + arbiter: arbiter, + sponsor: otherUser, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + }) + ); assertEq(locked, false); assertEq(expires, 0); } function test_checkCompactLocked_ExpiredLock() public { // Confirm that a lock is previously active - (bool locked, uint256 expires) = simpleAllocator.checkCompactLocked(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration, amount: defaultAmount })); + (bool locked, uint256 expires) = simpleAllocator.checkCompactLocked( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + }) + ); assertEq(locked, true); assertEq(expires, defaultExpiration); @@ -455,14 +669,32 @@ contract SimpleAllocator_CheckTokensLocked is Locked { vm.warp(defaultExpiration); // Check that the lock is no longer active - (locked, expires) = simpleAllocator.checkCompactLocked(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration, amount: defaultAmount })); + (locked, expires) = simpleAllocator.checkCompactLocked( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + }) + ); assertEq(locked, false); assertEq(expires, 0); } function test_checkCompactLocked_NonceConsumed() public { // Confirm that a lock is previously active - (bool locked, uint256 expires) = simpleAllocator.checkCompactLocked(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration, amount: defaultAmount })); + (bool locked, uint256 expires) = simpleAllocator.checkCompactLocked( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + }) + ); assertEq(locked, true); assertEq(expires, defaultExpiration); @@ -473,7 +705,16 @@ contract SimpleAllocator_CheckTokensLocked is Locked { compactContract.consume(nonces); // Check that the lock is no longer active - (locked, expires) = simpleAllocator.checkCompactLocked(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration, amount: defaultAmount })); + (locked, expires) = simpleAllocator.checkCompactLocked( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + }) + ); assertEq(locked, false); assertEq(expires, 0); } @@ -483,7 +724,16 @@ contract SimpleAllocator_CheckTokensLocked is Locked { vm.warp(defaultExpiration - 1); // Confirm that a lock is active - (bool locked, uint256 expires) = simpleAllocator.checkCompactLocked(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration, amount: defaultAmount })); + (bool locked, uint256 expires) = simpleAllocator.checkCompactLocked( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + }) + ); assertEq(locked, true); assertEq(expires, defaultExpiration); } From abd027b9f0cd70a6a4607bcd17670cbe6286645c Mon Sep 17 00:00:00 2001 From: vimageDE Date: Thu, 27 Feb 2025 11:48:20 +0100 Subject: [PATCH 11/35] removed old contracts --- src/allocators/RoutingAllocatorERC7683.sol | 545 --------------------- src/allocators/SimpleERC7683Allocator.sol | 399 --------------- test/SimpleERC7683Allocator.t.sol | 101 ---- 3 files changed, 1045 deletions(-) delete mode 100644 src/allocators/RoutingAllocatorERC7683.sol delete mode 100644 src/allocators/SimpleERC7683Allocator.sol delete mode 100644 test/SimpleERC7683Allocator.t.sol diff --git a/src/allocators/RoutingAllocatorERC7683.sol b/src/allocators/RoutingAllocatorERC7683.sol deleted file mode 100644 index 381997d..0000000 --- a/src/allocators/RoutingAllocatorERC7683.sol +++ /dev/null @@ -1,545 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.27; - -import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; -import { ERC6909 } from "@solady/tokens/ERC6909.sol"; -import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; -import { Compact, BatchCompact, BATCH_COMPACT_TYPEHASH, COMPACT_TYPEHASH } from "@uniswap/the-compact/types/EIP712Types.sol"; -import { IdLib } from "@uniswap/the-compact/lib/IdLib.sol"; -import { Lock } from "@uniswap/the-compact/types/Lock.sol"; -import { ResetPeriod } from "@uniswap/the-compact/types/ResetPeriod.sol"; -import { Scope } from "@uniswap/the-compact/types/Scope.sol"; -import { ITheCompact } from "@uniswap/the-compact/interfaces/ITheCompact.sol"; -import { SimpleAllocator } from "./SimpleAllocator.sol"; -import { Claim, Mandate } from "./types/TribunalStructs.sol"; -import { IOriginSettler } from "../interfaces/ERC7683/IOriginSettler.sol"; - -contract RoutingAllocatorERC7683 is SimpleAllocator, IOriginSettler { - using SafeTransferLib for address; - - struct OrderData { - address sponsor; - uint96 nonce; - uint32 claimDeadline; - Settlement[] settlements; - address arbiter; - bytes sponsorSignature; - /// TODO: add baselinePriorityFee? - /// TODO: add scalingFactor? - /// TODO: add salt? - } - - struct OrderDataGasless { - uint32 claimDeadline; - Settlement[] settlements; - address arbiter; - } - - struct Settlement { - Output input; - Output output; - bytes32 destinationSettler; // tribunal - } - - struct Witness { // for single / non-batch compact - uint256 originChainId; - uint256 targetChainId; - bytes32 targetTokenAddress; - uint256 targetMinAmount; - bytes32 recipient; - bytes32 destinationSettler; - uint32 fillDeadline; - } - - error InvalidOriginSettler(address originSettler, address expectedOriginSettler); - error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); - error InvalidChainId(uint256 chainId, uint256 expectedChainId); - error InvalidRecipient(address recipient, address expectedRecipient); - error InvalidNonce(uint256 nonce); - error NonceAlreadyInUse(uint96 nonce); - error InvalidSignature(address signer, address expectedSigner); - error InvalidAmount(uint256 amount, uint256 expectedAmount); - error InvalidTransferAmount(); - error FailedToRefund(address recipient, uint256 amount); - error InvalidCall(); - error InvalidId(uint256 actualId, uint256 expectedId); - error InvalidTokenId(uint256 tokenId, bytes32 expectedTokenPrefix); - - // The typehash of the OrderData struct - // keccak256("OrderData(address sponsor,uint96 nonce,uint32 claimDeadline,Settlement[] settlements,address arbiter,bytes sponsorSignature)Output(bytes32 token,uint256 amount,bytes32 recipient,uint256 chainId)Settlement(Output input,Output output,bytes32 destinationSettler)") - bytes32 constant ORDERDATA_TYPEHASH = 0xe8225b67751f9ff0d865fdc55742ea54087b20406855f954bd737ab200819ab8; - - // The typehash of the OrderDataGasless struct - // keccak256("OrderDataGasless(uint32 claimDeadline,Settlement[] settlements,address arbiter)Output(bytes32 token,uint256 amount,bytes32 recipient,uint256 chainId)Settlement(Output input,Output output,bytes32 destinationSettler)") - bytes32 constant ORDERDATA_GASLESS_TYPEHASH = 0x4679e16e516a2f88beb96ee00964e5f28b9e2ed596592f8c3d92dd70411611a9; - - // keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness) - // Witness(uint256 originChainId,uint256 targetChainId,bytes32 targetTokenAddress,uint256 targetMinAmount,bytes32 recipient,bytes32 destinationSettler,uint32 fillDeadline)") - bytes32 constant COMPACT_WITNESS_TYPEHASH = 0x2f0f51aa07316f3d8860366b556177042fafb2edffc93c799091bae7c194d9a6; - - // keccak256("BatchCompact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256[2][] idsAndAmounts,Witness witness) - // Witness(uint256[] originChainId,uint256[] targetChainId,bytes32[] targetTokenAddress,uint256[] targetMinAmount,bytes32[] recipient,bytes32[] destinationSettler,uint32 fillDeadline)") - bytes32 constant BATCH_COMPACT_WITNESS_TYPEHASH = 0x3158fb17880387b9302c66a40dd45893126fd532c2abb7deee6e53149c53646b; - - ResetPeriod constant DEFAULT_RESET_PERIOD = ResetPeriod.TenMinutes; - Scope constant DEFAULT_SCOPE = Scope.Multichain; - bytes32 immutable ALLOCATOR_ID_PREFIX; - - mapping(uint256 identifier => bool nonceUsed) private _userNonce; - - constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) - SimpleAllocator(compactContract_, minWithdrawalDelay_, maxWithdrawalDelay_) { - ALLOCATOR_ID_PREFIX = bytes32(IdLib.toId(Lock({ - token: address(0), - allocator: address(this), - resetPeriod: DEFAULT_RESET_PERIOD, - scope: DEFAULT_SCOPE - }))); - } - - /// @notice Opens a gasless cross-chain order on behalf of a user. - /// @dev To be called by the filler. - /// @dev This method must emit the Open event - /// @dev This deposits remaining tokens to fulfill the order in the compact contract - /// @dev This locks the users tokens - /// @param order The GaslessCrossChainOrder definition - /// @param signature The user's signature over the order - function openFor(GaslessCrossChainOrder calldata order, bytes calldata signature, bytes calldata) external { - // since we have the users signature, we can create locks in the name of the user - - // Check if orderDataType is the one expected by the allocator - if (order.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { - revert InvalidOrderDataType(order.orderDataType, ORDERDATA_GASLESS_TYPEHASH); - } - // Check the allocator is the chosen origin settler - // This check is not strictly necessary, since the user does not sign a allocator. The filler will not get payed if he maliciously choses a different allocator. - if (order.originSettler != address(this)) { - revert InvalidOriginSettler(order.originSettler, address(this)); - } - - // Decode the orderData - OrderDataGasless memory orderDataGasless = abi.decode(order.orderData, (OrderDataGasless)); - // Enforce a uint96 nonce since the contract handles the nonce management by combining the provided nonce with the user address - uint96 nonce = uint96(order.nonce); - if (order.nonce != nonce) { - revert InvalidNonce(order.nonce); - } - - OrderData memory orderData = _convertGaslessOrderData(order.user, nonce, orderDataGasless, signature); - - _open(orderData, order.fillDeadline, false); - } - - /// @notice Opens a cross-chain order - /// @dev To be called by the user - /// @dev This method must emit the Open event - /// @dev This deposits remaining tokens to fulfill the order in the compact contract - /// @dev This locks the users tokens - /// @param order The OnchainCrossChainOrder definition - function open(OnchainCrossChainOrder calldata order) external { - // Check if orderDataType is the one expected by the allocator - if (order.orderDataType != ORDERDATA_TYPEHASH) { - revert InvalidOrderDataType(order.orderDataType, ORDERDATA_TYPEHASH); - } - - // Decode the orderData - OrderData memory orderData = abi.decode(order.orderData, (OrderData)); - _open(orderData, order.fillDeadline, false); - } - - /// @notice Opens a cross-chain order for native tokens - /// @notice This function also works for ERC20 tokens, but refunding the remaining balance to the user is not necessary for ERC20 tokens - /// @dev To be called by the user - /// @dev This method must emit the Open event - /// @dev This deposits remaining tokens to fulfill the order in the compact contract - /// @dev This locks the users tokens - /// @dev This refunds the remaining balance to the user - /// @param order The OnchainCrossChainOrder definition - function openNativeTokenOrder(OnchainCrossChainOrder calldata order) external payable { - // Check if orderDataType is the one expected by the allocator - if (order.orderDataType != ORDERDATA_TYPEHASH) { - revert InvalidOrderDataType(order.orderDataType, ORDERDATA_TYPEHASH); - } - - // Decode the orderData - OrderData memory orderData = abi.decode(order.orderData, (OrderData)); - _open(orderData, order.fillDeadline, false); - // Refund the remaining native token balance to the user - _refundBalance(msg.sender); - } - - receive() external payable { - revert InvalidCall(); - } - - fallback() external payable { - revert InvalidCall(); - } - - /// @notice Resolves a specific GaslessCrossChainOrder into a generic ResolvedCrossChainOrder - /// @dev Intended to improve standardized integration of various order types and settlement contracts - /// @param order The GaslessCrossChainOrder definition - /// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order - function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata) external pure returns (ResolvedCrossChainOrder memory){ - OrderDataGasless memory orderDataGasless = abi.decode(order.orderData, (OrderDataGasless)); - - // Enforce a uint96 nonce since the contract handles the nonce management by combining the provided nonce with the user address - uint96 nonce = uint96(order.nonce); - if (order.nonce != nonce) { - revert InvalidNonce(order.nonce); - } - OrderData memory orderData = _convertGaslessOrderData(order.user, nonce, orderDataGasless, ""); - return _resolveOrder(order.fillDeadline, orderData); - } - - /// @notice Resolves a specific OnchainCrossChainOrder into a generic ResolvedCrossChainOrder - /// @dev Intended to improve standardized integration of various order types and settlement contracts - /// @param order The OnchainCrossChainOrder definition - /// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order - function resolve(OnchainCrossChainOrder calldata order) external pure returns (ResolvedCrossChainOrder memory){ - OrderData memory orderData = abi.decode(order.orderData, (OrderData)); - return _resolveOrder(order.fillDeadline, orderData); - } - - function getCompactWitnessString() external pure returns (string memory) { - return "Witness(uint256 originChainId, uint256 targetChainId, bytes32 targetTokenAddress, uint256 targetMinAmount, bytes32 recipient, bytes32 destinationSettler, uint32 fillDeadline)"; - } - - function getBatchCompactWitnessString() external pure returns (string memory) { - return "Witness(uint256[] originChainId,uint256[] targetChainId,bytes32[] targetTokenAddress,uint256[] targetMinAmount,bytes32[] recipient,bytes32[] destinationSettler,uint32 fillDeadline)"; - } - - function _deposit(address sponsor_, address token_, uint256 amount_, bool useDeposited_) internal { - uint256 id = _toId(token_); - uint256 depositAmount = amount_; - if(useDeposited_){ - // Check unlocked balance - uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(sponsor_, id); - bytes32 tokenHash = _getTokenHash(id, sponsor_); - - if (_claim[tokenHash] > block.timestamp) { - // Lock is still active, add the locked amount if the nonce has not yet been consumed. It cannot be bigger then the balance. - balance -= ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)) ? 0 : _amount[tokenHash]; - } - if( balance > amount_) { - // Cap balance to required amount - balance = amount_; - } - // Subtract the unlocked balance from the amount to deposit - depositAmount -= balance; - } - - if(depositAmount > 0) { - // Deposit the remaining amount - uint256 actualId; - if (token_ == address(0)) { - if(msg.value < depositAmount) { - revert InvalidAmount(msg.value, depositAmount); - } - // deposit native token - actualId = ITheCompact(COMPACT_CONTRACT).deposit{value: depositAmount}(address(this), DEFAULT_RESET_PERIOD, DEFAULT_SCOPE, sponsor_); - } else { - uint256 prevAmount = IERC20(token_).balanceOf(address(this)); - // Requires previous approval - token_.safeTransferFrom(sponsor_, address(this), depositAmount); - uint256 newAmount = IERC20(token_).balanceOf(address(this)); - - if(newAmount <= prevAmount) { - revert InvalidTransferAmount(); - } - // NOTE: settling fee-on-transfer tokens will result in fewer tokens - // being received. Be sure to accommodate for this when - // providing the desired deposit amount. - depositAmount = newAmount - prevAmount; - // Depositing the actual received amount after the transfer - actualId = ITheCompact(COMPACT_CONTRACT).deposit(token_, address(this), DEFAULT_RESET_PERIOD, DEFAULT_SCOPE, depositAmount, sponsor_); - } - if(actualId != id) { - revert InvalidId(actualId, id); - } - } - } - - function _open(OrderData memory orderData_, uint32 fillDeadline_, bool delegated_) internal { - // Check the user - if(!delegated_ && orderData_.sponsor != msg.sender) { - revert InvalidCaller(msg.sender, orderData_.sponsor); - } - - uint256 identifier = _createIdentifier(orderData_.sponsor, orderData_.nonce); - // Check the nonce - if (_userNonce[identifier]) { - revert NonceAlreadyInUse(orderData_.nonce); - } - _userNonce[identifier] = true; - - // NOTE: We do not enforce a specific tribunal, so we do not check the address. - // This will allow to support new tribunals after the deployment of the allocator - // Going with an immutable tribunal would limit support for new chains with a fully decentralized allocator - /// TODO: THINK ABOUT IF THE ARBITER, TRIBUNAL MUST BE ENFORCED OR NOT - - uint256 settlementsLength = orderData_.settlements.length; - - bytes32 digest; - bytes32 tokenHash; - if(settlementsLength > 1) { - uint256[2][] memory idsAndAmounts = new uint256[2][](settlementsLength); - - uint256[] memory originChainIds = new uint256[](settlementsLength); - uint256[] memory targetChainIds = new uint256[](settlementsLength); - bytes32[] memory targetTokenAddresses = new bytes32[](settlementsLength); - uint256[] memory targetMinAmounts = new uint256[](settlementsLength); - bytes32[] memory recipients = new bytes32[](settlementsLength); - bytes32[] memory destinationSettlers = new bytes32[](settlementsLength); - - // Iterate over the inputs and lock the tokens - for(uint256 i = 0; i < settlementsLength; ++i) { - if (orderData_.settlements[i].input.chainId != block.chainid) { - // MultiChainCompact not supported - revert InvalidChainId(orderData_.settlements[i].input.chainId, block.chainid); - } - if (_castToAddress(orderData_.settlements[i].input.recipient) != orderData_.sponsor) { - // Sponsor must be the same throughout all settlements - revert InvalidRecipient(_castToAddress(orderData_.settlements[i].input.recipient), orderData_.sponsor); - } - if (!_checkIdPrefix(uint256(orderData_.settlements[i].input.token))) { - revert InvalidTokenId(uint256(orderData_.settlements[i].input.token), ALLOCATOR_ID_PREFIX); - } - // Deposit the amount of tokens that is not already available and unlocked - _deposit(orderData_.sponsor, _castToAddress(orderData_.settlements[i].input.token), orderData_.settlements[i].input.amount, true); - // lock the tokens - tokenHash = _lockTokens(orderData_, identifier, i); - - idsAndAmounts[i] = [uint256(orderData_.settlements[i].input.token), orderData_.settlements[i].input.amount]; - originChainIds[i] = block.chainid; - targetChainIds[i] = orderData_.settlements[i].output.chainId; - targetTokenAddresses[i] = orderData_.settlements[i].output.token; - targetMinAmounts[i] = orderData_.settlements[i].output.amount; - recipients[i] = orderData_.settlements[i].output.recipient; - destinationSettlers[i] = orderData_.settlements[i].destinationSettler; - } - - // Work with a BatchCompact digest - digest = keccak256( - abi.encodePacked( - bytes2(0x1901), - ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - BATCH_COMPACT_WITNESS_TYPEHASH, - orderData_.arbiter, - orderData_.sponsor, - identifier, // TODO: IS THE CAST REQUIRED? - orderData_.claimDeadline, - idsAndAmounts, - keccak256( - abi.encode( - // Skips usage of a struct and supplies data directly - originChainIds, - targetChainIds, - targetTokenAddresses, - targetMinAmounts, - recipients, - destinationSettlers, - fillDeadline_ - ) - ) - ) - ) - ) - ); - } else { - if (orderData_.settlements[0].input.chainId != block.chainid) { - revert InvalidChainId(orderData_.settlements[0].input.chainId, block.chainid); - } - if (_castToAddress(orderData_.settlements[0].input.recipient) != orderData_.sponsor) { - // Sponsor must be the same throughout in the settlements - revert InvalidRecipient(_castToAddress(orderData_.settlements[0].input.recipient), orderData_.sponsor); - } - if (!_checkIdPrefix(uint256(orderData_.settlements[0].input.token))) { - revert InvalidTokenId(uint256(orderData_.settlements[0].input.token), ALLOCATOR_ID_PREFIX); - } - - // Deposit the amount of tokens that is not already available and unlocked - _deposit(orderData_.sponsor, _castToAddress(orderData_.settlements[0].input.token), orderData_.settlements[0].input.amount, true); - // lock the tokens - tokenHash = _lockTokens(orderData_, identifier, 0); - - // Work with a Compact digest - digest = keccak256( - abi.encodePacked( - bytes2(0x1901), - ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - COMPACT_WITNESS_TYPEHASH, - orderData_.arbiter, - orderData_.sponsor, - identifier, - orderData_.claimDeadline, - uint256(orderData_.settlements[0].input.token), // TODO: IS THE CAST REQUIRED HERE? - orderData_.settlements[0].input.amount, - keccak256( - abi.encode( - Witness({ - originChainId: orderData_.settlements[0].input.chainId, - targetChainId: orderData_.settlements[0].output.chainId, - targetTokenAddress: orderData_.settlements[0].output.token, - targetMinAmount: orderData_.settlements[0].output.amount, - recipient: orderData_.settlements[0].output.recipient, - destinationSettler: orderData_.settlements[0].destinationSettler, - fillDeadline: fillDeadline_ - }) - ) - ) - ) - ) - ) - ); - } - - // confirm the signature matches the sponsor to verify the users intent (deposit, lock and open order intents) - address signer = ECDSA.recover(digest, orderData_.sponsorSignature); - if (orderData_.sponsor != signer) { - revert InvalidSignature(orderData_.sponsor, signer); - } - - // Finalize the lock by storing the tokenHash based on the digest - _sponsor[digest] = tokenHash; - // The stored tokenHash will only be used to check for the expiration of the order in the isValidSignature function. - // Since the expiration is the same for all allocations, it does not matter which of the tokenHashes is stored for a batch compact. - - // Emit an open event - emit Open(bytes32(identifier), _resolveOrder(fillDeadline_, orderData_)); - } - - function _lockTokens(OrderData memory orderData_, uint256 identifier, uint256 index_) internal returns (bytes32 tokenHash_) { - return _lockTokens(orderData_.arbiter, orderData_.sponsor, identifier, orderData_.claimDeadline, uint256(orderData_.settlements[index_].input.token), orderData_.settlements[index_].input.amount); - } - - function _lockTokens(address arbiter, address sponsor, uint256 identifier, uint32 expires, uint256 id, uint256 amount) internal returns (bytes32 tokenHash_) { - tokenHash_ = _checkAllocation(Compact({ - arbiter: arbiter, - sponsor: sponsor, - nonce: identifier, - expires: expires, - id: id, - amount: amount - })); - _claim[tokenHash_] = expires; - _amount[tokenHash_] = amount; - _nonce[tokenHash_] = identifier; - - return tokenHash_; - - } - - function _resolveOrder(uint32 fillDeadline, OrderData memory orderData) internal pure returns (ResolvedCrossChainOrder memory) { - uint256 settlementLength = orderData.settlements.length; - FillInstruction[] memory fillInstructions = new FillInstruction[](settlementLength); - - Output[] memory outputs = new Output[](settlementLength); - Output[] memory inputs = new Output[](settlementLength); - - for(uint256 i = 0; i < settlementLength; ++i) { - inputs[i] = orderData.settlements[i].input; - outputs[i] = orderData.settlements[i].output; - - /// TODO: FILL THE MANDATE - Mandate memory mandate = Mandate({ - chainId: orderData.settlements[i].input.chainId, - tribunal: orderData.arbiter, - recipient: _castToAddress(orderData.settlements[i].output.recipient), - expires: fillDeadline, // TODO: is this correct? Or do we ignore the fill deadline and only care about the claim deadline? - token: _castToAddress(orderData.settlements[i].output.token), - minimumAmount: orderData.settlements[i].output.amount, - baselinePriorityFee: 0, // TODO: check whats happening here - scalingFactor: 0, // TODO: check whats happening here - salt: 0 // TODO: whats difference between salt and nonce? This is still sponsor signed data - }); - Claim memory claim = Claim({ - chainId: orderData.settlements[i].input.chainId, // TODO: IS THIS TARGET OR ORIGIN CHAIN ID? IT APPARENTLY SHOULD BE THE ORIGIN CHAIN ID, BUT WHERE IS THE TARGET CHAIN ID ADDED? - compact: Compact({ - arbiter: orderData.arbiter, - sponsor: orderData.sponsor, - nonce: orderData.nonce, - expires: orderData.claimDeadline, - id: uint256(orderData.settlements[i].input.token), // TODO: make it clear in doc that outputs and inputs are always connected via the index - amount: orderData.settlements[i].input.amount - }), - sponsorSignature: orderData.sponsorSignature, - allocatorSignature: "" // No signature required from this allocator, it will verify the claim on chain. - }); - - fillInstructions[i] = FillInstruction({ - destinationChainId: uint64(orderData.settlements[i].output.chainId), // TODO: WHY SUDDENLY A UINT64 INSTEAD OF UINT256? - destinationSettler: orderData.settlements[i].destinationSettler, - originData: abi.encode(claim, mandate) // TODO: FILL WITH THE ORIGIN DATA REQUIRED BY THE TRIBUNAL - }); - } - - ResolvedCrossChainOrder memory resolvedOrder = ResolvedCrossChainOrder({ - user: orderData.sponsor, - originChainId: orderData.settlements[0].input.chainId, // must be same for every input - openDeadline: orderData.claimDeadline, /// TODO: CAN THE OPEN DEADLINE BE THE CLAIM DEADLINE? - fillDeadline: fillDeadline, - orderId: bytes32(_createIdentifier(orderData.sponsor, orderData.nonce)), - maxSpent: inputs, - minReceived: outputs, - fillInstructions: fillInstructions - }); - return resolvedOrder; - } - - function _refundBalance(address recipient_) internal { - if (address(this).balance > 0) { - (bool success, ) = payable(recipient_).call{value: address(this).balance}(""); - if (!success) { - revert FailedToRefund(recipient_, address(this).balance); - } - } - } - - function _createIdentifier(address sponsor_, uint96 nonce_) internal pure returns (uint256 identifier_) { - assembly ("memory-safe") { - identifier_ := or(shl(160, nonce_), shr(96, shl(96,sponsor_))) - } - return identifier_; - } - - function _castToAddress(bytes32 address_) internal pure returns (address output_) { - assembly ("memory-safe") { - output_ := shr(96, shl(96, address_)) - } - } - - function _checkIdPrefix(uint256 id_) internal view returns (bool isSame_) { - bytes32 idPrefix = ALLOCATOR_ID_PREFIX; - assembly ("memory-safe") { - isSame_ := eq(idPrefix, shl(160, shr(160, id_))) - } - } - - function _toId(address token_) internal view returns (uint256 id_) { - bytes32 idPrefix = ALLOCATOR_ID_PREFIX; - assembly ("memory-safe") { - id_ := or(idPrefix, shr(96, shl(96, token_))) - } - return id_; - } - - function _convertGaslessOrderData(address sponsor_, uint96 nonce_, OrderDataGasless memory orderDataGasless_, bytes memory signature_) internal pure returns (OrderData memory orderData_) { - orderData_ = OrderData({ - sponsor: sponsor_, - nonce: nonce_, - claimDeadline: orderDataGasless_.claimDeadline, - settlements: orderDataGasless_.settlements, - arbiter: orderDataGasless_.arbiter, - sponsorSignature: signature_ - }); - return orderData_; - } -} - diff --git a/src/allocators/SimpleERC7683Allocator.sol b/src/allocators/SimpleERC7683Allocator.sol deleted file mode 100644 index 371e263..0000000 --- a/src/allocators/SimpleERC7683Allocator.sol +++ /dev/null @@ -1,399 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.27; - -import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import { Compact, BatchCompact, BATCH_COMPACT_TYPEHASH, COMPACT_TYPEHASH } from "@uniswap/the-compact/types/EIP712Types.sol"; -import { ITheCompact } from "@uniswap/the-compact/interfaces/ITheCompact.sol"; -import { SimpleAllocator } from "./SimpleAllocator.sol"; -import { Claim, Mandate } from "./types/TribunalStructs.sol"; -import { IOriginSettler } from "../interfaces/ERC7683/IOriginSettler.sol"; - -contract SimpleERC7683Allocator is SimpleAllocator, IOriginSettler { - - struct OrderData { - // COMPACT - address arbiter; // The account tasked with verifying and submitting the claim. - address sponsor; // The account to source the tokens from. - uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. - uint256 expires; // The time at which the claim expires. - uint256 id; // The token ID of the ERC6909 token to allocate. - uint256 amount; // The amount of ERC6909 tokens to allocate. - // MANDATE - uint256 chainId; // (implicit arg, included in EIP712 payload) - address tribunal; // (implicit arg, included in EIP712 payload) - address recipient; // Recipient of settled tokens - // uint256 expires; // Mandate expiration timestamp - address token; // Settlement token (address(0) for native) - uint256 minimumAmount; // Minimum settlement amount - uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in - uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) - bytes32 salt; // Replay protection parameter - } - - struct OrderDataGasless { - // COMPACT - // address arbiter; // The account tasked with verifying and submitting the claim. - // address sponsor; // The account to source the tokens from. - // uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. - uint256 expires; // The time at which the claim expires. - uint256 id; // The token ID of the ERC6909 token to allocate. - uint256 amount; // The amount of ERC6909 tokens to allocate. - // MANDATE - uint256 chainId; // (implicit arg, included in EIP712 payload) - address tribunal; // (implicit arg, included in EIP712 payload) - address recipient; // Recipient of settled tokens - // uint256 expires; // Mandate expiration timestamp - address token; // Settlement token (address(0) for native) - uint256 minimumAmount; // Minimum settlement amount - uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in - uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) - bytes32 salt; // Replay protection parameter - } - - error InvalidOriginSettler(address originSettler, address expectedOriginSettler); - error InvalidOrderDataType(bytes32 orderDataType, bytes32 expectedOrderDataType); - error InvalidChainId(uint256 chainId, uint256 expectedChainId); - error InvalidRecipient(address recipient, address expectedRecipient); - error InvalidNonce(uint256 nonce); - error NonceAlreadyInUse(uint256 nonce); - error InvalidSignature(address signer, address expectedSigner); - error InvalidRegistration(address sponsor, bytes32 claimHash); - error InvalidSponsor(address sponsor, address expectedSponsor); - - // The typehash of the OrderData struct - // keccak256("OrderData(address arbiter,address sponsor,uint256 nonce,uint256 id,uint256 amount,Mandate mandate) - // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") - bytes32 constant ORDERDATA_TYPEHASH = 0x9e0e1bdb0df35509b65bbc49d209dd42496c5a3f13998f9a74dc842d6932656b; - - // The typehash of the OrderDataGasless struct - // keccak256("OrderDataGasless(address arbiter,uint256 id,uint256 amount,Mandate mandate) - // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") - bytes32 constant ORDERDATA_GASLESS_TYPEHASH = 0x9ab67658b7c0f35b64fdadd7adee1e58b6399a8201f38c355d3a109a2d7081d7; - - // keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate) - // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") - bytes32 constant COMPACT_WITNESS_TYPEHASH = 0x27f09e0bb8ce2ae63380578af7af85055d3ada248c502e2378b85bc3d05ee0b0; - - bytes32 immutable _COMPACT_DOMAIN_SEPARATOR; - - /// FOR SINGLE COMPACT WE HAVE: - /// - arbiter - /// - sponsor - /// - nonce - /// - expires - /// - id - /// - amount - /// WHAT ADDITIONAL DATA NEEDS TO BE SIGNED: - // Witness( - // uint256 originChainId, - // uint256 targetChainId, - // bytes32 targetTokenAddress, - // uint256 targetMinAmount, - // bytes32 recipient, - // bytes32 destinationSettler, - // uint32 fillDeadline - // ) - - /// FOR BATCH COMPACT WE HAVE: - /// - arbiter - /// - sponsor - /// - nonce - /// - expires - /// - idsAndAmounts - /// WHAT ADDITIONAL DATA NEEDS TO BE SIGNED: - // Witness( - // uint256[] originChainId, - // uint256[] targetChainId, - // bytes32[] targetTokenAddress, - // uint256[] targetMinAmount, - // bytes32[] recipient, - // bytes32[] destinationSettler, - // uint32 fillDeadline - // ) - - /// TODO: batch compacts witness - - // The nonce of the allocator - mapping(uint256 nonce => bool nonceUsed) private _userNonce; - - constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) - SimpleAllocator(compactContract_, minWithdrawalDelay_, maxWithdrawalDelay_) { - _COMPACT_DOMAIN_SEPARATOR = ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(); - } - - /// @notice Opens a gasless cross-chain order on behalf of a user. - /// @dev To be called by the filler. - /// @dev This method must emit the Open event - /// @param order_ The GaslessCrossChainOrder definition - /// @param sponsorSignature_ The user's signature over the order - function openFor(GaslessCrossChainOrder calldata order_, bytes calldata sponsorSignature_, bytes calldata) external{ - // With the users signature, we can create locks in the name of the user - - // Check if orderDataType is the one expected by the allocator - if (order_.orderDataType != ORDERDATA_GASLESS_TYPEHASH) { - revert InvalidOrderDataType(order_.orderDataType, ORDERDATA_GASLESS_TYPEHASH); - } - - // Decode the orderData - OrderDataGasless memory orderDataGasless = abi.decode(order_.orderData, (OrderDataGasless)); - - OrderData memory orderData = _convertGaslessOrderData(order_.user, order_.nonce, order_.originSettler, orderDataGasless); - - _open(orderData, order_.fillDeadline, order_.user, sponsorSignature_); - } - - /// @notice Opens a cross-chain order - /// @dev To be called by the user - /// @dev This method must emit the Open event - /// @dev This locks the users tokens - /// @param order The OnchainCrossChainOrder definition - function open(OnchainCrossChainOrder calldata order) external{ - // TODO: Think about if this can only be used with a registered compact? Or do we want the sponsor signature in the orderData? - - // Check if orderDataType is the one expected by the allocator - if (order.orderDataType != ORDERDATA_TYPEHASH) { - revert InvalidOrderDataType(order.orderDataType, ORDERDATA_TYPEHASH); - } - - // Decode the orderData - OrderData memory orderData = abi.decode(order.orderData, (OrderData)); - if(orderData.sponsor != msg.sender) { - revert InvalidSponsor(orderData.sponsor, msg.sender); - } - - _open(orderData, order.fillDeadline, msg.sender, ""); - } - - /// @notice Resolves a specific GaslessCrossChainOrder into a generic ResolvedCrossChainOrder - /// @dev Intended to improve standardized integration of various order types and settlement contracts - /// @param order The GaslessCrossChainOrder definition - /// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order - function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata) external view returns (ResolvedCrossChainOrder memory){ - OrderDataGasless memory orderDataGasless = abi.decode(order.orderData, (OrderDataGasless)); - - OrderData memory orderData = _convertGaslessOrderData(order.user, order.nonce, order.originSettler, orderDataGasless); - return _resolveOrder(order.user, order.fillDeadline, order.nonce, orderData, ""); - } - - /// @notice Resolves a specific OnchainCrossChainOrder into a generic ResolvedCrossChainOrder - /// @dev Intended to improve standardized integration of various order types and settlement contracts - /// @param order The OnchainCrossChainOrder definition - /// @return ResolvedCrossChainOrder hydrated order data including the inputs and outputs of the order - function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory){ - OrderData memory orderData = abi.decode(order.orderData, (OrderData)); - return _resolveOrder(orderData.sponsor, order.fillDeadline, orderData.nonce, orderData, ""); - } - - function getCompactWitnessString() external pure returns (string memory) { - return "Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)"; - } - - function checkNonce(address sponsor_, uint256 nonce_) external view returns (bool nonceUnused_) { - _checkNonce(sponsor_, nonce_); - nonceUnused_ = !_userNonce[nonce_]; - return nonceUnused_; - } - - function _open(OrderData memory orderData_, uint32 fillDeadline_, address sponsor_, bytes memory sponsorSignature_) internal { - - // Enforce a nonce where the most significant 96 bits are the nonce and the least significant 160 bits are the sponsor - _checkNonce(sponsor_, orderData_.nonce); - - // Check the nonce - if (_userNonce[orderData_.nonce]) { - revert NonceAlreadyInUse(orderData_.nonce); - } - _userNonce[orderData_.nonce] = true; - - // We do not enforce a specific tribunal, so we do not check the address. This will allow to support new tribunals after the deployment of the allocator - // Going with an immutable tribunal would limit support for new chains with a fully decentralized allocator - /// TODO: THINK ABOUT IF THE ARBITER MUST BE ENFORCED OR NOT - - bytes32 tokenHash = _lockTokens(orderData_, sponsor_, orderData_.nonce); - - // Work with a Compact digest - bytes32 claimHash = keccak256( - abi.encode( - COMPACT_WITNESS_TYPEHASH, - orderData_.arbiter, - sponsor_, - orderData_.nonce, - orderData_.expires, - orderData_.id, - orderData_.amount, - keccak256( - abi.encode( - orderData_.chainId, - orderData_.tribunal, - orderData_.recipient, - fillDeadline_, - orderData_.token, - orderData_.minimumAmount, - orderData_.baselinePriorityFee, - orderData_.scalingFactor, - orderData_.salt - ) - ) - ) - ); - - // TODO: This means everyone can open an order for a user if they have registered the claim hash on the compact (just call openFor with an empty signature). Any issues with that? - // Do not currently see an issue, since its the same with the sponsors signature. - if(sponsorSignature_.length > 0) { - // confirm the signature matches the digest - bytes32 digest = keccak256( - abi.encodePacked( - bytes2(0x1901), - _COMPACT_DOMAIN_SEPARATOR, - claimHash - ) - ); - address signer = ECDSA.recover(digest, sponsorSignature_); - if (sponsor_ != signer) { - revert InvalidSignature(sponsor_, signer); - } - } else { - // confirm the claim hash is registered on the compact - (bool isActive, uint256 registrationExpiration) = ITheCompact(COMPACT_CONTRACT).getRegistrationStatus(sponsor_, claimHash, COMPACT_WITNESS_TYPEHASH); - if (!isActive || registrationExpiration < orderData_.expires) { - revert InvalidRegistration(sponsor_, claimHash); - } - } - - _sponsor[claimHash] = tokenHash; - - // Emit an open event - emit Open(bytes32(orderData_.nonce), _resolveOrder(sponsor_, fillDeadline_, orderData_.nonce, orderData_, sponsorSignature_)); - } - - function _lockTokens(OrderData memory orderData_, address sponsor_, uint256 identifier) internal returns (bytes32 tokenHash_) { - return _lockTokens(orderData_.arbiter, sponsor_, identifier, orderData_.expires, orderData_.id, orderData_.amount); - } - - function _lockTokens(address arbiter, address sponsor, uint256 identifier, uint256 expires, uint256 id, uint256 amount) internal returns (bytes32 tokenHash_) { - tokenHash_ = _checkAllocation(Compact({ - arbiter: arbiter, - sponsor: sponsor, - nonce: identifier, - expires: expires, - id: id, - amount: amount - })); - _claim[tokenHash_] = expires; - _amount[tokenHash_] = amount; - _nonce[tokenHash_] = identifier; - - return tokenHash_; - - } - - function _resolveOrder(address sponsor, uint32 fillDeadline, uint256 identifier, OrderData memory orderData, bytes memory sponsorSignature) internal view returns (ResolvedCrossChainOrder memory) { - FillInstruction[] memory fillInstructions = new FillInstruction[](1); - - Mandate memory mandate = Mandate({ - chainId: orderData.chainId, - tribunal: orderData.tribunal, - recipient: orderData.recipient, - expires: fillDeadline, - token: orderData.token, - minimumAmount: orderData.minimumAmount, - baselinePriorityFee: orderData.baselinePriorityFee, - scalingFactor: orderData.scalingFactor, - salt: orderData.salt - }); - Claim memory claim = Claim({ - chainId: block.chainid, - compact: Compact({ - arbiter: orderData.arbiter, - sponsor: sponsor, - nonce: orderData.nonce, - expires: orderData.expires, - id: orderData.id, - amount: orderData.amount - }), - sponsorSignature: sponsorSignature, - allocatorSignature: "" // No signature required from this allocator, it will verify the claim on chain. - }); - - fillInstructions[0] = FillInstruction({ - destinationChainId: orderData.chainId, - destinationSettler: bytes20(orderData.tribunal), - originData: abi.encode(claim, mandate) // TODO: FILL WITH THE ORIGIN DATA REQUIRED BY THE TRIBUNAL - }); - - Output memory spent = Output({ - token: bytes20(orderData.token), - amount: type(uint256).max, - recipient: bytes20(orderData.recipient), - chainId: orderData.chainId - }); - Output memory received = Output({ - token: bytes20(_idToToken(orderData.id)), - amount: orderData.amount, - recipient: bytes32(0), - chainId: block.chainid - }); - - Output[] memory maxSpent = new Output[](1); - maxSpent[0] = spent; - Output[] memory minReceived = new Output[](1); - minReceived[0] = received; - - ResolvedCrossChainOrder memory resolvedOrder = ResolvedCrossChainOrder({ - user: sponsor, - originChainId: block.chainid, - openDeadline: uint32(orderData.expires), - fillDeadline: fillDeadline, - orderId: bytes32(identifier), - maxSpent: maxSpent, - minReceived: minReceived, - fillInstructions: fillInstructions - }); - return resolvedOrder; - } - - function _checkNonce(address sponsor_, uint256 nonce_) internal pure { - // Enforce a nonce where the most significant 96 bits are the nonce and the least significant 160 bits are the sponsor - // This ensures that the nonce is unique for a given sponsor - address expectedSponsor; - assembly ("memory-safe") { - expectedSponsor := shr(96, shl(96, nonce_)) - } - if(expectedSponsor != sponsor_) { - revert InvalidNonce(nonce_); - } - } - - function _castToAddress(bytes32 address_) internal pure returns (address output_) { - assembly ("memory-safe") { - output_ := shr(96, shl(96, address_)) - } - } - - function _idToToken(uint256 id_) internal pure returns (address token_) { - assembly ("memory-safe") { - token_ := shr(96, shl(96, id_)) - } - } - - function _convertGaslessOrderData(address sponsor_, uint256 nonce_, address arbiter_, OrderDataGasless memory orderDataGasless_) internal pure returns (OrderData memory orderData_) { - orderData_ = OrderData({ - arbiter: arbiter_, - sponsor: sponsor_, - nonce: nonce_, - expires: orderDataGasless_.expires, - id: orderDataGasless_.id, - amount: orderDataGasless_.amount, - chainId: orderDataGasless_.chainId, - tribunal: orderDataGasless_.tribunal, - recipient: orderDataGasless_.recipient, - token: orderDataGasless_.token, - minimumAmount: orderDataGasless_.minimumAmount, - baselinePriorityFee: orderDataGasless_.baselinePriorityFee, - scalingFactor: orderDataGasless_.scalingFactor, - salt: orderDataGasless_.salt - }); - return orderData_; - } -} diff --git a/test/SimpleERC7683Allocator.t.sol b/test/SimpleERC7683Allocator.t.sol deleted file mode 100644 index 8d0ba8c..0000000 --- a/test/SimpleERC7683Allocator.t.sol +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.27; - -import { Test } from "forge-std/Test.sol"; -import { SimpleERC7683Allocator } from "src/allocators/SimpleERC7683Allocator.sol"; -import { IOriginSettler } from "src/interfaces/ERC7683/IOriginSettler.sol"; -import { ITheCompact } from "@uniswap/the-compact/interfaces/ITheCompact.sol"; -import { Compact, COMPACT_TYPEHASH } from "@uniswap/the-compact/types/EIP712Types.sol"; -import { ForcedWithdrawalStatus } from "@uniswap/the-compact/types/ForcedWithdrawalStatus.sol"; -import { TheCompactMock } from "src/test/TheCompactMock.sol"; -import { ERC20Mock } from "src/test/ERC20Mock.sol"; -import { ERC6909 } from "@solady/tokens/ERC6909.sol"; -import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; -import { console } from "forge-std/console.sol"; - -abstract contract MocksSetup is Test { - address user; - uint256 userPK; - address attacker; - uint256 attackerPK; - address arbiter; - ERC20Mock usdc; - TheCompactMock compactContract; - SimpleERC7683Allocator simpleERC7683Allocator; - uint256 usdcId; - - uint256 defaultResetPeriod = 60; - uint256 defaultAmount = 1000; - uint256 defaultNonce = 1; - uint256 defaultExpiration; - - function setUp() public virtual { - arbiter = makeAddr("arbiter"); - usdc = new ERC20Mock("USDC", "USDC"); - compactContract = new TheCompactMock(); - simpleERC7683Allocator = new SimpleERC7683Allocator(address(compactContract), 5, 100); - usdcId = compactContract.getTokenId(address(usdc), address(simpleERC7683Allocator)); - (user, userPK) = makeAddrAndKey("user"); - (attacker, attackerPK) = makeAddrAndKey("attacker"); - } -} - -abstract contract CreateHash is Test { - struct Allocator { - bytes32 hash; - } - - // stringified types - string EIP712_DOMAIN_TYPE = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"; // Hashed inside the function - // EIP712 domain type - string name = "The Compact"; - string version = "0"; - - function _hashCompact(Compact memory data, address verifyingContract) internal view returns (bytes32) { - // hash typed data - return keccak256( - abi.encodePacked( - "\x19\x01", // backslash is needed to escape the character - _domainSeparator(verifyingContract), - keccak256(abi.encode(COMPACT_TYPEHASH, data.arbiter, data.sponsor, data.nonce, data.expires, data.id, data.amount)) - ) - ); - } - - function _domainSeparator(address verifyingContract) internal view returns (bytes32) { - return keccak256(abi.encode(keccak256(bytes(EIP712_DOMAIN_TYPE)), keccak256(bytes(name)), keccak256(bytes(version)), block.chainid, verifyingContract)); - } - - function _signMessage(bytes32 hash_, uint256 signerPK_) internal pure returns (bytes memory) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPK_, hash_); - return abi.encodePacked(r, s, v); - } -} - -abstract contract Deposited is MocksSetup { - function setUp() public virtual override { - super.setUp(); - - vm.startPrank(user); - - usdc.mint(user, defaultAmount); - usdc.approve(address(compactContract), defaultAmount); - compactContract.deposit(address(usdc), address(simpleERC7683Allocator), defaultAmount); - - vm.stopPrank(); - } -} - -abstract contract Locked is Deposited { - function setUp() public virtual override { - super.setUp(); - - vm.startPrank(user); - - defaultExpiration = vm.getBlockTimestamp() + defaultResetPeriod; - simpleERC7683Allocator.lock(Compact({ arbiter: arbiter, sponsor: user, nonce: defaultNonce, id: usdcId, expires: defaultExpiration, amount: defaultAmount })); - - vm.stopPrank(); - } -} \ No newline at end of file From 1b4281a5994a716951fe24a1b3cbbc26732b3fbf Mon Sep 17 00:00:00 2001 From: vimageDE Date: Thu, 27 Feb 2025 12:35:55 +0100 Subject: [PATCH 12/35] improved coverage --- src/allocators/ERC7683Allocator.sol | 6 ---- test/ERC7683Allocator.t.sol | 56 +++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index ccae1e2..9b6f295 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -285,12 +285,6 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { } } - function _castToAddress(bytes32 address_) internal pure returns (address output_) { - assembly ("memory-safe") { - output_ := shr(96, shl(96, address_)) - } - } - function _idToToken(uint256 id_) internal pure returns (address token_) { assembly ("memory-safe") { token_ := shr(96, shl(96, id_)) diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 6310969..37e3341 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -914,3 +914,59 @@ contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { assertEq(resolved.fillInstructions[0].originData, resolvedCrossChainOrder.fillInstructions[0].originData); } } + +contract ERC7683Allocator_getCompactWitnessTypeString is MocksSetup { + function test_getCompactWitnessTypeString() public view { + assertEq(erc7683Allocator.getCompactWitnessTypeString(), "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt))"); + } +} + +contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData, CreateHash { + function test_revert_checkNonce(uint256 nonce_) public { + address expectedSponsor; + assembly ("memory-safe") { + expectedSponsor := shr(96, shl(96, nonce_)) + } + vm.assume(user != expectedSponsor); + + vm.expectRevert(abi.encodeWithSelector(IERC7683Allocator.InvalidNonce.selector, nonce_)); + erc7683Allocator.checkNonce(user, nonce_); + } + + function test_checkNonce_unused(uint96 nonce_) public view { + address sponsor = user; + uint256 nonce; + assembly ("memory-safe") { + nonce := or(shl(160, nonce_), shr(96, shl(96, sponsor))) + } + assertEq(erc7683Allocator.checkNonce(sponsor, nonce), true); + } + + function test_checkNonce_used() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); + + // register a claim + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); + + vm.stopPrank(); + + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + vm.prank(user); + erc7683Allocator.open(onChainCrossChainOrder_); + + + vm.prank(user); + vm.assertEq(erc7683Allocator.checkNonce(user, defaultNonce), false); + } +} From 01417c6bc0a2542f248bedfcafaeed1b3ac69d71 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Thu, 27 Feb 2025 15:50:44 +0100 Subject: [PATCH 13/35] use stable foundry version --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 90a129f..d0158b5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -24,7 +24,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: stable - name: Run Forge build run: | From ea8e05fb75b4b3310dfacd972284688aa299a92c Mon Sep 17 00:00:00 2001 From: vimageDE Date: Thu, 27 Feb 2025 15:53:54 +0100 Subject: [PATCH 14/35] remove compilation restrictions --- foundry.toml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/foundry.toml b/foundry.toml index 2b355c8..ced049e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -22,14 +22,6 @@ remappings = [ "@solady=lib/solady/src", ] -additional_compiler_profiles = [ - { name = "test", via_ir = false } -] - -compilation_restrictions = [ - { paths = "test/**", via_ir = false } -] - [profile.default.fuzz] runs = 1000 From e4ebdeb9d8961bd9d1a8c2008c16c8037b2a6720 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Thu, 27 Feb 2025 15:59:48 +0100 Subject: [PATCH 15/35] readme fixed --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 00cf797..61de21f 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,10 @@ If you intend to develop on this repo, follow the steps outlined in [CONTRIBUTIN ## Allocators The allocators are designed to be used with the [The Compact](https://github.com/uniswap/the-compact). Their purpose is to ensure that locked tokens are available to claim for fillers within the promised expiration time. This repository contains multiple allocators, each with different features: - - [ServerAllocator](src/allocators/ServerAllocator.sol): The ServerAllocator stands as an on chain verification contract for a server based allocator. It is ready for the callbacks of the [The Compact](https://github.com/uniswap/the-compact) during a claim and verifies the allocator signatures have been signed by an authorized address. It does not keep track of any locked down tokens, but instead relies on the server to do so. - - [SimpleAllocator](src/allocators/SimpleAllocator.sol): A simple, fully decentralized allocator that allows for a single claim per token. This means the contract will lock down all tokens of a sponsor for an id for a single claim, so it is not possible to start multiple claims for the same sponsor and id at the same time. The contract does though keep track of the amount of locked tokens and so it will faithfully attest for a transfer of those, even during an ongoing claim. The contract is a good starting point when learning about allocators and it is kept very simple on purpose to learn about the concept of an allocator or use this contract as a template. To be used in production, the contract would require the ability to work with witness data, since a real cross chain swap will always require a witness besides the Compact. An example implementation of a witness allocator can be found [here](src/allocators/SimpleWitnessAllocator.sol). - - [SimpleWitnessAllocator](src/allocators/SimpleWitnessAllocator.sol): This contract enhances the [SimpleAllocator](src/allocators/SimpleAllocator.sol) with the ability of processing witness data besides the Compact. This makes it a much more production ready allocator. - - [SimpleERC7683Allocator](src/allocators/SimpleERC7683Allocator.sol): This contract enhances the [SimpleAllocator](src/allocators/SimpleAllocator.sol) and making it compatible with the [ERC7683](https://eips.ethereum.org/EIPS/eip-7683) standard. The Allocator therefor also becomes a [IOriginSettler](src/interfaces/ERC7683/IOriginSettler.sol) and converts a OnchainCrossChainOrder to a `Compact`/`BatchCompact` and a `Claim` / `Mendate` as required by the tribunal on the target chain. +- [ServerAllocator](src/allocators/ServerAllocator.sol): The ServerAllocator stands as an on chain verification contract for a server based allocator. It is ready for the callbacks of the [The Compact](https://github.com/uniswap/the-compact) during a claim and verifies the allocator signatures have been signed by an authorized address. It does not keep track of any locked down tokens, but instead relies on the server to do so. +- [SimpleAllocator](src/allocators/SimpleAllocator.sol): A simple, fully decentralized allocator that allows for a single claim per token. This means the contract will lock down all tokens of a sponsor for an id for a single claim, so it is not possible to start multiple claims for the same sponsor and id at the same time. The contract does though keep track of the amount of locked tokens and so it will faithfully attest for a transfer of those, even during an ongoing claim. The contract is a good starting point when learning about allocators and it is kept very simple on purpose to learn about the concept of an allocator or use this contract as a template. To be used in production, the contract would require the ability to work with witness data, since a real cross chain swap will always require a witness besides the Compact. An example implementation of a witness allocator can be found [here](src/allocators/SimpleWitnessAllocator.sol). +- [SimpleWitnessAllocator](src/allocators/SimpleWitnessAllocator.sol): This contract enhances the [SimpleAllocator](src/allocators/SimpleAllocator.sol) with the ability of processing witness data besides the Compact. This makes it a much more production ready allocator. +- [SimpleERC7683Allocator](src/allocators/SimpleERC7683Allocator.sol): This contract enhances the [SimpleAllocator](src/allocators/SimpleAllocator.sol) and making it compatible with the [ERC7683](https://eips.ethereum.org/EIPS/eip-7683) standard. The Allocator therefor also becomes a [IOriginSettler](src/interfaces/ERC7683/IOriginSettler.sol) and converts a OnchainCrossChainOrder to a `Compact`/`BatchCompact` and a `Claim` / `Mendate` as required by the tribunal on the target chain. ## Deployment From 3c22b3f28f7ea98301276e50f99850f44165c739 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Fri, 28 Feb 2025 13:01:52 +0100 Subject: [PATCH 16/35] bound fuzz tests --- test/ServerAllocator.t.sol | 2 +- test/SimpleAllocator.t.sol | 42 ++++++++++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/test/ServerAllocator.t.sol b/test/ServerAllocator.t.sol index 0200441..f7b804a 100644 --- a/test/ServerAllocator.t.sol +++ b/test/ServerAllocator.t.sol @@ -244,7 +244,7 @@ contract ServerAllocator_Attest is SignerSet { function test_fuzz_registerAttest_attestExpired(uint256 expiration_) public { uint256 targetTime = vm.getBlockTimestamp() + 100; vm.warp(targetTime); - vm.assume(expiration_ < targetTime); + expiration_ = bound(expiration_, 0, targetTime - 1); vm.prank(signer); vm.expectRevert(abi.encodeWithSelector(IServerAllocator.Expired.selector, expiration_, vm.getBlockTimestamp())); diff --git a/test/SimpleAllocator.t.sol b/test/SimpleAllocator.t.sol index 1a2ff74..d4ca517 100644 --- a/test/SimpleAllocator.t.sol +++ b/test/SimpleAllocator.t.sol @@ -174,7 +174,7 @@ contract SimpleAllocator_Lock is MocksSetup { } function test_revert_InvalidExpiration_tooShort(uint128 delay_) public { - vm.assume(delay_ < simpleAllocator.MIN_WITHDRAWAL_DELAY()); + delay_ = uint128(bound(delay_, 0, simpleAllocator.MIN_WITHDRAWAL_DELAY() - 1)); uint256 expiration = vm.getBlockTimestamp() + delay_; vm.prank(user); vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidExpiration.selector, expiration)); @@ -208,9 +208,16 @@ contract SimpleAllocator_Lock is MocksSetup { } function test_revert_ForceWithdrawalAvailable_ExpirationLongerThenResetPeriod(uint8 delay_) public { - vm.assume(delay_ > simpleAllocator.MIN_WITHDRAWAL_DELAY()); - vm.assume(delay_ < simpleAllocator.MAX_WITHDRAWAL_DELAY()); - vm.assume(delay_ > defaultResetPeriod); + // Use bound to ensure delay_ is within valid range but greater than resetPeriod + delay_ = uint8( + bound( + delay_, + simpleAllocator.MIN_WITHDRAWAL_DELAY() > defaultResetPeriod + ? simpleAllocator.MIN_WITHDRAWAL_DELAY() + 1 + : defaultResetPeriod + 1, + simpleAllocator.MAX_WITHDRAWAL_DELAY() - 1 + ) + ); uint256 expiration = vm.getBlockTimestamp() + delay_; uint256 maxExpiration = vm.getBlockTimestamp() + defaultResetPeriod; @@ -319,9 +326,15 @@ contract SimpleAllocator_Lock is MocksSetup { } function test_successfullyLocked(uint256 nonce_, uint128 amount_, uint32 delay_) public { - vm.assume(delay_ > simpleAllocator.MIN_WITHDRAWAL_DELAY()); - vm.assume(delay_ < simpleAllocator.MAX_WITHDRAWAL_DELAY()); - vm.assume(delay_ <= defaultResetPeriod); + delay_ = uint32( + bound( + delay_, + simpleAllocator.MIN_WITHDRAWAL_DELAY() + 1, + defaultResetPeriod < simpleAllocator.MAX_WITHDRAWAL_DELAY() + ? defaultResetPeriod + : simpleAllocator.MAX_WITHDRAWAL_DELAY() - 1 + ) + ); vm.startPrank(user); @@ -356,9 +369,15 @@ contract SimpleAllocator_Lock is MocksSetup { uint128 amount_, uint32 delay_ ) public { - vm.assume(delay_ > simpleAllocator.MIN_WITHDRAWAL_DELAY()); - vm.assume(delay_ < simpleAllocator.MAX_WITHDRAWAL_DELAY()); - vm.assume(delay_ <= defaultResetPeriod); + delay_ = uint32( + bound( + delay_, + simpleAllocator.MIN_WITHDRAWAL_DELAY() + 1, + defaultResetPeriod < simpleAllocator.MAX_WITHDRAWAL_DELAY() + ? defaultResetPeriod + : simpleAllocator.MAX_WITHDRAWAL_DELAY() - 1 + ) + ); vm.assume(noncePrev_ != nonce_); vm.startPrank(user); @@ -474,7 +493,8 @@ contract SimpleAllocator_Attest is Deposited { } function test_successfullyAttested(uint32 lockedAmount_, uint32 transferAmount_) public { - vm.assume(uint256(transferAmount_) + uint256(lockedAmount_) <= defaultAmount); + transferAmount_ = uint32(bound(transferAmount_, 0, defaultAmount)); + lockedAmount_ = uint32(bound(lockedAmount_, 0, defaultAmount - transferAmount_)); address otherUser = makeAddr('otherUser'); From 69de0ee96fffb7646b9baf35bb04db08775258a6 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Fri, 28 Feb 2025 13:04:17 +0100 Subject: [PATCH 17/35] bytecode_hash = "none" to fix theCompact size --- foundry.toml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/foundry.toml b/foundry.toml index ced049e..f6597d1 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,7 +5,7 @@ libs = ["lib"] optimizer = true optimizer_runs = 999999 via_ir = true -solc = "0.8.27" +solc = "0.8.28" verbosity = 2 ffi = true evm_version = "cancun" @@ -13,7 +13,6 @@ fs_permissions = [ { access = "read-write", path = ".forge-snapshots"}, { access = "read", path = "script/" } ] - remappings = [ "forge-std=lib/forge-std/src", "@openzeppelin/contracts=lib/openzeppelin-contracts/contracts", @@ -22,6 +21,16 @@ remappings = [ "@solady=lib/solady/src", ] +[profile.ci] +inherit = "default" +optimizer_runs = 200 # Override optimizer runs to reduce the compact contract sizes +bytecode_hash = 'none' + +[profile.pr] +inherit = "default" +optimizer_runs = 200 # Override optimizer runs to reduce the compact contract sizes +bytecode_hash = 'none' + [profile.default.fuzz] runs = 1000 From ca98b6ab1a0dc51341fe2542d8dce83c04470f69 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Fri, 28 Feb 2025 14:54:29 +0100 Subject: [PATCH 18/35] contract + interface + tests --- src/allocators/SimpleAllocator.sol | 229 +++++++++ src/interfaces/ISimpleAllocator.sol | 62 +++ src/test/TheCompactMock.sol | 45 +- test/SimpleAllocator.t.sol | 762 ++++++++++++++++++++++++++++ 4 files changed, 1080 insertions(+), 18 deletions(-) create mode 100644 src/allocators/SimpleAllocator.sol create mode 100644 src/interfaces/ISimpleAllocator.sol create mode 100644 test/SimpleAllocator.t.sol diff --git a/src/allocators/SimpleAllocator.sol b/src/allocators/SimpleAllocator.sol new file mode 100644 index 0000000..5b3222d --- /dev/null +++ b/src/allocators/SimpleAllocator.sol @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {IAllocator} from '../interfaces/IAllocator.sol'; +import {ISimpleAllocator} from '../interfaces/ISimpleAllocator.sol'; +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; +import {ERC6909} from '@solady/tokens/ERC6909.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; +import {ResetPeriod} from '@uniswap/the-compact/lib/IdLib.sol'; +import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {ForcedWithdrawalStatus} from '@uniswap/the-compact/types/ForcedWithdrawalStatus.sol'; + +contract SimpleAllocator is ISimpleAllocator { + // keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount)") + bytes32 constant COMPACT_TYPEHASH = 0xcdca950b17b5efc016b74b912d8527dfba5e404a688cbc3dab16cb943287fec2; + + address public immutable COMPACT_CONTRACT; + uint256 public immutable MIN_WITHDRAWAL_DELAY; + uint256 public immutable MAX_WITHDRAWAL_DELAY; + + /// @dev mapping of tokenHash to the expiration of the lock + mapping(bytes32 tokenHash => uint256 expiration) internal _claim; + /// @dev mapping of tokenHash to the amount of the lock + mapping(bytes32 tokenHash => uint256 amount) internal _amount; + /// @dev mapping of tokenHash to the nonce of the lock + mapping(bytes32 tokenHash => uint256 nonce) internal _nonce; + /// @dev mapping of the lock digest to the tokenHash of the lock + mapping(bytes32 digest => bytes32 tokenHash) internal _sponsor; + + constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) { + COMPACT_CONTRACT = compactContract_; + MIN_WITHDRAWAL_DELAY = minWithdrawalDelay_; + MAX_WITHDRAWAL_DELAY = maxWithdrawalDelay_; + + ITheCompact(COMPACT_CONTRACT).__registerAllocator(address(this), ''); + } + + /// @inheritdoc ISimpleAllocator + function lock(Compact calldata compact_) external { + bytes32 tokenHash = _checkAllocation(compact_); + + bytes32 digest = keccak256( + abi.encodePacked( + bytes2(0x1901), + ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + COMPACT_TYPEHASH, + compact_.arbiter, + compact_.sponsor, + compact_.nonce, + compact_.expires, + compact_.id, + compact_.amount + ) + ) + ) + ); + + _claim[tokenHash] = compact_.expires; + _amount[tokenHash] = compact_.amount; + _nonce[tokenHash] = compact_.nonce; + _sponsor[digest] = tokenHash; + + emit Locked(compact_.sponsor, compact_.id, compact_.amount, compact_.expires); + } + + /// @inheritdoc IAllocator + function attest(address operator_, address from_, address, uint256 id_, uint256 amount_) + external + view + returns (bytes4) + { + if (msg.sender != COMPACT_CONTRACT) { + revert InvalidCaller(msg.sender, COMPACT_CONTRACT); + } + // For a transfer, the sponsor is the arbiter + if (operator_ != from_) { + revert InvalidCaller(operator_, from_); + } + uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(from_, id_); + // Check unlocked balance + bytes32 tokenHash = _getTokenHash(id_, from_); + + uint256 fullAmount = amount_; + if (_claim[tokenHash] > block.timestamp) { + // Lock is still active, add the locked amount if the nonce has not yet been consumed + fullAmount += ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)) + ? 0 + : _amount[tokenHash]; + } + if (balance < fullAmount) { + revert InsufficientBalance(from_, id_, balance, fullAmount); + } + + return 0x1a808f91; + } + + /// @inheritdoc IERC1271 + /// @dev we trust the compact contract to check the nonce is not already consumed + function isValidSignature(bytes32 hash, bytes calldata) external view returns (bytes4 magicValue) { + // The hash is the digest of the compact + bytes32 tokenHash = _sponsor[hash]; + if (tokenHash == bytes32(0) || _claim[tokenHash] <= block.timestamp) { + revert InvalidLock(hash, _claim[tokenHash]); + } + + return IERC1271.isValidSignature.selector; + } + + /// @inheritdoc ISimpleAllocator + function checkTokensLocked(uint256 id_, address sponsor_) + external + view + returns (uint256 amount_, uint256 expires_) + { + bytes32 tokenHash = _getTokenHash(id_, sponsor_); + uint256 expires = _claim[tokenHash]; + if ( + expires <= block.timestamp + || ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)) + ) { + return (0, 0); + } + + return (_amount[tokenHash], expires); + } + + /// @inheritdoc ISimpleAllocator + function checkCompactLocked(Compact calldata compact_) external view returns (bool locked_, uint256 expires_) { + bytes32 tokenHash = _getTokenHash(compact_.id, compact_.sponsor); + bytes32 digest = keccak256( + abi.encodePacked( + bytes2(0x1901), + ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + COMPACT_TYPEHASH, + compact_.arbiter, + compact_.sponsor, + compact_.nonce, + compact_.expires, + compact_.id, + compact_.amount + ) + ) + ) + ); + uint256 expires = _claim[tokenHash]; + bool active = _sponsor[digest] == tokenHash && expires > block.timestamp + && !ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)); + if (active) { + (ForcedWithdrawalStatus status, uint256 forcedWithdrawalAvailableAt) = + ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus(compact_.sponsor, compact_.id); + if (status == ForcedWithdrawalStatus.Enabled && forcedWithdrawalAvailableAt < expires) { + expires = forcedWithdrawalAvailableAt; + active = expires > block.timestamp; + } + } + return (active, active ? expires : 0); + } + + function _getTokenHash(uint256 id_, address sponsor_) internal pure returns (bytes32) { + return keccak256(abi.encode(id_, sponsor_)); + } + + function _checkAllocation(Compact memory compact_) internal view returns (bytes32) { + // Check msg.sender is sponsor + if (msg.sender != compact_.sponsor) { + revert InvalidCaller(msg.sender, compact_.sponsor); + } + bytes32 tokenHash = _getTokenHash(compact_.id, msg.sender); + // Check no lock is already active for this sponsor + if ( + _claim[tokenHash] > block.timestamp + && !ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(_nonce[tokenHash], address(this)) + ) { + revert ClaimActive(compact_.sponsor); + } + // Check expiration is not too soon or too late + if ( + compact_.expires < block.timestamp + MIN_WITHDRAWAL_DELAY + || compact_.expires > block.timestamp + MAX_WITHDRAWAL_DELAY + ) { + revert InvalidExpiration(compact_.expires); + } + (, address allocator, ResetPeriod resetPeriod,) = ITheCompact(COMPACT_CONTRACT).getLockDetails(compact_.id); + if (allocator != address(this)) { + revert InvalidAllocator(allocator); + } + // Check expiration is not longer then the tokens forced withdrawal time + if (compact_.expires > block.timestamp + _resetPeriodToSeconds(resetPeriod)) { + revert ForceWithdrawalAvailable(compact_.expires, block.timestamp + _resetPeriodToSeconds(resetPeriod)); + } + // Check expiration is not past an active force withdrawal + (, uint256 forcedWithdrawalExpiration) = + ITheCompact(COMPACT_CONTRACT).getForcedWithdrawalStatus(compact_.sponsor, compact_.id); + if (forcedWithdrawalExpiration != 0 && forcedWithdrawalExpiration < compact_.expires) { + revert ForceWithdrawalAvailable(compact_.expires, forcedWithdrawalExpiration); + } + // Check nonce is not yet consumed + if (ITheCompact(COMPACT_CONTRACT).hasConsumedAllocatorNonce(compact_.nonce, address(this))) { + revert NonceAlreadyConsumed(compact_.nonce); + } + + uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(msg.sender, compact_.id); + // Check balance is enough + if (balance < compact_.amount) { + revert InsufficientBalance(msg.sender, compact_.id, balance, compact_.amount); + } + + return tokenHash; + } + + /// @dev copied from IdLib.sol + function _resetPeriodToSeconds(ResetPeriod resetPeriod_) internal pure returns (uint256 duration) { + assembly ("memory-safe") { + // Bitpacked durations in 24-bit segments: + // 278d00 094890 015180 000f3c 000258 00003c 00000f 000001 + // 30 days 7 days 1 day 1 hour 10 min 1 min 15 sec 1 sec + let bitpacked := 0x278d00094890015180000f3c00025800003c00000f000001 + + // Shift right by period * 24 bits & mask the least significant 24 bits. + duration := and(shr(mul(resetPeriod_, 24), bitpacked), 0xffffff) + } + return duration; + } +} diff --git a/src/interfaces/ISimpleAllocator.sol b/src/interfaces/ISimpleAllocator.sol new file mode 100644 index 0000000..0e83fd2 --- /dev/null +++ b/src/interfaces/ISimpleAllocator.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {IAllocator} from '../interfaces/IAllocator.sol'; +import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; + +interface ISimpleAllocator is IAllocator { + /// @notice Thrown if a claim is already active + error ClaimActive(address sponsor); + + /// @notice Thrown if the caller is invalid + error InvalidCaller(address caller, address expected); + + /// @notice Thrown if the nonce has already been consumed on the compact contract + error NonceAlreadyConsumed(uint256 nonce); + + /// @notice Thrown if the sponsor does not have enough balance to lock the amount + error InsufficientBalance(address sponsor, uint256 id, uint256 balance, uint256 expectedBalance); + + /// @notice Thrown if the provided expiration is not valid + error InvalidExpiration(uint256 expires); + + /// @notice Thrown if the expiration is longer then the tokens forced withdrawal time + error ForceWithdrawalAvailable(uint256 expires, uint256 forcedWithdrawalExpiration); + + /// @notice Thrown if the allocator is not the one expected + error InvalidAllocator(address allocator); + + /// @notice Thrown if the provided lock is not available or expired + /// @dev The expiration will be '0' if no lock is available + error InvalidLock(bytes32 digest, uint256 expiration); + + /// @notice Emitted when a lock is successfully created + /// @param sponsor The address of the sponsor + /// @param id The id of the token + /// @param amount The amount of the token that was available for locking (the full balance of the token will get locked) + /// @param expires The expiration of the lock + event Locked(address indexed sponsor, uint256 indexed id, uint256 amount, uint256 expires); + + /// @notice Locks the tokens of an id for a claim + /// @dev Locks all tokens of a sponsor for an id + /// @param compact_ The compact that contains the data about the lock + function lock(Compact calldata compact_) external; + + /// @notice Checks if the tokens of a sponsor for an id are locked + /// @param id_ The id of the token + /// @param sponsor_ The address of the sponsor + /// @return amount_ The amount of the token that was available for locking (the full balance of the token will get locked) + /// @return expires_ The expiration of the lock + function checkTokensLocked(uint256 id_, address sponsor_) + external + view + returns (uint256 amount_, uint256 expires_); + + /// @notice Checks if the a lock for the compact exists and is active + /// @dev Also checks if the provided nonce has not yet been consumed on the compact contract + /// @param compact_ The compact that contains the data about the lock + /// @return locked_ Whether the compact is locked + /// @return expires_ The expiration of the lock + function checkCompactLocked(Compact calldata compact_) external view returns (bool locked_, uint256 expires_); +} diff --git a/src/test/TheCompactMock.sol b/src/test/TheCompactMock.sol index 0a63f0d..4dcee68 100644 --- a/src/test/TheCompactMock.sol +++ b/src/test/TheCompactMock.sol @@ -2,14 +2,14 @@ pragma solidity ^0.8.27; -import { IAllocator } from "src/interfaces/IAllocator.sol"; -import { ERC6909 } from "@solady/tokens/ERC6909.sol"; -import { ERC20 } from "@solady/tokens/ERC20.sol"; -import { IdLib } from "@uniswap/the-compact/lib/IdLib.sol"; -import { ForcedWithdrawalStatus } from "@uniswap/the-compact/types/ForcedWithdrawalStatus.sol"; -import { ResetPeriod } from "@uniswap/the-compact/types/ResetPeriod.sol"; -import { Scope } from "@uniswap/the-compact/types/Scope.sol"; -import { console2 } from "forge-std/console2.sol"; +import {ERC20} from '@solady/tokens/ERC20.sol'; +import {ERC6909} from '@solady/tokens/ERC6909.sol'; +import {IdLib} from '@uniswap/the-compact/lib/IdLib.sol'; +import {ForcedWithdrawalStatus} from '@uniswap/the-compact/types/ForcedWithdrawalStatus.sol'; +import {ResetPeriod} from '@uniswap/the-compact/types/ResetPeriod.sol'; +import {Scope} from '@uniswap/the-compact/types/Scope.sol'; +import {console2} from 'forge-std/console2.sol'; +import {IAllocator} from 'src/interfaces/IAllocator.sol'; contract TheCompactMock is ERC6909 { using IdLib for uint96; @@ -34,7 +34,7 @@ contract TheCompactMock is ERC6909 { return 0; } - function deposit(address token, uint256 amount, address allocator) external { + function deposit(address token, address allocator, uint256 amount) external { ERC20(token).transferFrom(msg.sender, address(this), amount); uint256 id = _getTokenId(token, allocator); tokens[id] = token; @@ -47,7 +47,9 @@ contract TheCompactMock is ERC6909 { _transfer(address(0), from, to, id, amount); } - function claim(address from, address to, address token, uint256 amount, address allocator, bytes calldata signature) external { + function claim(address from, address to, address token, uint256 amount, address allocator, bytes calldata signature) + external + { uint256 id = _getTokenId(token, allocator); IAllocator(allocator).isValidSignature(keccak256(abi.encode(from, id, amount)), signature); _transfer(address(0), from, to, id, amount); @@ -85,7 +87,11 @@ contract TheCompactMock is ERC6909 { return true; } - function getForcedWithdrawalStatus(address sponsor, uint256) external view returns (ForcedWithdrawalStatus, uint256) { + function getForcedWithdrawalStatus(address sponsor, uint256) + external + view + returns (ForcedWithdrawalStatus, uint256) + { uint256 expires = forcedWithdrawalStatus[sponsor]; return (expires == 0 ? ForcedWithdrawalStatus.Disabled : ForcedWithdrawalStatus.Enabled, expires); } @@ -99,8 +105,8 @@ contract TheCompactMock is ERC6909 { abi.encode( // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f, - keccak256("The Compact"), - keccak256("0"), + keccak256('The Compact'), + keccak256('0'), block.chainid, address(this) ) @@ -110,22 +116,25 @@ contract TheCompactMock is ERC6909 { function name( uint256 // id ) public view virtual override returns (string memory) { - return "TheCompactMock"; + return 'TheCompactMock'; } function symbol( uint256 // id ) public view virtual override returns (string memory) { - return "TCM"; + return 'TCM'; } function tokenURI( uint256 // id ) public view virtual override returns (string memory) { - return ""; + return ''; } - function _getTokenId(address token, address allocator) internal pure returns (uint256) { - return uint256(keccak256(abi.encode(token, allocator))); + function _getTokenId(address token, address allocator) internal pure returns (uint256 tokenId) { + assembly ("memory-safe") { + tokenId := or(shl(160, allocator), shr(96, shl(96, token))) + } + return tokenId; } } diff --git a/test/SimpleAllocator.t.sol b/test/SimpleAllocator.t.sol new file mode 100644 index 0000000..d4ca517 --- /dev/null +++ b/test/SimpleAllocator.t.sol @@ -0,0 +1,762 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; +import {ERC6909} from '@solady/tokens/ERC6909.sol'; +import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; +import {COMPACT_TYPEHASH, Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; +import {ForcedWithdrawalStatus} from '@uniswap/the-compact/types/ForcedWithdrawalStatus.sol'; +import {Test} from 'forge-std/Test.sol'; + +import {console} from 'forge-std/console.sol'; +import {SimpleAllocator} from 'src/allocators/SimpleAllocator.sol'; +import {ISimpleAllocator} from 'src/interfaces/ISimpleAllocator.sol'; +import {ERC20Mock} from 'src/test/ERC20Mock.sol'; +import {TheCompactMock} from 'src/test/TheCompactMock.sol'; + +abstract contract MocksSetup is Test { + address user; + uint256 userPK; + address attacker; + uint256 attackerPK; + address arbiter; + ERC20Mock usdc; + TheCompactMock compactContract; + SimpleAllocator simpleAllocator; + uint256 usdcId; + + uint256 defaultResetPeriod = 60; + uint256 defaultAmount = 1000; + uint256 defaultNonce = 1; + uint256 defaultExpiration; + + function setUp() public virtual { + arbiter = makeAddr('arbiter'); + usdc = new ERC20Mock('USDC', 'USDC'); + compactContract = new TheCompactMock(); + simpleAllocator = new SimpleAllocator(address(compactContract), 5, 100); + usdcId = compactContract.getTokenId(address(usdc), address(simpleAllocator)); + (user, userPK) = makeAddrAndKey('user'); + (attacker, attackerPK) = makeAddrAndKey('attacker'); + } +} + +abstract contract CreateHash is Test { + struct Allocator { + bytes32 hash; + } + + // stringified types + string EIP712_DOMAIN_TYPE = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'; // Hashed inside the funcion + // EIP712 domain type + string name = 'The Compact'; + string version = '0'; + + function _hashCompact(Compact memory data, address verifyingContract) internal view returns (bytes32) { + // hash typed data + return keccak256( + abi.encodePacked( + '\x19\x01', // backslash is needed to escape the character + _domainSeparator(verifyingContract), + keccak256( + abi.encode( + COMPACT_TYPEHASH, data.arbiter, data.sponsor, data.nonce, data.expires, data.id, data.amount + ) + ) + ) + ); + } + + function _domainSeparator(address verifyingContract) internal view returns (bytes32) { + return keccak256( + abi.encode( + keccak256(bytes(EIP712_DOMAIN_TYPE)), + keccak256(bytes(name)), + keccak256(bytes(version)), + block.chainid, + verifyingContract + ) + ); + } + + function _signMessage(bytes32 hash_, uint256 signerPK_) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPK_, hash_); + return abi.encodePacked(r, s, v); + } +} + +abstract contract Deposited is MocksSetup { + function setUp() public virtual override { + super.setUp(); + + vm.startPrank(user); + + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit(address(usdc), address(simpleAllocator), defaultAmount); + + vm.stopPrank(); + } +} + +abstract contract Locked is Deposited { + function setUp() public virtual override { + super.setUp(); + + vm.startPrank(user); + + defaultExpiration = vm.getBlockTimestamp() + defaultResetPeriod; + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + }) + ); + + vm.stopPrank(); + } +} + +contract SimpleAllocator_Lock is MocksSetup { + function test_revert_InvalidCaller() public { + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidCaller.selector, user, attacker)); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: attacker, + nonce: 1, + id: usdcId, + expires: block.timestamp + 1, + amount: 1000 + }) + ); + } + + function test_revert_ClaimActive() public { + vm.startPrank(user); + + // Mint, approve and deposit + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit(address(usdc), address(simpleAllocator), defaultAmount); + + // Successfully locked + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: block.timestamp + defaultResetPeriod, + amount: defaultAmount + }) + ); + + vm.warp(block.timestamp + defaultResetPeriod - 1); + + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.ClaimActive.selector, user)); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce + 1, + id: usdcId, + expires: block.timestamp + defaultResetPeriod, + amount: defaultAmount + }) + ); + } + + function test_revert_InvalidExpiration_tooShort(uint128 delay_) public { + delay_ = uint128(bound(delay_, 0, simpleAllocator.MIN_WITHDRAWAL_DELAY() - 1)); + uint256 expiration = vm.getBlockTimestamp() + delay_; + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidExpiration.selector, expiration)); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: 1, + id: usdcId, + expires: vm.getBlockTimestamp() + delay_, + amount: 1000 + }) + ); + } + + function test_revert_InvalidExpiration_tooLong(uint128 delay_) public { + vm.assume(delay_ > simpleAllocator.MAX_WITHDRAWAL_DELAY()); + uint256 expiration = vm.getBlockTimestamp() + delay_; + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidExpiration.selector, expiration)); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: 1, + id: usdcId, + expires: vm.getBlockTimestamp() + delay_, + amount: 1000 + }) + ); + } + + function test_revert_ForceWithdrawalAvailable_ExpirationLongerThenResetPeriod(uint8 delay_) public { + // Use bound to ensure delay_ is within valid range but greater than resetPeriod + delay_ = uint8( + bound( + delay_, + simpleAllocator.MIN_WITHDRAWAL_DELAY() > defaultResetPeriod + ? simpleAllocator.MIN_WITHDRAWAL_DELAY() + 1 + : defaultResetPeriod + 1, + simpleAllocator.MAX_WITHDRAWAL_DELAY() - 1 + ) + ); + + uint256 expiration = vm.getBlockTimestamp() + delay_; + uint256 maxExpiration = vm.getBlockTimestamp() + defaultResetPeriod; + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector(ISimpleAllocator.ForceWithdrawalAvailable.selector, expiration, maxExpiration) + ); + simpleAllocator.lock( + Compact({arbiter: arbiter, sponsor: user, nonce: 1, id: usdcId, expires: expiration, amount: 1000}) + ); + } + + function test_revert_ForceWithdrawalAvailable_ScheduledForceWithdrawal() public { + vm.startPrank(user); + compactContract.enableForceWithdrawal(usdcId); + + // move time forward + vm.warp(vm.getBlockTimestamp() + 1); + + // This expiration should be fine, if the force withdrawal was not enabled + uint256 expiration = vm.getBlockTimestamp() + defaultResetPeriod; + // check force withdrawal + (ForcedWithdrawalStatus status, uint256 expires) = compactContract.getForcedWithdrawalStatus(user, usdcId); + assertEq(status == ForcedWithdrawalStatus.Enabled, true); + assertEq(expires, expiration - 1); + + vm.expectRevert( + abi.encodeWithSelector(ISimpleAllocator.ForceWithdrawalAvailable.selector, expiration, expiration - 1) + ); + simpleAllocator.lock( + Compact({arbiter: arbiter, sponsor: user, nonce: 1, id: usdcId, expires: expiration, amount: 1000}) + ); + } + + function test_revert_ForceWithdrawalAvailable_ActiveForceWithdrawal() public { + vm.startPrank(user); + compactContract.enableForceWithdrawal(usdcId); + + // move time forward + uint256 forceWithdrawalTimestamp = vm.getBlockTimestamp() + defaultResetPeriod; + vm.warp(forceWithdrawalTimestamp); + + // This expiration should be fine, if the force withdrawal was not enabled + uint256 expiration = vm.getBlockTimestamp() + defaultResetPeriod; + // check force withdrawal + (ForcedWithdrawalStatus status, uint256 expires) = compactContract.getForcedWithdrawalStatus(user, usdcId); + assertEq(status == ForcedWithdrawalStatus.Enabled, true); + assertEq(expires, forceWithdrawalTimestamp); + + vm.expectRevert( + abi.encodeWithSelector( + ISimpleAllocator.ForceWithdrawalAvailable.selector, expiration, forceWithdrawalTimestamp + ) + ); + simpleAllocator.lock( + Compact({arbiter: arbiter, sponsor: user, nonce: 1, id: usdcId, expires: expiration, amount: 1000}) + ); + } + + function test_revert_NonceAlreadyConsumed(uint256 nonce_) public { + vm.startPrank(user); + uint256[] memory nonces = new uint256[](1); + nonces[0] = nonce_; + compactContract.consume(nonces); + assertEq(compactContract.hasConsumedAllocatorNonce(nonce_, address(simpleAllocator)), true); + + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.NonceAlreadyConsumed.selector, nonce_)); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: nonce_, + id: usdcId, + expires: block.timestamp + defaultResetPeriod, + amount: 1000 + }) + ); + } + + function test_revert_InsufficientBalance(uint256 balance_, uint256 amount_) public { + vm.assume(balance_ < amount_); + + vm.startPrank(user); + + // Mint, approve and deposit + usdc.mint(user, balance_); + usdc.approve(address(compactContract), balance_); + compactContract.deposit(address(usdc), address(simpleAllocator), balance_); + + // Check balance + assertEq(compactContract.balanceOf(user, usdcId), balance_); + + vm.expectRevert( + abi.encodeWithSelector(ISimpleAllocator.InsufficientBalance.selector, user, usdcId, balance_, amount_) + ); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: 1, + id: usdcId, + expires: block.timestamp + defaultResetPeriod, + amount: amount_ + }) + ); + } + + function test_successfullyLocked(uint256 nonce_, uint128 amount_, uint32 delay_) public { + delay_ = uint32( + bound( + delay_, + simpleAllocator.MIN_WITHDRAWAL_DELAY() + 1, + defaultResetPeriod < simpleAllocator.MAX_WITHDRAWAL_DELAY() + ? defaultResetPeriod + : simpleAllocator.MAX_WITHDRAWAL_DELAY() - 1 + ) + ); + + vm.startPrank(user); + + // Mint, approve and deposit + usdc.mint(user, amount_); + usdc.approve(address(compactContract), amount_); + compactContract.deposit(address(usdc), address(simpleAllocator), amount_); + + // Check no lock exists + (uint256 amountBefore, uint256 expiresBefore) = simpleAllocator.checkTokensLocked(usdcId, user); + + assertEq(amountBefore, 0); + assertEq(expiresBefore, 0); + + uint256 expiration = vm.getBlockTimestamp() + delay_; + vm.expectEmit(true, true, false, true); + emit ISimpleAllocator.Locked(user, usdcId, amount_, expiration); + simpleAllocator.lock( + Compact({arbiter: arbiter, sponsor: user, nonce: nonce_, id: usdcId, expires: expiration, amount: amount_}) + ); + + // Check lock exists + (uint256 amountAfter, uint256 expiresAfter) = simpleAllocator.checkTokensLocked(usdcId, user); + + assertEq(amountAfter, amount_); + assertEq(expiresAfter, expiration); + } + + function test_successfullyLocked_AfterNonceConsumption( + uint256 nonce_, + uint256 noncePrev_, + uint128 amount_, + uint32 delay_ + ) public { + delay_ = uint32( + bound( + delay_, + simpleAllocator.MIN_WITHDRAWAL_DELAY() + 1, + defaultResetPeriod < simpleAllocator.MAX_WITHDRAWAL_DELAY() + ? defaultResetPeriod + : simpleAllocator.MAX_WITHDRAWAL_DELAY() - 1 + ) + ); + vm.assume(noncePrev_ != nonce_); + + vm.startPrank(user); + + // Mint, approve and deposit + usdc.mint(user, amount_); + usdc.approve(address(compactContract), amount_); + compactContract.deposit(address(usdc), address(simpleAllocator), amount_); + + // Create a previous lock + uint256 expirationPrev = vm.getBlockTimestamp() + delay_; + vm.expectEmit(true, true, false, true); + emit ISimpleAllocator.Locked(user, usdcId, amount_, expirationPrev); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: noncePrev_, + id: usdcId, + expires: expirationPrev, + amount: amount_ + }) + ); + + // Check a previous lock exists + (uint256 amountBefore, uint256 expiresBefore) = simpleAllocator.checkTokensLocked(usdcId, user); + assertEq(amountBefore, amount_); + assertEq(expiresBefore, expirationPrev); + + // Check for revert if previous nonce not consumed + uint256 expiration = vm.getBlockTimestamp() + delay_; + + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.ClaimActive.selector, user)); + simpleAllocator.lock( + Compact({arbiter: arbiter, sponsor: user, nonce: nonce_, id: usdcId, expires: expiration, amount: amount_}) + ); + + // Consume previous nonce + uint256[] memory nonces = new uint256[](1); + nonces[0] = noncePrev_; + vm.stopPrank(); + vm.prank(address(simpleAllocator)); + compactContract.consume(nonces); + + vm.prank(user); + + vm.expectEmit(true, true, false, true); + emit ISimpleAllocator.Locked(user, usdcId, amount_, expiration); + simpleAllocator.lock( + Compact({arbiter: arbiter, sponsor: user, nonce: nonce_, id: usdcId, expires: expiration, amount: amount_}) + ); + + // Check lock exists + (uint256 amountAfter, uint256 expiresAfter) = simpleAllocator.checkTokensLocked(usdcId, user); + + assertEq(amountAfter, amount_); + assertEq(expiresAfter, expiration); + } +} + +contract SimpleAllocator_Attest is Deposited { + function test_revert_InvalidCaller_NotCompact() public { + vm.prank(attacker); + vm.expectRevert( + abi.encodeWithSelector(ISimpleAllocator.InvalidCaller.selector, attacker, address(compactContract)) + ); + simpleAllocator.attest(address(user), address(user), address(usdc), usdcId, defaultAmount); + } + + function test_revert_InvalidCaller_FromNotOperator() public { + vm.prank(attacker); + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidCaller.selector, attacker, user)); + compactContract.transfer(user, attacker, defaultAmount, address(usdc), address(simpleAllocator)); + } + + function test_revert_InsufficientBalance_NoActiveLock(uint128 falseAmount_) public { + vm.assume(falseAmount_ > defaultAmount); + + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector( + ISimpleAllocator.InsufficientBalance.selector, user, usdcId, defaultAmount, falseAmount_ + ) + ); + compactContract.transfer(user, attacker, falseAmount_, address(usdc), address(simpleAllocator)); + } + + function test_revert_InsufficientBalance_ActiveLock() public { + vm.startPrank(user); + + // Lock a single token + uint256 defaultExpiration_ = vm.getBlockTimestamp() + defaultResetPeriod; + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration_, + amount: 1 + }) + ); + + // At this point, the deposited defaultAmount is not fully available anymore, because one of the tokens was locked + + // Revert if we try to transfer all of the deposited tokens + vm.expectRevert( + abi.encodeWithSelector( + ISimpleAllocator.InsufficientBalance.selector, user, usdcId, defaultAmount, defaultAmount + 1 + ) + ); + compactContract.transfer(user, attacker, defaultAmount, address(usdc), address(simpleAllocator)); + } + + function test_successfullyAttested(uint32 lockedAmount_, uint32 transferAmount_) public { + transferAmount_ = uint32(bound(transferAmount_, 0, defaultAmount)); + lockedAmount_ = uint32(bound(lockedAmount_, 0, defaultAmount - transferAmount_)); + + address otherUser = makeAddr('otherUser'); + + vm.startPrank(user); + // Lock tokens + uint256 defaultExpiration_ = vm.getBlockTimestamp() + defaultResetPeriod; + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration_, + amount: lockedAmount_ + }) + ); + + vm.expectEmit(true, true, true, true); + emit ERC6909.Transfer(address(0), user, otherUser, usdcId, transferAmount_); + compactContract.transfer(user, otherUser, transferAmount_, address(usdc), address(simpleAllocator)); + + // Check that the other user has the tokens + assertEq(compactContract.balanceOf(otherUser, usdcId), transferAmount_); + assertEq(compactContract.balanceOf(user, usdcId), defaultAmount - transferAmount_); + } +} + +contract SimpleAllocator_IsValidSignature is Deposited, CreateHash { + function test_revert_InvalidLock_NoActiveLock() public { + bytes32 digest = _hashCompact( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: block.timestamp + defaultResetPeriod, + amount: defaultAmount + }), + address(compactContract) + ); + + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidLock.selector, digest, 0)); + simpleAllocator.isValidSignature(digest, ''); + } + + function test_revert_InvalidLock_ExpiredLock() public { + vm.startPrank(user); + + // Lock tokens + uint256 defaultExpiration_ = vm.getBlockTimestamp() + defaultResetPeriod; + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration_, + amount: defaultAmount + }) + ); + + // Move time forward so lock has expired + vm.warp(block.timestamp + defaultResetPeriod); + + bytes32 digest = _hashCompact( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration_, + amount: defaultAmount + }), + address(compactContract) + ); + + vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidLock.selector, digest, defaultExpiration_)); + simpleAllocator.isValidSignature(digest, ''); + } + + function test_successfullyValidated() public { + vm.startPrank(user); + + // Lock tokens + uint256 defaultExpiration_ = vm.getBlockTimestamp() + defaultResetPeriod; + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration_, + amount: defaultAmount + }) + ); + + // Move time forward so lock has expired + vm.warp(block.timestamp + defaultResetPeriod - 1); + + bytes32 digest = _hashCompact( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration_, + amount: defaultAmount + }), + address(compactContract) + ); + + bytes4 selector = simpleAllocator.isValidSignature(digest, ''); + assertEq(selector, IERC1271.isValidSignature.selector); + } +} + +contract SimpleAllocator_CheckTokensLocked is Locked { + function test_checkTokensLocked_NoActiveLock() public { + address otherUser = makeAddr('otherUser'); + (uint256 amount, uint256 expires) = simpleAllocator.checkTokensLocked(usdcId, otherUser); + assertEq(amount, 0); + assertEq(expires, 0); + } + + function test_checkTokensLocked_ExpiredLock() public { + (uint256 amount, uint256 expires) = simpleAllocator.checkTokensLocked(usdcId, user); + assertEq(amount, defaultAmount); + assertEq(expires, defaultExpiration); + + vm.warp(defaultExpiration); + + (amount, expires) = simpleAllocator.checkTokensLocked(usdcId, user); + assertEq(amount, 0); + assertEq(expires, 0); + } + + function test_checkTokensLocked_NonceConsumed() public { + (uint256 amount, uint256 expires) = simpleAllocator.checkTokensLocked(usdcId, user); + assertEq(amount, defaultAmount); + assertEq(expires, defaultExpiration); + + uint256[] memory nonces = new uint256[](1); + nonces[0] = defaultNonce; + vm.prank(address(simpleAllocator)); + compactContract.consume(nonces); + + (amount, expires) = simpleAllocator.checkTokensLocked(usdcId, user); + assertEq(amount, 0); + assertEq(expires, 0); + } + + function test_checkTokensLocked_ActiveLock() public { + vm.warp(defaultExpiration - 1); + + (uint256 amount, uint256 expires) = simpleAllocator.checkTokensLocked(usdcId, user); + assertEq(amount, defaultAmount); + assertEq(expires, defaultExpiration); + } + + function test_checkCompactLocked_NoActiveLock() public { + address otherUser = makeAddr('otherUser'); + (bool locked, uint256 expires) = simpleAllocator.checkCompactLocked( + Compact({ + arbiter: arbiter, + sponsor: otherUser, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + }) + ); + assertEq(locked, false); + assertEq(expires, 0); + } + + function test_checkCompactLocked_ExpiredLock() public { + // Confirm that a lock is previously active + (bool locked, uint256 expires) = simpleAllocator.checkCompactLocked( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + }) + ); + assertEq(locked, true); + assertEq(expires, defaultExpiration); + + // Move time forward so lock has expired + vm.warp(defaultExpiration); + + // Check that the lock is no longer active + (locked, expires) = simpleAllocator.checkCompactLocked( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + }) + ); + assertEq(locked, false); + assertEq(expires, 0); + } + + function test_checkCompactLocked_NonceConsumed() public { + // Confirm that a lock is previously active + (bool locked, uint256 expires) = simpleAllocator.checkCompactLocked( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + }) + ); + assertEq(locked, true); + assertEq(expires, defaultExpiration); + + // Consume nonce + uint256[] memory nonces = new uint256[](1); + nonces[0] = defaultNonce; + vm.prank(address(simpleAllocator)); + compactContract.consume(nonces); + + // Check that the lock is no longer active + (locked, expires) = simpleAllocator.checkCompactLocked( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + }) + ); + assertEq(locked, false); + assertEq(expires, 0); + } + + function test_checkCompactLocked_successfully() public { + // Move time forward to last second before expiration + vm.warp(defaultExpiration - 1); + + // Confirm that a lock is active + (bool locked, uint256 expires) = simpleAllocator.checkCompactLocked( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration, + amount: defaultAmount + }) + ); + assertEq(locked, true); + assertEq(expires, defaultExpiration); + } + + // Check a force withdrawal will impact the expiration +} From 04d104029f6c19fc667e7f197eaa08d71c16434b Mon Sep 17 00:00:00 2001 From: vimageDE Date: Fri, 28 Feb 2025 14:56:01 +0100 Subject: [PATCH 19/35] format fix --- src/interfaces/IAllocator.sol | 2 +- src/test/ERC20Mock.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/interfaces/IAllocator.sol b/src/interfaces/IAllocator.sol index 3f187ab..581edb6 100644 --- a/src/interfaces/IAllocator.sol +++ b/src/interfaces/IAllocator.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; -import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; interface IAllocator is IERC1271 { // Called on standard transfers; must return this function selector (0x1a808f91). diff --git a/src/test/ERC20Mock.sol b/src/test/ERC20Mock.sol index 61ea4e6..f8a0ce1 100644 --- a/src/test/ERC20Mock.sol +++ b/src/test/ERC20Mock.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; contract ERC20Mock is ERC20 { - constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) { } + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} function mint(address to, uint256 amount) external { _mint(to, amount); From d7cccaaf44125d34e5be5f7117523240da4ee10b Mon Sep 17 00:00:00 2001 From: vimageDE Date: Fri, 28 Feb 2025 15:20:40 +0100 Subject: [PATCH 20/35] deleted simpleWitnessAllocator contracts --- src/allocators/SimpleWitnessAllocator.sol | 79 ---------------------- src/interfaces/ISimpleWitnessAllocator.sol | 28 -------- 2 files changed, 107 deletions(-) delete mode 100644 src/allocators/SimpleWitnessAllocator.sol delete mode 100644 src/interfaces/ISimpleWitnessAllocator.sol diff --git a/src/allocators/SimpleWitnessAllocator.sol b/src/allocators/SimpleWitnessAllocator.sol deleted file mode 100644 index c6fc3c5..0000000 --- a/src/allocators/SimpleWitnessAllocator.sol +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.27; - -import {IAllocator} from '../interfaces/IAllocator.sol'; -import {ISimpleWitnessAllocator} from '../interfaces/ISimpleWitnessAllocator.sol'; -import {SimpleAllocator} from './SimpleAllocator.sol'; -import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; -import {ERC6909} from '@solady/tokens/ERC6909.sol'; -import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; -import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; - -contract SimpleWitnessAllocator is SimpleAllocator, ISimpleWitnessAllocator { - // abi.decode(bytes("Compact(address arbiter,address "), (bytes32)) - bytes32 constant COMPACT_TYPESTRING_FRAGMENT_ONE = - 0x436f6d70616374286164647265737320617262697465722c6164647265737320; - // abi.decode(bytes("sponsor,uint256 nonce,uint256 ex"), (bytes32)) - bytes32 constant COMPACT_TYPESTRING_FRAGMENT_TWO = - 0x73706f6e736f722c75696e74323536206e6f6e63652c75696e74323536206578; - // abi.decode(bytes("pires,uint256 id,uint256 amount)"), (bytes32)) - bytes32 constant COMPACT_TYPESTRING_FRAGMENT_THREE = - 0x70697265732c75696e743235362069642c75696e7432353620616d6f756e7429; - // uint200(abi.decode(bytes(",Witness witness)Witness("), (bytes25))) - uint200 constant WITNESS_TYPESTRING = 0x2C5769746E657373207769746E657373295769746E65737328; - - constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) - SimpleAllocator(compactContract_, minWithdrawalDelay_, maxWithdrawalDelay_) - {} - - /// @inheritdoc ISimpleWitnessAllocator - function lockWithWitness(Compact calldata compact_, bytes32 typestringHash_, bytes32 witnessHash_) external { - bytes32 tokenHash = _checkAllocation(compact_); - - bytes32 digest = keccak256( - abi.encodePacked( - bytes2(0x1901), - ITheCompact(COMPACT_CONTRACT).DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - typestringHash_, // keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness)Witness(uint256 witnessArgument)") - compact_.arbiter, - compact_.sponsor, - compact_.nonce, - compact_.expires, - compact_.id, - compact_.amount, - witnessHash_ - ) - ) - ) - ); - - _claim[tokenHash] = compact_.expires; - _amount[tokenHash] = compact_.amount; - _nonce[tokenHash] = compact_.nonce; - _sponsor[digest] = tokenHash; - - emit Locked(compact_.sponsor, compact_.id, compact_.amount, compact_.expires); - } - - /// @inheritdoc ISimpleWitnessAllocator - function getTypestringHashForWitness(string calldata witness_) external pure returns (bytes32 typestringHash_) { - assembly ("memory-safe") { - let memoryOffset := mload(0x40) - mstore(memoryOffset, COMPACT_TYPESTRING_FRAGMENT_ONE) - mstore(add(memoryOffset, 0x20), COMPACT_TYPESTRING_FRAGMENT_TWO) - mstore(add(memoryOffset, 0x40), COMPACT_TYPESTRING_FRAGMENT_THREE) - mstore(add(memoryOffset, sub(0x60, 0x01)), shl(56, WITNESS_TYPESTRING)) - let witnessPointer := add(memoryOffset, add(sub(0x60, 0x01), 0x19)) - calldatacopy(witnessPointer, witness_.offset, witness_.length) - let witnessEnd := add(witnessPointer, witness_.length) - mstore8(witnessEnd, 0x29) - typestringHash_ := keccak256(memoryOffset, sub(add(witnessEnd, 0x01), memoryOffset)) - - mstore(0x40, add(or(witnessEnd, 0x1f), 0x20)) - } - return typestringHash_; - } -} diff --git a/src/interfaces/ISimpleWitnessAllocator.sol b/src/interfaces/ISimpleWitnessAllocator.sol deleted file mode 100644 index 3825f98..0000000 --- a/src/interfaces/ISimpleWitnessAllocator.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.27; - -import {ISimpleAllocator} from './ISimpleAllocator.sol'; -import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; - -interface ISimpleWitnessAllocator is ISimpleAllocator { - /// @notice Locks the tokens of an id for a claim with a witness - /// @dev Locks all tokens of a sponsor for an id with a witness - /// @dev example for the typeHash: - /// keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness)Witness(uint256 witnessArgument)") - /// - /// @param compact_ The compact that contains the data about the lock - /// @param typeHash_ The type hash of the full compact, including the witness - /// @param witnessHash_ The witness hash of the witness - function lockWithWitness(Compact calldata compact_, bytes32 typeHash_, bytes32 witnessHash_) external; - - /// @notice Returns the witness typestring hash including a given witness argument - /// @dev example of a witness type string input: - /// "uint256 witnessArgument" - /// @dev full typestring: - /// Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Witness witness)Witness(uint256 witnessArgument) - /// - /// @param witness_ The witness typestring argument - /// @return typestringHash_ The full compact typestring hash, including the witness - function getTypestringHashForWitness(string calldata witness_) external pure returns (bytes32 typestringHash_); -} From e6d9d04737239fbed117297840f62240e6797ac2 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Fri, 28 Feb 2025 15:39:46 +0100 Subject: [PATCH 21/35] format fix --- test/ERC7683Allocator.t.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 37e3341..6c71085 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -917,7 +917,10 @@ contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { contract ERC7683Allocator_getCompactWitnessTypeString is MocksSetup { function test_getCompactWitnessTypeString() public view { - assertEq(erc7683Allocator.getCompactWitnessTypeString(), "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt))"); + assertEq( + erc7683Allocator.getCompactWitnessTypeString(), + 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt))' + ); } } @@ -965,7 +968,6 @@ contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData, CreateHash { vm.prank(user); erc7683Allocator.open(onChainCrossChainOrder_); - vm.prank(user); vm.assertEq(erc7683Allocator.checkNonce(user, defaultNonce), false); } From 6f92df34a53b0793d7b4ee809ec051c1d48a9d42 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Tue, 4 Mar 2025 16:50:39 +0100 Subject: [PATCH 22/35] fixed EIP712 compatibility --- src/allocators/ERC7683Allocator.sol | 20 ++++++++++++-------- test/ERC7683Allocator.t.sol | 8 +++++--- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index 9b6f295..b635c9c 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -10,22 +10,25 @@ import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { - // The typehash of the OrderData struct - // keccak256("OrderData(address arbiter,address sponsor,uint256 nonce,uint256 id,uint256 amount,Mandate mandate) - // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") + /// @notice The typehash of the OrderData struct + // keccak256("OrderData(address arbiter,address sponsor,uint256 nonce,uint256 id,uint256 amount,Mandate mandate) + // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") bytes32 public constant ORDERDATA_TYPEHASH = 0x9e0e1bdb0df35509b65bbc49d209dd42496c5a3f13998f9a74dc842d6932656b; - // The typehash of the OrderDataGasless struct - // keccak256("OrderDataGasless(address arbiter,uint256 id,uint256 amount,Mandate mandate) - // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") + /// @notice The typehash of the OrderDataGasless struct + // keccak256("OrderDataGasless(address arbiter,uint256 id,uint256 amount,Mandate mandate) + // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = 0x9ab67658b7c0f35b64fdadd7adee1e58b6399a8201f38c355d3a109a2d7081d7; - // keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate) - // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") + /// @notice keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate) + // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") bytes32 public constant COMPACT_WITNESS_TYPEHASH = 0x27f09e0bb8ce2ae63380578af7af85055d3ada248c502e2378b85bc3d05ee0b0; + /// @notice keccak256("Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") + bytes32 internal constant MANDATE_TYPEHASH = 0x52c75464356e20084ae43acac75087fbf0e0c678e7ffa326f369f37e88696036; + bytes32 immutable _COMPACT_DOMAIN_SEPARATOR; mapping(uint256 nonce => bool nonceUsed) private _userNonce; @@ -136,6 +139,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { orderData_.amount, keccak256( abi.encode( + MANDATE_TYPEHASH, orderData_.chainId, orderData_.tribunal, orderData_.recipient, diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 6c71085..2311dde 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -89,6 +89,8 @@ abstract contract CreateHash is Test { string compactWitnessTypeString = 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)'; + string mandateTypeString = + 'Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)'; string witnessTypeString = 'Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)'; @@ -118,7 +120,7 @@ abstract contract CreateHash is Test { data.expires, data.id, data.amount, - keccak256(abi.encode(mandate)) + keccak256(abi.encode(keccak256(bytes(mandateTypeString)), mandate)) ) ); } @@ -709,7 +711,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Create sponsor: user, nonce: defaultNonce, expires: compact_.expires, - witness: keccak256(abi.encode(mandate_)), + witness: keccak256(abi.encode(keccak256(bytes(mandateTypeString)), mandate_)), witnessTypestring: witnessTypeString, id: usdcId, allocatedAmount: defaultAmount, @@ -757,7 +759,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Create sponsor: user, nonce: defaultNonce, expires: compact_.expires, - witness: keccak256(abi.encode(mandate_)), + witness: keccak256(abi.encode(keccak256(bytes(mandateTypeString)), mandate_)), witnessTypestring: witnessTypeString, id: usdcId, allocatedAmount: defaultAmount, From 35ede04a011fc12e732405482e1bca23c02c99a1 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Tue, 4 Mar 2025 17:11:55 +0100 Subject: [PATCH 23/35] fixed ubuntu version --- .github/workflows/semgrep.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index b66798f..a1086d3 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -11,7 +11,7 @@ on: jobs: semgrep: name: semgrep/ci - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} container: From d34ef613d536f7b9cabbc9de4edbb00188412082 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Tue, 4 Mar 2025 17:39:46 +0100 Subject: [PATCH 24/35] Changed Mandate data to be Tribunal ready --- src/allocators/ERC7683Allocator.sol | 2 -- src/allocators/types/TribunalStructs.sol | 4 +-- test/ERC7683Allocator.t.sol | 36 +++++++++++------------- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index b635c9c..718f01b 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -216,8 +216,6 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { FillInstruction[] memory fillInstructions = new FillInstruction[](1); Mandate memory mandate = Mandate({ - chainId: orderData.chainId, - tribunal: orderData.tribunal, recipient: orderData.recipient, expires: fillDeadline, token: orderData.token, diff --git a/src/allocators/types/TribunalStructs.sol b/src/allocators/types/TribunalStructs.sol index f1d1dfb..0c377ad 100644 --- a/src/allocators/types/TribunalStructs.sol +++ b/src/allocators/types/TribunalStructs.sol @@ -12,8 +12,8 @@ struct Claim { } struct Mandate { - uint256 chainId; // (implicit arg, included in EIP712 payload) - address tribunal; // (implicit arg, included in EIP712 payload) + // uint256 chainId; // (implicit arg, included in EIP712 payload) + // address tribunal; // (implicit arg, included in EIP712 payload) address recipient; // Recipient of settled tokens uint256 expires; // Mandate expiration timestamp address token; // Settlement token (address(0) for native) diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 2311dde..b6e36bd 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -76,7 +76,7 @@ abstract contract MocksSetup is Test { } } -abstract contract CreateHash is Test { +abstract contract CreateHash is MocksSetup { struct Allocator { bytes32 hash; } @@ -120,7 +120,7 @@ abstract contract CreateHash is Test { data.expires, data.id, data.amount, - keccak256(abi.encode(keccak256(bytes(mandateTypeString)), mandate)) + keccak256(abi.encode(keccak256(bytes(mandateTypeString)), defaultOutputChainId, tribunal, mandate)) ) ); } @@ -157,7 +157,7 @@ abstract contract CreateHash is Test { } } -abstract contract CompactData is MocksSetup { +abstract contract CompactData is CreateHash { Compact private compact; Mandate private mandate; @@ -174,8 +174,6 @@ abstract contract CompactData is MocksSetup { }); mandate = Mandate({ - chainId: defaultOutputChainId, - tribunal: tribunal, recipient: user, expires: _getFillExpiration(), token: defaultOutputToken, @@ -205,7 +203,7 @@ abstract contract CompactData is MocksSetup { } } -abstract contract GaslessCrossChainOrderData is CompactData, CreateHash { +abstract contract GaslessCrossChainOrderData is CompactData { IOriginSettler.GaslessCrossChainOrder private gaslessCrossChainOrder; function setUp() public virtual override { @@ -226,8 +224,8 @@ abstract contract GaslessCrossChainOrderData is CompactData, CreateHash { compact_.arbiter, compact_.id, compact_.amount, - mandate_.chainId, - mandate_.tribunal, + defaultOutputChainId, + tribunal, mandate_.recipient, mandate_.token, mandate_.minimumAmount, @@ -259,8 +257,8 @@ abstract contract GaslessCrossChainOrderData is CompactData, CreateHash { compact_.arbiter, compact_.id, compact_.amount, - mandate_.chainId, - mandate_.tribunal, + defaultOutputChainId, + tribunal, mandate_.recipient, mandate_.token, mandate_.minimumAmount, @@ -302,8 +300,8 @@ abstract contract OnChainCrossChainOrderData is CompactData { compact_.expires, compact_.id, compact_.amount, - mandate_.chainId, - mandate_.tribunal, + defaultOutputChainId, + tribunal, mandate_.recipient, mandate_.token, mandate_.minimumAmount, @@ -320,7 +318,7 @@ abstract contract OnChainCrossChainOrderData is CompactData { function _getOnChainCrossChainOrder(Compact memory compact_, Mandate memory mandate_, bytes32 orderDataType_) internal - pure + view returns (IOriginSettler.OnchainCrossChainOrder memory) { IOriginSettler.OnchainCrossChainOrder memory onchainCrossChainOrder_ = IOriginSettler.OnchainCrossChainOrder({ @@ -333,8 +331,8 @@ abstract contract OnChainCrossChainOrderData is CompactData { compact_.expires, compact_.id, compact_.amount, - mandate_.chainId, - mandate_.tribunal, + defaultOutputChainId, + tribunal, mandate_.recipient, mandate_.token, mandate_.minimumAmount, @@ -543,7 +541,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { } } -contract ERC7683Allocator_open is OnChainCrossChainOrderData, CreateHash { +contract ERC7683Allocator_open is OnChainCrossChainOrderData { function test_revert_InvalidOrderDataType() public { // Order data type is invalid bytes32 falseOrderDataType = keccak256('false'); @@ -678,7 +676,7 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData, CreateHash { } } -contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, CreateHash { +contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData { function test_revert_InvalidLock() public { // Deposit tokens vm.startPrank(user); @@ -759,7 +757,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, Create sponsor: user, nonce: defaultNonce, expires: compact_.expires, - witness: keccak256(abi.encode(keccak256(bytes(mandateTypeString)), mandate_)), + witness: keccak256(abi.encode(keccak256(bytes(mandateTypeString)), defaultOutputChainId, tribunal, mandate_)), witnessTypestring: witnessTypeString, id: usdcId, allocatedAmount: defaultAmount, @@ -926,7 +924,7 @@ contract ERC7683Allocator_getCompactWitnessTypeString is MocksSetup { } } -contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData, CreateHash { +contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { function test_revert_checkNonce(uint256 nonce_) public { address expectedSponsor; assembly ("memory-safe") { From f91a8ccda8f15bdd29010fc25a3ca82b82b7d894 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Wed, 5 Mar 2025 11:05:53 +0100 Subject: [PATCH 25/35] Removed sponsor == operator requirement --- src/allocators/SimpleAllocator.sol | 10 +--------- test/SimpleAllocator.t.sol | 6 ------ 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/allocators/SimpleAllocator.sol b/src/allocators/SimpleAllocator.sol index 5b3222d..0124e89 100644 --- a/src/allocators/SimpleAllocator.sol +++ b/src/allocators/SimpleAllocator.sol @@ -67,18 +67,10 @@ contract SimpleAllocator is ISimpleAllocator { } /// @inheritdoc IAllocator - function attest(address operator_, address from_, address, uint256 id_, uint256 amount_) - external - view - returns (bytes4) - { + function attest(address, address from_, address, uint256 id_, uint256 amount_) external view returns (bytes4) { if (msg.sender != COMPACT_CONTRACT) { revert InvalidCaller(msg.sender, COMPACT_CONTRACT); } - // For a transfer, the sponsor is the arbiter - if (operator_ != from_) { - revert InvalidCaller(operator_, from_); - } uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(from_, id_); // Check unlocked balance bytes32 tokenHash = _getTokenHash(id_, from_); diff --git a/test/SimpleAllocator.t.sol b/test/SimpleAllocator.t.sol index d4ca517..e5a13e1 100644 --- a/test/SimpleAllocator.t.sol +++ b/test/SimpleAllocator.t.sol @@ -447,12 +447,6 @@ contract SimpleAllocator_Attest is Deposited { simpleAllocator.attest(address(user), address(user), address(usdc), usdcId, defaultAmount); } - function test_revert_InvalidCaller_FromNotOperator() public { - vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidCaller.selector, attacker, user)); - compactContract.transfer(user, attacker, defaultAmount, address(usdc), address(simpleAllocator)); - } - function test_revert_InsufficientBalance_NoActiveLock(uint128 falseAmount_) public { vm.assume(falseAmount_ > defaultAmount); From 32dafd3e4bb83e12c57820e70edfc06c4bcc3595 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Wed, 5 Mar 2025 11:34:37 +0100 Subject: [PATCH 26/35] use this.attest.selector --- src/allocators/SimpleAllocator.sol | 2 +- test/SimpleAllocator.t.sol | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/allocators/SimpleAllocator.sol b/src/allocators/SimpleAllocator.sol index 0124e89..80eea95 100644 --- a/src/allocators/SimpleAllocator.sol +++ b/src/allocators/SimpleAllocator.sol @@ -86,7 +86,7 @@ contract SimpleAllocator is ISimpleAllocator { revert InsufficientBalance(from_, id_, balance, fullAmount); } - return 0x1a808f91; + return this.attest.selector; } /// @inheritdoc IERC1271 diff --git a/test/SimpleAllocator.t.sol b/test/SimpleAllocator.t.sol index e5a13e1..3f8bbf9 100644 --- a/test/SimpleAllocator.t.sol +++ b/test/SimpleAllocator.t.sol @@ -486,6 +486,34 @@ contract SimpleAllocator_Attest is Deposited { compactContract.transfer(user, attacker, defaultAmount, address(usdc), address(simpleAllocator)); } + function test_successfullyAttested_returnsSelector() public { + bytes4 selector = bytes4(0x1a808f91); + + uint32 transferAmount = 10; + uint32 lockedAmount = 90; + + address otherUser = makeAddr('otherUser'); + + // Lock tokens + uint256 defaultExpiration_ = vm.getBlockTimestamp() + defaultResetPeriod; + vm.prank(user); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration_, + amount: lockedAmount + }) + ); + compactContract.transfer(user, otherUser, transferAmount, address(usdc), address(simpleAllocator)); + + vm.prank(address(compactContract)); + bytes4 returnedSelector = simpleAllocator.attest(user, user, otherUser, usdcId, transferAmount); + assertEq(returnedSelector, selector); + } + function test_successfullyAttested(uint32 lockedAmount_, uint32 transferAmount_) public { transferAmount_ = uint32(bound(transferAmount_, 0, defaultAmount)); lockedAmount_ = uint32(bound(lockedAmount_, 0, defaultAmount - transferAmount_)); From 1bc6d913445bee8604bfbaf4dec1014f8c48542d Mon Sep 17 00:00:00 2001 From: vimageDE Date: Wed, 5 Mar 2025 12:09:24 +0100 Subject: [PATCH 27/35] moved actual nonce to least significant bits --- src/allocators/ERC7683Allocator.sol | 4 ++-- test/ERC7683Allocator.t.sol | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index 718f01b..1bb8eaa 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -276,11 +276,11 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { } function _checkNonce(address sponsor_, uint256 nonce_) internal pure { - // Enforce a nonce where the most significant 96 bits are the nonce and the least significant 160 bits are the sponsor + // Enforce a nonce where the least significant 96 bits are the nonce and the most significant 160 bits are the sponsors address // This ensures that the nonce is unique for a given sponsor address expectedSponsor; assembly ("memory-safe") { - expectedSponsor := shr(96, shl(96, nonce_)) + expectedSponsor := shr(96, nonce_) } if (expectedSponsor != sponsor_) { revert InvalidNonce(nonce_); diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index b6e36bd..4727431 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -69,7 +69,7 @@ abstract contract MocksSetup is Test { }); usdcId = IdLib.toId(lock); (attacker, attackerPK) = makeAddrAndKey('attacker'); - defaultNonce = uint256(bytes32(abi.encodePacked(uint96(1), user))); + defaultNonce = uint256(bytes32(abi.encodePacked(user, uint96(1)))); ORDERDATA_GASLESS_TYPEHASH = erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(); ORDERDATA_TYPEHASH = erc7683Allocator.ORDERDATA_TYPEHASH(); @@ -940,7 +940,7 @@ contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { address sponsor = user; uint256 nonce; assembly ("memory-safe") { - nonce := or(shl(160, nonce_), shr(96, shl(96, sponsor))) + nonce := or(shl(96, sponsor), shr(160, shl(160, nonce_))) } assertEq(erc7683Allocator.checkNonce(sponsor, nonce), true); } From fe0477add0176cef697f5e44c9169532f9d7d117 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Thu, 6 Mar 2025 18:22:50 +0100 Subject: [PATCH 28/35] using bitmap for nonces --- src/allocators/ERC7683Allocator.sol | 70 ++++++++++++++++++----------- test/ERC7683Allocator.t.sol | 36 ++++++++++++--- 2 files changed, 75 insertions(+), 31 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index 1bb8eaa..0ed5990 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -29,9 +29,10 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { /// @notice keccak256("Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") bytes32 internal constant MANDATE_TYPEHASH = 0x52c75464356e20084ae43acac75087fbf0e0c678e7ffa326f369f37e88696036; - bytes32 immutable _COMPACT_DOMAIN_SEPARATOR; + /// @notice uint256(uint8(keccak256("ERC7683Allocator.nonce"))) + uint8 internal constant NONCE_MASTER_SLOT_SEED = 0x39; - mapping(uint256 nonce => bool nonceUsed) private _userNonce; + bytes32 immutable _COMPACT_DOMAIN_SEPARATOR; constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 maxWithdrawalDelay_) SimpleAllocator(compactContract_, minWithdrawalDelay_, maxWithdrawalDelay_) @@ -105,8 +106,13 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { /// @inheritdoc IERC7683Allocator function checkNonce(address sponsor_, uint256 nonce_) external view returns (bool nonceFree_) { - _checkNonce(sponsor_, nonce_); - nonceFree_ = !_userNonce[nonce_]; + uint96 nonceWithoutAddress = _checkNonce(sponsor_, nonce_); + uint96 wordPos = uint96(nonceWithoutAddress / 256); + uint96 bitPos = uint96(nonceWithoutAddress % 256); + assembly ("memory-safe") { + let masterSlot := or(shl(248, NONCE_MASTER_SLOT_SEED), or(shl(88, sponsor_), wordPos)) + nonceFree_ := iszero(and(sload(masterSlot), shl(bitPos, 1))) + } return nonceFree_; } @@ -114,13 +120,10 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { internal { // Enforce a nonce where the most significant 96 bits are the nonce and the least significant 160 bits are the sponsor - _checkNonce(sponsor_, orderData_.nonce); + uint96 nonceWithoutAddress = _checkNonce(sponsor_, orderData_.nonce); - // Check the nonce - if (_userNonce[orderData_.nonce]) { - revert NonceAlreadyInUse(orderData_.nonce); - } - _userNonce[orderData_.nonce] = true; + // Set a nonce or revert if it is already used + _setNonce(sponsor_, nonceWithoutAddress); // We do not enforce a specific tribunal or arbiter. This will allow to support new arbiters and tribunals after the deployment of the allocator // Going with an immutable arbiter and tribunal would limit support for new chains with a fully decentralized allocator @@ -180,28 +183,23 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { ); } - function _lockTokens(OrderData memory orderData_, address sponsor_, uint256 identifier) + function _lockTokens(OrderData memory orderData_, address sponsor_, uint256 nonce) internal returns (bytes32 tokenHash_) { - return - _lockTokens(orderData_.arbiter, sponsor_, identifier, orderData_.expires, orderData_.id, orderData_.amount); + return _lockTokens(orderData_.arbiter, sponsor_, nonce, orderData_.expires, orderData_.id, orderData_.amount); } - function _lockTokens( - address arbiter, - address sponsor, - uint256 identifier, - uint256 expires, - uint256 id, - uint256 amount - ) internal returns (bytes32 tokenHash_) { + function _lockTokens(address arbiter, address sponsor, uint256 nonce, uint256 expires, uint256 id, uint256 amount) + internal + returns (bytes32 tokenHash_) + { tokenHash_ = _checkAllocation( - Compact({arbiter: arbiter, sponsor: sponsor, nonce: identifier, expires: expires, id: id, amount: amount}) + Compact({arbiter: arbiter, sponsor: sponsor, nonce: nonce, expires: expires, id: id, amount: amount}) ); _claim[tokenHash_] = expires; _amount[tokenHash_] = amount; - _nonce[tokenHash_] = identifier; + _nonce[tokenHash_] = nonce; return tokenHash_; } @@ -209,7 +207,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { function _resolveOrder( address sponsor, uint32 fillDeadline, - uint256 identifier, + uint256 nonce, OrderData memory orderData, bytes memory sponsorSignature ) internal view returns (ResolvedCrossChainOrder memory) { @@ -267,7 +265,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { originChainId: block.chainid, openDeadline: uint32(orderData.expires), fillDeadline: fillDeadline, - orderId: bytes32(identifier), + orderId: bytes32(nonce), maxSpent: maxSpent, minReceived: minReceived, fillInstructions: fillInstructions @@ -275,18 +273,38 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { return resolvedOrder; } - function _checkNonce(address sponsor_, uint256 nonce_) internal pure { + function _checkNonce(address sponsor_, uint256 nonce_) internal pure returns (uint96 nonce) { // Enforce a nonce where the least significant 96 bits are the nonce and the most significant 160 bits are the sponsors address // This ensures that the nonce is unique for a given sponsor address expectedSponsor; assembly ("memory-safe") { expectedSponsor := shr(96, nonce_) + nonce := shr(160, shl(160, nonce_)) } if (expectedSponsor != sponsor_) { revert InvalidNonce(nonce_); } } + function _setNonce(address sponsor_, uint96 nonce_) internal { + bool used; + uint96 wordPos = nonce_ / 256; // uint96 divided by 256 means it becomes a uint88 (11 bytes) + uint96 bitPos = nonce_ % 256; + assembly ("memory-safe") { + // [NONCE_MASTER_SLOT_SEED - 1 byte][sponsor address - 20 bytes][wordPos - 11 bytes] + let masterSlot := or(shl(248, NONCE_MASTER_SLOT_SEED), or(shl(88, sponsor_), wordPos)) + let previouslyUsedNonces := sload(masterSlot) + if and(previouslyUsedNonces, shl(bitPos, 1)) { used := 1 } + { + let usedNonces := or(previouslyUsedNonces, shl(bitPos, 1)) + sstore(masterSlot, usedNonces) + } + } + if (used) { + revert NonceAlreadyInUse(uint256(bytes32(abi.encodePacked(sponsor_, nonce_)))); + } + } + function _idToToken(uint256 id_) internal pure returns (address token_) { assembly ("memory-safe") { token_ := shr(96, shl(96, id_)) diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 4727431..cc0ada3 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -925,10 +925,10 @@ contract ERC7683Allocator_getCompactWitnessTypeString is MocksSetup { } contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { - function test_revert_checkNonce(uint256 nonce_) public { + function test_revert_invalidNonce(uint256 nonce_) public { address expectedSponsor; assembly ("memory-safe") { - expectedSponsor := shr(96, shl(96, nonce_)) + expectedSponsor := shr(96, nonce_) } vm.assume(user != expectedSponsor); @@ -962,13 +962,39 @@ contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { bytes32 typeHash = _getTypeHash(); compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); + (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); + erc7683Allocator.open(onChainCrossChainOrder_); + + vm.assertEq(erc7683Allocator.checkNonce(user, defaultNonce), false); vm.stopPrank(); + } + + function test_checkNonce_fuzz(uint8 nonce_) public { + uint256 nonce = uint256(bytes32(abi.encodePacked(user, uint96(nonce_)))); + + bool sameNonce = nonce == defaultNonce; + + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); + + // register a claim + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); (IOriginSettler.OnchainCrossChainOrder memory onChainCrossChainOrder_) = _getOnChainCrossChainOrder(); - vm.prank(user); erc7683Allocator.open(onChainCrossChainOrder_); - vm.prank(user); - vm.assertEq(erc7683Allocator.checkNonce(user, defaultNonce), false); + vm.assertEq(erc7683Allocator.checkNonce(user, nonce), !sameNonce); + + vm.stopPrank(); } } From a2fb41b63071d7f70df96267beb928db55a411a0 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Thu, 6 Mar 2025 18:51:00 +0100 Subject: [PATCH 29/35] enable relayed locks in child contracts --- src/allocators/SimpleAllocator.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/allocators/SimpleAllocator.sol b/src/allocators/SimpleAllocator.sol index 80eea95..7eb40a7 100644 --- a/src/allocators/SimpleAllocator.sol +++ b/src/allocators/SimpleAllocator.sol @@ -38,7 +38,7 @@ contract SimpleAllocator is ISimpleAllocator { /// @inheritdoc ISimpleAllocator function lock(Compact calldata compact_) external { - bytes32 tokenHash = _checkAllocation(compact_); + bytes32 tokenHash = _checkAllocation(compact_, true); bytes32 digest = keccak256( abi.encodePacked( @@ -157,12 +157,12 @@ contract SimpleAllocator is ISimpleAllocator { return keccak256(abi.encode(id_, sponsor_)); } - function _checkAllocation(Compact memory compact_) internal view returns (bytes32) { + function _checkAllocation(Compact memory compact_, bool checkSponsor_) internal view returns (bytes32) { // Check msg.sender is sponsor - if (msg.sender != compact_.sponsor) { + if (checkSponsor_ && msg.sender != compact_.sponsor) { revert InvalidCaller(msg.sender, compact_.sponsor); } - bytes32 tokenHash = _getTokenHash(compact_.id, msg.sender); + bytes32 tokenHash = _getTokenHash(compact_.id, compact_.sponsor); // Check no lock is already active for this sponsor if ( _claim[tokenHash] > block.timestamp @@ -196,10 +196,10 @@ contract SimpleAllocator is ISimpleAllocator { revert NonceAlreadyConsumed(compact_.nonce); } - uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(msg.sender, compact_.id); + uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(compact_.sponsor, compact_.id); // Check balance is enough if (balance < compact_.amount) { - revert InsufficientBalance(msg.sender, compact_.id, balance, compact_.amount); + revert InsufficientBalance(compact_.sponsor, compact_.id, balance, compact_.amount); } return tokenHash; From 904d5d8d48844b887c851d2d7e448ba831edab15 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Thu, 6 Mar 2025 19:01:03 +0100 Subject: [PATCH 30/35] Merged SimpleAllocator --- src/allocators/SimpleAllocator.sol | 24 +++++++-------------- test/SimpleAllocator.t.sol | 34 ++++++++++++++++++++++++------ 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/allocators/SimpleAllocator.sol b/src/allocators/SimpleAllocator.sol index 5b3222d..7eb40a7 100644 --- a/src/allocators/SimpleAllocator.sol +++ b/src/allocators/SimpleAllocator.sol @@ -38,7 +38,7 @@ contract SimpleAllocator is ISimpleAllocator { /// @inheritdoc ISimpleAllocator function lock(Compact calldata compact_) external { - bytes32 tokenHash = _checkAllocation(compact_); + bytes32 tokenHash = _checkAllocation(compact_, true); bytes32 digest = keccak256( abi.encodePacked( @@ -67,18 +67,10 @@ contract SimpleAllocator is ISimpleAllocator { } /// @inheritdoc IAllocator - function attest(address operator_, address from_, address, uint256 id_, uint256 amount_) - external - view - returns (bytes4) - { + function attest(address, address from_, address, uint256 id_, uint256 amount_) external view returns (bytes4) { if (msg.sender != COMPACT_CONTRACT) { revert InvalidCaller(msg.sender, COMPACT_CONTRACT); } - // For a transfer, the sponsor is the arbiter - if (operator_ != from_) { - revert InvalidCaller(operator_, from_); - } uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(from_, id_); // Check unlocked balance bytes32 tokenHash = _getTokenHash(id_, from_); @@ -94,7 +86,7 @@ contract SimpleAllocator is ISimpleAllocator { revert InsufficientBalance(from_, id_, balance, fullAmount); } - return 0x1a808f91; + return this.attest.selector; } /// @inheritdoc IERC1271 @@ -165,12 +157,12 @@ contract SimpleAllocator is ISimpleAllocator { return keccak256(abi.encode(id_, sponsor_)); } - function _checkAllocation(Compact memory compact_) internal view returns (bytes32) { + function _checkAllocation(Compact memory compact_, bool checkSponsor_) internal view returns (bytes32) { // Check msg.sender is sponsor - if (msg.sender != compact_.sponsor) { + if (checkSponsor_ && msg.sender != compact_.sponsor) { revert InvalidCaller(msg.sender, compact_.sponsor); } - bytes32 tokenHash = _getTokenHash(compact_.id, msg.sender); + bytes32 tokenHash = _getTokenHash(compact_.id, compact_.sponsor); // Check no lock is already active for this sponsor if ( _claim[tokenHash] > block.timestamp @@ -204,10 +196,10 @@ contract SimpleAllocator is ISimpleAllocator { revert NonceAlreadyConsumed(compact_.nonce); } - uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(msg.sender, compact_.id); + uint256 balance = ERC6909(COMPACT_CONTRACT).balanceOf(compact_.sponsor, compact_.id); // Check balance is enough if (balance < compact_.amount) { - revert InsufficientBalance(msg.sender, compact_.id, balance, compact_.amount); + revert InsufficientBalance(compact_.sponsor, compact_.id, balance, compact_.amount); } return tokenHash; diff --git a/test/SimpleAllocator.t.sol b/test/SimpleAllocator.t.sol index d4ca517..3f8bbf9 100644 --- a/test/SimpleAllocator.t.sol +++ b/test/SimpleAllocator.t.sol @@ -447,12 +447,6 @@ contract SimpleAllocator_Attest is Deposited { simpleAllocator.attest(address(user), address(user), address(usdc), usdcId, defaultAmount); } - function test_revert_InvalidCaller_FromNotOperator() public { - vm.prank(attacker); - vm.expectRevert(abi.encodeWithSelector(ISimpleAllocator.InvalidCaller.selector, attacker, user)); - compactContract.transfer(user, attacker, defaultAmount, address(usdc), address(simpleAllocator)); - } - function test_revert_InsufficientBalance_NoActiveLock(uint128 falseAmount_) public { vm.assume(falseAmount_ > defaultAmount); @@ -492,6 +486,34 @@ contract SimpleAllocator_Attest is Deposited { compactContract.transfer(user, attacker, defaultAmount, address(usdc), address(simpleAllocator)); } + function test_successfullyAttested_returnsSelector() public { + bytes4 selector = bytes4(0x1a808f91); + + uint32 transferAmount = 10; + uint32 lockedAmount = 90; + + address otherUser = makeAddr('otherUser'); + + // Lock tokens + uint256 defaultExpiration_ = vm.getBlockTimestamp() + defaultResetPeriod; + vm.prank(user); + simpleAllocator.lock( + Compact({ + arbiter: arbiter, + sponsor: user, + nonce: defaultNonce, + id: usdcId, + expires: defaultExpiration_, + amount: lockedAmount + }) + ); + compactContract.transfer(user, otherUser, transferAmount, address(usdc), address(simpleAllocator)); + + vm.prank(address(compactContract)); + bytes4 returnedSelector = simpleAllocator.attest(user, user, otherUser, usdcId, transferAmount); + assertEq(returnedSelector, selector); + } + function test_successfullyAttested(uint32 lockedAmount_, uint32 transferAmount_) public { transferAmount_ = uint32(bound(transferAmount_, 0, defaultAmount)); lockedAmount_ = uint32(bound(lockedAmount_, 0, defaultAmount - transferAmount_)); From 4bed9de3595715eee7a121f5aee571538a83882d Mon Sep 17 00:00:00 2001 From: vimageDE Date: Thu, 6 Mar 2025 19:01:49 +0100 Subject: [PATCH 31/35] fixed relayed openFor call --- src/allocators/ERC7683Allocator.sol | 2 +- test/ERC7683Allocator.t.sol | 57 ++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index 0ed5990..3fbd053 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -195,7 +195,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { returns (bytes32 tokenHash_) { tokenHash_ = _checkAllocation( - Compact({arbiter: arbiter, sponsor: sponsor, nonce: nonce, expires: expires, id: id, amount: amount}) + Compact({arbiter: arbiter, sponsor: sponsor, nonce: nonce, expires: expires, id: id, amount: amount}), false ); _claim[tokenHash_] = expires; _amount[tokenHash_] = amount; diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index cc0ada3..88cf168 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -459,7 +459,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); } - function test_successful() public { + function test_successful_userHimself() public { // Deposit tokens vm.startPrank(user); usdc.mint(user, defaultAmount); @@ -514,6 +514,61 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); } + function test_successful_relayed() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); + vm.stopPrank(); + + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder(); + IOriginSettler.Output[] memory maxSpent = new IOriginSettler.Output[](1); + IOriginSettler.Output[] memory minReceived = new IOriginSettler.Output[](1); + IOriginSettler.FillInstruction[] memory fillInstructions = new IOriginSettler.FillInstruction[](1); + maxSpent[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(defaultOutputToken))), + amount: type(uint256).max, + recipient: bytes32(uint256(uint160(user))), + chainId: defaultOutputChainId + }); + minReceived[0] = IOriginSettler.Output({ + token: bytes32(uint256(uint160(address(usdc)))), + amount: defaultAmount, + recipient: '', + chainId: block.chainid + }); + Claim memory claim = Claim({ + chainId: block.chainid, + compact: _getCompact(), + sponsorSignature: sponsorSignature, + allocatorSignature: '' + }); + fillInstructions[0] = IOriginSettler.FillInstruction({ + destinationChainId: defaultOutputChainId, + destinationSettler: bytes32(uint256(uint160(tribunal))), + originData: abi.encode(claim, _getMandate()) + }); + + IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ + user: user, + originChainId: block.chainid, + openDeadline: uint32(_getClaimExpiration()), + fillDeadline: uint32(_getFillExpiration()), + orderId: bytes32(defaultNonce), + maxSpent: maxSpent, + minReceived: minReceived, + fillInstructions: fillInstructions + }); + vm.prank(makeAddr('filler')); + vm.expectEmit(true, false, false, true, address(erc7683Allocator)); + emit IOriginSettler.Open(bytes32(defaultNonce), resolvedCrossChainOrder); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + } + function test_revert_NonceAlreadyInUse() public { // Nonce is already used From 8cad10a7553c926f2927d158ebcbb7bdc616dac6 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Fri, 21 Mar 2025 13:29:11 +0100 Subject: [PATCH 32/35] added decayCurve to Mandate --- src/allocators/ERC7683Allocator.sol | 21 ++-- src/allocators/types/TribunalStructs.sol | 19 +-- src/interfaces/IERC7683Allocator.sol | 2 + test/ERC7683Allocator.t.sol | 154 ++++++++++++++--------- 4 files changed, 122 insertions(+), 74 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index 3fbd053..b2f1505 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -12,22 +12,22 @@ import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { /// @notice The typehash of the OrderData struct // keccak256("OrderData(address arbiter,address sponsor,uint256 nonce,uint256 id,uint256 amount,Mandate mandate) - // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") - bytes32 public constant ORDERDATA_TYPEHASH = 0x9e0e1bdb0df35509b65bbc49d209dd42496c5a3f13998f9a74dc842d6932656b; + // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") + bytes32 public constant ORDERDATA_TYPEHASH = 0x524227237e80c55bea046dee5ee8323384274111dd94aadae8ce9bbc3916facb; /// @notice The typehash of the OrderDataGasless struct // keccak256("OrderDataGasless(address arbiter,uint256 id,uint256 amount,Mandate mandate) - // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") + // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = - 0x9ab67658b7c0f35b64fdadd7adee1e58b6399a8201f38c355d3a109a2d7081d7; + 0x3d6dd96d82595484a68f0b4dcd56a17557e8e675c9aa8e149d6166912a791704; /// @notice keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate) - // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") + // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") bytes32 public constant COMPACT_WITNESS_TYPEHASH = - 0x27f09e0bb8ce2ae63380578af7af85055d3ada248c502e2378b85bc3d05ee0b0; + 0xfd9cda0e5e31a3a3476cb5b57b07e2a4d6a12815506f69c880696448cd9897a5; - /// @notice keccak256("Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)") - bytes32 internal constant MANDATE_TYPEHASH = 0x52c75464356e20084ae43acac75087fbf0e0c678e7ffa326f369f37e88696036; + /// @notice keccak256("Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") + bytes32 internal constant MANDATE_TYPEHASH = 0x74d9c10530859952346f3e046aa2981a24bb7524b8394eb45a9deddced9d6501; /// @notice uint256(uint8(keccak256("ERC7683Allocator.nonce"))) uint8 internal constant NONCE_MASTER_SLOT_SEED = 0x39; @@ -101,7 +101,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { /// @inheritdoc IERC7683Allocator function getCompactWitnessTypeString() external pure returns (string memory) { return - 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt))'; + 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt))'; } /// @inheritdoc IERC7683Allocator @@ -151,6 +151,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { orderData_.minimumAmount, orderData_.baselinePriorityFee, orderData_.scalingFactor, + keccak256(abi.encodePacked(orderData_.decayCurve)), orderData_.salt ) ) @@ -220,6 +221,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { minimumAmount: orderData.minimumAmount, baselinePriorityFee: orderData.baselinePriorityFee, scalingFactor: orderData.scalingFactor, + decayCurve: orderData.decayCurve, salt: orderData.salt }); Claim memory claim = Claim({ @@ -337,6 +339,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { minimumAmount: orderDataGasless_.minimumAmount, baselinePriorityFee: orderDataGasless_.baselinePriorityFee, scalingFactor: orderDataGasless_.scalingFactor, + decayCurve: orderDataGasless_.decayCurve, salt: orderDataGasless_.salt }); return orderData_; diff --git a/src/allocators/types/TribunalStructs.sol b/src/allocators/types/TribunalStructs.sol index 0c377ad..9d1ffaa 100644 --- a/src/allocators/types/TribunalStructs.sol +++ b/src/allocators/types/TribunalStructs.sol @@ -12,13 +12,14 @@ struct Claim { } struct Mandate { - // uint256 chainId; // (implicit arg, included in EIP712 payload) - // address tribunal; // (implicit arg, included in EIP712 payload) - address recipient; // Recipient of settled tokens - uint256 expires; // Mandate expiration timestamp - address token; // Settlement token (address(0) for native) - uint256 minimumAmount; // Minimum settlement amount - uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in - uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) - bytes32 salt; // Replay protection parameter + // uint256 chainId; // (implicit arg, included in EIP712 payload). + // address tribunal; // (implicit arg, included in EIP712 payload). + address recipient; // Recipient of filled tokens. + uint256 expires; // Mandate expiration timestamp. + address token; // Fill token (address(0) for native). + uint256 minimumAmount; // Minimum fill amount. + uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in. + uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline). + uint256[] decayCurve; // Block durations, fill increases, & claim decreases. + bytes32 salt; // Replay protection parameter. } diff --git a/src/interfaces/IERC7683Allocator.sol b/src/interfaces/IERC7683Allocator.sol index 81ab05f..4ab08af 100644 --- a/src/interfaces/IERC7683Allocator.sol +++ b/src/interfaces/IERC7683Allocator.sol @@ -22,6 +22,7 @@ interface IERC7683Allocator is IOriginSettler { uint256 minimumAmount; // Minimum settlement amount uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) + uint256[] decayCurve; // Block durations, fill increases, & claim decreases. bytes32 salt; // Replay protection parameter } @@ -42,6 +43,7 @@ interface IERC7683Allocator is IOriginSettler { uint256 minimumAmount; // Minimum settlement amount uint256 baselinePriorityFee; // Base fee threshold where scaling kicks in uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) + uint256[] decayCurve; // Block durations, fill increases, & claim decreases. bytes32 salt; // Replay protection parameter } diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 88cf168..a7e5a70 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -49,6 +49,7 @@ abstract contract MocksSetup is Test { uint256 defaultMinimumAmount = 1000; uint256 defaultBaselinePriorityFee = 0; uint256 defaultScalingFactor = 0; + uint256[] defaultDecayCurve = new uint256[](0); bytes32 defaultSalt = bytes32(0); bytes32 ORDERDATA_GASLESS_TYPEHASH; @@ -88,11 +89,11 @@ abstract contract CreateHash is MocksSetup { string version = '0'; string compactWitnessTypeString = - 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)'; + 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; string mandateTypeString = - 'Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)'; + 'Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; string witnessTypeString = - 'Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt)'; + 'Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)'; function _hashCompact(Compact memory data, Mandate memory mandate, address verifyingContract) internal @@ -120,7 +121,21 @@ abstract contract CreateHash is MocksSetup { data.expires, data.id, data.amount, - keccak256(abi.encode(keccak256(bytes(mandateTypeString)), defaultOutputChainId, tribunal, mandate)) + keccak256( + abi.encode( + keccak256(bytes(mandateTypeString)), + defaultOutputChainId, + tribunal, + mandate.recipient, + mandate.expires, + mandate.token, + mandate.minimumAmount, + mandate.baselinePriorityFee, + mandate.scalingFactor, + keccak256(abi.encodePacked(mandate.decayCurve)), + mandate.salt + ) + ) ) ); } @@ -180,6 +195,7 @@ abstract contract CompactData is CreateHash { minimumAmount: defaultMinimumAmount, baselinePriorityFee: defaultBaselinePriorityFee, scalingFactor: defaultScalingFactor, + decayCurve: defaultDecayCurve, salt: defaultSalt }); } @@ -221,17 +237,20 @@ abstract contract GaslessCrossChainOrderData is CompactData { fillDeadline: uint32(_getFillExpiration()), orderDataType: erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(), orderData: abi.encode( - compact_.arbiter, - compact_.id, - compact_.amount, - defaultOutputChainId, - tribunal, - mandate_.recipient, - mandate_.token, - mandate_.minimumAmount, - mandate_.baselinePriorityFee, - mandate_.scalingFactor, - mandate_.salt + IERC7683Allocator.OrderDataGasless({ + arbiter: compact_.arbiter, + id: compact_.id, + amount: compact_.amount, + chainId: defaultOutputChainId, + tribunal: tribunal, + recipient: mandate_.recipient, + token: mandate_.token, + minimumAmount: mandate_.minimumAmount, + baselinePriorityFee: mandate_.baselinePriorityFee, + scalingFactor: mandate_.scalingFactor, + decayCurve: mandate_.decayCurve, + salt: mandate_.salt + }) ) }); } @@ -254,17 +273,20 @@ abstract contract GaslessCrossChainOrderData is CompactData { fillDeadline: uint32(mandate_.expires), orderDataType: orderDataGaslessTypeHash_, orderData: abi.encode( - compact_.arbiter, - compact_.id, - compact_.amount, - defaultOutputChainId, - tribunal, - mandate_.recipient, - mandate_.token, - mandate_.minimumAmount, - mandate_.baselinePriorityFee, - mandate_.scalingFactor, - mandate_.salt + IERC7683Allocator.OrderDataGasless({ + arbiter: compact_.arbiter, + id: compact_.id, + amount: compact_.amount, + chainId: defaultOutputChainId, + tribunal: tribunal, + recipient: mandate_.recipient, + token: mandate_.token, + minimumAmount: mandate_.minimumAmount, + baselinePriorityFee: mandate_.baselinePriorityFee, + scalingFactor: mandate_.scalingFactor, + decayCurve: mandate_.decayCurve, + salt: mandate_.salt + }) ) }); @@ -294,20 +316,23 @@ abstract contract OnChainCrossChainOrderData is CompactData { fillDeadline: uint32(_getFillExpiration()), orderDataType: erc7683Allocator.ORDERDATA_TYPEHASH(), orderData: abi.encode( - compact_.arbiter, - compact_.sponsor, - compact_.nonce, - compact_.expires, - compact_.id, - compact_.amount, - defaultOutputChainId, - tribunal, - mandate_.recipient, - mandate_.token, - mandate_.minimumAmount, - mandate_.baselinePriorityFee, - mandate_.scalingFactor, - mandate_.salt + IERC7683Allocator.OrderData({ + arbiter: compact_.arbiter, + sponsor: compact_.sponsor, + nonce: compact_.nonce, + expires: compact_.expires, + id: compact_.id, + amount: compact_.amount, + chainId: defaultOutputChainId, + tribunal: tribunal, + recipient: mandate_.recipient, + token: mandate_.token, + minimumAmount: mandate_.minimumAmount, + baselinePriorityFee: mandate_.baselinePriorityFee, + scalingFactor: mandate_.scalingFactor, + decayCurve: mandate_.decayCurve, + salt: mandate_.salt + }) ) }); } @@ -325,20 +350,23 @@ abstract contract OnChainCrossChainOrderData is CompactData { fillDeadline: uint32(mandate_.expires), orderDataType: orderDataType_, orderData: abi.encode( - compact_.arbiter, - compact_.sponsor, - compact_.nonce, - compact_.expires, - compact_.id, - compact_.amount, - defaultOutputChainId, - tribunal, - mandate_.recipient, - mandate_.token, - mandate_.minimumAmount, - mandate_.baselinePriorityFee, - mandate_.scalingFactor, - mandate_.salt + IERC7683Allocator.OrderData({ + arbiter: compact_.arbiter, + sponsor: compact_.sponsor, + nonce: compact_.nonce, + expires: compact_.expires, + id: compact_.id, + amount: compact_.amount, + chainId: defaultOutputChainId, + tribunal: tribunal, + recipient: mandate_.recipient, + token: mandate_.token, + minimumAmount: mandate_.minimumAmount, + baselinePriorityFee: mandate_.baselinePriorityFee, + scalingFactor: mandate_.scalingFactor, + decayCurve: mandate_.decayCurve, + salt: mandate_.salt + }) ) }); return onchainCrossChainOrder_; @@ -812,7 +840,21 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData { sponsor: user, nonce: defaultNonce, expires: compact_.expires, - witness: keccak256(abi.encode(keccak256(bytes(mandateTypeString)), defaultOutputChainId, tribunal, mandate_)), + witness: keccak256( + abi.encode( + keccak256(bytes(mandateTypeString)), + defaultOutputChainId, + tribunal, + mandate_.recipient, + mandate_.expires, + mandate_.token, + mandate_.minimumAmount, + mandate_.baselinePriorityFee, + mandate_.scalingFactor, + keccak256(abi.encodePacked(mandate_.decayCurve)), + mandate_.salt + ) + ), witnessTypestring: witnessTypeString, id: usdcId, allocatedAmount: defaultAmount, @@ -974,7 +1016,7 @@ contract ERC7683Allocator_getCompactWitnessTypeString is MocksSetup { function test_getCompactWitnessTypeString() public view { assertEq( erc7683Allocator.getCompactWitnessTypeString(), - 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,bytes32 salt))' + 'Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate)Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt))' ); } } From 719699f1e39e710a7f5340ff71962139ceb19843 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Fri, 21 Mar 2025 16:05:57 +0100 Subject: [PATCH 33/35] additional input params --- src/allocators/ERC7683Allocator.sol | 35 +++++++++++++++++++++------- src/interfaces/IERC7683Allocator.sol | 14 +++++++++++ test/ERC7683Allocator.t.sol | 28 +++++++++++++++------- 3 files changed, 60 insertions(+), 17 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index b2f1505..f7607cb 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -11,15 +11,15 @@ import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { /// @notice The typehash of the OrderData struct - // keccak256("OrderData(address arbiter,address sponsor,uint256 nonce,uint256 id,uint256 amount,Mandate mandate) - // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") - bytes32 public constant ORDERDATA_TYPEHASH = 0x524227237e80c55bea046dee5ee8323384274111dd94aadae8ce9bbc3916facb; + // keccak256("OrderData(address arbiter,address sponsor,uint256 nonce,uint256 id,uint256 amount, + // uint256 chainId,address tribunal,address recipient,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,uint256 targetBlock,uint256 maximumBlocksAfterTarget)") + bytes32 public constant ORDERDATA_TYPEHASH = 0x9687614112a074c792f7035dc9365f34672a3aa8d3c312500bd47ddcaa0383b5; /// @notice The typehash of the OrderDataGasless struct - // keccak256("OrderDataGasless(address arbiter,uint256 id,uint256 amount,Mandate mandate) - // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") + // keccak256("OrderDataGasless(address arbiter,uint256 id,uint256 amount, + // uint256 chainId,address tribunal,address recipient,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,uint256 targetBlock,uint256 maximumBlocksAfterTarget)") bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = - 0x3d6dd96d82595484a68f0b4dcd56a17557e8e675c9aa8e149d6166912a791704; + 0xe6c40de3837db693910c63e423da4d9f6157257aaad00a0786775e629249ee73; /// @notice keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate) // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") @@ -116,6 +116,23 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { return nonceFree_; } + /// @inheritdoc IERC7683Allocator + function completeOriginData(bytes memory originData_, address claimant_) + external + pure + returns (bytes memory originData) + { + ( + Claim memory claim, + Mandate memory mandate, /* empty receiver of the tokens */ + , + uint256 targetBlock, + uint256 maximumBlocksAfterTarget + ) = abi.decode(originData_, (Claim, Mandate, address, uint256, uint256)); + originData = abi.encode(claim, mandate, claimant_, targetBlock, maximumBlocksAfterTarget); + return originData; + } + function _open(OrderData memory orderData_, uint32 fillDeadline_, address sponsor_, bytes memory sponsorSignature_) internal { @@ -241,7 +258,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { fillInstructions[0] = FillInstruction({ destinationChainId: orderData.chainId, destinationSettler: _addressToBytes32(orderData.tribunal), - originData: abi.encode(claim, mandate) + originData: abi.encode(claim, mandate, address(0), orderData.targetBlock, orderData.maximumBlocksAfterTarget) }); Output memory spent = Output({ @@ -340,7 +357,9 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { baselinePriorityFee: orderDataGasless_.baselinePriorityFee, scalingFactor: orderDataGasless_.scalingFactor, decayCurve: orderDataGasless_.decayCurve, - salt: orderDataGasless_.salt + salt: orderDataGasless_.salt, + targetBlock: orderDataGasless_.targetBlock, + maximumBlocksAfterTarget: orderDataGasless_.maximumBlocksAfterTarget }); return orderData_; } diff --git a/src/interfaces/IERC7683Allocator.sol b/src/interfaces/IERC7683Allocator.sol index 4ab08af..45741fe 100644 --- a/src/interfaces/IERC7683Allocator.sol +++ b/src/interfaces/IERC7683Allocator.sol @@ -24,6 +24,9 @@ interface IERC7683Allocator is IOriginSettler { uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) uint256[] decayCurve; // Block durations, fill increases, & claim decreases. bytes32 salt; // Replay protection parameter + // ADDITIONAL INPUT + uint256 targetBlock; // The block number at the target chain on which the PGA is executed / the reverse dutch auction starts. + uint256 maximumBlocksAfterTarget; // Blocks after target block that are still fillable. } struct OrderDataGasless { @@ -45,6 +48,9 @@ interface IERC7683Allocator is IOriginSettler { uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) uint256[] decayCurve; // Block durations, fill increases, & claim decreases. bytes32 salt; // Replay protection parameter + // ADDITIONAL INPUT + uint256 targetBlock; // The block number at the target chain on which the PGA is executed / the reverse dutch auction starts. + uint256 maximumBlocksAfterTarget; // Blocks after target block that are still fillable. } error InvalidOriginSettler(address originSettler, address expectedOriginSettler); @@ -78,4 +84,12 @@ interface IERC7683Allocator is IOriginSettler { /// @notice Checks if a nonce is free to be used /// @dev The nonce is the most significant 96 bits. The least significant 160 bits must be the sponsor address function checkNonce(address sponsor_, uint256 nonce_) external view returns (bool nonceFree_); + + /// @notice Completes the origin data by adding the filler as a claimant in the fillInstructions from the ResolvedCrossChainOrder + /// @param originData_ The origin data from the open event + /// @param claimant_ The address claiming the origin tokens after a successful fill (typically the address of the filler) + function completeOriginData(bytes memory originData_, address claimant_) + external + pure + returns (bytes memory completeOriginData); } diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index a7e5a70..d6630ee 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -51,6 +51,8 @@ abstract contract MocksSetup is Test { uint256 defaultScalingFactor = 0; uint256[] defaultDecayCurve = new uint256[](0); bytes32 defaultSalt = bytes32(0); + uint256 defaultTargetBlock = 100; + uint256 defaultMaximumBlocksAfterTarget = 10; bytes32 ORDERDATA_GASLESS_TYPEHASH; bytes32 ORDERDATA_TYPEHASH; @@ -249,7 +251,9 @@ abstract contract GaslessCrossChainOrderData is CompactData { baselinePriorityFee: mandate_.baselinePriorityFee, scalingFactor: mandate_.scalingFactor, decayCurve: mandate_.decayCurve, - salt: mandate_.salt + salt: mandate_.salt, + targetBlock: defaultTargetBlock, + maximumBlocksAfterTarget: defaultMaximumBlocksAfterTarget }) ) }); @@ -285,7 +289,9 @@ abstract contract GaslessCrossChainOrderData is CompactData { baselinePriorityFee: mandate_.baselinePriorityFee, scalingFactor: mandate_.scalingFactor, decayCurve: mandate_.decayCurve, - salt: mandate_.salt + salt: mandate_.salt, + targetBlock: defaultTargetBlock, + maximumBlocksAfterTarget: defaultMaximumBlocksAfterTarget }) ) }); @@ -331,7 +337,9 @@ abstract contract OnChainCrossChainOrderData is CompactData { baselinePriorityFee: mandate_.baselinePriorityFee, scalingFactor: mandate_.scalingFactor, decayCurve: mandate_.decayCurve, - salt: mandate_.salt + salt: mandate_.salt, + targetBlock: defaultTargetBlock, + maximumBlocksAfterTarget: defaultMaximumBlocksAfterTarget }) ) }); @@ -365,7 +373,9 @@ abstract contract OnChainCrossChainOrderData is CompactData { baselinePriorityFee: mandate_.baselinePriorityFee, scalingFactor: mandate_.scalingFactor, decayCurve: mandate_.decayCurve, - salt: mandate_.salt + salt: mandate_.salt, + targetBlock: defaultTargetBlock, + maximumBlocksAfterTarget: defaultMaximumBlocksAfterTarget }) ) }); @@ -523,7 +533,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate()) + originData: abi.encode(claim, _getMandate(), address(0), defaultTargetBlock, defaultMaximumBlocksAfterTarget) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ @@ -578,7 +588,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate()) + originData: abi.encode(claim, _getMandate(), address(0), defaultTargetBlock, defaultMaximumBlocksAfterTarget) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ @@ -739,7 +749,7 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate()) + originData: abi.encode(claim, _getMandate(), address(0), defaultTargetBlock, defaultMaximumBlocksAfterTarget) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ @@ -904,7 +914,7 @@ contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate()) + originData: abi.encode(claim, _getMandate(), address(0), defaultTargetBlock, defaultMaximumBlocksAfterTarget) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ @@ -970,7 +980,7 @@ contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate()) + originData: abi.encode(claim, _getMandate(), address(0), defaultTargetBlock, defaultMaximumBlocksAfterTarget) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ From af49b8b0de01e25004a118c805b23fd34b088b7f Mon Sep 17 00:00:00 2001 From: vimageDE Date: Fri, 21 Mar 2025 17:01:30 +0100 Subject: [PATCH 34/35] removed claimant from originData --- src/allocators/ERC7683Allocator.sol | 19 ++++--------------- src/interfaces/IERC7683Allocator.sol | 8 ++------ test/ERC7683Allocator.t.sol | 10 +++++----- 3 files changed, 11 insertions(+), 26 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index f7607cb..2713198 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -117,20 +117,9 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { } /// @inheritdoc IERC7683Allocator - function completeOriginData(bytes memory originData_, address claimant_) - external - pure - returns (bytes memory originData) - { - ( - Claim memory claim, - Mandate memory mandate, /* empty receiver of the tokens */ - , - uint256 targetBlock, - uint256 maximumBlocksAfterTarget - ) = abi.decode(originData_, (Claim, Mandate, address, uint256, uint256)); - originData = abi.encode(claim, mandate, claimant_, targetBlock, maximumBlocksAfterTarget); - return originData; + function createFillerData(address claimant_) external pure returns (bytes memory fillerData) { + fillerData = abi.encode(claimant_); + return fillerData; } function _open(OrderData memory orderData_, uint32 fillDeadline_, address sponsor_, bytes memory sponsorSignature_) @@ -258,7 +247,7 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { fillInstructions[0] = FillInstruction({ destinationChainId: orderData.chainId, destinationSettler: _addressToBytes32(orderData.tribunal), - originData: abi.encode(claim, mandate, address(0), orderData.targetBlock, orderData.maximumBlocksAfterTarget) + originData: abi.encode(claim, mandate, orderData.targetBlock, orderData.maximumBlocksAfterTarget) }); Output memory spent = Output({ diff --git a/src/interfaces/IERC7683Allocator.sol b/src/interfaces/IERC7683Allocator.sol index 45741fe..f0af6d5 100644 --- a/src/interfaces/IERC7683Allocator.sol +++ b/src/interfaces/IERC7683Allocator.sol @@ -85,11 +85,7 @@ interface IERC7683Allocator is IOriginSettler { /// @dev The nonce is the most significant 96 bits. The least significant 160 bits must be the sponsor address function checkNonce(address sponsor_, uint256 nonce_) external view returns (bool nonceFree_); - /// @notice Completes the origin data by adding the filler as a claimant in the fillInstructions from the ResolvedCrossChainOrder - /// @param originData_ The origin data from the open event + /// @notice Creates the filler data for the open event to be used on the IDestinationSettler /// @param claimant_ The address claiming the origin tokens after a successful fill (typically the address of the filler) - function completeOriginData(bytes memory originData_, address claimant_) - external - pure - returns (bytes memory completeOriginData); + function createFillerData(address claimant_) external pure returns (bytes memory fillerData); } diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index d6630ee..124ec35 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -533,7 +533,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate(), address(0), defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ @@ -588,7 +588,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate(), address(0), defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ @@ -749,7 +749,7 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate(), address(0), defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ @@ -914,7 +914,7 @@ contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate(), address(0), defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ @@ -980,7 +980,7 @@ contract ERC7683Allocator_resolve is OnChainCrossChainOrderData { fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate(), address(0), defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ From 10162ae0cf5f4ba85f3d792328c4e1fdf2973200 Mon Sep 17 00:00:00 2001 From: vimageDE Date: Fri, 21 Mar 2025 18:21:38 +0100 Subject: [PATCH 35/35] Moved RDA params into qualification data --- src/allocators/ERC7683Allocator.sol | 24 +++++-- src/interfaces/IERC7683Allocator.sol | 3 - test/ERC7683Allocator.t.sol | 96 ++++++++++++++++++++++++---- 3 files changed, 99 insertions(+), 24 deletions(-) diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol index 2713198..1011b16 100644 --- a/src/allocators/ERC7683Allocator.sol +++ b/src/allocators/ERC7683Allocator.sol @@ -5,6 +5,8 @@ pragma solidity ^0.8.27; import {IERC7683Allocator} from '../interfaces/IERC7683Allocator.sol'; import {SimpleAllocator} from './SimpleAllocator.sol'; import {Claim, Mandate} from './types/TribunalStructs.sol'; + +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; import {ECDSA} from '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; @@ -17,9 +19,12 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { /// @notice The typehash of the OrderDataGasless struct // keccak256("OrderDataGasless(address arbiter,uint256 id,uint256 amount, - // uint256 chainId,address tribunal,address recipient,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt,uint256 targetBlock,uint256 maximumBlocksAfterTarget)") + // uint256 chainId,address tribunal,address recipient,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = - 0xe6c40de3837db693910c63e423da4d9f6157257aaad00a0786775e629249ee73; + 0xe9b624fa654c7f07ce16d31bf0165a4030d4022f62987afad8ef9d30fc8a0b88; + + /// @notice keccak256("QualifiedClaim(bytes32 claimHash,uint256 targetBlock,uint256 maximumBlocksAfterTarget)") + bytes32 public constant QUALIFICATION_TYPEHASH = 0x59866b84bd1f6c909cf2a31efd20c59e6c902e50f2c196994e5aa85cdc7d7ce0; /// @notice keccak256("Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Mandate mandate) // Mandate(uint256 chainId,address tribunal,address recipient,uint256 expires,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") @@ -163,10 +168,10 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { ) ) ); - bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT_DOMAIN_SEPARATOR, claimHash)); // We check for the length, which means this could also be triggered by a zero length signature provided in the openFor function. This enables relaying of orders if the claim was registered on the compact. if (sponsorSignature_.length > 0) { + bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT_DOMAIN_SEPARATOR, claimHash)); // confirm the signature matches the digest address signer = ECDSA.recover(digest, sponsorSignature_); if (sponsor_ != signer) { @@ -181,7 +186,13 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { } } - _sponsor[digest] = tokenHash; + bytes32 qualifiedClaimHash = keccak256( + abi.encode(QUALIFICATION_TYPEHASH, claimHash, orderData_.targetBlock, orderData_.maximumBlocksAfterTarget) + ); + bytes32 qualifiedDigest = + keccak256(abi.encodePacked(bytes2(0x1901), _COMPACT_DOMAIN_SEPARATOR, qualifiedClaimHash)); + + _sponsor[qualifiedDigest] = tokenHash; // Emit an open event emit Open( @@ -207,7 +218,6 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { _claim[tokenHash_] = expires; _amount[tokenHash_] = amount; _nonce[tokenHash_] = nonce; - return tokenHash_; } @@ -347,8 +357,8 @@ contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { scalingFactor: orderDataGasless_.scalingFactor, decayCurve: orderDataGasless_.decayCurve, salt: orderDataGasless_.salt, - targetBlock: orderDataGasless_.targetBlock, - maximumBlocksAfterTarget: orderDataGasless_.maximumBlocksAfterTarget + targetBlock: 0, + maximumBlocksAfterTarget: 0 }); return orderData_; } diff --git a/src/interfaces/IERC7683Allocator.sol b/src/interfaces/IERC7683Allocator.sol index f0af6d5..29b7c21 100644 --- a/src/interfaces/IERC7683Allocator.sol +++ b/src/interfaces/IERC7683Allocator.sol @@ -48,9 +48,6 @@ interface IERC7683Allocator is IOriginSettler { uint256 scalingFactor; // Fee scaling multiplier (1e18 baseline) uint256[] decayCurve; // Block durations, fill increases, & claim decreases. bytes32 salt; // Replay protection parameter - // ADDITIONAL INPUT - uint256 targetBlock; // The block number at the target chain on which the PGA is executed / the reverse dutch auction starts. - uint256 maximumBlocksAfterTarget; // Blocks after target block that are still fillable. } error InvalidOriginSettler(address originSettler, address expectedOriginSettler); diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol index 124ec35..5e45c0b 100644 --- a/test/ERC7683Allocator.t.sol +++ b/test/ERC7683Allocator.t.sol @@ -8,7 +8,7 @@ import {TheCompact} from '@uniswap/the-compact/TheCompact.sol'; import {ITheCompact} from '@uniswap/the-compact/interfaces/ITheCompact.sol'; import {IdLib} from '@uniswap/the-compact/lib/IdLib.sol'; -import {ClaimWithWitness} from '@uniswap/the-compact/types/Claims.sol'; +import {QualifiedClaimWithWitness} from '@uniswap/the-compact/types/Claims.sol'; import {COMPACT_TYPEHASH, Compact} from '@uniswap/the-compact/types/EIP712Types.sol'; import {ForcedWithdrawalStatus} from '@uniswap/the-compact/types/ForcedWithdrawalStatus.sol'; @@ -251,9 +251,7 @@ abstract contract GaslessCrossChainOrderData is CompactData { baselinePriorityFee: mandate_.baselinePriorityFee, scalingFactor: mandate_.scalingFactor, decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - targetBlock: defaultTargetBlock, - maximumBlocksAfterTarget: defaultMaximumBlocksAfterTarget + salt: mandate_.salt }) ) }); @@ -289,9 +287,7 @@ abstract contract GaslessCrossChainOrderData is CompactData { baselinePriorityFee: mandate_.baselinePriorityFee, scalingFactor: mandate_.scalingFactor, decayCurve: mandate_.decayCurve, - salt: mandate_.salt, - targetBlock: defaultTargetBlock, - maximumBlocksAfterTarget: defaultMaximumBlocksAfterTarget + salt: mandate_.salt }) ) }); @@ -533,7 +529,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ @@ -588,7 +584,7 @@ contract ERC7683Allocator_openFor is GaslessCrossChainOrderData { fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({ @@ -769,7 +765,11 @@ contract ERC7683Allocator_open is OnChainCrossChainOrderData { } } -contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData { +contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData, GaslessCrossChainOrderData { + function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { + super.setUp(); + } + function test_revert_InvalidLock() public { // Deposit tokens vm.startPrank(user); @@ -796,7 +796,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData { // we do NOT open the order or lock the tokens // claim should be fail, because we mess with the nonce - ClaimWithWitness memory claim = ClaimWithWitness({ + QualifiedClaimWithWitness memory claim = QualifiedClaimWithWitness({ allocatorSignature: '', sponsorSignature: '', sponsor: user, @@ -804,6 +804,8 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData { expires: compact_.expires, witness: keccak256(abi.encode(keccak256(bytes(mandateTypeString)), mandate_)), witnessTypestring: witnessTypeString, + qualificationTypehash: erc7683Allocator.QUALIFICATION_TYPEHASH(), + qualificationPayload: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget), id: usdcId, allocatedAmount: defaultAmount, claimant: filler, @@ -817,7 +819,7 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData { vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); } - function test_isValidSignature_successful() public { + function test_isValidSignature_successful_open() public { // Deposit tokens vm.startPrank(user); usdc.mint(user, defaultAmount); @@ -844,7 +846,71 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData { vm.stopPrank(); // claim should be successful - ClaimWithWitness memory claim = ClaimWithWitness({ + QualifiedClaimWithWitness memory claim = QualifiedClaimWithWitness({ + allocatorSignature: '', + sponsorSignature: '', + sponsor: user, + nonce: defaultNonce, + expires: compact_.expires, + witness: keccak256( + abi.encode( + keccak256(bytes(mandateTypeString)), + defaultOutputChainId, + tribunal, + mandate_.recipient, + mandate_.expires, + mandate_.token, + mandate_.minimumAmount, + mandate_.baselinePriorityFee, + mandate_.scalingFactor, + keccak256(abi.encodePacked(mandate_.decayCurve)), + mandate_.salt + ) + ), + witnessTypestring: witnessTypeString, + qualificationTypehash: erc7683Allocator.QUALIFICATION_TYPEHASH(), + qualificationPayload: abi.encode(defaultTargetBlock, defaultMaximumBlocksAfterTarget), + id: usdcId, + allocatedAmount: defaultAmount, + claimant: filler, + amount: defaultAmount + }); + vm.prank(arbiter); + compactContract.claim(claim); + + vm.assertEq(compactContract.balanceOf(user, usdcId), 0); + vm.assertEq(compactContract.balanceOf(filler, usdcId), defaultAmount); + } + + function test_isValidSignature_successful_openFor() public { + // Deposit tokens + vm.startPrank(user); + usdc.mint(user, defaultAmount); + usdc.approve(address(compactContract), defaultAmount); + compactContract.deposit( + address(usdc), address(erc7683Allocator), defaultResetPeriod, defaultScope, defaultAmount, user + ); + + // register a claim + Compact memory compact_ = _getCompact(); + Mandate memory mandate_ = _getMandate(); + + bytes32 claimHash = _hashCompact(compact_, mandate_); + bytes32 typeHash = _getTypeHash(); + compactContract.register(claimHash, typeHash, defaultResetPeriodTimestamp); + + address filler = makeAddr('filler'); + vm.assertEq(compactContract.balanceOf(user, usdcId), defaultAmount); + vm.assertEq(compactContract.balanceOf(filler, usdcId), 0); + + // we open the order and lock the tokens + (IOriginSettler.GaslessCrossChainOrder memory gaslessCrossChainOrder_, bytes memory sponsorSignature) = + _getGaslessCrossChainOrder(); + erc7683Allocator.openFor(gaslessCrossChainOrder_, sponsorSignature, ''); + vm.stopPrank(); + + // claim should be successful + QualifiedClaimWithWitness memory claim = QualifiedClaimWithWitness({ allocatorSignature: '', sponsorSignature: '', sponsor: user, @@ -866,6 +932,8 @@ contract ERC7683Allocator_isValidSignature is OnChainCrossChainOrderData { ) ), witnessTypestring: witnessTypeString, + qualificationTypehash: erc7683Allocator.QUALIFICATION_TYPEHASH(), + qualificationPayload: abi.encode(uint256(0), uint256(0)), id: usdcId, allocatedAmount: defaultAmount, claimant: filler, @@ -914,7 +982,7 @@ contract ERC7683Allocator_resolveFor is GaslessCrossChainOrderData { fillInstructions[0] = IOriginSettler.FillInstruction({ destinationChainId: defaultOutputChainId, destinationSettler: bytes32(uint256(uint160(tribunal))), - originData: abi.encode(claim, _getMandate(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) + originData: abi.encode(claim, _getMandate(), uint256(0), uint256(0)) }); IOriginSettler.ResolvedCrossChainOrder memory resolvedCrossChainOrder = IOriginSettler.ResolvedCrossChainOrder({