Skip to content

Commit

Permalink
Merge pull request #1 from Uniswap/hyperlane
Browse files Browse the repository at this point in the history
Implement simple hyperlane intent arbiter
  • Loading branch information
0age authored Nov 21, 2024
2 parents 999763b + 7ffdac2 commit 2e46e4a
Show file tree
Hide file tree
Showing 11 changed files with 4,028 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ jobs:
with:
submodules: recursive


- name: Set Node.js 21.x
uses: actions/setup-node@v3
with:
node-version: 21.x

- name: Run install
uses: borales/actions-yarn@v4
with:
cmd: install # will run `yarn install` command

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ docs/

# Dotenv file
.env
node_modules
Binary file added .yarn/install-state.gz
Binary file not shown.
934 changes: 934 additions & 0 deletions .yarn/releases/yarn-4.5.1.cjs

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
nodeLinker: node-modules

yarnPath: .yarn/releases/yarn-4.5.1.cjs
4 changes: 4 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
[profile.default]
src = "src"
out = "out"
evm_version = "cancun"
via_ir = true
optimizer_runs = 200
bytecode_hash = 'none'
libs = ["lib"]
remappings = [
"ds-test/=lib/the-compact/lib/permit2/lib/forge-std/lib/ds-test/src/",
Expand Down
12 changes: 12 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "arbiters",
"version": "1.0.0",
"main": "index.js",
"repository": "[email protected]:uniswap/arbiters.git",
"author": "",
"license": "MIT",
"dependencies": {
"@hyperlane-xyz/core": "^5.8.0"
},
"packageManager": "[email protected]"
}
13 changes: 13 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
ds-test/=lib/the-compact/lib/permit2/lib/forge-std/lib/ds-test/src/
forge-gas-snapshot/=lib/the-compact/lib/permit2/lib/forge-gas-snapshot/src/
forge-std/=lib/the-compact/lib/forge-std/src/
openzeppelin-contracts/=lib/the-compact/lib/permit2/lib/openzeppelin-contracts/
permit2/=lib/the-compact/lib/permit2/
solady/=lib/the-compact/lib/solady/src/
soledge/=lib/the-compact/lib/soledge/src/
solmate/=lib/the-compact/lib/permit2/lib/solmate/
the-compact/=lib/the-compact/
tstorish/=lib/the-compact/lib/tstorish/src/
hyperlane/=node_modules/@hyperlane-xyz/core
@openzeppelin/contracts/=node_modules/@openzeppelin/contracts
@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable
153 changes: 153 additions & 0 deletions src/HyperlaneArbiter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// SPDX-License-Identifier: MIT
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";

struct Intent {
uint256 fee;
uint32 chainId;
address token;
address recipient;
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;

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,
Compact calldata compact,
Intent calldata intent,
bytes calldata allocatorSignature,
bytes calldata sponsorSignature
) external payable {
require(block.chainid == intent.chainId, "invalid chain");

// TODO: support Permit2 fills
address filler = msg.sender;
intent.token.safeTransferFrom(filler, intent.recipient, intent.amount);

_dispatch(
claimChain, Message.encode(compact, allocatorSignature, sponsorSignature, hash(intent), intent.fee, filler)
);
}

function hash(Intent memory intent) public pure returns (bytes32) {
return
keccak256(abi.encode(TYPEHASH, intent.fee, intent.chainId, intent.token, intent.recipient, intent.amount));
}

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
});

theCompact.claim(claimPayload);
}
}
102 changes: 102 additions & 0 deletions test/HyperlaneArbiter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
pragma solidity ^0.8.0;

import "the-compact/test/TheCompact.t.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 = uint32(block.chainid); // match the compact chain id
uint32 destination = 2;

MockMailbox originMailbox;
MockMailbox destinationMailbox;

HyperlaneArbiter originArbiter;
HyperlaneArbiter destinationArbiter;

function hyperlane_setup() public {
originMailbox = new MockMailbox(origin);
destinationMailbox = new MockMailbox(destination);

originMailbox.addRemoteMailbox(destination, destinationMailbox);
destinationMailbox.addRemoteMailbox(origin, originMailbox);

originArbiter = new HyperlaneArbiter(address(originMailbox), address(theCompact));
destinationArbiter = new HyperlaneArbiter(address(destinationMailbox), address(0));

originArbiter.enrollRemoteRouter(destination, address(destinationArbiter).addressToBytes32());
destinationArbiter.enrollRemoteRouter(origin, address(originArbiter).addressToBytes32());
}

function test_hyperlane_claimWithWitness() public {
hyperlane_setup();

ResetPeriod resetPeriod = ResetPeriod.TenMinutes;
Scope scope = Scope.Multichain;
uint256 amount = 1e18;
uint256 nonce = 0;
uint256 expires = block.timestamp + 1000;
address claimant = 0x1111111111111111111111111111111111111111;
address arbiter = address(originArbiter);

vm.prank(allocator);
theCompact.__registerAllocator(allocator, "");

vm.prank(swapper);
uint256 id = theCompact.deposit{value: amount}(allocator, resetPeriod, scope, swapper);
assertEq(theCompact.balanceOf(swapper, id), amount);

uint256 fee = amount - 1;
uint32 chainId = destination;

Intent memory intent = Intent(fee, chainId, address(token), swapper, amount);
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 token,address recipient,uint256 amount)"
),
arbiter,
swapper,
nonce,
expires,
id,
amount,
witness
)
);

bytes32 digest = keccak256(abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), claimHash));

(bytes32 r, bytes32 vs) = vm.signCompact(swapperPrivateKey, digest);
bytes memory sponsorSignature = abi.encodePacked(r, vs);

(r, vs) = vm.signCompact(allocatorPrivateKey, digest);
bytes memory allocatorSignature = abi.encodePacked(r, vs);

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();

vm.chainId(origin);
originMailbox.processNextInboundMessage();

assertEq(address(theCompact).balance, amount);
assertEq(claimant.balance, 0);

assertEq(theCompact.balanceOf(swapper, id), amount - fee);
assertEq(theCompact.balanceOf(claimant, id), fee);
}
}
Loading

0 comments on commit 2e46e4a

Please sign in to comment.