diff --git a/README.md b/README.md index b734bd1..67c725d 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,7 @@ 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. +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. ## Deployment diff --git a/src/allocators/ERC7683Allocator.sol b/src/allocators/ERC7683Allocator.sol deleted file mode 100644 index 1011b16..0000000 --- a/src/allocators/ERC7683Allocator.sol +++ /dev/null @@ -1,365 +0,0 @@ -// SPDX-License-Identifier: MIT - -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'; - -contract ERC7683Allocator is SimpleAllocator, IERC7683Allocator { - /// @notice The typehash of the OrderData struct - // 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, - // uint256 chainId,address tribunal,address recipient,address token,uint256 minimumAmount,uint256 baselinePriorityFee,uint256 scalingFactor,uint256[] decayCurve,bytes32 salt)") - bytes32 public constant ORDERDATA_GASLESS_TYPEHASH = - 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)") - bytes32 public constant COMPACT_WITNESS_TYPEHASH = - 0xfd9cda0e5e31a3a3476cb5b57b07e2a4d6a12815506f69c880696448cd9897a5; - - /// @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; - - bytes32 immutable _COMPACT_DOMAIN_SEPARATOR; - - constructor(address compactContract_, uint256 minWithdrawalDelay_, uint256 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 - { - // 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,uint256[] decayCurve,bytes32 salt))'; - } - - /// @inheritdoc IERC7683Allocator - function checkNonce(address sponsor_, uint256 nonce_) external view returns (bool nonceFree_) { - 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_; - } - - /// @inheritdoc IERC7683Allocator - 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_) - internal - { - // Enforce a nonce where the most significant 96 bits are the nonce and the least significant 160 bits are the sponsor - uint96 nonceWithoutAddress = _checkNonce(sponsor_, orderData_.nonce); - - // 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 - - 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( - MANDATE_TYPEHASH, - orderData_.chainId, - orderData_.tribunal, - orderData_.recipient, - fillDeadline_, - orderData_.token, - orderData_.minimumAmount, - orderData_.baselinePriorityFee, - orderData_.scalingFactor, - keccak256(abi.encodePacked(orderData_.decayCurve)), - orderData_.salt - ) - ) - ) - ); - - // 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) { - 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); - } - } - - 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( - bytes32(orderData_.nonce), - _resolveOrder(sponsor_, fillDeadline_, orderData_.nonce, orderData_, sponsorSignature_) - ); - } - - function _lockTokens(OrderData memory orderData_, address sponsor_, uint256 nonce) - internal - returns (bytes32 tokenHash_) - { - return _lockTokens(orderData_.arbiter, sponsor_, nonce, orderData_.expires, orderData_.id, orderData_.amount); - } - - 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: nonce, expires: expires, id: id, amount: amount}), false - ); - _claim[tokenHash_] = expires; - _amount[tokenHash_] = amount; - _nonce[tokenHash_] = nonce; - return tokenHash_; - } - - function _resolveOrder( - address sponsor, - uint32 fillDeadline, - uint256 nonce, - OrderData memory orderData, - bytes memory sponsorSignature - ) internal view returns (ResolvedCrossChainOrder memory) { - FillInstruction[] memory fillInstructions = new FillInstruction[](1); - - Mandate memory mandate = Mandate({ - recipient: orderData.recipient, - expires: fillDeadline, - token: orderData.token, - minimumAmount: orderData.minimumAmount, - baselinePriorityFee: orderData.baselinePriorityFee, - scalingFactor: orderData.scalingFactor, - decayCurve: orderData.decayCurve, - 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, orderData.targetBlock, orderData.maximumBlocksAfterTarget) - }); - - 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(nonce), - maxSpent: maxSpent, - minReceived: minReceived, - fillInstructions: fillInstructions - }); - return resolvedOrder; - } - - 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_)) - } - } - - 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, - decayCurve: orderDataGasless_.decayCurve, - salt: orderDataGasless_.salt, - targetBlock: 0, - maximumBlocksAfterTarget: 0 - }); - return orderData_; - } -} diff --git a/src/allocators/SimpleAllocator.sol b/src/allocators/SimpleAllocator.sol deleted file mode 100644 index 7eb40a7..0000000 --- a/src/allocators/SimpleAllocator.sol +++ /dev/null @@ -1,221 +0,0 @@ -// 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_, true); - - 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, address from_, address, uint256 id_, uint256 amount_) external view returns (bytes4) { - if (msg.sender != COMPACT_CONTRACT) { - revert InvalidCaller(msg.sender, COMPACT_CONTRACT); - } - 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 this.attest.selector; - } - - /// @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_, bool checkSponsor_) internal view returns (bytes32) { - // Check msg.sender is sponsor - if (checkSponsor_ && msg.sender != compact_.sponsor) { - revert InvalidCaller(msg.sender, compact_.sponsor); - } - bytes32 tokenHash = _getTokenHash(compact_.id, compact_.sponsor); - // 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(compact_.sponsor, compact_.id); - // Check balance is enough - if (balance < compact_.amount) { - revert InsufficientBalance(compact_.sponsor, 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/allocators/types/TribunalStructs.sol b/src/allocators/types/TribunalStructs.sol deleted file mode 100644 index 9d1ffaa..0000000 --- a/src/allocators/types/TribunalStructs.sol +++ /dev/null @@ -1,25 +0,0 @@ -// 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 { - // 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/ERC7683/IOriginSettler.sol b/src/interfaces/ERC7683/IOriginSettler.sol deleted file mode 100644 index 026ae91..0000000 --- a/src/interfaces/ERC7683/IOriginSettler.sol +++ /dev/null @@ -1,133 +0,0 @@ -// 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 - 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 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); -} diff --git a/src/interfaces/IAllocator.sol b/src/interfaces/IAllocator.sol deleted file mode 100644 index 581edb6..0000000 --- a/src/interfaces/IAllocator.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; - -interface IAllocator is IERC1271 { - // Called on standard transfers; must return this function selector (0x1a808f91). - function attest(address operator, address from, address to, uint256 id, uint256 amount) external returns (bytes4); - - // isValidSignature of IERC1271 will be called during a claim and must verify the signature of the allocation. -} diff --git a/src/interfaces/IERC7683Allocator.sol b/src/interfaces/IERC7683Allocator.sol deleted file mode 100644 index 29b7c21..0000000 --- a/src/interfaces/IERC7683Allocator.sol +++ /dev/null @@ -1,88 +0,0 @@ -// 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) - 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 { - // 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) - uint256[] decayCurve; // Block durations, fill increases, & claim decreases. - 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_); - - /// @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 createFillerData(address claimant_) external pure returns (bytes memory fillerData); -} diff --git a/src/interfaces/ISimpleAllocator.sol b/src/interfaces/ISimpleAllocator.sol deleted file mode 100644 index 0e83fd2..0000000 --- a/src/interfaces/ISimpleAllocator.sol +++ /dev/null @@ -1,62 +0,0 @@ -// 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/ERC20Mock.sol b/src/test/ERC20Mock.sol deleted file mode 100644 index f8a0ce1..0000000 --- a/src/test/ERC20Mock.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; - -contract ERC20Mock is ERC20 { - 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 deleted file mode 100644 index 4dcee68..0000000 --- a/src/test/TheCompactMock.sol +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.27; - -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; - using IdLib for uint256; - using IdLib for address; - - // Mock Variables - uint32 private constant DEFAULT_RESET_PERIOD = 60; - ResetPeriod private constant DEFAULT_RESET_PERIOD_TYPE = ResetPeriod.OneMinute; - Scope private constant DEFAULT_SCOPE = Scope.Multichain; - address private DEFAULT_ALLOCATOR; - - // Mock State - mapping(uint256 id => address token) public tokens; - mapping(uint256 nonce => bool consumed) public consumedNonces; - mapping(address allocator => bool registered) public registeredAllocators; - mapping(address user => uint256 availableAt) public forcedWithdrawalStatus; - - function __registerAllocator(address allocator, bytes calldata) external returns (uint96) { - registeredAllocators[allocator] = true; - DEFAULT_ALLOCATOR = allocator; - return 0; - } - - 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; - _mint(msg.sender, id, amount); - } - - function transfer(address from, address to, uint256 amount, address token, address allocator) external { - uint256 id = _getTokenId(token, allocator); - IAllocator(allocator).attest(msg.sender, from, to, id, amount); - _transfer(address(0), from, to, id, amount); - } - - 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); - } - - function withdraw(address token, uint256 amount, address allocator) external { - uint256 id = _getTokenId(token, allocator); - IAllocator(allocator).attest(msg.sender, msg.sender, msg.sender, id, amount); - ERC20(token).transferFrom(address(this), msg.sender, amount); - _burn(msg.sender, id, amount); - } - - function consume(uint256[] calldata nonces) external returns (bool) { - for (uint256 i = 0; i < nonces.length; ++i) { - consumedNonces[nonces[i]] = true; - } - return true; - } - - function hasConsumedAllocatorNonce(uint256 nonce, address) external view returns (bool) { - return consumedNonces[nonce]; - } - - function getLockDetails(uint256 id) external view returns (address, address, ResetPeriod, Scope) { - return (tokens[id], DEFAULT_ALLOCATOR, DEFAULT_RESET_PERIOD_TYPE, DEFAULT_SCOPE); - } - - function enableForceWithdrawal(uint256) external returns (uint256) { - forcedWithdrawalStatus[msg.sender] = block.timestamp + DEFAULT_RESET_PERIOD; - return block.timestamp + DEFAULT_RESET_PERIOD; - } - - function disableForceWithdrawal(uint256) external returns (bool) { - forcedWithdrawalStatus[msg.sender] = 0; - return true; - } - - function getForcedWithdrawalStatus(address sponsor, uint256) - external - view - returns (ForcedWithdrawalStatus, uint256) - { - uint256 expires = forcedWithdrawalStatus[sponsor]; - return (expires == 0 ? ForcedWithdrawalStatus.Disabled : ForcedWithdrawalStatus.Enabled, expires); - } - - function getTokenId(address token, address allocator) external pure returns (uint256) { - return _getTokenId(token, allocator); - } - - function DOMAIN_SEPARATOR() public view returns (bytes32) { - return keccak256( - abi.encode( - // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') - 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f, - keccak256('The Compact'), - keccak256('0'), - block.chainid, - address(this) - ) - ); - } - - function name( - uint256 // id - ) public view virtual override returns (string memory) { - return 'TheCompactMock'; - } - - function symbol( - uint256 // id - ) public view virtual override returns (string memory) { - return 'TCM'; - } - - function tokenURI( - uint256 // id - ) public view virtual override returns (string memory) { - 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))) - } - return tokenId; - } -} diff --git a/test/ERC7683Allocator.t.sol b/test/ERC7683Allocator.t.sol deleted file mode 100644 index 5e45c0b..0000000 --- a/test/ERC7683Allocator.t.sol +++ /dev/null @@ -1,1175 +0,0 @@ -// 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 {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 {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'; -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; - 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; - uint256[] defaultDecayCurve = new uint256[](0); - bytes32 defaultSalt = bytes32(0); - uint256 defaultTargetBlock = 100; - uint256 defaultMaximumBlocksAfterTarget = 10; - - 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), 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(user, uint96(1)))); - - ORDERDATA_GASLESS_TYPEHASH = erc7683Allocator.ORDERDATA_GASLESS_TYPEHASH(); - ORDERDATA_TYPEHASH = erc7683Allocator.ORDERDATA_TYPEHASH(); - } -} - -abstract contract CreateHash is MocksSetup { - 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,uint256[] decayCurve,bytes32 salt)'; - string mandateTypeString = - '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,uint256[] decayCurve,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( - keccak256(bytes(mandateTypeString)), - defaultOutputChainId, - tribunal, - mandate.recipient, - mandate.expires, - mandate.token, - mandate.minimumAmount, - mandate.baselinePriorityFee, - mandate.scalingFactor, - keccak256(abi.encodePacked(mandate.decayCurve)), - mandate.salt - ) - ) - ) - ); - } - - 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 CreateHash { - 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({ - recipient: user, - expires: _getFillExpiration(), - token: defaultOutputToken, - minimumAmount: defaultMinimumAmount, - baselinePriorityFee: defaultBaselinePriorityFee, - scalingFactor: defaultScalingFactor, - decayCurve: defaultDecayCurve, - 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 { - 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( - 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 - }) - ) - }); - } - - 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( - 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 - }) - ) - }); - - (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( - 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, - targetBlock: defaultTargetBlock, - maximumBlocksAfterTarget: defaultMaximumBlocksAfterTarget - }) - ) - }); - } - - function _getOnChainCrossChainOrder() internal view returns (IOriginSettler.OnchainCrossChainOrder memory) { - return onchainCrossChainOrder; - } - - function _getOnChainCrossChainOrder(Compact memory compact_, Mandate memory mandate_, bytes32 orderDataType_) - internal - view - returns (IOriginSettler.OnchainCrossChainOrder memory) - { - IOriginSettler.OnchainCrossChainOrder memory onchainCrossChainOrder_ = IOriginSettler.OnchainCrossChainOrder({ - fillDeadline: uint32(mandate_.expires), - orderDataType: orderDataType_, - orderData: abi.encode( - 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, - targetBlock: defaultTargetBlock, - maximumBlocksAfterTarget: defaultMaximumBlocksAfterTarget - }) - ) - }); - 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_userHimself() 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(), uint256(0), uint256(0)) - }); - - 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_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(), uint256(0), uint256(0)) - }); - - 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 - - // 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 { - 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(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) - }); - - 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, GaslessCrossChainOrderData { - function setUp() public override(OnChainCrossChainOrderData, GaslessCrossChainOrderData) { - super.setUp(); - } - - 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 - QualifiedClaimWithWitness memory claim = QualifiedClaimWithWitness({ - allocatorSignature: '', - sponsorSignature: '', - sponsor: user, - nonce: defaultNonce, - 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, - 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_open() 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 - 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, - 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(uint256(0), uint256(0)), - 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); - } -} - -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(), uint256(0), uint256(0)) - }); - - 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(), defaultTargetBlock, defaultMaximumBlocksAfterTarget) - }); - - 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); - } -} - -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,uint256[] decayCurve,bytes32 salt))' - ); - } -} - -contract ERC7683Allocator_checkNonce is OnChainCrossChainOrderData { - function test_revert_invalidNonce(uint256 nonce_) public { - address expectedSponsor; - assembly ("memory-safe") { - expectedSponsor := shr(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(96, sponsor), shr(160, shl(160, nonce_))) - } - 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); - - (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(); - erc7683Allocator.open(onChainCrossChainOrder_); - - vm.assertEq(erc7683Allocator.checkNonce(user, nonce), !sameNonce); - - vm.stopPrank(); - } -} diff --git a/test/SimpleAllocator.t.sol b/test/SimpleAllocator.t.sol deleted file mode 100644 index 3f8bbf9..0000000 --- a/test/SimpleAllocator.t.sol +++ /dev/null @@ -1,784 +0,0 @@ -// 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_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_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_)); - - 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 -}