commit
11d9d0bf0d
@ -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_ |
||||
_<<http://www.apache.org/licenses/>>_ |
||||
|
||||
### 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…
Reference in new issue