Skip to content

Commit

Permalink
Implement single tx fill and settle
Browse files Browse the repository at this point in the history
  • Loading branch information
yorhodes committed Nov 20, 2024
1 parent 877a5ea commit 7ffdac2
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 67 deletions.
144 changes: 109 additions & 35 deletions src/HyperlaneArbiter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,77 +3,151 @@ 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;
address recipient;
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);
}
}
46 changes: 14 additions & 32 deletions test/HyperlaneArbiter.t.sol
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
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";

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;
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand Down

0 comments on commit 7ffdac2

Please sign in to comment.