Absorb token

pull/2435/head
Yorke Rhodes 1 year ago
commit 11d9d0bf0d
No known key found for this signature in database
GPG Key ID: 9EEACF1DA75C5627
  1. 1
      typescript/token
  2. 7
      typescript/token/.eslintignore
  3. 31
      typescript/token/.eslintrc
  4. 3
      typescript/token/.github/CODEOWNERS
  5. 106
      typescript/token/.github/workflows/ci.yml
  6. 9
      typescript/token/.gitignore
  7. 2
      typescript/token/.prettierignore
  8. 21
      typescript/token/.prettierrc
  9. 3
      typescript/token/.solcover.js
  10. 8
      typescript/token/.solhint.json
  11. 785
      typescript/token/.yarn/releases/yarn-3.2.0.cjs
  12. 3
      typescript/token/.yarnrc.yml
  13. 193
      typescript/token/LICENSE.md
  14. 253
      typescript/token/README.md
  15. 22
      typescript/token/configs/warp-route-chain-config.json
  16. 27
      typescript/token/configs/warp-route-token-config.json
  17. 74
      typescript/token/contracts/HypERC20.sol
  18. 70
      typescript/token/contracts/HypERC20Collateral.sol
  19. 66
      typescript/token/contracts/HypERC721.sol
  20. 73
      typescript/token/contracts/HypERC721Collateral.sol
  21. 79
      typescript/token/contracts/HypNative.sol
  22. 33
      typescript/token/contracts/extensions/HypERC721URICollateral.sol
  23. 79
      typescript/token/contracts/extensions/HypERC721URIStorage.sol
  24. 33
      typescript/token/contracts/libs/Message.sol
  25. 103
      typescript/token/contracts/libs/TokenRouter.sol
  26. 14
      typescript/token/contracts/test/ERC20Test.sol
  27. 20
      typescript/token/contracts/test/ERC721Test.sol
  28. 26
      typescript/token/hardhat.config.ts
  29. 192
      typescript/token/lcov.base.info
  30. 63
      typescript/token/package.json
  31. 80
      typescript/token/src/app.ts
  32. 70
      typescript/token/src/config.ts
  33. 20
      typescript/token/src/contracts.ts
  34. 381
      typescript/token/src/deploy.ts
  35. 16
      typescript/token/src/index.ts
  36. 291
      typescript/token/test/erc20.test.ts
  37. 324
      typescript/token/test/erc721.test.ts
  38. 34
      typescript/token/tsconfig.json
  39. 14919
      typescript/token/yarn.lock

@ -1 +0,0 @@
Subproject commit 1bcdd324f4d055a9cae6cc4648c9210e5cd9668b

@ -0,0 +1,7 @@
node_modules
dist
coverage
types
hardhat.config.ts
scripts
test

@ -0,0 +1,31 @@
{
"env": {
"node": true,
"browser": true,
"es2021": true
},
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"no-eval": ["error"],
"no-ex-assign": ["error"],
"no-constant-condition": ["off"],
"@typescript-eslint/ban-ts-comment": ["off"],
"@typescript-eslint/explicit-module-boundary-types": ["off"],
"@typescript-eslint/no-explicit-any": ["off"],
"@typescript-eslint/no-floating-promises": ["off"],
"@typescript-eslint/no-non-null-assertion": ["off"],
"@typescript-eslint/no-require-imports": ["warn"]
}
}

@ -0,0 +1,3 @@
* @yorhodes @jmrossy @asaj
contracts @yorhodes

@ -0,0 +1,106 @@
name: ci
on:
# Triggers the workflow on push or pull request events but only for the main branch
push:
branches: [main]
pull_request:
branches: [main]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: .//node_modules
key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }}
- name: yarn-install
# Check out the lockfile from main, reinstall, and then
# verify the lockfile matches what was committed.
run: |
yarn install
CHANGES=$(git status -s)
if [[ ! -z $CHANGES ]]; then
echo "Changes found: $CHANGES"
git diff
exit 1
fi
build:
runs-on: ubuntu-latest
needs: [install]
steps:
- uses: actions/checkout@v2
- name: yarn-cache
uses: actions/cache@v2
with:
path: .//node_modules
key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }}
- name: build-cache
uses: actions/cache@v2
with:
path: ./*
key: ${{ github.sha }}
- name: build
run: yarn run build
prettier:
runs-on: ubuntu-latest
needs: [install]
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: .//node_modules
key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }}
- name: prettier
run: |
yarn run prettier
CHANGES=$(git status -s)
if [[ ! -z $CHANGES ]]; then
echo "Changes found: $CHANGES"
exit 1
fi
lint:
runs-on: ubuntu-latest
needs: [build]
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: ./*
key: ${{ github.sha }}
- name: lint
run: yarn run lint
test:
runs-on: ubuntu-latest
needs: [build]
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: ./*
key: ${{ github.sha }}
- name: test with coverage
run: yarn run coverage
- uses: osmind-development-org/lcov-reporter-action@v0.3.2
with:
title: "Hardhat Coverage Report"
lcov-file: ./coverage/lcov.info
lcov-base: ./lcov.base.info
delete-old-comments: true

@ -0,0 +1,9 @@
node_modules/
cache/
artifacts/
types/
dist/
coverage/
coverage.json
*.swp
.yarn/install-state.gz

@ -0,0 +1,2 @@
src/types
test/outputs

@ -0,0 +1,21 @@
{
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "all",
"overrides": [
{
"files": "*.sol",
"options": {
"printWidth": 80,
"tabWidth": 4,
"useTabs": false,
"singleQuote": false,
"bracketSpacing": false,
"explicitTypes": "always"
}
}
],
"importOrder": ["^@hyperlane-xyz/(.*)$", "^../(.*)$", "^./(.*)$"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true
}

@ -0,0 +1,3 @@
module.exports = {
skipFiles: ['test'],
};

@ -0,0 +1,8 @@
{
"extends": "solhint:recommended",
"rules": {
"compiler-version": ["error", ">=0.6.0"],
"func-visibility": ["warn", {"ignoreConstructors":true}],
"not-rely-on-time": "off"
}
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,3 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.2.0.cjs

@ -0,0 +1,193 @@
# Apache License
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
- **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
- **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
- **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
- **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@ -0,0 +1,253 @@
# Hyperlane Tokens and Warp Routes
This repo contains contracts and SDK tooling for Hyperlane-connected ERC20 and ERC721 tokens. The contracts herein can be used to create [Hyperlane Warp Routes](https://docs.hyperlane.xyz/docs/deploy/deploy-warp-route) across different chains.
For instructions on deploying Warp Routes, see [the deployment documentation](https://docs.hyperlane.xyz/docs/deploy/deploy-warp-route/deploy-a-warp-route) and the [Hyperlane-Deploy repository](https://github.com/hyperlane-xyz/hyperlane-deploy).
## Warp Route Architecture
A Warp Route is a collection of [`TokenRouter`](./contracts/libs/TokenRouter.sol) contracts deployed across a set of Hyperlane chains. These contracts leverage the `Router` pattern to implement access control and routing logic for remote token transfers. These contracts send and receive [`Messages`](./contracts/libs/Message.sol) which encode payloads containing a transfer `amount` and `recipient` address.
```mermaid
%%{ init: {
"theme": "neutral",
"themeVariables": {
"mainBkg": "#025AA1",
"textColor": "white",
"clusterBkg": "white"
},
"themeCSS": ".edgeLabel { color: black }"
}}%%
graph LR
subgraph "Ethereum"
HYP_E[TokenRouter]
style HYP_E fill:orange
Mailbox_E[(Mailbox)]
end
subgraph "Polygon"
HYP_P[TokenRouter]
style HYP_P fill:orange
Mailbox_P[(Mailbox)]
end
subgraph "Gnosis"
HYP_G[TokenRouter]
style HYP_G fill:orange
Mailbox_G[(Mailbox)]
end
HYP_E -. "router" .- HYP_P -. "router" .- HYP_G
```
The Token Router contract comes in several flavors and a warp route can be composed of a combination of these flavors.
- [`Native`](./contracts/HypNative.sol) - for warping native assets (e.g. ETH) from the canonical chain
- [`Collateral`](./contracts/HypERC20Collateral.sol) - for warping tokens, ERC20 or ERC721, from the canonical chain
- [`Synthetic`](./contracts/HypERC20.sol) - for representing tokens, Native/ERC20 or ERC721, on a non-canonical chain
## Interchain Security Models
Warp routes are unique amongst token bridging solutions because they provide modular security. Because the `TokenRouter` implements the `IMessageRecipient` interface, it can be configured with a custom interchain security module. Please refer to the relevant guide to specifying interchain security modules on the [Messaging API receive docs](https://docs.hyperlane.xyz/docs/apis/messaging-api/receive#interchain-security-modules).
## Remote Transfer Lifecycle Diagrams
To initiate a remote transfer, users call the `TokenRouter.transferRemote` function with the `destination` chain ID, `recipient` address, and transfer `amount`.
```solidity
interface TokenRouter {
function transferRemote(
uint32 destination,
bytes32 recipient,
uint256 amount
) public returns (bytes32 messageId);
}
```
**NOTE:** The [Relayer](https://docs.hyperlane.xyz/docs/protocol/agents/relayer) shown below must be compensated. Please refer to the relevant guide on [paying for interchain gas](https://docs.hyperlane.xyz/docs/build-with-hyperlane/guides/paying-for-interchain-gas) on the `messageID` returned from the `transferRemote` call.
Depending on the flavor of TokenRouter on the source and destination chain, this flow looks slightly different. The following diagrams illustrate these differences.
### Transfer Alice's `amount` native ETH from Ethereum to Bob on Polygon
```mermaid
%%{ init: {
"theme": "neutral",
"themeVariables": {
"mainBkg": "#025AA1",
"textColor": "white",
"clusterBkg": "white"
},
"themeCSS": ".edgeLabel { color: black }"
}}%%
graph TB
Bob((Bob))
style Bob fill:black
Alice((Alice))
style Alice fill:black
Relayer([Relayer])
subgraph "Ethereum"
HYP_E[NativeTokenRouter]
style HYP_E fill:orange
Mailbox_E[(Mailbox)]
end
Alice == "transferRemote(Polygon, Bob, amount)\n{value: amount}" ==> HYP_E
linkStyle 0 color:green;
HYP_E -- "dispatch(Polygon, (Bob, amount))" --> Mailbox_E
subgraph "Polygon"
HYP_P[SyntheticTokenRouter]
style HYP_P fill:orange
Mailbox_P[(Mailbox)]
end
Mailbox_E -. "indexing" .-> Relayer
Relayer == "process(Ethereum, (Bob, amount))" ==> Mailbox_P
Mailbox_P -- "handle(Ethereum, (Bob, amount))" --> HYP_P
HYP_E -. "router" .- HYP_P
HYP_P -- "mint(Bob, amount)" --> Bob
linkStyle 6 color:green;
```
### Transfer Alice's ERC20 `amount` from Ethereum to Bob on Polygon
```mermaid
%%{ init: {
"theme": "neutral",
"themeVariables": {
"mainBkg": "#025AA1",
"textColor": "white",
"clusterBkg": "white"
},
"themeCSS": ".edgeLabel { color: black }"
}}%%
graph TB
Alice((Alice))
Bob((Bob))
style Alice fill:black
style Bob fill:black
Relayer([Relayer])
subgraph "Ethereum"
Token_E[ERC20]
style Token_E fill:green
HYP_E[CollateralTokenRouter]
style HYP_E fill:orange
Mailbox_E[(Mailbox)]
end
Alice == "approve(CollateralTokenRouter, infinity)" ==> Token_E
Alice == "transferRemote(Polygon, Bob, amount)" ==> HYP_E
Token_E -- "transferFrom(Alice, amount)" --> HYP_E
linkStyle 2 color:green;
HYP_E -- "dispatch(Polygon, (Bob, amount))" --> Mailbox_E
subgraph "Polygon"
HYP_P[SyntheticRouter]
style HYP_P fill:orange
Mailbox_P[(Mailbox)]
end
Mailbox_E -. "indexing" .-> Relayer
Relayer == "process(Ethereum, (Bob, amount))" ==> Mailbox_P
Mailbox_P -- "handle(Ethereum, (Bob, amount))" --> HYP_P
HYP_E -. "router" .- HYP_P
HYP_P -- "mint(Bob, amount)" --> Bob
linkStyle 8 color:green;
```
### Transfer Alice's `amount` synthetic MATIC from Ethereum back to Bob as native MATIC on Polygon
```mermaid
%%{ init: {
"theme": "neutral",
"themeVariables": {
"mainBkg": "#025AA1",
"textColor": "white",
"clusterBkg": "white"
},
"themeCSS": ".edgeLabel { color: black }"
}}%%
graph TB
Bob((Bob))
style Bob fill:black
Alice((Alice))
style Alice fill:black
Relayer([Relayer])
subgraph "Ethereum"
HYP_E[SyntheticTokenRouter]
style HYP_E fill:orange
Mailbox_E[(Mailbox)]
end
Alice == "transferRemote(Polygon, Bob, amount)" ==> HYP_E
Alice -- "burn(Alice, amount)" --> HYP_E
linkStyle 1 color:green;
HYP_E -- "dispatch(Polygon, (Bob, amount))" --> Mailbox_E
subgraph "Polygon"
HYP_P[NativeTokenRouter]
style HYP_P fill:orange
Mailbox_P[(Mailbox)]
end
Mailbox_E -. "indexing" .-> Relayer
Relayer == "process(Ethereum, (Bob, amount))" ==> Mailbox_P
Mailbox_P -- "handle(Ethereum, (Bob, amount))" --> HYP_P
HYP_E -. "router" .- HYP_P
HYP_P -- "transfer(){value: amount}" --> Bob
linkStyle 7 color:green;
```
**NOTE:** ERC721 collateral variants are assumed to [enumerable](https://docs.openzeppelin.com/contracts/4.x/api/token/erc721#IERC721Enumerable) and [metadata](https://docs.openzeppelin.com/contracts/4.x/api/token/erc721#IERC721Metadata) compliant.
## Versions
| Git Ref | Release Date | Notes |
| ------------------------ | ------------ | ------------------------------ |
| [audit-v2-remediation]() | 2023-02-15 | Hyperlane V2 Audit remediation |
| [main]() | ~ | Bleeding edge |
## Setup for local development
```sh
# Install dependencies
yarn
# Build source and generate types
yarn build:dev
```
## Unit testing
```sh
# Run all unit tests
yarn test
# Lint check code
yarn lint
```
## Learn more
For more information, see the [Hyperlane introduction documentation](https://docs.hyperlane.xyz/docs/introduction/readme) or the [details about Warp Routes](https://docs.hyperlane.xyz/docs/deploy/deploy-warp-route).

@ -0,0 +1,22 @@
{
"goerli": {
"chainId": 5,
"name": "goerli",
"displayName": "Goerli",
"nativeToken": { "name": "Ether", "symbol": "ETH", "decimals": 18 },
"publicRpcUrls": [{ "http": "https://eth-goerli.public.blastapi.io" }],
"blockExplorers": [
{
"name": "Goerliscan",
"url": "https://goerli.etherscan.io",
"apiUrl": "https://api-goerli.etherscan.io",
"family": "etherscan"
}
],
"blocks": {
"confirmations": 1,
"reorgPeriod": 2,
"estimateBlockTime": 13
}
}
}

@ -0,0 +1,27 @@
{
"goerli": {
"type": "collateral",
"token": "0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6",
"owner": "0x5bA371aeA18734Cb7195650aFdfCa4f9251aa513",
"mailbox": "0xCC737a94FecaeC165AbCf12dED095BB13F037685",
"interchainGasPaymaster": "0xF90cB82a76492614D07B82a7658917f3aC811Ac1"
},
"alfajores": {
"type": "synthetic",
"owner": "0x5bA371aeA18734Cb7195650aFdfCa4f9251aa513",
"mailbox": "0xCC737a94FecaeC165AbCf12dED095BB13F037685",
"interchainGasPaymaster": "0xF90cB82a76492614D07B82a7658917f3aC811Ac1"
},
"fuji": {
"type": "synthetic",
"owner": "0x5bA371aeA18734Cb7195650aFdfCa4f9251aa513",
"mailbox": "0xCC737a94FecaeC165AbCf12dED095BB13F037685",
"interchainGasPaymaster": "0xF90cB82a76492614D07B82a7658917f3aC811Ac1"
},
"moonbasealpha": {
"type": "synthetic",
"owner": "0x5bA371aeA18734Cb7195650aFdfCa4f9251aa513",
"mailbox": "0xCC737a94FecaeC165AbCf12dED095BB13F037685",
"interchainGasPaymaster": "0xF90cB82a76492614D07B82a7658917f3aC811Ac1"
}
}

@ -0,0 +1,74 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.0;
import {TokenRouter} from "./libs/TokenRouter.sol";
import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
/**
* @title Hyperlane ERC20 Token Router that extends ERC20 with remote transfer functionality.
* @author Abacus Works
* @dev Supply on each chain is not constant but the aggregate supply across all chains is.
*/
contract HypERC20 is ERC20Upgradeable, TokenRouter {
uint8 private immutable _decimals;
constructor(uint8 decimals) {
_decimals = decimals;
}
/**
* @notice Initializes the Hyperlane router, ERC20 metadata, and mints initial supply to deployer.
* @param _mailbox The address of the mailbox contract.
* @param _interchainGasPaymaster The address of the interchain gas paymaster contract.
* @param _totalSupply The initial supply of the token.
* @param _name The name of the token.
* @param _symbol The symbol of the token.
*/
function initialize(
address _mailbox,
address _interchainGasPaymaster,
uint256 _totalSupply,
string memory _name,
string memory _symbol
) external initializer {
// transfers ownership to `msg.sender`
__HyperlaneConnectionClient_initialize(
_mailbox,
_interchainGasPaymaster
);
// Initialize ERC20 metadata
__ERC20_init(_name, _symbol);
_mint(msg.sender, _totalSupply);
}
function decimals() public view override returns (uint8) {
return _decimals;
}
/**
* @dev Burns `_amount` of token from `msg.sender` balance.
* @inheritdoc TokenRouter
*/
function _transferFromSender(uint256 _amount)
internal
override
returns (bytes memory)
{
_burn(msg.sender, _amount);
return bytes(""); // no metadata
}
/**
* @dev Mints `_amount` of token to `_recipient` balance.
* @inheritdoc TokenRouter
*/
function _transferTo(
address _recipient,
uint256 _amount,
bytes calldata // no metadata
) internal override {
_mint(_recipient, _amount);
}
}

@ -0,0 +1,70 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.0;
import {TokenRouter} from "./libs/TokenRouter.sol";
import {Message} from "./libs/Message.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
/**
* @title Hyperlane ERC20 Token Collateral that wraps an existing ERC20 with remote transfer functionality.
* @author Abacus Works
*/
contract HypERC20Collateral is TokenRouter {
using SafeERC20 for IERC20;
IERC20 public immutable wrappedToken;
/**
* @notice Constructor
* @param erc20 Address of the token to keep as collateral
*/
constructor(address erc20) {
wrappedToken = IERC20(erc20);
}
/**
* @notice Initializes the Hyperlane router.
* @param _mailbox The address of the mailbox contract.
* @param _interchainGasPaymaster The address of the interchain gas paymaster contract.
*/
function initialize(address _mailbox, address _interchainGasPaymaster)
external
initializer
{
__HyperlaneConnectionClient_initialize(
_mailbox,
_interchainGasPaymaster
);
}
function balanceOf(address _account) external view returns (uint256) {
return wrappedToken.balanceOf(_account);
}
/**
* @dev Transfers `_amount` of `wrappedToken` from `msg.sender` to this contract.
* @inheritdoc TokenRouter
*/
function _transferFromSender(uint256 _amount)
internal
override
returns (bytes memory)
{
wrappedToken.safeTransferFrom(msg.sender, address(this), _amount);
return bytes(""); // no metadata
}
/**
* @dev Transfers `_amount` of `wrappedToken` from this contract to `_recipient`.
* @inheritdoc TokenRouter
*/
function _transferTo(
address _recipient,
uint256 _amount,
bytes calldata // no metadata
) internal override {
wrappedToken.safeTransfer(_recipient, _amount);
}
}

@ -0,0 +1,66 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.0;
import {TokenRouter} from "./libs/TokenRouter.sol";
import {ERC721EnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
/**
* @title Hyperlane ERC721 Token Router that extends ERC721 with remote transfer functionality.
* @author Abacus Works
*/
contract HypERC721 is ERC721EnumerableUpgradeable, TokenRouter {
/**
* @notice Initializes the Hyperlane router, ERC721 metadata, and mints initial supply to deployer.
* @param _mailbox The address of the mailbox contract.
* @param _interchainGasPaymaster The address of the interchain gas paymaster contract.
* @param _mintAmount The amount of NFTs to mint to `msg.sender`.
* @param _name The name of the token.
* @param _symbol The symbol of the token.
*/
function initialize(
address _mailbox,
address _interchainGasPaymaster,
uint256 _mintAmount,
string memory _name,
string memory _symbol
) external initializer {
// transfers ownership to `msg.sender`
__HyperlaneConnectionClient_initialize(
_mailbox,
_interchainGasPaymaster
);
__ERC721_init(_name, _symbol);
for (uint256 i = 0; i < _mintAmount; i++) {
_safeMint(msg.sender, i);
}
}
/**
* @dev Asserts `msg.sender` is owner and burns `_tokenId`.
* @inheritdoc TokenRouter
*/
function _transferFromSender(uint256 _tokenId)
internal
virtual
override
returns (bytes memory)
{
require(ownerOf(_tokenId) == msg.sender, "!owner");
_burn(_tokenId);
return bytes(""); // no metadata
}
/**
* @dev Mints `_tokenId` to `_recipient`.
* @inheritdoc TokenRouter
*/
function _transferTo(
address _recipient,
uint256 _tokenId,
bytes calldata // no metadata
) internal virtual override {
_safeMint(_recipient, _tokenId);
}
}

@ -0,0 +1,73 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.0;
import {TokenRouter} from "./libs/TokenRouter.sol";
import {Message} from "./libs/Message.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
/**
* @title Hyperlane ERC721 Token Collateral that wraps an existing ERC721 with remote transfer functionality.
* @author Abacus Works
*/
contract HypERC721Collateral is TokenRouter {
IERC721 public immutable wrappedToken;
/**
* @notice Constructor
* @param erc721 Address of the token to keep as collateral
*/
constructor(address erc721) {
wrappedToken = IERC721(erc721);
}
function ownerOf(uint256 _tokenId) external view returns (address) {
return IERC721(wrappedToken).ownerOf(_tokenId);
}
/**
* @notice Initializes the Hyperlane router.
* @param _mailbox The address of the mailbox contract.
* @param _interchainGasPaymaster The address of the interchain gas paymaster contract.
*/
function initialize(address _mailbox, address _interchainGasPaymaster)
external
initializer
{
__HyperlaneConnectionClient_initialize(
_mailbox,
_interchainGasPaymaster
);
}
function balanceOf(address _account) external view returns (uint256) {
return IERC721(wrappedToken).balanceOf(_account);
}
/**
* @dev Transfers `_tokenId` of `wrappedToken` from `msg.sender` to this contract.
* @inheritdoc TokenRouter
*/
function _transferFromSender(uint256 _tokenId)
internal
virtual
override
returns (bytes memory)
{
// safeTransferFrom not used here because recipient is this contract
wrappedToken.transferFrom(msg.sender, address(this), _tokenId);
return bytes(""); // no metadata
}
/**
* @dev Transfers `_tokenId` of `wrappedToken` from this contract to `_recipient`.
* @inheritdoc TokenRouter
*/
function _transferTo(
address _recipient,
uint256 _tokenId,
bytes calldata // no metadata
) internal override {
wrappedToken.safeTransferFrom(address(this), _recipient, _tokenId);
}
}

@ -0,0 +1,79 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.0;
import {TokenRouter} from "./libs/TokenRouter.sol";
import {Message} from "./libs/Message.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
/**
* @title Hyperlane Native Token Router that extends ERC20 with remote transfer functionality.
* @author Abacus Works
* @dev Supply on each chain is not constant but the aggregate supply across all chains is.
*/
contract HypNative is TokenRouter {
/**
* @notice Initializes the Hyperlane router, ERC20 metadata, and mints initial supply to deployer.
* @param _mailbox The address of the mailbox contract.
* @param _interchainGasPaymaster The address of the interchain gas paymaster contract.
*/
function initialize(address _mailbox, address _interchainGasPaymaster)
external
initializer
{
// transfers ownership to `msg.sender`
__HyperlaneConnectionClient_initialize(
_mailbox,
_interchainGasPaymaster
);
}
/**
* @inheritdoc TokenRouter
* @dev uses (`msg.value` - `_amount`) as interchain gas payment and `msg.sender` as refund address.
*/
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;
messageId = _dispatchWithGas(
_destination,
Message.format(_recipient, _amount, ""),
gasPayment,
msg.sender
);
emit SentTransferRemote(_destination, _recipient, _amount);
}
function balanceOf(address _account) external view returns (uint256) {
return _account.balance;
}
/**
* @dev No-op because native amount is transferred in `msg.value`
* @dev Compiler will not include this in the bytecode.
* @inheritdoc TokenRouter
*/
function _transferFromSender(uint256)
internal
pure
override
returns (bytes memory)
{
return bytes(""); // no metadata
}
/**
* @dev Sends `_amount` of native token to `_recipient` balance.
* @inheritdoc TokenRouter
*/
function _transferTo(
address _recipient,
uint256 _amount,
bytes calldata // no metadata
) internal override {
Address.sendValue(payable(_recipient), _amount);
}
}

@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.0;
import {HypERC721Collateral} from "../HypERC721Collateral.sol";
import {IERC721MetadataUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/IERC721MetadataUpgradeable.sol";
/**
* @title Hyperlane ERC721 Token Collateral that wraps an existing ERC721 with remote transfer and URI relay functionality.
* @author Abacus Works
*/
contract HypERC721URICollateral is HypERC721Collateral {
constructor(address erc721) HypERC721Collateral(erc721) {}
/**
* @dev Transfers `_tokenId` of `wrappedToken` from `msg.sender` to this contract.
* @return The URI of `_tokenId` on `wrappedToken`.
* @inheritdoc HypERC721Collateral
*/
function _transferFromSender(uint256 _tokenId)
internal
override
returns (bytes memory)
{
HypERC721Collateral._transferFromSender(_tokenId);
return
bytes(
IERC721MetadataUpgradeable(address(wrappedToken)).tokenURI(
_tokenId
)
);
}
}

@ -0,0 +1,79 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.0;
import {HypERC721} from "../HypERC721.sol";
import {ERC721URIStorageUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
import {ERC721EnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
/**
* @title Hyperlane ERC721 Token that extends ERC721URIStorage with remote transfer and URI relay functionality.
* @author Abacus Works
*/
contract HypERC721URIStorage is HypERC721, ERC721URIStorageUpgradeable {
/**
* @return _tokenURI The URI of `_tokenId`.
* @inheritdoc HypERC721
*/
function _transferFromSender(uint256 _tokenId)
internal
override
returns (bytes memory _tokenURI)
{
_tokenURI = bytes(tokenURI(_tokenId)); // requires minted
HypERC721._transferFromSender(_tokenId);
}
/**
* @dev Sets the URI for `_tokenId` to `_tokenURI`.
* @inheritdoc HypERC721
*/
function _transferTo(
address _recipient,
uint256 _tokenId,
bytes calldata _tokenURI
) internal override {
HypERC721._transferTo(_recipient, _tokenId, _tokenURI);
_setTokenURI(_tokenId, string(_tokenURI)); // requires minted
}
function tokenURI(uint256 tokenId)
public
view
override(ERC721Upgradeable, ERC721URIStorageUpgradeable)
returns (string memory)
{
return ERC721URIStorageUpgradeable.tokenURI(tokenId);
}
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId,
uint256 batchSize
) internal override(ERC721EnumerableUpgradeable, ERC721Upgradeable) {
ERC721EnumerableUpgradeable._beforeTokenTransfer(
from,
to,
tokenId,
batchSize
);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721EnumerableUpgradeable, ERC721Upgradeable)
returns (bool)
{
return ERC721EnumerableUpgradeable.supportsInterface(interfaceId);
}
function _burn(uint256 tokenId)
internal
override(ERC721URIStorageUpgradeable, ERC721Upgradeable)
{
ERC721URIStorageUpgradeable._burn(tokenId);
}
}

@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
library Message {
function format(
bytes32 _recipient,
uint256 _amount,
bytes memory _metadata
) internal pure returns (bytes memory) {
return abi.encodePacked(_recipient, _amount, _metadata);
}
function recipient(bytes calldata message) internal pure returns (bytes32) {
return bytes32(message[0:32]);
}
function amount(bytes calldata message) internal pure returns (uint256) {
return uint256(bytes32(message[32:64]));
}
// alias for ERC721
function tokenId(bytes calldata message) internal pure returns (uint256) {
return amount(message);
}
function metadata(bytes calldata message)
internal
pure
returns (bytes calldata)
{
return message[64:];
}
}

@ -0,0 +1,103 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.0;
import {GasRouter} from "@hyperlane-xyz/core/contracts/GasRouter.sol";
import {TypeCasts} from "@hyperlane-xyz/core/contracts/libs/TypeCasts.sol";
import {Message} from "./Message.sol";
/**
* @title Hyperlane Token Router that extends Router with abstract token (ERC20/ERC721) remote transfer functionality.
* @author Abacus Works
*/
abstract contract TokenRouter is GasRouter {
using TypeCasts for bytes32;
using TypeCasts for address;
using Message for bytes;
/**
* @dev Emitted on `transferRemote` when a transfer message is dispatched.
* @param destination The identifier of the destination chain.
* @param recipient The address of the recipient on the destination chain.
* @param amount The amount of tokens burnt on the origin chain.
*/
event SentTransferRemote(
uint32 indexed destination,
bytes32 indexed recipient,
uint256 amount
);
/**
* @dev Emitted on `_handle` when a transfer message is processed.
* @param origin The identifier of the origin chain.
* @param recipient The address of the recipient on the destination chain.
* @param amount The amount of tokens minted on the destination chain.
*/
event ReceivedTransferRemote(
uint32 indexed origin,
bytes32 indexed recipient,
uint256 amount
);
/**
* @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.
* @return messageId The identifier of the dispatched message.
*/
function transferRemote(
uint32 _destination,
bytes32 _recipient,
uint256 _amountOrId
) public payable virtual returns (bytes32 messageId) {
bytes memory metadata = _transferFromSender(_amountOrId);
messageId = _dispatchWithGas(
_destination,
Message.format(_recipient, _amountOrId, metadata),
msg.value, // interchain gas payment
msg.sender // refund address
);
emit SentTransferRemote(_destination, _recipient, _amountOrId);
}
/**
* @dev Should transfer `_amountOrId` of tokens from `msg.sender` to this token router.
* @dev Called by `transferRemote` before message dispatch.
* @dev Optionally returns `metadata` associated with the transfer to be passed in message.
*/
function _transferFromSender(uint256 _amountOrId)
internal
virtual
returns (bytes memory metadata);
/**
* @dev Mints tokens to recipient when router receives transfer message.
* @dev Emits `ReceivedTransferRemote` event on the destination chain.
* @param _origin The identifier of the origin chain.
* @param _message The encoded remote transfer message containing the recipient address and amount.
*/
function _handle(
uint32 _origin,
bytes32,
bytes calldata _message
) internal override {
bytes32 recipient = _message.recipient();
uint256 amount = _message.amount();
bytes calldata metadata = _message.metadata();
_transferTo(recipient.bytes32ToAddress(), amount, metadata);
emit ReceivedTransferRemote(_origin, recipient, amount);
}
/**
* @dev Should transfer `_amountOrId` of tokens from this token router to `_recipient`.
* @dev Called by `handle` after message decoding.
* @dev Optionally handles `metadata` associated with transfer passed in message.
*/
function _transferTo(
address _recipient,
uint256 _amountOrId,
bytes calldata metadata
) internal virtual;
}

@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract ERC20Test is ERC20 {
constructor(
string memory name,
string memory symbol,
uint256 totalSupply
) ERC20(name, symbol) {
_mint(msg.sender, totalSupply);
}
}

@ -0,0 +1,20 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.0;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
contract ERC721Test is ERC721Enumerable {
constructor(
string memory name,
string memory symbol,
uint256 _mintAmount
) ERC721(name, symbol) {
for (uint256 i = 0; i < _mintAmount; i++) {
_mint(msg.sender, i);
}
}
function _baseURI() internal pure override returns (string memory) {
return "TEST-BASE-URI";
}
}

@ -0,0 +1,26 @@
import '@nomiclabs/hardhat-ethers';
import '@nomiclabs/hardhat-waffle';
import '@typechain/hardhat';
import 'hardhat-gas-reporter';
import 'solidity-coverage';
/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
solidity: {
compilers: [
{
version: '0.8.16',
},
],
},
gasReporter: {
currency: 'USD',
},
typechain: {
outDir: './src/types',
target: 'ethers-v5',
alwaysGenerateOverloads: false, // should overloads with full signatures like deposit(uint256) be generated always, even if there are no overloads?
},
};

@ -0,0 +1,192 @@
TN:
SF:/Users/yorhodes/hyperlane/abacus-monorepo/typescript/token/contracts/extensions/HypERC721URICollateral.sol
FN:14,constructor
FN:22,_transferFromSender
FNF:2
FNH:2
FNDA:24,constructor
FNDA:54,_transferFromSender
DA:27,54
DA:28,52
LF:2
LH:2
BRF:0
BRH:0
end_of_record
TN:
SF:/Users/yorhodes/hyperlane/abacus-monorepo/typescript/token/contracts/extensions/HypERC721URIStorage.sol
FN:19,constructor
FN:25,_transferFromSender
FN:38,_transferTo
FN:47,tokenURI
FN:56,_beforeTokenTransfer
FN:70,supportsInterface
FN:79,_burn
FNF:7
FNH:6
FNDA:120,constructor
FNDA:41,_transferFromSender
FNDA:65,_transferTo
FNDA:44,tokenURI
FNDA:4916,_beforeTokenTransfer
FNDA:0,supportsInterface
FNDA:39,_burn
DA:30,41
DA:31,40
DA:43,65
DA:44,65
DA:53,44
DA:62,4916
DA:76,0
DA:83,39
LF:8
LH:7
BRF:0
BRH:0
end_of_record
TN:
SF:/Users/yorhodes/hyperlane/abacus-monorepo/typescript/token/contracts/HypERC20.sol
FN:18,constructor
FN:34,initialize
FN:50,_transferFromSender
FN:63,_transferTo
FNF:4
FNH:4
FNDA:99,constructor
FNDA:429,initialize
FNDA:10,_transferFromSender
FNDA:48,_transferTo
DA:36,429
DA:42,429
DA:43,429
DA:55,10
DA:56,9
DA:68,48
LF:6
LH:6
BRF:0
BRH:0
end_of_record
TN:
SF:/Users/yorhodes/hyperlane/abacus-monorepo/typescript/token/contracts/HypERC20Collateral.sol
FN:20,constructor
FN:31,initialize
FN:43,_transferFromSender
FN:59,_transferTo
FNF:4
FNH:3
FNDA:18,constructor
FNDA:72,initialize
FNDA:40,_transferFromSender
FNDA:0,_transferTo
DA:21,18
DA:33,72
DA:48,40
DA:52,39
DA:64,0
LF:5
LH:4
BRDA:48,1,0,39
BRDA:48,1,1,1
BRDA:64,2,0,0
BRDA:64,2,1,0
BRF:4
BRH:2
end_of_record
TN:
SF:/Users/yorhodes/hyperlane/abacus-monorepo/typescript/token/contracts/HypERC721.sol
FN:17,constructor
FN:33,initialize
FN:50,_transferFromSender
FN:65,_transferTo
FNF:4
FNH:4
FNDA:234,constructor
FNDA:998,initialize
FNDA:81,_transferFromSender
FNDA:117,_transferTo
DA:35,998
DA:40,998
DA:41,998
DA:42,9600
DA:56,81
DA:57,78
DA:58,78
DA:70,117
LF:8
LH:8
BRDA:56,1,0,78
BRDA:56,1,1,3
BRF:2
BRH:2
end_of_record
TN:
SF:/Users/yorhodes/hyperlane/abacus-monorepo/typescript/token/contracts/HypERC721Collateral.sol
FN:20,constructor
FN:31,initialize
FN:43,_transferFromSender
FN:57,_transferTo
FNF:4
FNH:3
FNDA:45,constructor
FNDA:180,initialize
FNDA:95,_transferFromSender
FNDA:0,_transferTo
DA:21,45
DA:33,180
DA:49,95
DA:50,91
DA:62,0
LF:5
LH:4
BRF:0
BRH:0
end_of_record
TN:
SF:/Users/yorhodes/hyperlane/abacus-monorepo/typescript/token/contracts/libs/Message.sol
FN:5,format
FN:13,recipient
FN:17,amount
FN:22,tokenId
FN:26,metadata
FNF:5
FNH:4
FNDA:217,format
FNDA:165,recipient
FNDA:165,amount
FNDA:0,tokenId
FNDA:165,metadata
DA:10,217
DA:14,165
DA:18,165
DA:23,0
DA:31,165
LF:5
LH:4
BRF:0
BRH:0
end_of_record
TN:
SF:/Users/yorhodes/hyperlane/abacus-monorepo/typescript/token/contracts/libs/TokenRouter.sol
FN:45,constructor
FN:57,transferRemote
FN:89,_handle
FNF:3
FNH:3
FNDA:396,constructor
FNDA:227,transferRemote
FNDA:165,_handle
DA:46,396
DA:62,227
DA:63,217
DA:70,185
DA:94,165
DA:95,165
DA:96,165
DA:97,165
DA:98,156
LF:9
LH:9
BRF:0
BRH:0
end_of_record

@ -0,0 +1,63 @@
{
"name": "@hyperlane-xyz/hyperlane-token",
"description": "A template for interchain ERC20 and ERC721 tokens using Hyperlane",
"version": "1.3.5",
"dependencies": {
"@hyperlane-xyz/core": "1.3.5",
"@hyperlane-xyz/sdk": "1.3.5",
"@hyperlane-xyz/utils": "1.3.5",
"@openzeppelin/contracts-upgradeable": "^4.8.0",
"ethers": "^5.7.2"
},
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.2.1",
"@nomiclabs/hardhat-waffle": "^2.0.3",
"@trivago/prettier-plugin-sort-imports": "^3.2.0",
"@typechain/ethers-v5": "10.0.0",
"@typechain/hardhat": "^6.0.0",
"@types/mocha": "^9.1.0",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.27.0",
"chai": "^4.3.0",
"eslint": "^8.16.0",
"eslint-config-prettier": "^8.5.0",
"ethereum-waffle": "^3.4.4",
"hardhat": "^2.8.4",
"hardhat-gas-reporter": "^1.0.7",
"prettier": "^2.4.1",
"prettier-plugin-solidity": "^1.0.0-beta.5",
"solhint": "^3.3.2",
"solhint-plugin-prettier": "^0.0.5",
"solidity-coverage": "^0.7.14",
"ts-node": "^10.8.0",
"typechain": "8.0.0",
"typescript": "^4.7.2"
},
"files": [
"/dist",
"/contracts"
],
"homepage": "https://www.hyperlane.xyz",
"keywords": [
"Hyperlane",
"Solidity",
"Token"
],
"license": "Apache-2.0",
"main": "dist/index.js",
"packageManager": "yarn@3.2.0",
"repository": {
"type": "git",
"url": "https://github.com/hyperlane-xyz/hyperlane-token"
},
"scripts": {
"clean": "hardhat clean && rm -rf dist cache src/types",
"build": "hardhat compile && tsc",
"coverage": "hardhat coverage",
"lint": "eslint . --ext .ts",
"prettier": "prettier --write ./contracts ./test",
"test": "hardhat test ./test/*.test.ts",
"deploy-warp-route": "DEBUG=* ts-node scripts/deploy"
},
"types": "dist/index.d.ts"
}

@ -0,0 +1,80 @@
import { BigNumberish } from 'ethers';
import { ChainName, HyperlaneContracts, RouterApp } from '@hyperlane-xyz/sdk';
import { types } from '@hyperlane-xyz/utils';
import {
HypERC20Factories,
HypERC721Factories,
TokenFactories,
} from './contracts';
import { TokenRouter } from './types';
class HyperlaneTokenApp<
Factories extends TokenFactories,
> extends RouterApp<Factories> {
router(contracts: HyperlaneContracts<TokenFactories>): TokenRouter {
return contracts.router;
}
async transfer(
origin: ChainName,
destination: ChainName,
recipient: types.Address,
amountOrId: BigNumberish,
) {
const originRouter = this.getContracts(origin).router;
const destProvider = this.multiProvider.getProvider(destination);
const destinationNetwork = await destProvider.getNetwork();
const gasPayment = await originRouter.quoteGasPayment(
destinationNetwork.chainId,
);
return this.multiProvider.handleTx(
origin,
originRouter.transferRemote(
destinationNetwork.chainId,
recipient,
amountOrId,
{
value: gasPayment,
},
),
);
}
}
export class HypERC20App extends HyperlaneTokenApp<HypERC20Factories> {
async transfer(
origin: ChainName,
destination: ChainName,
recipient: types.Address,
amount: BigNumberish,
) {
const originRouter = this.getContracts(origin).router;
const signerAddress = await this.multiProvider.getSignerAddress(origin);
const balance = await originRouter.balanceOf(signerAddress);
if (balance.lt(amount))
console.warn(
`Signer ${signerAddress} has insufficient balance ${balance}, needs ${amount} on ${origin}`,
);
return super.transfer(origin, destination, recipient, amount);
}
}
export class HypERC721App extends HyperlaneTokenApp<HypERC721Factories> {
async transfer(
origin: ChainName,
destination: ChainName,
recipient: types.Address,
tokenId: BigNumberish,
) {
const originRouter = this.getContracts(origin).router;
const signerAddress = await this.multiProvider.getSignerAddress(origin);
const owner = await originRouter.ownerOf(tokenId);
if (signerAddress != owner)
console.warn(
`Signer ${signerAddress} not owner of token ${tokenId} on ${origin}`,
);
return super.transfer(origin, destination, recipient, tokenId);
}
}

@ -0,0 +1,70 @@
import { ethers } from 'ethers';
import { GasRouterConfig } from '@hyperlane-xyz/sdk';
export enum TokenType {
synthetic = 'synthetic',
syntheticUri = 'syntheticUri',
collateral = 'collateral',
collateralUri = 'collateralUri',
native = 'native',
}
export type TokenMetadata = {
name: string;
symbol: string;
totalSupply: ethers.BigNumberish;
};
export type ERC20Metadata = TokenMetadata & {
decimals: number;
};
export const isTokenMetadata = (metadata: any): metadata is TokenMetadata =>
metadata.name && metadata.symbol && metadata.totalSupply !== undefined; // totalSupply can be 0
export const isErc20Metadata = (metadata: any): metadata is ERC20Metadata =>
metadata.decimals && isTokenMetadata(metadata);
export type SyntheticConfig = TokenMetadata & {
type: TokenType.synthetic | TokenType.syntheticUri;
};
export type CollateralConfig = {
type: TokenType.collateral | TokenType.collateralUri;
token: string;
};
export type NativeConfig = {
type: TokenType.native;
};
export type TokenConfig = SyntheticConfig | CollateralConfig | NativeConfig;
export const isCollateralConfig = (
config: TokenConfig,
): config is CollateralConfig =>
config.type === TokenType.collateral ||
config.type === TokenType.collateralUri;
export const isSyntheticConfig = (
config: TokenConfig,
): config is SyntheticConfig =>
config.type === TokenType.synthetic || config.type === TokenType.syntheticUri;
export const isNativeConfig = (config: TokenConfig): config is NativeConfig =>
config.type === TokenType.native;
export const isUriConfig = (config: TokenConfig) =>
config.type === TokenType.syntheticUri ||
config.type === TokenType.collateralUri;
export type HypERC20Config = GasRouterConfig & SyntheticConfig & ERC20Metadata;
export type HypERC20CollateralConfig = GasRouterConfig & CollateralConfig;
export type HypNativeConfig = GasRouterConfig & NativeConfig;
export type ERC20RouterConfig =
| HypERC20Config
| HypERC20CollateralConfig
| HypNativeConfig;
export type HypERC721Config = GasRouterConfig & SyntheticConfig;
export type HypERC721CollateralConfig = GasRouterConfig & CollateralConfig;
export type ERC721RouterConfig = HypERC721Config | HypERC721CollateralConfig;

@ -0,0 +1,20 @@
import {
HypERC20Collateral__factory,
HypERC20__factory,
HypERC721Collateral__factory,
HypERC721URICollateral__factory,
HypERC721__factory,
HypNative__factory,
} from './types';
export type HypERC20Factories = {
router: HypERC20__factory | HypERC20Collateral__factory | HypNative__factory;
};
export type HypERC721Factories = {
router:
| HypERC721__factory
| HypERC721Collateral__factory
| HypERC721URICollateral__factory;
};
export type TokenFactories = HypERC20Factories | HypERC721Factories;

@ -0,0 +1,381 @@
import { providers } from 'ethers';
import {
ChainMap,
ChainName,
GasRouterDeployer,
HyperlaneContracts,
MultiProvider,
objMap,
} from '@hyperlane-xyz/sdk';
import { GasConfig, RouterConfig } from '@hyperlane-xyz/sdk/dist/router/types';
import {
CollateralConfig,
ERC20Metadata,
ERC20RouterConfig,
ERC721RouterConfig,
HypERC20CollateralConfig,
HypERC20Config,
HypERC721CollateralConfig,
HypERC721Config,
HypNativeConfig,
TokenConfig,
TokenMetadata,
isCollateralConfig,
isErc20Metadata,
isNativeConfig,
isSyntheticConfig,
isTokenMetadata,
isUriConfig,
} from './config';
import { HypERC20Factories, HypERC721Factories } from './contracts';
import {
ERC20__factory,
ERC721EnumerableUpgradeable__factory,
HypERC20,
HypERC20Collateral,
HypERC20Collateral__factory,
HypERC20__factory,
HypERC721,
HypERC721Collateral,
HypERC721Collateral__factory,
HypERC721URICollateral__factory,
HypERC721URIStorage__factory,
HypERC721__factory,
HypNative,
HypNative__factory,
} from './types';
export class HypERC20Deployer extends GasRouterDeployer<
ERC20RouterConfig,
HypERC20Factories
> {
constructor(multiProvider: MultiProvider) {
super(multiProvider, {} as HypERC20Factories); // factories not used in deploy
}
static async fetchMetadata(
provider: providers.Provider,
config: CollateralConfig,
): Promise<ERC20Metadata> {
const erc20 = ERC20__factory.connect(config.token, provider);
const [name, symbol, totalSupply, decimals] = await Promise.all([
erc20.name(),
erc20.symbol(),
erc20.totalSupply(),
erc20.decimals(),
]);
return { name, symbol, totalSupply, decimals };
}
static gasOverheadDefault(config: TokenConfig): number {
switch (config.type) {
case 'synthetic':
return 64_000;
case 'native':
return 44_000;
case 'collateral':
default:
return 68_000;
}
}
protected async deployCollateral(
chain: ChainName,
config: HypERC20CollateralConfig,
): Promise<HypERC20Collateral> {
const router = await this.deployContractFromFactory(
chain,
new HypERC20Collateral__factory(),
'HypERC20Collateral',
[config.token],
);
await this.multiProvider.handleTx(
chain,
router.initialize(config.mailbox, config.interchainGasPaymaster),
);
return router;
}
protected async deployNative(
chain: ChainName,
config: HypNativeConfig,
): Promise<HypNative> {
const router = await this.deployContractFromFactory(
chain,
new HypNative__factory(),
'HypNative',
[],
);
await this.multiProvider.handleTx(
chain,
router.initialize(config.mailbox, config.interchainGasPaymaster),
);
return router;
}
protected async deploySynthetic(
chain: ChainName,
config: HypERC20Config,
): Promise<HypERC20> {
const router = await this.deployContractFromFactory(
chain,
new HypERC20__factory(),
'HypERC20',
[config.decimals],
);
await this.multiProvider.handleTx(
chain,
router.initialize(
config.mailbox,
config.interchainGasPaymaster,
config.totalSupply,
config.name,
config.symbol,
),
);
return router;
}
router(contracts: HyperlaneContracts<HypERC20Factories>) {
return contracts.router;
}
async deployContracts(chain: ChainName, config: HypERC20Config) {
let router: HypERC20 | HypERC20Collateral | HypNative;
if (isCollateralConfig(config)) {
router = await this.deployCollateral(chain, config);
} else if (isNativeConfig(config)) {
router = await this.deployNative(chain, config);
} else if (isSyntheticConfig(config)) {
router = await this.deploySynthetic(chain, config);
} else {
throw new Error('Invalid ERC20 token router config');
}
return { router };
}
async buildTokenMetadata(
configMap: ChainMap<TokenConfig>,
): Promise<ChainMap<ERC20Metadata>> {
let tokenMetadata: ERC20Metadata | undefined;
for (const [chain, config] of Object.entries(configMap)) {
if (isCollateralConfig(config)) {
const collateralMetadata = await HypERC20Deployer.fetchMetadata(
this.multiProvider.getProvider(chain),
config,
);
tokenMetadata = {
...collateralMetadata,
totalSupply: 0,
};
} else if (isNativeConfig(config)) {
const chainMetadata = this.multiProvider.getChainMetadata(chain);
if (chainMetadata.nativeToken) {
tokenMetadata = {
...chainMetadata.nativeToken,
totalSupply: 0,
};
} else {
throw new Error(
`Warp route config specifies native token but chain metadata for ${chain} does not provide native token details`,
);
}
} else if (isErc20Metadata(config)) {
tokenMetadata = config;
}
}
if (!isErc20Metadata(tokenMetadata)) {
throw new Error('Invalid ERC20 token metadata');
}
return objMap(configMap, () => tokenMetadata!);
}
buildGasOverhead(configMap: ChainMap<TokenConfig>): ChainMap<GasConfig> {
return objMap(configMap, (_, config) => ({
gas: HypERC20Deployer.gasOverheadDefault(config),
}));
}
async deploy(configMap: ChainMap<TokenConfig & RouterConfig>) {
const tokenMetadata = await this.buildTokenMetadata(configMap);
const gasOverhead = this.buildGasOverhead(configMap);
const mergedConfig = objMap(configMap, (chain, config) => {
return {
...tokenMetadata[chain],
...gasOverhead[chain],
...config,
};
}) as ChainMap<ERC20RouterConfig>;
return super.deploy(mergedConfig);
}
}
export class HypERC721Deployer extends GasRouterDeployer<
ERC721RouterConfig,
HypERC721Factories
> {
constructor(multiProvider: MultiProvider) {
super(multiProvider, {} as HypERC721Factories); // factories not used in deploy
}
static async fetchMetadata(
provider: providers.Provider,
config: CollateralConfig,
): Promise<TokenMetadata> {
const erc721 = ERC721EnumerableUpgradeable__factory.connect(
config.token,
provider,
);
const [name, symbol, totalSupply] = await Promise.all([
erc721.name(),
erc721.symbol(),
erc721.totalSupply(),
]);
return { name, symbol, totalSupply };
}
static gasOverheadDefault(config: TokenConfig): number {
switch (config.type) {
case 'synthetic':
return 160_000;
case 'syntheticUri':
return 163_000;
case 'collateral':
case 'collateralUri':
default:
return 80_000;
}
}
protected async deployCollateral(
chain: ChainName,
config: HypERC721CollateralConfig,
): Promise<HypERC721Collateral> {
let router: HypERC721Collateral;
if (isUriConfig(config)) {
router = await this.deployContractFromFactory(
chain,
new HypERC721URICollateral__factory(),
'HypERC721URICollateral',
[config.token],
);
} else {
router = await this.deployContractFromFactory(
chain,
new HypERC721Collateral__factory(),
'HypERC721Collateral',
[config.token],
);
}
await this.multiProvider.handleTx(
chain,
router.initialize(config.mailbox, config.interchainGasPaymaster),
);
return router;
}
protected async deploySynthetic(
chain: ChainName,
config: HypERC721Config,
): Promise<HypERC721> {
let router: HypERC721;
if (isUriConfig(config)) {
router = await this.deployContractFromFactory(
chain,
new HypERC721URIStorage__factory(),
'HypERC721URIStorage',
[],
);
} else {
router = await this.deployContractFromFactory(
chain,
new HypERC721__factory(),
'HypERC721',
[],
);
}
await this.multiProvider.handleTx(
chain,
router.initialize(
config.mailbox,
config.interchainGasPaymaster,
config.totalSupply,
config.name,
config.symbol,
),
);
return router;
}
router(contracts: HyperlaneContracts<HypERC721Factories>) {
return contracts.router;
}
async deployContracts(chain: ChainName, config: HypERC721Config) {
let router: HypERC721 | HypERC721Collateral;
if (isCollateralConfig(config)) {
router = await this.deployCollateral(chain, config);
} else if (isSyntheticConfig(config)) {
router = await this.deploySynthetic(chain, config);
} else {
throw new Error('Invalid ERC721 token router config');
}
return { router };
}
async buildTokenMetadata(
configMap: ChainMap<TokenConfig>,
): Promise<ChainMap<TokenMetadata>> {
let tokenMetadata: TokenMetadata | undefined;
for (const [chain, config] of Object.entries(configMap)) {
if (isCollateralConfig(config)) {
const collateralMetadata = await HypERC721Deployer.fetchMetadata(
this.multiProvider.getProvider(chain),
config,
);
tokenMetadata = {
...collateralMetadata,
totalSupply: 0,
};
} else if (isTokenMetadata(config)) {
tokenMetadata = config;
}
}
if (!isTokenMetadata(tokenMetadata)) {
throw new Error('Invalid ERC721 token metadata');
}
return objMap(configMap, () => tokenMetadata!);
}
buildGasOverhead(configMap: ChainMap<TokenConfig>): ChainMap<GasConfig> {
return objMap(configMap, (_, config) => ({
gas: HypERC721Deployer.gasOverheadDefault(config),
}));
}
async deploy(configMap: ChainMap<TokenConfig & RouterConfig>) {
const tokenMetadata = await this.buildTokenMetadata(configMap);
const gasOverhead = this.buildGasOverhead(configMap);
const mergedConfig = objMap(configMap, (chain, config) => {
return {
...tokenMetadata[chain],
...gasOverhead[chain],
...config,
};
}) as ChainMap<ERC721RouterConfig>;
return super.deploy(mergedConfig);
}
}

@ -0,0 +1,16 @@
export { HypERC20App, HypERC721App } from './app';
export {
CollateralConfig,
HypERC20CollateralConfig,
HypERC20Config,
HypERC721CollateralConfig,
HypERC721Config,
isCollateralConfig,
isUriConfig,
SyntheticConfig,
TokenConfig,
TokenType,
} from './config';
export { HypERC20Factories, HypERC721Factories } from './contracts';
export { HypERC20Deployer, HypERC721Deployer } from './deploy';
export * from './types';

@ -0,0 +1,291 @@
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import '@nomiclabs/hardhat-waffle';
import { expect } from 'chai';
import { BigNumber, BigNumberish } from 'ethers';
import { ethers } from 'hardhat';
import { InterchainGasPaymaster__factory } from '@hyperlane-xyz/core';
import {
ChainMap,
Chains,
HyperlaneContractsMap,
MultiProvider,
RouterConfig,
TestCoreApp,
TestCoreDeployer,
deployTestIgpsAndGetRouterConfig,
objMap,
} from '@hyperlane-xyz/sdk';
import { utils } from '@hyperlane-xyz/utils';
import { TokenConfig, TokenType } from '../src/config';
import { HypERC20Factories } from '../src/contracts';
import { HypERC20Deployer } from '../src/deploy';
import {
ERC20,
ERC20Test__factory,
ERC20__factory,
HypERC20,
HypERC20Collateral,
HypNative,
} from '../src/types';
const localChain = Chains.test1;
const remoteChain = Chains.test2;
let localDomain: number;
let remoteDomain: number;
const totalSupply = 3000;
const amount = 10;
const tokenMetadata = {
name: 'HypERC20',
symbol: 'HYP',
decimals: 18,
totalSupply,
};
for (const variant of [
TokenType.synthetic,
TokenType.collateral,
TokenType.native,
]) {
describe(`HypERC20${variant}`, async () => {
let owner: SignerWithAddress;
let recipient: SignerWithAddress;
let core: TestCoreApp;
let deployer: HypERC20Deployer;
let contracts: HyperlaneContractsMap<HypERC20Factories>;
let localTokenConfig: TokenConfig;
let local: HypERC20 | HypERC20Collateral | HypNative;
let remote: HypERC20;
let interchainGasPayment: BigNumber;
beforeEach(async () => {
[owner, recipient] = await ethers.getSigners();
const multiProvider = MultiProvider.createTestMultiProvider({
signer: owner,
});
localDomain = multiProvider.getDomainId(localChain);
remoteDomain = multiProvider.getDomainId(remoteChain);
const coreDeployer = new TestCoreDeployer(multiProvider);
const coreContractsMaps = await coreDeployer.deploy();
core = new TestCoreApp(coreContractsMaps, multiProvider);
const routerConfig = await deployTestIgpsAndGetRouterConfig(
multiProvider,
owner.address,
core.contractsMap,
);
let erc20: ERC20 | undefined;
if (variant === TokenType.collateral) {
erc20 = await new ERC20Test__factory(owner).deploy(
tokenMetadata.name,
tokenMetadata.symbol,
tokenMetadata.totalSupply,
);
localTokenConfig = {
type: variant,
token: erc20.address,
};
} else if (variant === TokenType.native) {
localTokenConfig = {
type: variant,
};
} else if (variant === TokenType.synthetic) {
localTokenConfig = { type: variant, ...tokenMetadata };
}
const config = objMap(routerConfig, (key) => ({
...routerConfig[key],
...(key === localChain
? localTokenConfig
: { type: TokenType.synthetic }),
owner: owner.address,
})) as ChainMap<TokenConfig & RouterConfig>;
deployer = new HypERC20Deployer(multiProvider);
contracts = await deployer.deploy(config);
local = contracts[localChain].router;
interchainGasPayment = await local.quoteGasPayment(remoteDomain);
if (variant === TokenType.native) {
interchainGasPayment = interchainGasPayment.add(amount);
}
if (variant === TokenType.collateral) {
await erc20!.approve(local.address, amount);
}
remote = contracts[remoteChain].router as HypERC20;
});
it('should not be initializable again', async () => {
const initializeTx =
variant === TokenType.collateral || variant === TokenType.native
? (local as HypERC20Collateral).initialize(
ethers.constants.AddressZero,
ethers.constants.AddressZero,
)
: (local as HypERC20).initialize(
ethers.constants.AddressZero,
ethers.constants.AddressZero,
0,
'',
'',
);
await expect(initializeTx).to.be.revertedWith(
'Initializable: contract is already initialized',
);
});
if (variant === TokenType.synthetic) {
it('should mint total supply to deployer', async () => {
await expectBalance(local, recipient, 0);
await expectBalance(local, owner, totalSupply);
await expectBalance(remote, recipient, 0);
await expectBalance(remote, owner, totalSupply);
});
it('should allow for local transfers', async () => {
await (local as HypERC20).transfer(recipient.address, amount);
await expectBalance(local, recipient, amount);
await expectBalance(local, owner, totalSupply - amount);
await expectBalance(remote, recipient, 0);
await expectBalance(remote, owner, totalSupply);
});
}
it('benchmark handle gas overhead', async () => {
const localRaw = local.connect(ethers.provider);
const mailboxAddress = core.contractsMap[localChain].mailbox.address;
if (variant === TokenType.collateral) {
const tokenAddress = await (local as HypERC20Collateral).wrappedToken();
const token = ERC20__factory.connect(tokenAddress, owner);
await token.transfer(local.address, totalSupply);
} else if (variant === TokenType.native) {
const remoteDomain = core.multiProvider.getDomainId(remoteChain);
// deposit amount
await local.transferRemote(
remoteDomain,
utils.addressToBytes32(remote.address),
amount,
{ value: interchainGasPayment },
);
}
const message = `${utils.addressToBytes32(
recipient.address,
)}${BigNumber.from(amount).toHexString().slice(2).padStart(64, '0')}`;
const handleGas = await localRaw.estimateGas.handle(
remoteDomain,
utils.addressToBytes32(remote.address),
message,
{ from: mailboxAddress },
);
console.log(handleGas);
});
it('should allow for remote transfers', async () => {
const localOwner = await local.balanceOf(owner.address);
const localRecipient = await local.balanceOf(recipient.address);
const remoteOwner = await remote.balanceOf(owner.address);
const remoteRecipient = await remote.balanceOf(recipient.address);
await local.transferRemote(
remoteDomain,
utils.addressToBytes32(recipient.address),
amount,
{
value: interchainGasPayment,
},
);
let expectedLocal = localOwner.sub(amount);
await expectBalance(local, recipient, localRecipient);
if (variant === TokenType.native) {
// account for tx fees, rewards, etc.
expectedLocal = await local.balanceOf(owner.address);
}
await expectBalance(local, owner, expectedLocal);
await expectBalance(remote, recipient, remoteRecipient);
await expectBalance(remote, owner, remoteOwner);
await core.processMessages();
await expectBalance(local, recipient, localRecipient);
if (variant === TokenType.native) {
// account for tx fees, rewards, etc.
expectedLocal = await local.balanceOf(owner.address);
}
await expectBalance(local, owner, expectedLocal);
await expectBalance(remote, recipient, remoteRecipient.add(amount));
await expectBalance(remote, owner, remoteOwner);
});
it('allows interchain gas payment for remote transfers', async () => {
const interchainGasPaymaster = new InterchainGasPaymaster__factory()
.attach(await local.interchainGasPaymaster())
.connect(owner);
await expect(
local.transferRemote(
remoteDomain,
utils.addressToBytes32(recipient.address),
amount,
{ value: interchainGasPayment },
),
).to.emit(interchainGasPaymaster, 'GasPayment');
});
it('should prevent remote transfer of unowned balance', async () => {
const revertReason = (): string => {
switch (variant) {
case TokenType.synthetic:
return 'ERC20: burn amount exceeds balance';
case TokenType.collateral:
return 'ERC20: insufficient allowance';
case TokenType.native:
return 'Native: amount exceeds msg.value';
}
return '';
};
const value =
variant === TokenType.native ? amount - 1 : interchainGasPayment;
await expect(
local
.connect(recipient)
.transferRemote(
remoteDomain,
utils.addressToBytes32(recipient.address),
amount,
{ value },
),
).to.be.revertedWith(revertReason());
});
it('should emit TransferRemote events', async () => {
expect(
await local.transferRemote(
remoteDomain,
utils.addressToBytes32(recipient.address),
amount,
{ value: interchainGasPayment },
),
)
.to.emit(local, 'SentTransferRemote')
.withArgs(remoteDomain, recipient.address, amount);
expect(await core.processMessages())
.to.emit(local, 'ReceivedTransferRemote')
.withArgs(localDomain, recipient.address, amount);
});
});
}
const expectBalance = async (
token: HypERC20 | HypERC20Collateral | ERC20 | HypNative,
signer: SignerWithAddress,
balance: BigNumberish,
) => {
return expect(await token.balanceOf(signer.address)).to.eq(balance);
};

@ -0,0 +1,324 @@
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import '@nomiclabs/hardhat-waffle';
import { expect } from 'chai';
import { BigNumber, BigNumberish } from 'ethers';
import { ethers } from 'hardhat';
import { InterchainGasPaymaster__factory } from '@hyperlane-xyz/core';
import {
Chains,
HyperlaneContractsMap,
MultiProvider,
TestCoreApp,
TestCoreDeployer,
deployTestIgpsAndGetRouterConfig,
objMap,
} from '@hyperlane-xyz/sdk';
import { utils } from '@hyperlane-xyz/utils';
import { TokenConfig, TokenType } from '../src/config';
import { HypERC721Factories } from '../src/contracts';
import { HypERC721Deployer } from '../src/deploy';
import {
ERC721,
ERC721Test__factory,
ERC721__factory,
HypERC721,
HypERC721Collateral,
HypERC721URICollateral,
HypERC721URIStorage,
} from '../src/types';
const localChain = Chains.test1;
const remoteChain = Chains.test2;
let localDomain: number;
let remoteDomain: number;
const totalSupply = 50;
const tokenId = 10;
const tokenId2 = 20;
const tokenId3 = 30;
const tokenId4 = 40;
const tokenMetadata = {
name: 'HypERC721',
symbol: 'HYP',
totalSupply,
};
for (const withCollateral of [true, false]) {
for (const withUri of [true, false]) {
const tokenConfig: TokenConfig = {
type: withUri ? TokenType.syntheticUri : TokenType.synthetic,
...tokenMetadata,
};
const configMap = {
test1: tokenConfig,
test2: {
...tokenConfig,
totalSupply: 0,
},
test3: {
...tokenConfig,
totalSupply: 0,
},
};
describe(`HypERC721${withUri ? 'URI' : ''}${
withCollateral ? 'Collateral' : ''
}`, async () => {
let owner: SignerWithAddress;
let recipient: SignerWithAddress;
let core: TestCoreApp;
let deployer: HypERC721Deployer;
let contracts: HyperlaneContractsMap<HypERC721Factories>;
let local: HypERC721 | HypERC721Collateral | HypERC721URICollateral;
let remote: HypERC721 | HypERC721Collateral | HypERC721URIStorage;
let interchainGasPayment: BigNumberish;
beforeEach(async () => {
[owner, recipient] = await ethers.getSigners();
const multiProvider = MultiProvider.createTestMultiProvider({
signer: owner,
});
localDomain = multiProvider.getDomainId(localChain);
remoteDomain = multiProvider.getDomainId(remoteChain);
const coreDeployer = new TestCoreDeployer(multiProvider);
const coreContractsMaps = await coreDeployer.deploy();
core = new TestCoreApp(coreContractsMaps, multiProvider);
const coreConfig = await deployTestIgpsAndGetRouterConfig(
multiProvider,
owner.address,
core.contractsMap,
);
const configWithTokenInfo = objMap(coreConfig, (key) => ({
...coreConfig[key],
...configMap[key],
owner: owner.address,
}));
let erc721: ERC721 | undefined;
if (withCollateral) {
erc721 = await new ERC721Test__factory(owner).deploy(
tokenConfig.name,
tokenConfig.symbol,
tokenConfig.totalSupply,
);
configWithTokenInfo.test1 = {
type: withUri ? TokenType.collateralUri : TokenType.collateral,
token: erc721.address,
...coreConfig.test1,
};
}
deployer = new HypERC721Deployer(multiProvider);
contracts = await deployer.deploy(configWithTokenInfo);
local = contracts[localChain].router;
if (withCollateral) {
// approve wrapper to transfer tokens
await erc721!.approve(local.address, tokenId);
await erc721!.approve(local.address, tokenId2);
await erc721!.approve(local.address, tokenId3);
await erc721!.approve(local.address, tokenId4);
}
interchainGasPayment = await local.quoteGasPayment(remoteDomain);
remote = contracts[remoteChain].router;
});
it('should not be initializable again', async () => {
const initializeTx = withCollateral
? (local as HypERC721Collateral).initialize(
ethers.constants.AddressZero,
ethers.constants.AddressZero,
)
: (local as HypERC721).initialize(
ethers.constants.AddressZero,
ethers.constants.AddressZero,
0,
'',
'',
);
await expect(initializeTx).to.be.revertedWith(
'Initializable: contract is already initialized',
);
});
it('should mint total supply to deployer on local domain', async () => {
await expectBalance(local, recipient, 0);
await expectBalance(local, owner, totalSupply);
await expectBalance(remote, recipient, 0);
await expectBalance(remote, owner, 0);
});
// do not test underlying ERC721 collateral functionality
if (!withCollateral) {
it('should allow for local transfers', async () => {
await (local as HypERC721).transferFrom(
owner.address,
recipient.address,
tokenId,
);
await expectBalance(local, recipient, 1);
await expectBalance(local, owner, totalSupply - 1);
await expectBalance(remote, recipient, 0);
await expectBalance(remote, owner, 0);
});
}
it('should not allow transfers of nonexistent identifiers', async () => {
const invalidTokenId = totalSupply + 10;
if (!withCollateral) {
await expect(
(local as HypERC721).transferFrom(
owner.address,
recipient.address,
invalidTokenId,
),
).to.be.revertedWith('ERC721: invalid token ID');
}
await expect(
local.transferRemote(
remoteDomain,
utils.addressToBytes32(recipient.address),
invalidTokenId,
{ value: interchainGasPayment },
),
).to.be.revertedWith('ERC721: invalid token ID');
});
it('should allow for remote transfers', async () => {
await local.transferRemote(
remoteDomain,
utils.addressToBytes32(recipient.address),
tokenId2,
{ value: interchainGasPayment },
);
await expectBalance(local, recipient, 0);
await expectBalance(local, owner, totalSupply - 1);
await expectBalance(remote, recipient, 0);
await expectBalance(remote, owner, 0);
await core.processMessages();
await expectBalance(local, recipient, 0);
await expectBalance(local, owner, totalSupply - 1);
await expectBalance(remote, recipient, 1);
await expectBalance(remote, owner, 0);
});
if (withUri && withCollateral) {
it('should relay URI with remote transfer', async () => {
const remoteUri = remote as HypERC721URIStorage;
await expect(remoteUri.tokenURI(tokenId2)).to.be.revertedWith('');
await local.transferRemote(
remoteDomain,
utils.addressToBytes32(recipient.address),
tokenId2,
{ value: interchainGasPayment },
);
await expect(remoteUri.tokenURI(tokenId2)).to.be.revertedWith('');
await core.processMessages();
expect(await remoteUri.tokenURI(tokenId2)).to.equal(
`TEST-BASE-URI${tokenId2}`,
);
});
}
it('should prevent remote transfer of unowned id', async () => {
const revertReason = withCollateral
? 'ERC721: transfer from incorrect owner'
: '!owner';
await expect(
local
.connect(recipient)
.transferRemote(
remoteDomain,
utils.addressToBytes32(recipient.address),
tokenId2,
{ value: interchainGasPayment },
),
).to.be.revertedWith(revertReason);
});
it('benchmark handle gas overhead', async () => {
const localRaw = local.connect(ethers.provider);
const mailboxAddress = core.contractsMap[localChain].mailbox.address;
let tokenIdToUse: number;
if (withCollateral) {
const tokenAddress = await (
local as HypERC721Collateral
).wrappedToken();
const token = ERC721__factory.connect(tokenAddress, owner);
await token.transferFrom(owner.address, local.address, tokenId);
tokenIdToUse = tokenId;
} else {
tokenIdToUse = totalSupply + 1;
}
const message = `${utils.addressToBytes32(
recipient.address,
)}${BigNumber.from(tokenIdToUse)
.toHexString()
.slice(2)
.padStart(64, '0')}`;
try {
const gas = await localRaw.estimateGas.handle(
remoteDomain,
utils.addressToBytes32(remote.address),
message,
{ from: mailboxAddress },
);
console.log(gas);
} catch (e) {
console.log('FAILED');
}
});
it('allows interchain gas payment for remote transfers', async () => {
const interchainGasPaymaster = new InterchainGasPaymaster__factory()
.attach(await local.interchainGasPaymaster())
.connect(owner);
await expect(
local.transferRemote(
remoteDomain,
utils.addressToBytes32(recipient.address),
tokenId3,
{
value: interchainGasPayment,
},
),
).to.emit(interchainGasPaymaster, 'GasPayment');
});
it('should emit TransferRemote events', async () => {
expect(
await local.transferRemote(
remoteDomain,
utils.addressToBytes32(recipient.address),
tokenId4,
{ value: interchainGasPayment },
),
)
.to.emit(local, 'SentTransferRemote')
.withArgs(remoteDomain, recipient.address, tokenId4);
expect(await core.processMessages())
.to.emit(local, 'ReceivedTransferRemote')
.withArgs(localDomain, recipient.address, tokenId4);
});
});
}
}
const expectBalance = async (
token: HypERC721 | HypERC721Collateral | ERC721,
signer: SignerWithAddress,
balance: number,
) => {
expect(await token.balanceOf(signer.address)).to.eq(balance);
};

@ -0,0 +1,34 @@
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"incremental": false,
"lib": ["es2015", "es5", "dom"],
"module": "commonjs",
"moduleResolution": "node",
"noEmitOnError": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": false,
"noImplicitReturns": true,
"noUnusedLocals": true,
"preserveSymlinks": true,
"preserveWatchOutput": true,
"pretty": false,
"sourceMap": true,
"target": "es6",
"strict": true,
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./src",
},
"exclude": [
"./node_modules/",
"./scripts/",
"./test/",
"./dist/",
"./src/types/hardhat.d.ts",
"hardhat.config.ts"
],
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save