Skip to content

Commit

Permalink
feat(contracts): ValueRouter (#4814)
Browse files Browse the repository at this point in the history
### Description

- goal: use a unified `transferRemote` interface to send msg value from
A to B agnostic or the hook/ism needed to do so
- design doc:
https://www.notion.so/hyperlanexyz/Native-bridge-value-transfer-API-1126d35200d680a0aa42d653d335a446

### Drive-by changes

- overrideMsgValue and overrideGasLimit which actually override the
respective fields

### Related issues

<!--
- Fixes #[issue number here]
-->

### Backward compatibility

Yes

### Testing

Unit

---------

Co-authored-by: Yorke Rhodes <[email protected]>
  • Loading branch information
aroralanuk and yorhodes authored Dec 20, 2024
1 parent a51b50c commit b36bf0c
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/pretty-keys-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/core': minor
---

Added a new router HypNativeCollateral with a unified interface for sending value hook/ism agnostic
19 changes: 19 additions & 0 deletions solidity/contracts/hooks/libs/StandardHookMetadata.sol
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,23 @@ library StandardHookMetadata {
) internal pure returns (bytes memory) {
return formatMetadata(uint256(0), uint256(0), _refundAddress, "");
}

/**
* @notice Overrides the msg.value in the metadata.
* @param _metadata encoded standard hook metadata.
* @param _msgValue msg.value for the message.
* @return encoded standard hook metadata.
*/
function overrideMsgValue(
bytes calldata _metadata,
uint256 _msgValue
) internal view returns (bytes memory) {
return
formatMetadata(
_msgValue,
gasLimit(_metadata, 0),
refundAddress(_metadata, msg.sender),
getCustomMetadata(_metadata)
);
}
}
84 changes: 84 additions & 0 deletions solidity/contracts/token/HypNativeCollateral.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;

/*@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@ HYPERLANE @@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@@
@@@@@@@@@ @@@@@@@@*/

// ============ Internal Imports ============
import {TokenRouter} from "./libs/TokenRouter.sol";
import {HypNative} from "./HypNative.sol";
import {StandardHookMetadata} from "../hooks/libs/StandardHookMetadata.sol";

/**
* @title HypNativeCollateral
* @author Abacus Works
* @notice This contract facilitates the transfer of value between chains using value transfer hooks
*/
contract HypNativeCollateral is HypNative {
constructor(address _mailbox) HypNative(_mailbox) {}

// ============ External Functions ============

/// @inheritdoc TokenRouter
function transferRemote(
uint32 _destination,
bytes32 _recipient,
uint256 _amount
) external payable virtual override returns (bytes32 messageId) {
bytes calldata emptyBytes;
assembly {
emptyBytes.length := 0
emptyBytes.offset := 0
}
return
transferRemote(
_destination,
_recipient,
_amount,
emptyBytes,
address(hook)
);
}

/**
* @inheritdoc TokenRouter
* @dev use _hook with caution, make sure that this hook can handle msg.value transfer using the metadata.msgValue()
*/
function transferRemote(
uint32 _destination,
bytes32 _recipient,
uint256 _amount,
bytes calldata _hookMetadata,
address _hook
) public payable virtual override returns (bytes32 messageId) {
uint256 quote = _GasRouter_quoteDispatch(
_destination,
_hookMetadata,
_hook
);

bytes memory hookMetadata = StandardHookMetadata.overrideMsgValue(
_hookMetadata,
_amount
);

return
_transferRemote(
_destination,
_recipient,
_amount,
_amount + quote,
hookMetadata,
_hook
);
}
}
152 changes: 152 additions & 0 deletions solidity/test/token/HypNative.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;

import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {HypTokenTest} from "./HypERC20.t.sol";
import {HypERC20} from "../../contracts/token/HypERC20.sol";
import {TokenRouter} from "../../contracts/token/libs/TokenRouter.sol";
import {HypNativeCollateral} from "../../contracts/token/HypNativeCollateral.sol";
import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol";
import {TestIsm} from "../../contracts/test/TestIsm.sol";

contract HypNativeCollateralTest is HypTokenTest {
using TypeCasts for address;

HypNativeCollateral internal localValueRouter;
HypNativeCollateral internal remoteValueRouter;
TestPostDispatchHook internal valueHook;
TestIsm internal ism;

function setUp() public override {
super.setUp();

localValueRouter = new HypNativeCollateral(address(localMailbox));
remoteValueRouter = new HypNativeCollateral(address(remoteMailbox));

localToken = TokenRouter(payable(address(localValueRouter)));
remoteToken = HypERC20(payable(address(remoteValueRouter)));

ism = new TestIsm();

valueHook = new TestPostDispatchHook();
valueHook.setFee(1e10);

localValueRouter.initialize(
address(valueHook),
address(ism),
address(this)
);
remoteValueRouter.initialize(
address(valueHook),
address(ism),
address(this)
);

localValueRouter.enrollRemoteRouter(
DESTINATION,
address(remoteToken).addressToBytes32()
);
remoteValueRouter.enrollRemoteRouter(
ORIGIN,
address(localToken).addressToBytes32()
);

vm.deal(ALICE, TRANSFER_AMT * 10);
}

function testRemoteTransfer() public {
uint256 quote = localValueRouter.quoteGasPayment(DESTINATION);
uint256 msgValue = TRANSFER_AMT + quote;

vm.expectEmit(true, true, false, true);
emit TokenRouter.SentTransferRemote(
DESTINATION,
BOB.addressToBytes32(),
TRANSFER_AMT
);

vm.prank(ALICE);
localToken.transferRemote{value: msgValue}(
DESTINATION,
BOB.addressToBytes32(),
TRANSFER_AMT
);

vm.assertEq(address(localToken).balance, 0);
vm.assertEq(address(valueHook).balance, msgValue);

vm.deal(address(remoteToken), TRANSFER_AMT);
vm.prank(address(remoteMailbox));

remoteToken.handle(
ORIGIN,
address(localToken).addressToBytes32(),
abi.encodePacked(BOB.addressToBytes32(), TRANSFER_AMT)
);

assertEq(BOB.balance, TRANSFER_AMT);
assertEq(address(valueHook).balance, msgValue);
}

// when msg.value is >= quote + amount, it should revert in
function testRemoteTransfer_insufficientValue() public {
vm.expectRevert();
vm.prank(ALICE);
localToken.transferRemote{value: TRANSFER_AMT}(
DESTINATION,
BOB.addressToBytes32(),
TRANSFER_AMT
);
}

function testTransfer_withHookSpecified(
uint256 fee,
bytes calldata metadata
) public override {
vm.assume(fee < TRANSFER_AMT);
uint256 msgValue = TRANSFER_AMT + fee;
vm.deal(ALICE, msgValue);

TestPostDispatchHook hook = new TestPostDispatchHook();
hook.setFee(fee);

vm.prank(ALICE);
localToken.transferRemote{value: msgValue}(
DESTINATION,
BOB.addressToBytes32(),
TRANSFER_AMT,
metadata,
address(hook)
);

vm.assertEq(address(localToken).balance, 0);
vm.assertEq(address(valueHook).balance, 0);
}

function testTransfer_withHookSpecified_revertsInsufficientValue(
uint256 fee,
bytes calldata metadata
) public {
vm.assume(fee < TRANSFER_AMT);
uint256 msgValue = TRANSFER_AMT + fee;
vm.deal(ALICE, msgValue);

TestPostDispatchHook hook = new TestPostDispatchHook();
hook.setFee(fee);

vm.prank(ALICE);
vm.expectRevert();
localToken.transferRemote{value: msgValue - 1}(
DESTINATION,
BOB.addressToBytes32(),
TRANSFER_AMT,
metadata,
address(hook)
);
}

function testBenchmark_overheadGasUsage() public override {
vm.deal(address(localValueRouter), TRANSFER_AMT);
super.testBenchmark_overheadGasUsage();
}
}

0 comments on commit b36bf0c

Please sign in to comment.