From 7ffdac2715678529281ad0c9cb336dd1610f27b4 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Wed, 20 Nov 2024 18:12:37 -0500 Subject: [PATCH] Implement single tx fill and settle --- src/HyperlaneArbiter.sol | 144 +++++++++++++++++++++++++++--------- test/HyperlaneArbiter.t.sol | 46 ++++-------- 2 files changed, 123 insertions(+), 67 deletions(-) diff --git a/src/HyperlaneArbiter.sol b/src/HyperlaneArbiter.sol index c06d373..cd1f1c7 100644 --- a/src/HyperlaneArbiter.sol +++ b/src/HyperlaneArbiter.sol @@ -3,13 +3,12 @@ pragma solidity ^0.8.27; import {TheCompact} from "the-compact/src/TheCompact.sol"; import {ClaimWithWitness} from "the-compact/src/types/Claims.sol"; +import {Compact} from "the-compact/src/types/EIP712Types.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {Router} from "hyperlane/contracts/client/Router.sol"; -// witness data struct Intent { - // from sponsor allocated amount to claimant uint256 fee; uint32 chainId; address token; @@ -17,63 +16,138 @@ struct Intent { uint256 amount; } -struct Fill { - address claimant; - uint256 fee; -} - -string constant TYPESTRING = "Intent(uint256 fee,uint32 chainId,address recipient,address token,uint256 amount)"; +string constant TYPESTRING = "Intent(uint256 fee,uint32 chainId,address token,address recipient,uint256 amount)"; bytes32 constant TYPEHASH = keccak256(bytes(TYPESTRING)); +string constant WITNESS_TYPESTRING = + "Intent intent)Intent(uint256 fee,uint32 chainId,address token,address recipient,uint256 amount)"; + +library Message { + function encode( + Compact calldata compact, + bytes calldata allocatorSignature, + bytes calldata sponsorSignature, + bytes32 witness, + uint256 fee, + address filler + ) internal pure returns (bytes memory) { + return abi.encodePacked( + compact.arbiter, + compact.sponsor, + compact.nonce, + compact.expires, + compact.id, + compact.amount, + allocatorSignature, + sponsorSignature, + witness, + fee, + filler + ); + } + + function decode(bytes calldata message) + internal + pure + returns ( + // TODO: calldata + Compact memory compact, + bytes calldata allocatorSignature, + bytes calldata sponsorSignature, + bytes32 witness, + uint256 fee, + address filler + ) + { + assert(message.length == 380); + compact = Compact({ + arbiter: address(bytes20(message[0:20])), + sponsor: address(bytes20(message[20:40])), + nonce: uint256(bytes32(message[40:72])), + expires: uint256(bytes32(message[72:104])), + id: uint256(bytes32(message[104:136])), + amount: uint256(bytes32(message[136:168])) + }); + allocatorSignature = message[168:232]; + sponsorSignature = message[232:296]; + witness = bytes32(message[296:328]); + fee = uint256(bytes32(message[328:360])); + filler = address(bytes20(message[360:380])); + } +} + contract HyperlaneArbiter is Router { + using Message for bytes; using SafeTransferLib for address; TheCompact public immutable theCompact; - mapping(bytes32 witness => Fill) public fills; - constructor(address _mailbox, address _theCompact) Router(_mailbox) { theCompact = TheCompact(_theCompact); } + /** + * @notice Fills a compact intent and dispatches the claim to the arbiter. + * @dev msg.value is used to cover all hyperlane fees (relay, etc). + * @param claimChain The chain ID of the claim. + * @param compact The compact intent to fill. + * @dev signatures must be compliant with https://eips.ethereum.org/EIPS/eip-2098 + * @param allocatorSignature The allocator's signature. + * @param sponsorSignature The sponsor's signature. + */ function fill( uint32 claimChain, - Intent calldata intent // adding discriminator + Compact calldata compact, + Intent calldata intent, + bytes calldata allocatorSignature, + bytes calldata sponsorSignature ) external payable { - // filler must pay for message dispatch require(block.chainid == intent.chainId, "invalid chain"); // TODO: support Permit2 fills - address claimant = msg.sender; - intent.token.safeTransferFrom(claimant, intent.recipient, intent.amount); + address filler = msg.sender; + intent.token.safeTransferFrom(filler, intent.recipient, intent.amount); - bytes32 witness = hash(intent); - _dispatch(claimChain, abi.encodePacked(witness, intent.fee, claimant)); + _dispatch( + claimChain, Message.encode(compact, allocatorSignature, sponsorSignature, hash(intent), intent.fee, filler) + ); } - function hash(Intent calldata intent) public pure returns (bytes32) { + function hash(Intent memory intent) public pure returns (bytes32) { return - keccak256(abi.encode(TYPEHASH, intent.fee, intent.chainId, intent.recipient, intent.token, intent.amount)); - } - - function _handle(uint32, /*origin*/ bytes32, /*sender*/ bytes calldata message) internal override { - bytes32 witness = bytes32(message[0:32]); - uint256 fee = uint256(bytes32(message[32:64])); - address claimaint = address(bytes20(message[64:84])); - - require(fills[witness].claimant == address(0), "intent already filled"); - fills[witness] = Fill(claimaint, fee); + keccak256(abi.encode(TYPEHASH, intent.fee, intent.chainId, intent.token, intent.recipient, intent.amount)); } - function claim(ClaimWithWitness calldata claimPayload) external { - Fill storage witnessFill = fills[claimPayload.witness]; - require(witnessFill.fee == claimPayload.amount, "invalid claim amount"); - require(witnessFill.claimant == claimPayload.claimant, "invalid claimant"); + function _handle( + uint32, + /*origin*/ + bytes32, + /*sender*/ + bytes calldata message + ) internal override { + ( + Compact memory compact, + bytes memory allocatorSignature, + bytes memory sponsorSignature, + bytes32 witness, + uint256 fee, + address filler + ) = message.decode(); + + ClaimWithWitness memory claimPayload = ClaimWithWitness({ + witnessTypestring: WITNESS_TYPESTRING, + witness: witness, + allocatorSignature: allocatorSignature, + sponsorSignature: sponsorSignature, + sponsor: compact.sponsor, + nonce: compact.nonce, + expires: compact.expires, + id: compact.id, + allocatedAmount: compact.amount, + amount: fee, + claimant: filler + }); - // assuming that the compact does - // 1. sponsor signature verification - // 2. replay protection - // 3. expiration check theCompact.claim(claimPayload); } } diff --git a/test/HyperlaneArbiter.t.sol b/test/HyperlaneArbiter.t.sol index 9a90fc6..14416f9 100644 --- a/test/HyperlaneArbiter.t.sol +++ b/test/HyperlaneArbiter.t.sol @@ -1,7 +1,7 @@ pragma solidity ^0.8.0; import "the-compact/test/TheCompact.t.sol"; -import "../src/HyperlaneArbiter.sol"; +import {HyperlaneArbiter, Intent, WITNESS_TYPESTRING} from "../src/HyperlaneArbiter.sol"; import {MockMailbox} from "hyperlane/contracts/mock/MockMailbox.sol"; import {TypeCasts} from "hyperlane/contracts/libs/TypeCasts.sol"; @@ -9,7 +9,7 @@ import {TypeCasts} from "hyperlane/contracts/libs/TypeCasts.sol"; contract HyperlaneArbiterTest is TheCompactTest { using TypeCasts for address; - uint32 origin = 1; + uint32 origin = uint32(block.chainid); // match the compact chain id uint32 destination = 2; MockMailbox originMailbox; @@ -53,29 +53,15 @@ contract HyperlaneArbiterTest is TheCompactTest { uint256 fee = amount - 1; uint32 chainId = destination; - string memory witnessTypestring = - "Intent intent)Intent(uint256 fee,uint32 chainId,address recipient,address token,uint256 amount)"; - Intent memory intent = Intent(fee, chainId, address(token), swapper, amount); - - vm.chainId(destination); - - token.mint(claimant, amount); - - vm.startPrank(claimant); - // TODO: permit2 approvals - token.approve(address(destinationArbiter), amount); - destinationArbiter.fill(origin, intent); - vm.stopPrank(); - - originMailbox.processNextInboundMessage(); + Compact memory compact = Compact(arbiter, swapper, nonce, expires, id, amount); bytes32 witness = originArbiter.hash(intent); bytes32 claimHash = keccak256( abi.encode( keccak256( - "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Intent intent)Intent(uint256 fee,uint32 chainId,address recipient,address token,uint256 amount)" + "Compact(address arbiter,address sponsor,uint256 nonce,uint256 expires,uint256 id,uint256 amount,Intent intent)Intent(uint256 fee,uint32 chainId,address token,address recipient,uint256 amount)" ), arbiter, swapper, @@ -95,21 +81,17 @@ contract HyperlaneArbiterTest is TheCompactTest { (r, vs) = vm.signCompact(allocatorPrivateKey, digest); bytes memory allocatorSignature = abi.encodePacked(r, vs); - ClaimWithWitness memory claim = ClaimWithWitness( - allocatorSignature, - sponsorSignature, - swapper, - nonce, - expires, - witness, - witnessTypestring, - id, - amount, - claimant, - fee - ); + vm.chainId(destination); + token.mint(claimant, amount); + + vm.startPrank(claimant); + // TODO: permit2 approvals + token.approve(address(destinationArbiter), amount); + destinationArbiter.fill(origin, compact, intent, allocatorSignature, sponsorSignature); + vm.stopPrank(); - originArbiter.claim(claim); + vm.chainId(origin); + originMailbox.processNextInboundMessage(); assertEq(address(theCompact).balance, amount); assertEq(claimant.balance, 0);