Add `receive` fn to `HypNative` and `Scaled` variant (#2671)

Co-authored-by: Trevor Porter <trkporter@ucdavis.edu>
Co-authored-by: Nam Chu Hoai <nambrot@googlemail.com>
pull/2726/head
Yorke Rhodes 1 year ago committed by GitHub
parent 892cc5df9c
commit 06124b441a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      .github/workflows/node.yml
  2. 3
      .gitmodules
  3. 25
      typescript/token/contracts/HypNative.sol
  4. 48
      typescript/token/contracts/extensions/HypNativeScaled.sol
  5. 24
      typescript/token/contracts/libs/TokenRouter.sol
  6. 10
      typescript/token/contracts/test/ERC20Test.sol
  7. 3
      typescript/token/foundry.toml
  8. 1
      typescript/token/lib/forge-std
  9. 15
      typescript/token/src/config.ts
  10. 23
      typescript/token/src/deploy.ts
  11. 155
      typescript/token/test/HypNativeScaled.t.sol
  12. 1
      typescript/token/test/erc20.test.ts

@ -50,6 +50,9 @@ jobs:
with: with:
node-version: 18 node-version: 18
- name: Install Foundry
uses: onbjerg/foundry-toolchain@v1
- name: yarn-cache - name: yarn-cache
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
@ -99,6 +102,9 @@ jobs:
with: with:
submodules: recursive submodules: recursive
- name: Install Foundry
uses: onbjerg/foundry-toolchain@v1
- uses: actions/cache@v3 - uses: actions/cache@v3
with: with:
path: ./* path: ./*
@ -176,7 +182,7 @@ jobs:
run: yarn workspace @hyperlane-xyz/core run test run: yarn workspace @hyperlane-xyz/core run test
- name: Run Slither - name: Run Slither
uses: crytic/slither-action@main uses: crytic/slither-action@v0.3.0
id: slither id: slither
with: with:
target: 'solidity/' target: 'solidity/'

3
.gitmodules vendored

@ -1,3 +1,6 @@
[submodule "solidity/lib/forge-std"] [submodule "solidity/lib/forge-std"]
path = solidity/lib/forge-std path = solidity/lib/forge-std
url = https://github.com/foundry-rs/forge-std url = https://github.com/foundry-rs/forge-std
[submodule "typescript/token/lib/forge-std"]
path = typescript/token/lib/forge-std
url = https://github.com/foundry-rs/forge-std

@ -11,6 +11,13 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol";
* @dev Supply on each chain is not constant but the aggregate supply across all chains is. * @dev Supply on each chain is not constant but the aggregate supply across all chains is.
*/ */
contract HypNative is TokenRouter { contract HypNative is TokenRouter {
/**
* @dev Emitted when native tokens are donated to the contract.
* @param sender The address of the sender.
* @param amount The amount of native tokens donated.
*/
event Donation(address indexed sender, uint256 amount);
/** /**
* @notice Initializes the Hyperlane router, ERC20 metadata, and mints initial supply to deployer. * @notice Initializes the Hyperlane router, ERC20 metadata, and mints initial supply to deployer.
* @param _mailbox The address of the mailbox contract. * @param _mailbox The address of the mailbox contract.
@ -35,16 +42,10 @@ contract HypNative is TokenRouter {
uint32 _destination, uint32 _destination,
bytes32 _recipient, bytes32 _recipient,
uint256 _amount uint256 _amount
) public payable override returns (bytes32 messageId) { ) public payable virtual override returns (bytes32 messageId) {
require(msg.value >= _amount, "Native: amount exceeds msg.value"); require(msg.value >= _amount, "Native: amount exceeds msg.value");
uint256 gasPayment = msg.value - _amount; uint256 gasPayment = msg.value - _amount;
messageId = _dispatchWithGas( return _transferRemote(_destination, _recipient, _amount, gasPayment);
_destination,
Message.format(_recipient, _amount, ""),
gasPayment,
msg.sender
);
emit SentTransferRemote(_destination, _recipient, _amount);
} }
function balanceOf(address _account) external view returns (uint256) { function balanceOf(address _account) external view returns (uint256) {
@ -52,9 +53,9 @@ contract HypNative is TokenRouter {
} }
/** /**
* @inheritdoc TokenRouter
* @dev No-op because native amount is transferred in `msg.value` * @dev No-op because native amount is transferred in `msg.value`
* @dev Compiler will not include this in the bytecode. * @dev Compiler will not include this in the bytecode.
* @inheritdoc TokenRouter
*/ */
function _transferFromSender(uint256) function _transferFromSender(uint256)
internal internal
@ -73,7 +74,11 @@ contract HypNative is TokenRouter {
address _recipient, address _recipient,
uint256 _amount, uint256 _amount,
bytes calldata // no metadata bytes calldata // no metadata
) internal override { ) internal virtual override {
Address.sendValue(payable(_recipient), _amount); Address.sendValue(payable(_recipient), _amount);
} }
receive() external payable {
emit Donation(msg.sender, msg.value);
}
} }

@ -0,0 +1,48 @@
// 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) {
scale = _scale;
}
/**
* @inheritdoc HypNative
* @dev Sends scaled `msg.value` (divided by `scale`) to `_recipient`.
*/
function transferRemote(
uint32 _destination,
bytes32 _recipient,
uint256 _amount
) public payable override returns (bytes32 messageId) {
require(msg.value >= _amount, "Native: amount exceeds msg.value");
uint256 gasPayment = msg.value - _amount;
uint256 scaledAmount = _amount / scale;
return
_transferRemote(_destination, _recipient, scaledAmount, gasPayment);
}
/**
* @dev Sends scaled `_amount` (multipled 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);
}
}

@ -51,12 +51,32 @@ abstract contract TokenRouter is GasRouter {
uint32 _destination, uint32 _destination,
bytes32 _recipient, bytes32 _recipient,
uint256 _amountOrId uint256 _amountOrId
) public payable virtual returns (bytes32 messageId) { ) external payable virtual returns (bytes32 messageId) {
return
_transferRemote(_destination, _recipient, _amountOrId, msg.value);
}
/**
* @notice Transfers `_amountOrId` token to `_recipient` on `_destination` domain.
* @dev Delegates transfer logic to `_transferFromSender` implementation.
* @dev Emits `SentTransferRemote` event on the origin chain.
* @param _destination The identifier of the destination chain.
* @param _recipient The address of the recipient on the destination chain.
* @param _amountOrId The amount or identifier of tokens to be sent to the remote recipient.
* @param _gasPayment The amount of native token to pay for interchain gas.
* @return messageId The identifier of the dispatched message.
*/
function _transferRemote(
uint32 _destination,
bytes32 _recipient,
uint256 _amountOrId,
uint256 _gasPayment
) internal returns (bytes32 messageId) {
bytes memory metadata = _transferFromSender(_amountOrId); bytes memory metadata = _transferFromSender(_amountOrId);
messageId = _dispatchWithGas( messageId = _dispatchWithGas(
_destination, _destination,
Message.format(_recipient, _amountOrId, metadata), Message.format(_recipient, _amountOrId, metadata),
msg.value, // interchain gas payment _gasPayment,
msg.sender // refund address msg.sender // refund address
); );
emit SentTransferRemote(_destination, _recipient, _amountOrId); emit SentTransferRemote(_destination, _recipient, _amountOrId);

@ -4,11 +4,19 @@ pragma solidity >=0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract ERC20Test is ERC20 { contract ERC20Test is ERC20 {
uint8 public immutable _decimals;
constructor( constructor(
string memory name, string memory name,
string memory symbol, string memory symbol,
uint256 totalSupply uint256 totalSupply,
uint8 __decimals
) ERC20(name, symbol) { ) ERC20(name, symbol) {
_decimals = __decimals;
_mint(msg.sender, totalSupply); _mint(msg.sender, totalSupply);
} }
function decimals() public view override returns (uint8) {
return _decimals;
}
} }

@ -3,5 +3,8 @@ src = "contracts"
out = "out" out = "out"
libs = ["lib"] libs = ["lib"]
allow_paths = ["../../node_modules", "../../solidity"] allow_paths = ["../../node_modules", "../../solidity"]
solc = '0.8.15'
optimizer = true
optimizer_runs = 999_999
# See more config options https://github.com/foundry-rs/foundry/tree/master/config # See more config options https://github.com/foundry-rs/foundry/tree/master/config

@ -0,0 +1 @@
Subproject commit 74cfb77e308dd188d2f58864aaf44963ae6b88b1

@ -16,26 +16,29 @@ export type TokenMetadata = {
totalSupply: ethers.BigNumberish; totalSupply: ethers.BigNumberish;
}; };
export type ERC20Metadata = TokenMetadata & { export type TokenDecimals = {
decimals: number; decimals: number;
scale?: number;
}; };
export type ERC20Metadata = TokenMetadata & TokenDecimals;
export const isTokenMetadata = (metadata: any): metadata is TokenMetadata => export const isTokenMetadata = (metadata: any): metadata is TokenMetadata =>
metadata.name && metadata.symbol && metadata.totalSupply !== undefined; // totalSupply can be 0 metadata.name && metadata.symbol && metadata.totalSupply !== undefined; // totalSupply can be 0
export const isErc20Metadata = (metadata: any): metadata is ERC20Metadata => export const isErc20Metadata = (metadata: any): metadata is ERC20Metadata =>
metadata.decimals && isTokenMetadata(metadata); metadata.decimals && isTokenMetadata(metadata);
export type SyntheticConfig = TokenMetadata & { export type SyntheticConfig = {
type: TokenType.synthetic | TokenType.syntheticUri; type: TokenType.synthetic | TokenType.syntheticUri;
}; } & TokenMetadata;
export type CollateralConfig = { export type CollateralConfig = {
type: TokenType.collateral | TokenType.collateralUri; type: TokenType.collateral | TokenType.collateralUri;
token: string; token: string;
} & Partial<ERC20Metadata>; } & Partial<ERC20Metadata>;
export type NativeConfig = { export type NativeConfig = {
type: TokenType.native; type: TokenType.native;
}; } & Partial<TokenDecimals>;
export type TokenConfig = SyntheticConfig | CollateralConfig | NativeConfig; export type TokenConfig = SyntheticConfig | CollateralConfig | NativeConfig;
@ -58,7 +61,9 @@ export const isUriConfig = (config: TokenConfig) =>
config.type === TokenType.collateralUri; config.type === TokenType.collateralUri;
export type HypERC20Config = GasRouterConfig & SyntheticConfig & ERC20Metadata; export type HypERC20Config = GasRouterConfig & SyntheticConfig & ERC20Metadata;
export type HypERC20CollateralConfig = GasRouterConfig & CollateralConfig; export type HypERC20CollateralConfig = GasRouterConfig &
CollateralConfig &
Partial<ERC20Metadata>;
export type HypNativeConfig = GasRouterConfig & NativeConfig; export type HypNativeConfig = GasRouterConfig & NativeConfig;
export type ERC20RouterConfig = export type ERC20RouterConfig =
| HypERC20Config | HypERC20Config

@ -45,6 +45,7 @@ import {
HypERC721URIStorage__factory, HypERC721URIStorage__factory,
HypERC721__factory, HypERC721__factory,
HypNative, HypNative,
HypNativeScaled__factory,
HypNative__factory, HypNative__factory,
} from './types'; } from './types';
@ -140,12 +141,22 @@ export class HypERC20Deployer extends GasRouterDeployer<
chain: ChainName, chain: ChainName,
config: HypNativeConfig, config: HypNativeConfig,
): Promise<HypNative> { ): Promise<HypNative> {
const router = await this.deployContractFromFactory( let router: HypNative;
chain, if (config.scale) {
new HypNative__factory(), router = await this.deployContractFromFactory(
'HypNative', chain,
[], new HypNativeScaled__factory(),
); 'HypNativeScaled',
[config.scale],
);
} else {
router = await this.deployContractFromFactory(
chain,
new HypNative__factory(),
'HypNative',
[],
);
}
await this.multiProvider.handleTx( await this.multiProvider.handleTx(
chain, chain,
router.initialize(config.mailbox, config.interchainGasPaymaster), router.initialize(config.mailbox, config.interchainGasPaymaster),

@ -0,0 +1,155 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.0;
import "forge-std/Test.sol";
import {HypNativeScaled} from "../contracts/extensions/HypNativeScaled.sol";
import {HypERC20} from "../contracts/HypERC20.sol";
import {TypeCasts} from "@hyperlane-xyz/core/contracts/libs/TypeCasts.sol";
import {MockHyperlaneEnvironment} from "@hyperlane-xyz/core/contracts/mock/MockHyperlaneEnvironment.sol";
contract HypNativeScaledTest is Test {
uint32 nativeDomain = 1;
uint32 synthDomain = 2;
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);
synth = new HypERC20(decimals);
synth.initialize(
address(environment.mailboxes(synthDomain)),
address(environment.igps(synthDomain)),
mintAmount * (10**decimals),
"Zebec BSC Token",
"ZBC"
);
native = new HypNativeScaled(scale);
native.initialize(
address(environment.mailboxes(nativeDomain)),
address(environment.igps(nativeDomain))
);
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.assume(nativeValue < address(this).balance);
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 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
);
}
}

@ -82,6 +82,7 @@ for (const variant of [
tokenMetadata.name, tokenMetadata.name,
tokenMetadata.symbol, tokenMetadata.symbol,
tokenMetadata.totalSupply, tokenMetadata.totalSupply,
tokenMetadata.decimals,
); );
localTokenConfig = { localTokenConfig = {
type: variant, type: variant,

Loading…
Cancel
Save