Make helloworld and token submodules (#1475)

* Update CI to checkout submodules

* Fix github actions syntax

* Try removing locks

* Reactivate submodules
pull/1480/head
Yorke Rhodes 2 years ago committed by GitHub
parent 6c40b382b8
commit 4a8c47c73c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .github/workflows/e2e.yml
  2. 20
      .github/workflows/node.yml
  3. 8
      .gitmodules
  4. 1
      typescript/helloworld
  5. 5
      typescript/helloworld/.eslintrc
  6. 11
      typescript/helloworld/.gitignore
  7. 2
      typescript/helloworld/.prettierignore
  8. 3
      typescript/helloworld/.solcover.js
  9. 8
      typescript/helloworld/.solhint.json
  10. 27
      typescript/helloworld/README.md
  11. 120
      typescript/helloworld/contracts/HelloWorld.sol
  12. 26
      typescript/helloworld/hardhat.config.ts
  13. 70
      typescript/helloworld/package.json
  14. 91
      typescript/helloworld/scripts/sync-with-template-repo.ts
  15. 107
      typescript/helloworld/src/app/app.ts
  16. 11
      typescript/helloworld/src/app/contracts.ts
  17. 15
      typescript/helloworld/src/deploy/check.ts
  18. 8
      typescript/helloworld/src/deploy/config.ts
  19. 44
      typescript/helloworld/src/deploy/deploy.ts
  20. 10
      typescript/helloworld/src/index.ts
  21. 50
      typescript/helloworld/src/scripts/check.ts
  22. 39
      typescript/helloworld/src/scripts/deploy.ts
  23. 56
      typescript/helloworld/src/test/deploy.test.ts
  24. 83
      typescript/helloworld/src/test/helloworld.test.ts
  25. 11
      typescript/helloworld/tsconfig.json
  26. 1
      typescript/token
  27. 6
      typescript/token/.eslintignore
  28. 31
      typescript/token/.eslintrc
  29. 9
      typescript/token/.gitignore
  30. 2
      typescript/token/.prettierignore
  31. 21
      typescript/token/.prettierrc
  32. 3
      typescript/token/.solcover.js
  33. 8
      typescript/token/.solhint.json
  34. 64
      typescript/token/contracts/HypERC20.sol
  35. 61
      typescript/token/contracts/HypERC20Collateral.sol
  36. 66
      typescript/token/contracts/HypERC721.sol
  37. 59
      typescript/token/contracts/HypERC721Collateral.sol
  38. 29
      typescript/token/contracts/extensions/HypERC721URICollateral.sol
  39. 79
      typescript/token/contracts/extensions/HypERC721URIStorage.sol
  40. 33
      typescript/token/contracts/libs/Message.sol
  41. 99
      typescript/token/contracts/libs/TokenRouter.sol
  42. 14
      typescript/token/contracts/test/ERC20Test.sol
  43. 20
      typescript/token/contracts/test/ERC721Test.sol
  44. 28
      typescript/token/hardhat.config.ts
  45. 63
      typescript/token/package.json
  46. 65
      typescript/token/scripts/deploy.ts
  47. 43
      typescript/token/src/app.ts
  48. 42
      typescript/token/src/config.ts
  49. 14
      typescript/token/src/contracts.ts
  50. 114
      typescript/token/src/deploy.ts
  51. 219
      typescript/token/test/erc20.test.ts
  52. 294
      typescript/token/test/erc721.test.ts
  53. 36
      typescript/token/tsconfig.json

@ -23,6 +23,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: remove submodule locks
run: git submodule foreach rm yarn.lock
- name: setup rust
uses: actions-rs/toolchain@v1

@ -15,6 +15,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: remove submodule locks
run: git submodule foreach rm yarn.lock
- uses: actions/cache@v3
with:
path: '**/node_modules'
@ -25,7 +31,7 @@ jobs:
# verify the lockfile matches what was committed.
run: |
yarn install
CHANGES=$(git status -s)
CHANGES=$(git status -s --ignore-submodules)
if [[ ! -z $CHANGES ]]; then
echo "Changes found: $CHANGES"
git diff
@ -37,6 +43,11 @@ jobs:
needs: [yarn-install]
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: remove submodule locks
run: git submodule foreach rm yarn.lock
- name: yarn-cache
uses: actions/cache@v3
@ -80,6 +91,9 @@ jobs:
needs: [yarn-build]
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: actions/cache@v3
with:
path: ./*
@ -89,10 +103,6 @@ jobs:
run: yarn workspace @hyperlane-xyz/sdk run test
- name: infra
run: yarn workspace @hyperlane-xyz/infra run test
- name: helloworld
run: yarn workspace @hyperlane-xyz/helloworld run test
- name: hyperlane-token
run: yarn workspace @hyperlane-xyz/hyperlane-token run test
test-sol:
env:

8
.gitmodules vendored

@ -0,0 +1,8 @@
[submodule "typescript/token"]
path = typescript/token
url = git@github.com:hyperlane-xyz/hyperlane-token.git
branch = main
[submodule "typescript/helloworld"]
path = typescript/helloworld
url = git@github.com:hyperlane-xyz/hyperlane-app-template.git
branch = main

@ -0,0 +1 @@
Subproject commit 646f1e7beb18a77e5ca2535dba130f75793aea0f

@ -1,5 +0,0 @@
{
"rules": {
"no-console": ["off"]
}
}

@ -1,11 +0,0 @@
artifacts/
cache/
coverage/
coverage.json
dist/
node_modules/
src/types/
src/deploy/output
test/outputs/
*.swp
.yarn/install-state.gz

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

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

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

@ -1,27 +0,0 @@
# Hyperlane 'Hello World' App Template
A basic Hyperlane application with a router contract that can dispatch messages.
## Setup
```sh
# Install dependencies
yarn
# Build source and generate types
yarn build
```
## Test
```sh
# Run all unit tests
yarn test
# Lint check code
yarn lint
```
## Learn more
For more information, see the [Hyperlane documentation](https://docs.hyperlane.xyz/hyperlane-docs/developers/getting-started).

@ -1,120 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;
// ============ External Imports ============
import {Router} from "@hyperlane-xyz/core/contracts/Router.sol";
/*
* @title The Hello World App
* @dev You can use this simple app as a starting point for your own application.
*/
contract HelloWorld is Router {
// A counter of how many messages have been sent from this contract.
uint256 public sent;
// A counter of how many messages have been received by this contract.
uint256 public received;
// Keyed by domain, a counter of how many messages that have been sent
// from this contract to the domain.
mapping(uint32 => uint256) public sentTo;
// Keyed by domain, a counter of how many messages that have been received
// by this contract from the domain.
mapping(uint32 => uint256) public receivedFrom;
// Keyed by domain, a generous upper bound on the amount of gas to use in the
// handle function when a message is processed. Used for paying for gas.
mapping(uint32 => uint256) public handleGasAmounts;
// ============ Events ============
event SentHelloWorld(
uint32 indexed origin,
uint32 indexed destination,
string message
);
event ReceivedHelloWorld(
uint32 indexed origin,
uint32 indexed destination,
bytes32 sender,
string message
);
event HandleGasAmountSet(
uint32 indexed destination,
uint256 handleGasAmount
);
constructor(address _mailbox, address _interchainGasPaymaster) {
// Transfer ownership of the contract to deployer
_transferOwnership(msg.sender);
// Set the addresses for the Mailbox and IGP
// Alternatively, this could be done later in an initialize method
_setMailbox(_mailbox);
_setInterchainGasPaymaster(_interchainGasPaymaster);
}
// ============ External functions ============
/**
* @notice Sends a message to the _destinationDomain. Any msg.value is
* used as interchain gas payment.
* @param _destinationDomain The destination domain to send the message to.
* @param _message The message to send.
*/
function sendHelloWorld(uint32 _destinationDomain, string calldata _message)
external
payable
{
sent += 1;
sentTo[_destinationDomain] += 1;
_dispatchWithGas(
_destinationDomain,
bytes(_message),
handleGasAmounts[_destinationDomain],
msg.value,
msg.sender
);
emit SentHelloWorld(
mailbox.localDomain(),
_destinationDomain,
_message
);
}
/**
* @notice Sets the amount of gas the recipient's handle function uses on
* the destination domain, which is used when paying for gas.
* @dev Reverts if called by a non-owner.
* @param _destinationDomain The destination domain,
* @param _handleGasAmount The handle gas amount.
*/
function setHandleGasAmount(
uint32 _destinationDomain,
uint256 _handleGasAmount
) external onlyOwner {
handleGasAmounts[_destinationDomain] = _handleGasAmount;
emit HandleGasAmountSet(_destinationDomain, _handleGasAmount);
}
// ============ Internal functions ============
/**
* @notice Handles a message from a remote router.
* @dev Only called for messages sent from a remote router, as enforced by Router.sol.
* @param _origin The domain of the origin of the message.
* @param _sender The sender of the message.
* @param _message The message body.
*/
function _handle(
uint32 _origin,
bytes32 _sender,
bytes calldata _message
) internal override {
received += 1;
receivedFrom[_origin] += 1;
emit ReceivedHelloWorld(
_origin,
mailbox.localDomain(),
_sender,
string(_message)
);
}
}

@ -1,26 +0,0 @@
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?
},
};

@ -1,70 +0,0 @@
{
"name": "@hyperlane-xyz/helloworld",
"description": "A basic skeleton of an Hyperlane app",
"version": "1.0.0-beta3",
"dependencies": {
"@hyperlane-xyz/sdk": "1.0.0-beta3",
"@openzeppelin/contracts-upgradeable": "^4.8.0",
"ethers": "^5.6.8"
},
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.0.5",
"@nomiclabs/hardhat-waffle": "^2.0.2",
"@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",
"HelloWorld",
"Solidity",
"Typescript"
],
"license": "Apache-2.0",
"main": "dist/src/index.js",
"packageManager": "yarn@3.2.0",
"repository": {
"type": "git",
"url": "https://github.com/hyperlane-xyz/hyperlane-app-template"
},
"scripts": {
"build": "hardhat compile && tsc",
"clean": "hardhat clean && rm -rf dist cache src/types",
"coverage": "hardhat coverage",
"lint": "eslint . --ext .ts",
"prettier": "prettier --write ./contracts ./src",
"test": "hardhat test ./src/test/**/*.test.ts",
"sync": "ts-node scripts/sync-with-template-repo.ts"
},
"types": "dist/src/index.d.ts",
"resolutions": {
"underscore": "^1.13",
"fetch-ponyfill": "^7.1",
"lodash": "^4.17.21",
"async": "^2.6.4",
"undici": "^5.11"
},
"stableVersion": "1.0.0-beta2"
}

@ -1,91 +0,0 @@
/**
* This script copies over files from this monorepo to the template repo
* It assumes the template repo is available locally at PATH_TO_TEMPLATE_REPO
* It will aggregate commits to this folder as the commit message
* Pull requests must still be created and merged on the template repo
*
* Usage: yarn sync
* Flags: --no-commit to skip automatic git committing and pushing
*
* Possible improvements:
* 1. Clone template automatically if it doesn't exist
* 2. Auto generate commit message based on changes since last sync
* 3. Run in CI using github token
*/
import { execSync } from 'child_process';
import { existsSync } from 'fs';
const SKIP_GIT_FLAG = '--no-commit';
const PATH_TO_TEMPLATE_REPO = '../../../hyperlane-app-template';
const SYNC_WHITELIST = [
'contracts',
'src',
'.gitignore',
'.prettierignore',
'.solcover.js',
'.solhint.json',
'hardhat.config.ts',
'package.json',
'README.md',
];
async function main() {
console.info('Attempting to sync files with template repo');
console.info('Using repo path:', PATH_TO_TEMPLATE_REPO);
const args = process.argv.slice(2);
const skipGit = args.includes(SKIP_GIT_FLAG);
if (skipGit) {
console.info('Skip git flag set, will not run any git commands');
} else {
console.info(
'Skip git flag not set, will automatically commit and push changes to new branch',
);
}
if (!existsSync(PATH_TO_TEMPLATE_REPO)) {
throw new Error('No folder found at repo path');
}
const t = new Date();
const date = `${t.getFullYear()}-${
t.getMonth() + 1
}-${t.getDate()}-${t.getHours()}-${t.getMinutes()}`;
const branchName = `sync-${date}`;
if (!skipGit) {
execSync(`git checkout main && git pull`, { cwd: PATH_TO_TEMPLATE_REPO });
execSync(`git checkout -b ${branchName}`, {
cwd: PATH_TO_TEMPLATE_REPO,
});
}
for (const f of SYNC_WHITELIST) {
console.info(`Copying ${f}`);
execSync(`cp -r ${f} ${PATH_TO_TEMPLATE_REPO}`);
}
console.info(`Running yarn to ensure up to date lockfile`);
execSync(`yarn install`, { cwd: PATH_TO_TEMPLATE_REPO });
if (!skipGit) {
console.info(`Committing changes`);
execSync(`git add . && git commit -m "Sync with monorepo"`, {
cwd: PATH_TO_TEMPLATE_REPO,
});
execSync(`git push -u origin ${branchName}`, {
cwd: PATH_TO_TEMPLATE_REPO,
});
console.info(
`Changes pushed to branch ${branchName}, please create pull request manually`,
);
} else {
console.info(`Please commit changes and create pull request manually`);
}
}
main()
.then(() => console.info('Sync complete!'))
.catch((e) => console.error('Sync failed', e));

@ -1,107 +0,0 @@
import { BigNumber, ethers } from 'ethers';
import {
ChainMap,
ChainName,
ChainNameToDomainId,
HyperlaneApp,
HyperlaneCore,
MultiProvider,
Remotes,
} from '@hyperlane-xyz/sdk';
import { debug } from '@hyperlane-xyz/utils';
import { HelloWorldContracts } from './contracts';
type Counts = {
sent: number;
received: number;
};
export class HelloWorldApp<
Chain extends ChainName = ChainName,
> extends HyperlaneApp<HelloWorldContracts, Chain> {
constructor(
public readonly core: HyperlaneCore<Chain>,
contractsMap: ChainMap<Chain, HelloWorldContracts>,
multiProvider: MultiProvider<Chain>,
) {
super(contractsMap, multiProvider);
}
async sendHelloWorld<From extends Chain>(
from: From,
to: Remotes<Chain, From>,
message: string,
value: BigNumber,
): Promise<ethers.ContractReceipt> {
const sender = this.getContracts(from).router;
const toDomain = ChainNameToDomainId[to];
const chainConnection = this.multiProvider.getChainConnection(from);
// apply gas buffer due to https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/634
const estimated = await sender.estimateGas.sendHelloWorld(
toDomain,
message,
{ ...chainConnection.overrides, value },
);
const gasLimit = estimated.mul(12).div(10);
const tx = await sender.sendHelloWorld(toDomain, message, {
...chainConnection.overrides,
gasLimit,
value,
});
debug('Sending hello message', {
from,
to,
message,
tx,
});
return tx.wait(chainConnection.confirmations);
}
async waitForMessageReceipt(
receipt: ethers.ContractReceipt,
): Promise<ethers.ContractReceipt[]> {
return this.core.waitForMessageProcessing(receipt);
}
async waitForMessageProcessed(
receipt: ethers.ContractReceipt,
): Promise<void> {
return this.core.waitForMessageProcessed(receipt);
}
async channelStats<From extends Chain>(
from: From,
to: Remotes<Chain, From>,
): Promise<Counts> {
const sent = await this.getContracts(from).router.sentTo(
ChainNameToDomainId[to],
);
const received = await this.getContracts(to).router.receivedFrom(
ChainNameToDomainId[from],
);
return { sent: sent.toNumber(), received: received.toNumber() };
}
async stats(): Promise<Record<Chain, Record<Chain, Counts>>> {
const entries: Array<[Chain, Record<Chain, Counts>]> = await Promise.all(
this.chains().map(async (source) => {
const destinationEntries = await Promise.all(
this.remoteChains(source).map(async (destination) => [
destination,
await this.channelStats(source, destination),
]),
);
return [
source,
Object.fromEntries(destinationEntries) as Record<Chain, Counts>,
];
}),
);
return Object.fromEntries(entries) as Record<Chain, Record<Chain, Counts>>;
}
}

@ -1,11 +0,0 @@
import { RouterContracts, RouterFactories } from '@hyperlane-xyz/sdk';
import { HelloWorld, HelloWorld__factory } from '../types';
export type HelloWorldFactories = RouterFactories<HelloWorld>;
export const helloWorldFactories: HelloWorldFactories = {
router: new HelloWorld__factory(),
};
export type HelloWorldContracts = RouterContracts<HelloWorld>;

@ -1,15 +0,0 @@
import { ChainName, HyperlaneRouterChecker } from '@hyperlane-xyz/sdk';
import { HelloWorldApp } from '../app/app';
import { HelloWorldContracts } from '../app/contracts';
import { HelloWorldConfig } from './config';
export class HelloWorldChecker<
Chain extends ChainName,
> extends HyperlaneRouterChecker<
Chain,
HelloWorldApp<Chain>,
HelloWorldConfig,
HelloWorldContracts
> {}

@ -1,8 +0,0 @@
import { RouterConfig, chainConnectionConfigs } from '@hyperlane-xyz/sdk';
export type HelloWorldConfig = RouterConfig;
// SET DESIRED NETWORKS HERE
export const prodConfigs = {
alfajores: chainConnectionConfigs.alfajores,
};

@ -1,44 +0,0 @@
import {
ChainMap,
ChainName,
HyperlaneCore,
HyperlaneRouterDeployer,
MultiProvider,
} from '@hyperlane-xyz/sdk';
import {
HelloWorldContracts,
HelloWorldFactories,
helloWorldFactories,
} from '../app/contracts';
import { HelloWorldConfig } from './config';
export class HelloWorldDeployer<
Chain extends ChainName,
> extends HyperlaneRouterDeployer<
Chain,
HelloWorldConfig,
HelloWorldContracts,
HelloWorldFactories
> {
constructor(
multiProvider: MultiProvider<Chain>,
configMap: ChainMap<Chain, HelloWorldConfig>,
protected core: HyperlaneCore<Chain>,
) {
super(multiProvider, configMap, helloWorldFactories, {});
}
// Custom contract deployment logic can go here
// If no custom logic is needed, call deployContract for the router
async deployContracts(chain: Chain, config: HelloWorldConfig) {
const router = await this.deployContract(chain, 'router', [
config.mailbox,
config.interchainGasPaymaster,
]);
return {
router,
};
}
}

@ -1,10 +0,0 @@
export { HelloWorldChecker } from './deploy/check';
export { HelloWorldConfig } from './deploy/config';
export { HelloWorldDeployer } from './deploy/deploy';
export { HelloWorldApp } from './app/app';
export {
HelloWorldContracts,
HelloWorldFactories,
helloWorldFactories,
} from './app/contracts';
export * as types from './types';

@ -1,50 +0,0 @@
import {
ChainMap,
ChainName,
HyperlaneCore,
MultiProvider,
buildContracts,
getChainToOwnerMap,
objMap,
} from '@hyperlane-xyz/sdk';
import { HelloWorldApp } from '../app/app';
import { HelloWorldContracts, helloWorldFactories } from '../app/contracts';
import { HelloWorldChecker } from '../deploy/check';
import { prodConfigs } from '../deploy/config';
// COPY FROM OUTPUT OF DEPLOYMENT SCRIPT OR IMPORT FROM ELSEWHERE
const deploymentAddresses = {};
// SET CONTRACT OWNER ADDRESS HERE
const ownerAddress = '0x123...';
async function check() {
console.info('Preparing utilities');
const chainProviders = objMap(prodConfigs, (_, config) => ({
provider: config.provider,
confirmations: config.confirmations,
overrides: config.overrides,
}));
const multiProvider = new MultiProvider(chainProviders);
const contractsMap = buildContracts(
deploymentAddresses,
helloWorldFactories,
) as ChainMap<ChainName, HelloWorldContracts>;
const core = HyperlaneCore.fromEnvironment('testnet2', multiProvider);
const app = new HelloWorldApp(core, contractsMap, multiProvider);
const config = core.extendWithConnectionClientConfig(
getChainToOwnerMap(prodConfigs, ownerAddress),
);
console.info('Starting check');
const helloWorldChecker = new HelloWorldChecker(multiProvider, app, config);
await helloWorldChecker.check();
helloWorldChecker.expectEmpty();
}
check()
.then(() => console.info('Check complete'))
.catch(console.error);

@ -1,39 +0,0 @@
import { Wallet } from 'ethers';
import {
HyperlaneCore,
MultiProvider,
getChainToOwnerMap,
objMap,
serializeContracts,
} from '@hyperlane-xyz/sdk';
import { prodConfigs } from '../deploy/config';
import { HelloWorldDeployer } from '../deploy/deploy';
async function main() {
console.info('Getting signer');
const signer = new Wallet('SET KEY HERE OR CREATE YOUR OWN SIGNER');
console.info('Preparing utilities');
const chainProviders = objMap(prodConfigs, (_, config) => ({
...config,
signer: signer.connect(config.provider),
}));
const multiProvider = new MultiProvider(chainProviders);
const core = HyperlaneCore.fromEnvironment('testnet2', multiProvider);
const config = core.extendWithConnectionClientConfig(
getChainToOwnerMap(prodConfigs, signer.address),
);
const deployer = new HelloWorldDeployer(multiProvider, config, core);
const chainToContracts = await deployer.deploy();
const addresses = serializeContracts(chainToContracts);
console.info('===Contract Addresses===');
console.info(JSON.stringify(addresses));
}
main()
.then(() => console.info('Deploy complete'))
.catch(console.error);

@ -1,56 +0,0 @@
import '@nomiclabs/hardhat-waffle';
import { ethers } from 'hardhat';
import {
ChainMap,
MultiProvider,
TestChainNames,
TestCoreApp,
TestCoreDeployer,
getChainToOwnerMap,
getTestMultiProvider,
testChainConnectionConfigs,
} from '@hyperlane-xyz/sdk';
import { HelloWorldApp } from '../app/app';
import { HelloWorldContracts } from '../app/contracts';
import { HelloWorldChecker } from '../deploy/check';
import { HelloWorldConfig } from '../deploy/config';
import { HelloWorldDeployer } from '../deploy/deploy';
describe('deploy', async () => {
let multiProvider: MultiProvider<TestChainNames>;
let core: TestCoreApp;
let config: ChainMap<TestChainNames, HelloWorldConfig>;
let deployer: HelloWorldDeployer<TestChainNames>;
let contracts: Record<TestChainNames, HelloWorldContracts>;
let app: HelloWorldApp<TestChainNames>;
before(async () => {
const [signer] = await ethers.getSigners();
multiProvider = getTestMultiProvider(signer);
const coreDeployer = new TestCoreDeployer(multiProvider);
const coreContractsMaps = await coreDeployer.deploy();
core = new TestCoreApp(coreContractsMaps, multiProvider);
config = core.extendWithConnectionClientConfig(
getChainToOwnerMap(testChainConnectionConfigs, signer.address),
);
deployer = new HelloWorldDeployer(multiProvider, config, core);
});
it('deploys', async () => {
contracts = await deployer.deploy();
});
it('builds app', async () => {
contracts = await deployer.deploy();
app = new HelloWorldApp(core, contracts, multiProvider);
});
it('checks', async () => {
const checker = new HelloWorldChecker(multiProvider, app, config);
await checker.check();
checker.expectEmpty();
});
});

@ -1,83 +0,0 @@
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import { expect } from 'chai';
import { ethers } from 'hardhat';
import {
ChainMap,
ChainNameToDomainId,
MultiProvider,
TestChainNames,
TestCoreApp,
TestCoreDeployer,
getChainToOwnerMap,
getTestMultiProvider,
testChainConnectionConfigs,
} from '@hyperlane-xyz/sdk';
import { HelloWorldConfig } from '../deploy/config';
import { HelloWorldDeployer } from '../deploy/deploy';
import { HelloWorld } from '../types';
describe('HelloWorld', async () => {
const localChain = 'test1';
const remoteChain = 'test2';
const localDomain = ChainNameToDomainId[localChain];
const remoteDomain = ChainNameToDomainId[remoteChain];
let signer: SignerWithAddress;
let local: HelloWorld;
let remote: HelloWorld;
let multiProvider: MultiProvider<TestChainNames>;
let coreApp: TestCoreApp;
let config: ChainMap<TestChainNames, HelloWorldConfig>;
before(async () => {
[signer] = await ethers.getSigners();
multiProvider = getTestMultiProvider(signer);
const coreDeployer = new TestCoreDeployer(multiProvider);
const coreContractsMaps = await coreDeployer.deploy();
coreApp = new TestCoreApp(coreContractsMaps, multiProvider);
config = coreApp.extendWithConnectionClientConfig(
getChainToOwnerMap(testChainConnectionConfigs, signer.address),
);
});
beforeEach(async () => {
const helloWorld = new HelloWorldDeployer(multiProvider, config, coreApp);
const contracts = await helloWorld.deploy();
local = contracts[localChain].router;
remote = contracts[remoteChain].router;
// The all counts start empty
expect(await local.sent()).to.equal(0);
expect(await local.received()).to.equal(0);
expect(await remote.sent()).to.equal(0);
expect(await remote.received()).to.equal(0);
});
it('sends a message', async () => {
await expect(local.sendHelloWorld(remoteDomain, 'Hello')).to.emit(
local,
'SentHelloWorld',
);
// The sent counts are correct
expect(await local.sent()).to.equal(1);
expect(await local.sentTo(remoteDomain)).to.equal(1);
// The received counts are correct
expect(await local.received()).to.equal(0);
});
it('handles a message', async () => {
await local.sendHelloWorld(remoteDomain, 'World');
// Mock processing of the message by Hyperlane
await coreApp.processOutboundMessages(localChain);
// The initial message has been dispatched.
expect(await local.sent()).to.equal(1);
// The initial message has been processed.
expect(await remote.received()).to.equal(1);
expect(await remote.receivedFrom(localDomain)).to.equal(1);
});
});

@ -1,11 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist/",
"rootDir": "./",
"noImplicitAny": false,
},
"exclude": ["./node_modules/", "./dist/", "./src/types/hardhat.d.ts"],
"include": ["./src/"],
"files": ["hardhat.config.ts"]
}

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

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

@ -1,31 +0,0 @@
{
"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"]
}
}

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

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

@ -1,21 +0,0 @@
{
"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
}

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

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

@ -1,64 +0,0 @@
// 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 {
/**
* @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);
}
/**
* @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);
}
}

@ -1,61 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.0;
import {TokenRouter} from "./libs/TokenRouter.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/**
* @title Hyperlane ERC20 Token Collateral that wraps an existing ERC20 with remote transfer functionality.
* @author Abacus Works
*/
contract HypERC20Collateral is TokenRouter {
IERC20 public immutable wrappedToken;
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
);
}
/**
* @dev Transfers `_amount` of `wrappedToken` from `msg.sender` to this contract.
* @inheritdoc TokenRouter
*/
function _transferFromSender(uint256 _amount)
internal
override
returns (bytes memory)
{
require(
wrappedToken.transferFrom(msg.sender, address(this), _amount),
"!transferFrom"
);
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 {
require(wrappedToken.transfer(_recipient, _amount), "!transfer");
}
}

@ -1,66 +0,0 @@
// 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++) {
_mint(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 {
_mint(_recipient, _tokenId);
}
}

@ -1,59 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.0;
import {TokenRouter} from "./libs/TokenRouter.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 {
address public immutable wrappedToken;
constructor(address erc721) {
wrappedToken = erc721;
}
/**
* @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
);
}
/**
* @dev Transfers `_tokenId` of `wrappedToken` from `msg.sender` to this contract.
* @inheritdoc TokenRouter
*/
function _transferFromSender(uint256 _tokenId)
internal
virtual
override
returns (bytes memory)
{
IERC721(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 {
IERC721(wrappedToken).transferFrom(address(this), _recipient, _tokenId);
}
}

@ -1,29 +0,0 @@
// 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(wrappedToken).tokenURI(_tokenId));
}
}

@ -1,79 +0,0 @@
// 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);
}
}

@ -1,33 +0,0 @@
// 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:];
}
}

@ -1,99 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.0;
import {Router} from "@hyperlane-xyz/core/contracts/Router.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 Router {
using TypeCasts for bytes32;
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.
*/
function transferRemote(
uint32 _destination,
bytes32 _recipient,
uint256 _amountOrId
) external payable {
bytes memory metadata = _transferFromSender(_amountOrId);
_dispatch(
_destination,
Message.format(_recipient, _amountOrId, metadata)
);
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;
}

@ -1,14 +0,0 @@
// 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);
}
}

@ -1,20 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract ERC721Test is ERC721 {
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";
}
}

@ -1,28 +0,0 @@
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: {
version: '0.8.17',
settings: {
optimizer: {
enabled: true,
runs: 999_999,
},
},
},
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?
},
};

@ -1,63 +0,0 @@
{
"name": "@hyperlane-xyz/hyperlane-token",
"description": "A template for interchain ERC20 and ERC721 tokens using Hyperlane",
"version": "1.0.0-beta3",
"dependencies": {
"@hyperlane-xyz/core": "1.0.0-beta3",
"@hyperlane-xyz/sdk": "1.0.0-beta3",
"@hyperlane-xyz/utils": "1.0.0-beta3",
"@openzeppelin/contracts-upgradeable": "^4.8.0",
"ethers": "^5.6.8"
},
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.0.5",
"@nomiclabs/hardhat-waffle": "^2.0.2",
"@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"
},
"types": "dist/index.d.ts",
"stableVersion": "1.0.0-beta2"
}

@ -1,65 +0,0 @@
import { Wallet, ethers } from 'ethers';
import {
Chains,
HyperlaneCore,
MultiProvider,
objMap,
} from '@hyperlane-xyz/sdk';
import { RouterConfig, chainConnectionConfigs } from '@hyperlane-xyz/sdk';
import { TokenConfig, TokenType } from '../src/config';
import { HypERC20Deployer } from '../src/deploy';
const connectionConfigs = {
goerli: {
...chainConnectionConfigs.goerli,
provider: new ethers.providers.JsonRpcProvider(
'https://eth-goerli.public.blastapi.io',
5,
),
},
fuji: chainConnectionConfigs.fuji,
alfajores: chainConnectionConfigs.alfajores,
moonbasealpha: chainConnectionConfigs.moonbasealpha,
};
async function deployNFTWrapper() {
console.info('Getting signer');
const signer = new Wallet(
'3ed2c141ec02887887e94c1fbe5647fe5741c038deca248afbaaccf2c27d9258',
);
const multiProvider = new MultiProvider(connectionConfigs);
multiProvider.rotateSigner(signer);
const core = HyperlaneCore.fromEnvironment('testnet2', multiProvider);
const config = objMap(
connectionConfigs,
(chain, _) =>
({
type: TokenType.synthetic,
name: 'Dai',
symbol: 'DAI',
totalSupply: 0,
owner: signer.address,
mailbox: '0x1d3aAC239538e6F1831C8708803e61A9EA299Eec',
interchainGasPaymaster:
core.getContracts(chain).interchainGasPaymaster.address,
} as TokenConfig & RouterConfig),
);
config.goerli = {
type: TokenType.collateral,
token: '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6',
owner: signer.address,
mailbox: '0x1d3aAC239538e6F1831C8708803e61A9EA299Eec',
interchainGasPaymaster: core.getContracts(Chains.goerli)
.interchainGasPaymaster.address,
} as TokenConfig & RouterConfig;
const deployer = new HypERC20Deployer(multiProvider, config, core);
await deployer.deploy();
}
deployNFTWrapper().then(console.log).catch(console.error);

@ -1,43 +0,0 @@
import {
ChainName,
HyperlaneApp,
objMap,
promiseObjAll,
} from '@hyperlane-xyz/sdk';
import { HypERC20Contracts, HypERC721Contracts } from './contracts';
export class HypERC20App<Chain extends ChainName> extends HyperlaneApp<
HypERC20Contracts,
Chain
> {
getSecurityModules = () =>
promiseObjAll(
objMap(this.contractsMap, (_, contracts) =>
contracts.router.interchainSecurityModule(),
),
);
getOwners = () =>
promiseObjAll(
objMap(this.contractsMap, (_, contracts) => contracts.router.owner()),
);
}
// TODO: dedupe?
export class HypERC721App<Chain extends ChainName> extends HyperlaneApp<
HypERC721Contracts,
Chain
> {
getSecurityModules = () =>
promiseObjAll(
objMap(this.contractsMap, (_, contracts) =>
contracts.router.interchainSecurityModule(),
),
);
getOwners = () =>
promiseObjAll(
objMap(this.contractsMap, (_, contracts) => contracts.router.owner()),
);
}

@ -1,42 +0,0 @@
import { ethers } from 'ethers';
import { RouterConfig } from '@hyperlane-xyz/sdk';
export enum TokenType {
synthetic,
syntheticUri,
collateral,
collateralUri,
}
export type SyntheticConfig = {
type: TokenType.synthetic | TokenType.syntheticUri;
name: string;
symbol: string;
totalSupply: ethers.BigNumberish;
};
export type CollateralConfig = {
type: TokenType.collateral | TokenType.collateralUri;
token: string;
};
export type TokenConfig = SyntheticConfig | CollateralConfig;
export const isCollateralConfig = (
config: RouterConfig & TokenConfig,
): config is RouterConfig & CollateralConfig => {
return (
config.type === TokenType.collateral ||
config.type === TokenType.collateralUri
);
};
export const isUriConfig = (config: RouterConfig & TokenConfig) =>
config.type === TokenType.syntheticUri ||
config.type === TokenType.collateralUri;
export type HypERC20Config = RouterConfig & TokenConfig;
export type HypERC20CollateralConfig = RouterConfig & CollateralConfig;
export type HypERC721Config = RouterConfig & TokenConfig;
export type HypERC721CollateralConfig = RouterConfig & CollateralConfig;

@ -1,14 +0,0 @@
import { RouterContracts } from '@hyperlane-xyz/sdk';
import {
HypERC20,
HypERC20Collateral,
HypERC721,
HypERC721Collateral,
HypERC721URICollateral,
} from './types';
export type HypERC20Contracts = RouterContracts<HypERC20 | HypERC20Collateral>;
export type HypERC721Contracts = RouterContracts<
HypERC721 | HypERC721Collateral | HypERC721URICollateral
>;

@ -1,114 +0,0 @@
import { ChainName, HyperlaneRouterDeployer } from '@hyperlane-xyz/sdk';
import {
HypERC20CollateralConfig,
HypERC20Config,
HypERC721CollateralConfig,
HypERC721Config,
isCollateralConfig,
isUriConfig,
} from './config';
import { HypERC20Contracts, HypERC721Contracts } from './contracts';
import {
HypERC20Collateral__factory,
HypERC20__factory,
HypERC721Collateral__factory,
HypERC721URICollateral__factory,
HypERC721URIStorage__factory,
HypERC721__factory,
} from './types';
export class HypERC20Deployer<
Chain extends ChainName, // inferred from configured chains passed to constructor
> extends HyperlaneRouterDeployer<
Chain,
HypERC20Config | HypERC20CollateralConfig,
HypERC20Contracts,
any // RouterFactories doesn't work well when router has multiple types
> {
async deployContracts(
chain: Chain,
config: HypERC20Config | HypERC20CollateralConfig,
) {
const connection = this.multiProvider.getChainConnection(chain);
if (isCollateralConfig(config)) {
const router = await this.deployContractFromFactory(
chain,
new HypERC20Collateral__factory(),
'HypERC20Collateral',
[config.token],
);
await connection.handleTx(
router.initialize(config.mailbox, config.interchainGasPaymaster),
);
return { router };
} else {
const router = await this.deployContractFromFactory(
chain,
new HypERC20__factory(),
'HypERC20',
[],
);
await connection.handleTx(
router.initialize(
config.mailbox,
config.interchainGasPaymaster,
config.totalSupply,
config.name,
config.symbol,
),
);
return { router };
}
}
}
// TODO: dedupe?
export class HypERC721Deployer<
Chain extends ChainName,
> extends HyperlaneRouterDeployer<
Chain,
HypERC721Config | HypERC721CollateralConfig,
HypERC721Contracts,
any
> {
async deployContracts(
chain: Chain,
config: HypERC721Config | HypERC721CollateralConfig,
) {
const connection = this.multiProvider.getChainConnection(chain);
if (isCollateralConfig(config)) {
const router = await this.deployContractFromFactory(
chain,
isUriConfig(config)
? new HypERC721URICollateral__factory()
: new HypERC721Collateral__factory(),
`HypERC721${isUriConfig(config) ? 'URI' : ''}Collateral`,
[config.token],
);
await connection.handleTx(
router.initialize(config.mailbox, config.interchainGasPaymaster),
);
return { router };
} else {
const router = await this.deployContractFromFactory(
chain,
isUriConfig(config)
? new HypERC721URIStorage__factory()
: new HypERC721__factory(),
`HypERC721${isUriConfig(config) ? 'URIStorage' : ''}`,
[],
);
await connection.handleTx(
router.initialize(
config.mailbox,
config.interchainGasPaymaster,
config.totalSupply,
config.name,
config.symbol,
),
);
return { router };
}
}
}

@ -1,219 +0,0 @@
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import '@nomiclabs/hardhat-waffle';
import { expect } from 'chai';
import { ethers } from 'hardhat';
import {
ChainMap,
ChainNameToDomainId,
TestChainNames,
TestCoreApp,
TestCoreDeployer,
getTestMultiProvider,
objMap,
} from '@hyperlane-xyz/sdk';
import { utils } from '@hyperlane-xyz/utils';
import {
HypERC20CollateralConfig,
HypERC20Config,
SyntheticConfig,
TokenType,
} from '../src/config';
import { HypERC20Contracts } from '../src/contracts';
import { HypERC20Deployer } from '../src/deploy';
import {
ERC20,
ERC20Test__factory,
ERC20__factory,
HypERC20,
HypERC20Collateral,
} from '../src/types';
const localChain = 'test1';
const remoteChain = 'test2';
const localDomain = ChainNameToDomainId[localChain];
const remoteDomain = ChainNameToDomainId[remoteChain];
const totalSupply = 3000;
const amount = 10;
const testInterchainGasPayment = 123456789;
const tokenConfig: SyntheticConfig = {
type: TokenType.synthetic,
name: 'HypERC20',
symbol: 'HYP',
totalSupply,
};
for (const withCollateral of [true, false]) {
describe(`HypERC20${withCollateral ? 'Collateral' : ''}`, async () => {
let owner: SignerWithAddress;
let recipient: SignerWithAddress;
let core: TestCoreApp;
let deployer: HypERC20Deployer<TestChainNames>;
let contracts: Record<TestChainNames, HypERC20Contracts>;
let local: HypERC20 | HypERC20Collateral;
let remote: HypERC20 | HypERC20Collateral;
beforeEach(async () => {
[owner, recipient] = await ethers.getSigners();
const multiProvider = getTestMultiProvider(owner);
const coreDeployer = new TestCoreDeployer(multiProvider);
const coreContractsMaps = await coreDeployer.deploy();
core = new TestCoreApp(coreContractsMaps, multiProvider);
const coreConfig = core.getConnectionClientConfigMap();
const configWithTokenInfo: ChainMap<
TestChainNames,
HypERC20Config | HypERC20CollateralConfig
> = objMap(coreConfig, (key) => ({
...coreConfig[key],
...tokenConfig,
owner: owner.address,
}));
let erc20: ERC20 | undefined;
if (withCollateral) {
erc20 = await new ERC20Test__factory(owner).deploy(
tokenConfig.name,
tokenConfig.symbol,
tokenConfig.totalSupply,
);
configWithTokenInfo.test1 = {
...configWithTokenInfo.test1,
type: TokenType.collateral,
token: erc20.address,
};
}
deployer = new HypERC20Deployer(multiProvider, configWithTokenInfo, core);
contracts = await deployer.deploy();
local = contracts[localChain].router as HypERC20;
if (withCollateral) {
await erc20!.approve(local.address, amount);
}
remote = contracts[remoteChain].router as HypERC20;
});
it('should not be initializable again', async () => {
const initializeTx = withCollateral
? (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',
);
});
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);
});
// do not test underlying ERC20 collateral functionality
if (!withCollateral) {
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('should allow for remote transfers', async () => {
await local.transferRemote(
remoteDomain,
utils.addressToBytes32(recipient.address),
amount,
);
await expectBalance(local, recipient, 0);
await expectBalance(local, owner, totalSupply - amount);
await expectBalance(remote, recipient, 0);
await expectBalance(remote, owner, totalSupply);
await core.processMessages();
await expectBalance(local, recipient, 0);
await expectBalance(local, owner, totalSupply - amount);
await expectBalance(remote, recipient, amount);
await expectBalance(remote, owner, totalSupply);
});
it.skip('allows interchain gas payment for remote transfers', async () => {
const interchainGasPaymaster =
core.contractsMap[localChain].interchainGasPaymaster.contract;
await expect(
local.transferRemote(
remoteDomain,
utils.addressToBytes32(recipient.address),
amount,
{
value: testInterchainGasPayment,
},
),
).to.emit(interchainGasPaymaster, 'GasPayment');
});
it('should prevent remote transfer of unowned balance', async () => {
const revertReason = withCollateral
? 'ERC20: insufficient allowance'
: 'ERC20: burn amount exceeds balance';
await expect(
local
.connect(recipient)
.transferRemote(
remoteDomain,
utils.addressToBytes32(recipient.address),
amount,
),
).to.be.revertedWith(revertReason);
});
it('should emit TransferRemote events', async () => {
expect(
await local.transferRemote(
remoteDomain,
utils.addressToBytes32(recipient.address),
amount,
),
)
.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,
signer: SignerWithAddress,
balance: number,
) => {
if (Object.keys(token.interface.functions).includes('wrappedToken()')) {
const wrappedToken = await (token as HypERC20Collateral).wrappedToken();
token = ERC20__factory.connect(wrappedToken, signer);
}
return expectTokenBalance(token as HypERC20, signer, balance);
};
const expectTokenBalance = async (
token: ERC20,
signer: SignerWithAddress,
balance: number,
) => expect(await token.balanceOf(signer.address)).to.eq(balance);

@ -1,294 +0,0 @@
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import '@nomiclabs/hardhat-waffle';
import { expect } from 'chai';
import { ethers } from 'hardhat';
import {
ChainMap,
ChainNameToDomainId,
TestChainNames,
TestCoreApp,
TestCoreDeployer,
getTestMultiProvider,
objMap,
} from '@hyperlane-xyz/sdk';
import { utils } from '@hyperlane-xyz/utils';
import {
HypERC721CollateralConfig,
HypERC721Config,
SyntheticConfig,
TokenType,
} from '../src/config';
import { HypERC721Contracts } from '../src/contracts';
import { HypERC721Deployer } from '../src/deploy';
import {
ERC721,
ERC721Test__factory,
ERC721__factory,
HypERC721,
HypERC721Collateral,
HypERC721URICollateral,
HypERC721URIStorage,
} from '../src/types';
const localChain = 'test1';
const remoteChain = 'test2';
const localDomain = ChainNameToDomainId[localChain];
const remoteDomain = ChainNameToDomainId[remoteChain];
const totalSupply = 50;
const tokenId = 10;
const tokenId2 = 20;
const tokenId3 = 30;
const tokenId4 = 40;
const testInterchainGasPayment = 123456789;
for (const withCollateral of [true, false]) {
for (const withUri of [true, false]) {
const tokenConfig: SyntheticConfig = {
type: withUri ? TokenType.syntheticUri : TokenType.synthetic,
name: 'HypERC721',
symbol: 'HYP',
totalSupply,
};
const configMap = {
test1: {
...tokenConfig,
totalSupply,
},
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<TestChainNames>;
let contracts: Record<TestChainNames, HypERC721Contracts>;
let local: HypERC721 | HypERC721Collateral | HypERC721URICollateral;
let remote: HypERC721 | HypERC721Collateral | HypERC721URIStorage;
beforeEach(async () => {
[owner, recipient] = await ethers.getSigners();
const multiProvider = getTestMultiProvider(owner);
const coreDeployer = new TestCoreDeployer(multiProvider);
const coreContractsMaps = await coreDeployer.deploy();
core = new TestCoreApp(coreContractsMaps, multiProvider);
const coreConfig = core.getConnectionClientConfigMap();
const configWithTokenInfo: ChainMap<
TestChainNames,
HypERC721Config | HypERC721CollateralConfig
> = 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 = {
...configWithTokenInfo.test1,
type: withUri ? TokenType.collateralUri : TokenType.collateral,
token: erc721.address,
};
}
deployer = new HypERC721Deployer(
multiProvider,
configWithTokenInfo,
core,
);
contracts = await deployer.deploy();
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);
}
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,
),
).to.be.revertedWith('ERC721: invalid token ID');
});
it('should allow for remote transfers', async () => {
await local.transferRemote(
remoteDomain,
utils.addressToBytes32(recipient.address),
tokenId2,
);
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,
);
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,
),
).to.be.revertedWith(revertReason);
});
it.skip('allows interchain gas payment for remote transfers', async () => {
const interchainGasPaymaster =
core.contractsMap[localChain].interchainGasPaymaster.contract;
await expect(
local.transferRemote(
remoteDomain,
utils.addressToBytes32(recipient.address),
tokenId3,
{
value: testInterchainGasPayment,
},
),
).to.emit(interchainGasPaymaster, 'GasPayment');
});
it('should emit TransferRemote events', async () => {
expect(
await local.transferRemote(
remoteDomain,
utils.addressToBytes32(recipient.address),
tokenId4,
),
)
.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,
) => {
if (Object.keys(token.interface.functions).includes('wrappedToken()')) {
const wrappedToken = await (token as HypERC721Collateral).wrappedToken();
token = ERC721__factory.connect(wrappedToken, signer);
}
return expectTokenBalance(token as HypERC721, signer, balance);
};
const expectTokenBalance = async (
token: ERC721,
signer: SignerWithAddress,
balance: number,
) => expect(await token.balanceOf(signer.address)).to.eq(balance);

@ -1,36 +0,0 @@
{
"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,
"outDir": "./dist",
"rootDir": "./",
"resolveJsonModule": true
},
"exclude": [
"./node_modules/",
"./dist/",
"./types/hardhat.d.ts"
],
"include": [
"./src",
"./test",
],
"files": ["hardhat.config.ts"]
}
Loading…
Cancel
Save