Add URI support (#12)

* Add HypERC721URIStorage

* Add HypERC721URICollateral
pull/2435/head
Yorke Rhodes 2 years ago committed by GitHub
parent cb0ba36841
commit 272a195149
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 28
      contracts/HypERC20.sol
  2. 37
      contracts/HypERC20Collateral.sol
  3. 29
      contracts/HypERC721.sol
  4. 39
      contracts/HypERC721Collateral.sol
  5. 29
      contracts/extensions/HypERC721URICollateral.sol
  6. 73
      contracts/extensions/HypERC721URIStorage.sol
  7. 20
      contracts/libs/Message.sol
  8. 42
      contracts/libs/TokenRouter.sol
  9. 4
      contracts/test/ERC721Test.sol
  10. 28
      src/config.ts
  11. 3
      src/contracts.ts
  12. 11
      src/deploy.ts
  13. 27
      test/erc20.test.ts
  14. 387
      test/erc721.test.ts

@ -6,7 +6,7 @@ import {TokenRouter} from "./libs/TokenRouter.sol";
import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
/** /**
* @title Hyperlane Token that extends the ERC20 token standard to enable native interchain transfers. * @title Hyperlane ERC20 Token Router that extends ERC20 with remote transfer functionality.
* @author Abacus Works * @author Abacus Works
* @dev Supply on each chain is not constant but the aggregate supply across all chains is. * @dev Supply on each chain is not constant but the aggregate supply across all chains is.
*/ */
@ -40,16 +40,28 @@ contract HypERC20 is ERC20Upgradeable, TokenRouter {
_mint(msg.sender, _totalSupply); _mint(msg.sender, _totalSupply);
} }
// called in `TokenRouter.transferRemote` before `Mailbox.dispatch` /**
function _transferFromSender(uint256 _amount) internal override { * @dev Burns `_amount` of token from `msg.sender` balance.
_burn(msg.sender, _amount); * @inheritdoc TokenRouter
} */
function _transferFromSender(uint256 _amount)
// called by `TokenRouter.handle`
function _transferTo(address _recipient, uint256 _amount)
internal internal
override 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); _mint(_recipient, _amount);
} }
} }

@ -6,7 +6,7 @@ import {TokenRouter} from "./libs/TokenRouter.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/** /**
* @title Collateralize ERC20 token and route messages to HypERC20 tokens. * @title Hyperlane ERC20 Token Collateral that wraps an existing ERC20 with remote transfer functionality.
* @author Abacus Works * @author Abacus Works
*/ */
contract HypERC20Collateral is TokenRouter { contract HypERC20Collateral is TokenRouter {
@ -16,6 +16,12 @@ contract HypERC20Collateral is TokenRouter {
wrappedToken = IERC20(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.
* @param _interchainSecurityModule The address of the interchain security module contract.
*/
function initialize( function initialize(
address _mailbox, address _mailbox,
address _interchainGasPaymaster, address _interchainGasPaymaster,
@ -28,14 +34,31 @@ contract HypERC20Collateral is TokenRouter {
); );
} }
function _transferFromSender(uint256 _amount) internal override { /**
require(wrappedToken.transferFrom(msg.sender, address(this), _amount)); * @dev Transfers `_amount` of `wrappedToken` from `msg.sender` to this contract.
} * @inheritdoc TokenRouter
*/
function _transferTo(address _recipient, uint256 _amount) function _transferFromSender(uint256 _amount)
internal internal
override override
returns (bytes memory)
{ {
require(wrappedToken.transfer(_recipient, _amount)); 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");
} }
} }

@ -6,7 +6,7 @@ import {TokenRouter} from "./libs/TokenRouter.sol";
import {ERC721EnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; import {ERC721EnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
/** /**
* @title Hyperlane Token that extends the ERC721 token standard to enable native interchain transfers. * @title Hyperlane ERC721 Token Router that extends ERC721 with remote transfer functionality.
* @author Abacus Works * @author Abacus Works
*/ */
contract HypERC721 is ERC721EnumerableUpgradeable, TokenRouter { contract HypERC721 is ERC721EnumerableUpgradeable, TokenRouter {
@ -40,17 +40,30 @@ contract HypERC721 is ERC721EnumerableUpgradeable, TokenRouter {
} }
} }
// called in `TokenRouter.transferRemote` before `Mailbox.dispatch` /**
function _transferFromSender(uint256 _tokenId) internal override { * @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"); require(ownerOf(_tokenId) == msg.sender, "!owner");
_burn(_tokenId); _burn(_tokenId);
return bytes(""); // no metadata
} }
// called by `TokenRouter.handle` /**
function _transferTo(address _recipient, uint256 _tokenId) * @dev Mints `_tokenId` to `_recipient`.
internal * @inheritdoc TokenRouter
override */
{ function _transferTo(
address _recipient,
uint256 _tokenId,
bytes calldata // no metadata
) internal virtual override {
_mint(_recipient, _tokenId); _mint(_recipient, _tokenId);
} }
} }

@ -6,16 +6,22 @@ import {TokenRouter} from "./libs/TokenRouter.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
/** /**
* @title Collateralize ERC20 token and route messages to HypERC20 tokens. * @title Hyperlane ERC721 Token Collateral that wraps an existing ERC721 with remote transfer functionality.
* @author Abacus Works * @author Abacus Works
*/ */
contract HypERC721Collateral is TokenRouter { contract HypERC721Collateral is TokenRouter {
IERC721 public immutable wrappedToken; address public immutable wrappedToken;
constructor(address erc721) { constructor(address erc721) {
wrappedToken = IERC721(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.
* @param _interchainSecurityModule The address of the interchain security module contract.
*/
function initialize( function initialize(
address _mailbox, address _mailbox,
address _interchainGasPaymaster, address _interchainGasPaymaster,
@ -28,14 +34,29 @@ contract HypERC721Collateral is TokenRouter {
); );
} }
function _transferFromSender(uint256 _amount) internal override { /**
wrappedToken.transferFrom(msg.sender, address(this), _amount); * @dev Transfers `_tokenId` of `wrappedToken` from `msg.sender` to this contract.
} * @inheritdoc TokenRouter
*/
function _transferTo(address _recipient, uint256 _amount) function _transferFromSender(uint256 _tokenId)
internal internal
virtual
override override
returns (bytes memory)
{ {
wrappedToken.transferFrom(address(this), _recipient, _amount); 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);
} }
} }

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

@ -0,0 +1,73 @@
// 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
) internal override(ERC721EnumerableUpgradeable, ERC721Upgradeable) {
ERC721EnumerableUpgradeable._beforeTokenTransfer(from, to, tokenId);
}
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);
}
}

@ -2,12 +2,12 @@
pragma solidity >=0.8.0; pragma solidity >=0.8.0;
library Message { library Message {
function format(bytes32 _recipient, uint256 _amount) function format(
internal bytes32 _recipient,
pure uint256 _amount,
returns (bytes memory) bytes memory _metadata
{ ) internal pure returns (bytes memory) {
return abi.encodePacked(_recipient, _amount); return abi.encodePacked(_recipient, _amount, _metadata);
} }
function recipient(bytes calldata message) internal pure returns (bytes32) { function recipient(bytes calldata message) internal pure returns (bytes32) {
@ -22,4 +22,12 @@ library Message {
function tokenId(bytes calldata message) internal pure returns (uint256) { function tokenId(bytes calldata message) internal pure returns (uint256) {
return amount(message); return amount(message);
} }
function metadata(bytes calldata message)
internal
pure
returns (bytes calldata)
{
return message[64:];
}
} }

@ -6,9 +6,8 @@ import {TypeCasts} from "@hyperlane-xyz/core/contracts/libs/TypeCasts.sol";
import {Message} from "./Message.sol"; import {Message} from "./Message.sol";
/** /**
* @title Hyperlane Token that extends the ERC20 token standard to enable native interchain transfers. * @title Hyperlane Token Router that extends Router with abstract token (ERC20/ERC721) remote transfer functionality.
* @author Abacus Works * @author Abacus Works
* @dev Supply on each chain is not constant but the aggregate supply across all chains is.
*/ */
abstract contract TokenRouter is Router { abstract contract TokenRouter is Router {
using TypeCasts for bytes32; using TypeCasts for bytes32;
@ -39,29 +38,36 @@ abstract contract TokenRouter is Router {
); );
/** /**
* @notice Transfers `_amount` of tokens from `msg.sender` to `_recipient` on the `_destination` chain. * @notice Transfers `_amountOrId` token to `_recipient` on `_destination` domain.
* @dev Burns `_amount` of tokens from `msg.sender` on the origin chain and dispatches * @dev Delegates transfer logic to `_transferFromSender` implementation.
* message to the `destination` chain to mint `_amount` of tokens to `recipient`.
* @dev Emits `SentTransferRemote` event on the origin chain. * @dev Emits `SentTransferRemote` event on the origin chain.
* @param _destination The identifier of the destination chain. * @param _destination The identifier of the destination chain.
* @param _recipient The address of the recipient on the destination chain. * @param _recipient The address of the recipient on the destination chain.
* @param _amount The amount of tokens to be sent to the remote recipient. * @param _amountOrId The amount or identifier of tokens to be sent to the remote recipient.
*/ */
function transferRemote( function transferRemote(
uint32 _destination, uint32 _destination,
bytes32 _recipient, bytes32 _recipient,
uint256 _amount uint256 _amountOrId
) external payable { ) external payable {
_transferFromSender(_amount); bytes memory metadata = _transferFromSender(_amountOrId);
_dispatchWithGas( _dispatchWithGas(
_destination, _destination,
Message.format(_recipient, _amount), Message.format(_recipient, _amountOrId, metadata),
msg.value msg.value
); );
emit SentTransferRemote(_destination, _recipient, _amount); emit SentTransferRemote(_destination, _recipient, _amountOrId);
} }
function _transferFromSender(uint256 _amount) internal virtual; /**
* @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 Mints tokens to recipient when router receives transfer message.
@ -76,9 +82,19 @@ abstract contract TokenRouter is Router {
) internal override { ) internal override {
bytes32 recipient = _message.recipient(); bytes32 recipient = _message.recipient();
uint256 amount = _message.amount(); uint256 amount = _message.amount();
_transferTo(recipient.bytes32ToAddress(), amount); bytes calldata metadata = _message.metadata();
_transferTo(recipient.bytes32ToAddress(), amount, metadata);
emit ReceivedTransferRemote(_origin, recipient, amount); emit ReceivedTransferRemote(_origin, recipient, amount);
} }
function _transferTo(address _recipient, uint256 _amount) internal virtual; /**
* @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;
} }

@ -13,4 +13,8 @@ contract ERC721Test is ERC721 {
_mint(msg.sender, i); _mint(msg.sender, i);
} }
} }
function _baseURI() internal pure override returns (string memory) {
return "TEST-BASE-URI";
}
} }

@ -2,22 +2,38 @@ import { ethers } from 'ethers';
import { RouterConfig } from '@hyperlane-xyz/sdk'; import { RouterConfig } from '@hyperlane-xyz/sdk';
export enum TokenType {
synthetic,
syntheticUri,
collateral,
collateralUri,
}
export type SyntheticConfig = { export type SyntheticConfig = {
type: "SYNTHETIC"; type: TokenType.synthetic | TokenType.syntheticUri;
name: string; name: string;
symbol: string; symbol: string;
totalSupply: ethers.BigNumberish; totalSupply: ethers.BigNumberish;
}; };
export type CollateralConfig = { export type CollateralConfig = {
type: "COLLATERAL"; type: TokenType.collateral | TokenType.collateralUri;
token: string; token: string;
} };
export type TokenConfig = SyntheticConfig | CollateralConfig; export type TokenConfig = SyntheticConfig | CollateralConfig;
export const isCollateralConfig = (config: RouterConfig & TokenConfig): config is RouterConfig & CollateralConfig => { export const isCollateralConfig = (
return config.type === "COLLATERAL"; 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 HypERC20Config = RouterConfig & TokenConfig;
export type HypERC20CollateralConfig = RouterConfig & CollateralConfig; export type HypERC20CollateralConfig = RouterConfig & CollateralConfig;

@ -5,9 +5,10 @@ import {
HypERC20Collateral, HypERC20Collateral,
HypERC721, HypERC721,
HypERC721Collateral, HypERC721Collateral,
HypERC721URICollateral,
} from './types'; } from './types';
export type HypERC20Contracts = RouterContracts<HypERC20 | HypERC20Collateral>; export type HypERC20Contracts = RouterContracts<HypERC20 | HypERC20Collateral>;
export type HypERC721Contracts = RouterContracts< export type HypERC721Contracts = RouterContracts<
HypERC721 | HypERC721Collateral HypERC721 | HypERC721Collateral | HypERC721URICollateral
>; >;

@ -11,12 +11,13 @@ import {
HypERC721Config, HypERC721Config,
isCollateralConfig, isCollateralConfig,
HypERC721CollateralConfig, HypERC721CollateralConfig,
isUriConfig,
} from './config'; } from './config';
import { import {
HypERC20Contracts, HypERC20Contracts,
HypERC721Contracts, HypERC721Contracts,
} from './contracts'; } from './contracts';
import { HypERC20Collateral__factory, HypERC20__factory, HypERC721Collateral__factory, HypERC721__factory } from './types'; import { HypERC20Collateral__factory, HypERC20__factory, HypERC721Collateral__factory, HypERC721URICollateral__factory, HypERC721URIStorage__factory, HypERC721__factory } from './types';
export class HypERC20Deployer< export class HypERC20Deployer<
Chain extends ChainName // inferred from configured chains passed to constructor Chain extends ChainName // inferred from configured chains passed to constructor
@ -85,8 +86,8 @@ export class HypERC721Deployer<
if (isCollateralConfig(config)) { if (isCollateralConfig(config)) {
const router = await this.deployContractFromFactory( const router = await this.deployContractFromFactory(
chain, chain,
new HypERC721Collateral__factory(), isUriConfig(config) ? new HypERC721URICollateral__factory() : new HypERC721Collateral__factory(),
'HypERC721Collateral', `HypERC721${isUriConfig(config) ? 'URI' : ''}Collateral`,
[config.token], [config.token],
); );
await connection.handleTx( await connection.handleTx(
@ -100,8 +101,8 @@ export class HypERC721Deployer<
} else { } else {
const router = await this.deployContractFromFactory( const router = await this.deployContractFromFactory(
chain, chain,
new HypERC721__factory(), isUriConfig(config) ? new HypERC721URIStorage__factory() : new HypERC721__factory(),
'HypERC721', `HypERC721${isUriConfig(config) ? 'URIStorage' : ''}`,
[], [],
); );
await connection.handleTx(router.initialize( await connection.handleTx(router.initialize(

@ -18,6 +18,7 @@ import {
HypERC20CollateralConfig, HypERC20CollateralConfig,
HypERC20Config, HypERC20Config,
SyntheticConfig, SyntheticConfig,
TokenType,
} from '../src/config'; } from '../src/config';
import { HypERC20Contracts } from '../src/contracts'; import { HypERC20Contracts } from '../src/contracts';
import { HypERC20Deployer } from '../src/deploy'; import { HypERC20Deployer } from '../src/deploy';
@ -38,7 +39,7 @@ const amount = 10;
const testInterchainGasPayment = 123456789; const testInterchainGasPayment = 123456789;
const tokenConfig: SyntheticConfig = { const tokenConfig: SyntheticConfig = {
type: 'SYNTHETIC', type: TokenType.synthetic,
name: 'HypERC20', name: 'HypERC20',
symbol: 'HYP', symbol: 'HYP',
totalSupply, totalSupply,
@ -80,7 +81,7 @@ for (const withCollateral of [true, false]) {
); );
configWithTokenInfo.test1 = { configWithTokenInfo.test1 = {
...configWithTokenInfo.test1, ...configWithTokenInfo.test1,
type: 'COLLATERAL', type: TokenType.collateral,
token: erc20.address, token: erc20.address,
}; };
} }
@ -123,18 +124,16 @@ for (const withCollateral of [true, false]) {
await expectBalance(remote, owner, totalSupply); await expectBalance(remote, owner, totalSupply);
}); });
it('should allow for local transfers', async () => { // do not test underlying ERC20 collateral functionality
// do not test underlying ERC20 collateral functionality if (!withCollateral) {
if (withCollateral) { it('should allow for local transfers', async () => {
return; await (local as HypERC20).transfer(recipient.address, amount);
} await expectBalance(local, recipient, amount);
await expectBalance(local, owner, totalSupply - amount);
await (local as HypERC20).transfer(recipient.address, amount); await expectBalance(remote, recipient, 0);
await expectBalance(local, recipient, amount); await expectBalance(remote, owner, totalSupply);
await expectBalance(local, owner, totalSupply - amount); });
await expectBalance(remote, recipient, 0); }
await expectBalance(remote, owner, totalSupply);
});
it('should allow for remote transfers', async () => { it('should allow for remote transfers', async () => {
await local.transferRemote( await local.transferRemote(

@ -17,7 +17,8 @@ import { utils } from '@hyperlane-xyz/utils';
import { import {
HypERC721CollateralConfig, HypERC721CollateralConfig,
HypERC721Config, HypERC721Config,
TokenConfig, SyntheticConfig,
TokenType,
} from '../src/config'; } from '../src/config';
import { HypERC721Contracts } from '../src/contracts'; import { HypERC721Contracts } from '../src/contracts';
import { HypERC721Deployer } from '../src/deploy'; import { HypERC721Deployer } from '../src/deploy';
@ -27,6 +28,8 @@ import {
ERC721__factory, ERC721__factory,
HypERC721, HypERC721,
HypERC721Collateral, HypERC721Collateral,
HypERC721URICollateral,
HypERC721URIStorage,
} from '../src/types'; } from '../src/types';
const localChain = 'test1'; const localChain = 'test1';
@ -40,216 +43,238 @@ const tokenId3 = 30;
const tokenId4 = 40; const tokenId4 = 40;
const testInterchainGasPayment = 123456789; const testInterchainGasPayment = 123456789;
const tokenConfig: TokenConfig = { for (const withCollateral of [true, false]) {
type: 'SYNTHETIC', for (const withUri of [true, false]) {
name: 'HypERC721', const tokenConfig: SyntheticConfig = {
symbol: 'HYP', type: withUri ? TokenType.syntheticUri : TokenType.synthetic,
totalSupply, name: 'HypERC721',
}; symbol: 'HYP',
totalSupply,
};
const configMap = { const configMap = {
test1: { test1: {
...tokenConfig, ...tokenConfig,
totalSupply, totalSupply,
}, },
test2: { test2: {
...tokenConfig, ...tokenConfig,
totalSupply: 0, totalSupply: 0,
}, },
test3: { test3: {
...tokenConfig, ...tokenConfig,
totalSupply: 0, 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;
for (const withCollateral of [true, false]) { beforeEach(async () => {
describe(`HypERC721${withCollateral ? 'Collateral' : ''}`, async () => { [owner, recipient] = await ethers.getSigners();
let owner: SignerWithAddress; const multiProvider = getTestMultiProvider(owner);
let recipient: SignerWithAddress;
let core: TestCoreApp;
let deployer: HypERC721Deployer<TestChainNames>;
let contracts: Record<TestChainNames, HypERC721Contracts>;
let local: HypERC721 | HypERC721Collateral;
let remote: HypERC721 | HypERC721Collateral;
beforeEach(async () => { const coreDeployer = new TestCoreDeployer(multiProvider);
[owner, recipient] = await ethers.getSigners(); const coreContractsMaps = await coreDeployer.deploy();
const multiProvider = getTestMultiProvider(owner); 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,
}));
const coreDeployer = new TestCoreDeployer(multiProvider); let erc721: ERC721 | undefined;
const coreContractsMaps = await coreDeployer.deploy(); if (withCollateral) {
core = new TestCoreApp(coreContractsMaps, multiProvider); erc721 = await new ERC721Test__factory(owner).deploy(
const coreConfig = core.getConnectionClientConfigMap(); tokenConfig.name,
const configWithTokenInfo: ChainMap< tokenConfig.symbol,
TestChainNames, tokenConfig.totalSupply,
HypERC721Config | HypERC721CollateralConfig );
> = objMap(coreConfig, (key) => ({ configWithTokenInfo.test1 = {
...coreConfig[key], ...configWithTokenInfo.test1,
...configMap[key], type: withUri ? TokenType.collateralUri : TokenType.collateral,
owner: owner.address, token: erc721.address,
})); };
}
let erc721: ERC721 | undefined; deployer = new HypERC721Deployer(
if (withCollateral) { multiProvider,
erc721 = await new ERC721Test__factory(owner).deploy( configWithTokenInfo,
tokenConfig.name, core,
tokenConfig.symbol,
tokenConfig.totalSupply,
); );
configWithTokenInfo.test1 = { contracts = await deployer.deploy();
...configWithTokenInfo.test1, local = contracts[localChain].router;
type: 'COLLATERAL', if (withCollateral) {
token: erc721.address, // approve wrapper to transfer tokens
}; await erc721!.approve(local.address, tokenId);
} await erc721!.approve(local.address, tokenId2);
await erc721!.approve(local.address, tokenId3);
deployer = new HypERC721Deployer( await erc721!.approve(local.address, tokenId4);
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; remote = contracts[remoteChain].router;
}); });
it('should not be initializable again', async () => { it('should not be initializable again', async () => {
const initializeTx = withCollateral const initializeTx = withCollateral
? (local as HypERC721Collateral).initialize( ? (local as HypERC721Collateral).initialize(
ethers.constants.AddressZero, ethers.constants.AddressZero,
ethers.constants.AddressZero, ethers.constants.AddressZero,
ethers.constants.AddressZero, ethers.constants.AddressZero,
) )
: (local as HypERC721).initialize( : (local as HypERC721).initialize(
ethers.constants.AddressZero, ethers.constants.AddressZero,
ethers.constants.AddressZero, ethers.constants.AddressZero,
ethers.constants.AddressZero, ethers.constants.AddressZero,
0, 0,
'', '',
'', '',
); );
await expect(initializeTx).to.be.revertedWith( await expect(initializeTx).to.be.revertedWith(
'Initializable: contract is already initialized', 'Initializable: contract is already initialized',
); );
}); });
it('should mint total supply to deployer on local domain', async () => { it('should mint total supply to deployer on local domain', async () => {
await expectBalance(local, recipient, 0); await expectBalance(local, recipient, 0);
await expectBalance(local, owner, totalSupply); await expectBalance(local, owner, totalSupply);
await expectBalance(remote, recipient, 0); await expectBalance(remote, recipient, 0);
await expectBalance(remote, owner, 0); await expectBalance(remote, owner, 0);
}); });
it('should allow for local transfers', async () => {
// do not test underlying ERC721 collateral functionality // do not test underlying ERC721 collateral functionality
if (withCollateral) {
return;
}
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) { if (!withCollateral) {
await expect( it('should allow for local transfers', async () => {
(local as HypERC721).transferFrom( await (local as HypERC721).transferFrom(
owner.address, owner.address,
recipient.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, invalidTokenId,
), ),
).to.be.revertedWith('ERC721: invalid token ID'); ).to.be.revertedWith('ERC721: invalid token ID');
} });
await expect(
local.transferRemote( it('should allow for remote transfers', async () => {
await local.transferRemote(
remoteDomain, remoteDomain,
utils.addressToBytes32(recipient.address), utils.addressToBytes32(recipient.address),
invalidTokenId, tokenId2,
), );
).to.be.revertedWith('ERC721: invalid token ID');
});
it('should allow for remote transfers', async () => { await expectBalance(local, recipient, 0);
await local.transferRemote( await expectBalance(local, owner, totalSupply - 1);
remoteDomain, await expectBalance(remote, recipient, 0);
utils.addressToBytes32(recipient.address), await expectBalance(remote, owner, 0);
tokenId2,
);
await expectBalance(local, recipient, 0); await core.processMessages();
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);
});
await expectBalance(local, recipient, 0); if (withUri && withCollateral) {
await expectBalance(local, owner, totalSupply - 1); it('should relay URI with remote transfer', async () => {
await expectBalance(remote, recipient, 1); const remoteUri = remote as HypERC721URIStorage;
await expectBalance(remote, owner, 0); await expect(remoteUri.tokenURI(tokenId2)).to.be.revertedWith('');
});
it('should prevent remote transfer of unowned id', async () => { await local.transferRemote(
const revertReason = withCollateral
? 'ERC721: transfer from incorrect owner'
: '!owner';
await expect(
local
.connect(recipient)
.transferRemote(
remoteDomain, remoteDomain,
utils.addressToBytes32(recipient.address), utils.addressToBytes32(recipient.address),
tokenId2, tokenId2,
), );
).to.be.revertedWith(revertReason);
});
it('allows interchain gas payment for remote transfers', async () => { await expect(remoteUri.tokenURI(tokenId2)).to.be.revertedWith('');
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 () => { await core.processMessages();
expect(
await local.transferRemote( expect(await remoteUri.tokenURI(tokenId2)).to.equal(
remoteDomain, `TEST-BASE-URI${tokenId2}`,
utils.addressToBytes32(recipient.address), );
tokenId4, });
), }
)
.to.emit(local, 'SentTransferRemote') it('should prevent remote transfer of unowned id', async () => {
.withArgs(remoteDomain, recipient.address, tokenId4); const revertReason = withCollateral
expect(await core.processMessages()) ? 'ERC721: transfer from incorrect owner'
.to.emit(local, 'ReceivedTransferRemote') : '!owner';
.withArgs(localDomain, recipient.address, tokenId4); await expect(
local
.connect(recipient)
.transferRemote(
remoteDomain,
utils.addressToBytes32(recipient.address),
tokenId2,
),
).to.be.revertedWith(revertReason);
});
it('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 ( const expectBalance = async (

Loading…
Cancel
Save