diff --git a/.changeset/pretty-keys-give.md b/.changeset/pretty-keys-give.md new file mode 100644 index 0000000000..d5c5e7300a --- /dev/null +++ b/.changeset/pretty-keys-give.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/core': minor +--- + +Added a new router HypNativeCollateral with a unified interface for sending value hook/ism agnostic diff --git a/solidity/contracts/hooks/libs/StandardHookMetadata.sol b/solidity/contracts/hooks/libs/StandardHookMetadata.sol index e12f6822ad..f672e9b58b 100644 --- a/solidity/contracts/hooks/libs/StandardHookMetadata.sol +++ b/solidity/contracts/hooks/libs/StandardHookMetadata.sol @@ -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) + ); + } } diff --git a/solidity/contracts/token/HypNativeCollateral.sol b/solidity/contracts/token/HypNativeCollateral.sol new file mode 100644 index 0000000000..b11fa1030c --- /dev/null +++ b/solidity/contracts/token/HypNativeCollateral.sol @@ -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 + ); + } +} diff --git a/solidity/test/token/HypNative.t.sol b/solidity/test/token/HypNative.t.sol new file mode 100644 index 0000000000..5d9004b41f --- /dev/null +++ b/solidity/test/token/HypNative.t.sol @@ -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(); + } +}