diff --git a/solidity/contracts/token/extensions/HypNativeScaled.sol b/solidity/contracts/token/extensions/HypNativeScaled.sol new file mode 100644 index 0000000000..ad129fce80 --- /dev/null +++ b/solidity/contracts/token/extensions/HypNativeScaled.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.0; + +import {HypNative} from "../HypNative.sol"; +import {TokenRouter} from "../libs/TokenRouter.sol"; + +/** + * @title Hyperlane Native Token that scales native value by a fixed factor for consistency with other tokens. + * @dev The scale factor multiplies the `message.amount` to the local native token amount. + * Conversely, it divides the local native `msg.value` amount by `scale` to encode the `message.amount`. + * @author Abacus Works + */ +contract HypNativeScaled is HypNative { + uint256 public immutable scale; + + constructor(uint256 _scale, address _mailbox) HypNative(_mailbox) { + scale = _scale; + } + + /** + * @inheritdoc HypNative + * @dev Sends scaled `msg.value` (divided by `scale`) to `_recipient`. + */ + function transferRemote( + uint32 _destination, + bytes32 _recipient, + uint256 _amount + ) external payable override returns (bytes32 messageId) { + require(msg.value >= _amount, "Native: amount exceeds msg.value"); + uint256 _hookPayment = msg.value - _amount; + uint256 _scaledAmount = _amount / scale; + return + _transferRemote( + _destination, + _recipient, + _scaledAmount, + _hookPayment + ); + } + + /** + * @inheritdoc TokenRouter + * @dev uses (`msg.value` - `_amount`) as hook payment. + */ + function transferRemote( + uint32 _destination, + bytes32 _recipient, + uint256 _amount, + bytes calldata _hookMetadata, + address _hook + ) external payable override returns (bytes32 messageId) { + require(msg.value >= _amount, "Native: amount exceeds msg.value"); + uint256 _hookPayment = msg.value - _amount; + uint256 _scaledAmount = _amount / scale; + return + _transferRemote( + _destination, + _recipient, + _scaledAmount, + _hookPayment, + _hookMetadata, + _hook + ); + } + + /** + * @dev Sends scaled `_amount` (multiplied by `scale`) to `_recipient`. + * @inheritdoc TokenRouter + */ + function _transferTo( + address _recipient, + uint256 _amount, + bytes calldata metadata // no metadata + ) internal override { + uint256 scaledAmount = _amount * scale; + HypNative._transferTo(_recipient, scaledAmount, metadata); + } +} diff --git a/solidity/test/token/HypNativeScaled.t.sol b/solidity/test/token/HypNativeScaled.t.sol new file mode 100644 index 0000000000..ffded65655 --- /dev/null +++ b/solidity/test/token/HypNativeScaled.t.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.0; + +import "forge-std/Test.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol"; +import {HypNativeScaled} from "../../contracts/token/extensions/HypNativeScaled.sol"; +import {HypERC20} from "../../contracts/token/HypERC20.sol"; +import {HypNative} from "../../contracts/token/HypNative.sol"; +import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; +import {MockHyperlaneEnvironment} from "../../contracts/mock/MockHyperlaneEnvironment.sol"; + +contract HypNativeScaledTest is Test { + uint32 nativeDomain = 1; + uint32 synthDomain = 2; + + address internal constant ALICE = address(0x1); + + uint8 decimals = 9; + uint256 mintAmount = 123456789; + uint256 nativeDecimals = 18; + uint256 scale = 10 ** (nativeDecimals - decimals); + + event Donation(address indexed sender, uint256 amount); + event SentTransferRemote( + uint32 indexed destination, + bytes32 indexed recipient, + uint256 amount + ); + event ReceivedTransferRemote( + uint32 indexed origin, + bytes32 indexed recipient, + uint256 amount + ); + + HypNativeScaled native; + HypERC20 synth; + + MockHyperlaneEnvironment environment; + + function setUp() public { + environment = new MockHyperlaneEnvironment(synthDomain, nativeDomain); + + HypERC20 implementationSynth = new HypERC20( + decimals, + address(environment.mailboxes(synthDomain)) + ); + TransparentUpgradeableProxy proxySynth = new TransparentUpgradeableProxy( + address(implementationSynth), + address(9), + abi.encodeWithSelector( + HypERC20.initialize.selector, + mintAmount * (10 ** decimals), + "Zebec BSC Token", + "ZBC", + address(0), + address(0), + address(this) + ) + ); + synth = HypERC20(address(proxySynth)); + + HypNativeScaled implementationNative = new HypNativeScaled( + scale, + address(environment.mailboxes(nativeDomain)) + ); + TransparentUpgradeableProxy proxyNative = new TransparentUpgradeableProxy( + address(implementationNative), + address(9), + abi.encodeWithSelector( + HypNative.initialize.selector, + address(0), + address(0), + address(this) + ) + ); + + native = HypNativeScaled(payable(address(proxyNative))); + + native.enrollRemoteRouter( + synthDomain, + TypeCasts.addressToBytes32(address(synth)) + ); + synth.enrollRemoteRouter( + nativeDomain, + TypeCasts.addressToBytes32(address(native)) + ); + } + + function test_constructor() public { + assertEq(native.scale(), scale); + } + + uint256 receivedValue; + + receive() external payable { + receivedValue = msg.value; + } + + function test_receive(uint256 amount) public { + vm.assume(amount < address(this).balance); + vm.expectEmit(true, true, true, true); + emit Donation(address(this), amount); + (bool success, bytes memory returnData) = address(native).call{ + value: amount + }(""); + assert(success); + assertEq(returnData.length, 0); + } + + function test_handle(uint256 amount) public { + vm.assume(amount <= mintAmount); + + uint256 synthAmount = amount * (10 ** decimals); + uint256 nativeAmount = amount * (10 ** nativeDecimals); + + vm.deal(address(native), nativeAmount); + + bytes32 recipient = TypeCasts.addressToBytes32(address(this)); + synth.transferRemote(nativeDomain, recipient, synthAmount); + + vm.expectEmit(true, true, true, true); + emit ReceivedTransferRemote(synthDomain, recipient, synthAmount); + environment.processNextPendingMessage(); + + assertEq(receivedValue, nativeAmount); + } + + function test_handle_reverts_whenAmountExceedsSupply( + uint256 amount + ) public { + vm.assume(amount <= mintAmount); + + bytes32 recipient = TypeCasts.addressToBytes32(address(this)); + synth.transferRemote(nativeDomain, recipient, amount); + + uint256 nativeValue = amount * scale; + vm.deal(address(native), nativeValue / 2); + + if (amount > 0) { + vm.expectRevert(bytes("Address: insufficient balance")); + } + environment.processNextPendingMessage(); + } + + function test_tranferRemote(uint256 amount) public { + vm.assume(amount <= mintAmount); + + uint256 nativeValue = amount * (10 ** nativeDecimals); + uint256 synthAmount = amount * (10 ** decimals); + address recipient = address(0xdeadbeef); + bytes32 bRecipient = TypeCasts.addressToBytes32(recipient); + + vm.deal(address(this), nativeValue); + vm.expectEmit(true, true, true, true); + emit SentTransferRemote(synthDomain, bRecipient, synthAmount); + native.transferRemote{value: nativeValue}( + synthDomain, + bRecipient, + nativeValue + ); + environment.processNextPendingMessageFromDestination(); + assertEq(synth.balanceOf(recipient), synthAmount); + } + + function testTransfer_withHookSpecified( + uint256 amount, + bytes calldata metadata + ) public { + vm.assume(amount <= mintAmount); + + uint256 nativeValue = amount * (10 ** nativeDecimals); + uint256 synthAmount = amount * (10 ** decimals); + address recipient = address(0xdeadbeef); + bytes32 bRecipient = TypeCasts.addressToBytes32(recipient); + + TestPostDispatchHook hook = new TestPostDispatchHook(); + + vm.deal(address(this), nativeValue); + vm.expectEmit(true, true, true, true); + emit SentTransferRemote(synthDomain, bRecipient, synthAmount); + native.transferRemote{value: nativeValue}( + synthDomain, + bRecipient, + nativeValue, + metadata, + address(hook) + ); + environment.processNextPendingMessageFromDestination(); + assertEq(synth.balanceOf(recipient), synthAmount); + } + + function test_transferRemote_reverts_whenAmountExceedsValue( + uint256 nativeValue + ) public { + vm.assume(nativeValue < address(this).balance); + + address recipient = address(0xdeadbeef); + bytes32 bRecipient = TypeCasts.addressToBytes32(recipient); + vm.expectRevert("Native: amount exceeds msg.value"); + native.transferRemote{value: nativeValue}( + synthDomain, + bRecipient, + nativeValue + 1 + ); + + vm.expectRevert("Native: amount exceeds msg.value"); + native.transferRemote{value: nativeValue}( + synthDomain, + bRecipient, + nativeValue + 1, + bytes(""), + address(0) + ); + } +}