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