From 272a1951490f30f55e310a7cee7ead76b75c864b Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Fri, 16 Dec 2022 14:00:11 -0500 Subject: [PATCH] Add URI support (#12) * Add HypERC721URIStorage * Add HypERC721URICollateral --- contracts/HypERC20.sol | 28 +- contracts/HypERC20Collateral.sol | 37 +- contracts/HypERC721.sol | 29 +- contracts/HypERC721Collateral.sol | 39 +- .../extensions/HypERC721URICollateral.sol | 29 ++ contracts/extensions/HypERC721URIStorage.sol | 73 ++++ contracts/libs/Message.sol | 20 +- contracts/libs/TokenRouter.sol | 42 +- contracts/test/ERC721Test.sol | 4 + src/config.ts | 28 +- src/contracts.ts | 3 +- src/deploy.ts | 11 +- test/erc20.test.ts | 27 +- test/erc721.test.ts | 387 ++++++++++-------- 14 files changed, 499 insertions(+), 258 deletions(-) create mode 100644 contracts/extensions/HypERC721URICollateral.sol create mode 100644 contracts/extensions/HypERC721URIStorage.sol diff --git a/contracts/HypERC20.sol b/contracts/HypERC20.sol index 1f48dc178..b28924639 100644 --- a/contracts/HypERC20.sol +++ b/contracts/HypERC20.sol @@ -6,7 +6,7 @@ import {TokenRouter} from "./libs/TokenRouter.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 * @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); } - // called in `TokenRouter.transferRemote` before `Mailbox.dispatch` - function _transferFromSender(uint256 _amount) internal override { - _burn(msg.sender, _amount); - } - - // called by `TokenRouter.handle` - function _transferTo(address _recipient, uint256 _amount) + /** + * @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); } } diff --git a/contracts/HypERC20Collateral.sol b/contracts/HypERC20Collateral.sol index c5495323e..8ebfe6afe 100644 --- a/contracts/HypERC20Collateral.sol +++ b/contracts/HypERC20Collateral.sol @@ -6,7 +6,7 @@ import {TokenRouter} from "./libs/TokenRouter.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 */ contract HypERC20Collateral is TokenRouter { @@ -16,6 +16,12 @@ contract HypERC20Collateral is TokenRouter { 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( address _mailbox, address _interchainGasPaymaster, @@ -28,14 +34,31 @@ contract HypERC20Collateral is TokenRouter { ); } - function _transferFromSender(uint256 _amount) internal override { - require(wrappedToken.transferFrom(msg.sender, address(this), _amount)); - } - - function _transferTo(address _recipient, uint256 _amount) + /** + * @dev Transfers `_amount` of `wrappedToken` from `msg.sender` to this contract. + * @inheritdoc TokenRouter + */ + function _transferFromSender(uint256 _amount) internal 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"); } } diff --git a/contracts/HypERC721.sol b/contracts/HypERC721.sol index 8a75b2e7a..2810bc343 100644 --- a/contracts/HypERC721.sol +++ b/contracts/HypERC721.sol @@ -6,7 +6,7 @@ import {TokenRouter} from "./libs/TokenRouter.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 */ 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"); _burn(_tokenId); + return bytes(""); // no metadata } - // called by `TokenRouter.handle` - function _transferTo(address _recipient, uint256 _tokenId) - internal - override - { + /** + * @dev Mints `_tokenId` to `_recipient`. + * @inheritdoc TokenRouter + */ + function _transferTo( + address _recipient, + uint256 _tokenId, + bytes calldata // no metadata + ) internal virtual override { _mint(_recipient, _tokenId); } } diff --git a/contracts/HypERC721Collateral.sol b/contracts/HypERC721Collateral.sol index d50ee3c5c..0580ae8a5 100644 --- a/contracts/HypERC721Collateral.sol +++ b/contracts/HypERC721Collateral.sol @@ -6,16 +6,22 @@ import {TokenRouter} from "./libs/TokenRouter.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 */ contract HypERC721Collateral is TokenRouter { - IERC721 public immutable wrappedToken; + address public immutable wrappedToken; 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( address _mailbox, address _interchainGasPaymaster, @@ -28,14 +34,29 @@ contract HypERC721Collateral is TokenRouter { ); } - function _transferFromSender(uint256 _amount) internal override { - wrappedToken.transferFrom(msg.sender, address(this), _amount); - } - - function _transferTo(address _recipient, uint256 _amount) + /** + * @dev Transfers `_tokenId` of `wrappedToken` from `msg.sender` to this contract. + * @inheritdoc TokenRouter + */ + function _transferFromSender(uint256 _tokenId) internal + virtual 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); } } diff --git a/contracts/extensions/HypERC721URICollateral.sol b/contracts/extensions/HypERC721URICollateral.sol new file mode 100644 index 000000000..c320cbc03 --- /dev/null +++ b/contracts/extensions/HypERC721URICollateral.sol @@ -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)); + } +} diff --git a/contracts/extensions/HypERC721URIStorage.sol b/contracts/extensions/HypERC721URIStorage.sol new file mode 100644 index 000000000..b568b490a --- /dev/null +++ b/contracts/extensions/HypERC721URIStorage.sol @@ -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); + } +} diff --git a/contracts/libs/Message.sol b/contracts/libs/Message.sol index 444fab583..887188a34 100644 --- a/contracts/libs/Message.sol +++ b/contracts/libs/Message.sol @@ -2,12 +2,12 @@ pragma solidity >=0.8.0; library Message { - function format(bytes32 _recipient, uint256 _amount) - internal - pure - returns (bytes memory) - { - return abi.encodePacked(_recipient, _amount); + 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) { @@ -22,4 +22,12 @@ library Message { 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:]; + } } diff --git a/contracts/libs/TokenRouter.sol b/contracts/libs/TokenRouter.sol index 39cc7ec66..0fae6166b 100644 --- a/contracts/libs/TokenRouter.sol +++ b/contracts/libs/TokenRouter.sol @@ -6,9 +6,8 @@ import {TypeCasts} from "@hyperlane-xyz/core/contracts/libs/TypeCasts.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 - * @dev Supply on each chain is not constant but the aggregate supply across all chains is. */ abstract contract TokenRouter is Router { 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. - * @dev Burns `_amount` of tokens from `msg.sender` on the origin chain and dispatches - * message to the `destination` chain to mint `_amount` of tokens to `recipient`. + * @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 _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( uint32 _destination, bytes32 _recipient, - uint256 _amount + uint256 _amountOrId ) external payable { - _transferFromSender(_amount); + bytes memory metadata = _transferFromSender(_amountOrId); _dispatchWithGas( _destination, - Message.format(_recipient, _amount), + Message.format(_recipient, _amountOrId, metadata), 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. @@ -76,9 +82,19 @@ abstract contract TokenRouter is Router { ) internal override { bytes32 recipient = _message.recipient(); uint256 amount = _message.amount(); - _transferTo(recipient.bytes32ToAddress(), amount); + bytes calldata metadata = _message.metadata(); + _transferTo(recipient.bytes32ToAddress(), amount, metadata); 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; } diff --git a/contracts/test/ERC721Test.sol b/contracts/test/ERC721Test.sol index 269af1967..1de55441e 100644 --- a/contracts/test/ERC721Test.sol +++ b/contracts/test/ERC721Test.sol @@ -13,4 +13,8 @@ contract ERC721Test is ERC721 { _mint(msg.sender, i); } } + + function _baseURI() internal pure override returns (string memory) { + return "TEST-BASE-URI"; + } } diff --git a/src/config.ts b/src/config.ts index 863a924a1..86798807e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,22 +2,38 @@ import { ethers } from 'ethers'; import { RouterConfig } from '@hyperlane-xyz/sdk'; +export enum TokenType { + synthetic, + syntheticUri, + collateral, + collateralUri, +} + export type SyntheticConfig = { - type: "SYNTHETIC"; + type: TokenType.synthetic | TokenType.syntheticUri; name: string; symbol: string; totalSupply: ethers.BigNumberish; }; export type CollateralConfig = { - type: "COLLATERAL"; + 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 === "COLLATERAL"; -} +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; diff --git a/src/contracts.ts b/src/contracts.ts index e74610fad..ddfe92766 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -5,9 +5,10 @@ import { HypERC20Collateral, HypERC721, HypERC721Collateral, + HypERC721URICollateral, } from './types'; export type HypERC20Contracts = RouterContracts; export type HypERC721Contracts = RouterContracts< - HypERC721 | HypERC721Collateral + HypERC721 | HypERC721Collateral | HypERC721URICollateral >; diff --git a/src/deploy.ts b/src/deploy.ts index 4e0bd5347..fb04bbde4 100644 --- a/src/deploy.ts +++ b/src/deploy.ts @@ -11,12 +11,13 @@ import { HypERC721Config, isCollateralConfig, HypERC721CollateralConfig, + isUriConfig, } from './config'; import { HypERC20Contracts, HypERC721Contracts, } 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< Chain extends ChainName // inferred from configured chains passed to constructor @@ -85,8 +86,8 @@ export class HypERC721Deployer< if (isCollateralConfig(config)) { const router = await this.deployContractFromFactory( chain, - new HypERC721Collateral__factory(), - 'HypERC721Collateral', + isUriConfig(config) ? new HypERC721URICollateral__factory() : new HypERC721Collateral__factory(), + `HypERC721${isUriConfig(config) ? 'URI' : ''}Collateral`, [config.token], ); await connection.handleTx( @@ -100,8 +101,8 @@ export class HypERC721Deployer< } else { const router = await this.deployContractFromFactory( chain, - new HypERC721__factory(), - 'HypERC721', + isUriConfig(config) ? new HypERC721URIStorage__factory() : new HypERC721__factory(), + `HypERC721${isUriConfig(config) ? 'URIStorage' : ''}`, [], ); await connection.handleTx(router.initialize( diff --git a/test/erc20.test.ts b/test/erc20.test.ts index be21ac838..20781fdaf 100644 --- a/test/erc20.test.ts +++ b/test/erc20.test.ts @@ -18,6 +18,7 @@ import { HypERC20CollateralConfig, HypERC20Config, SyntheticConfig, + TokenType, } from '../src/config'; import { HypERC20Contracts } from '../src/contracts'; import { HypERC20Deployer } from '../src/deploy'; @@ -38,7 +39,7 @@ const amount = 10; const testInterchainGasPayment = 123456789; const tokenConfig: SyntheticConfig = { - type: 'SYNTHETIC', + type: TokenType.synthetic, name: 'HypERC20', symbol: 'HYP', totalSupply, @@ -80,7 +81,7 @@ for (const withCollateral of [true, false]) { ); configWithTokenInfo.test1 = { ...configWithTokenInfo.test1, - type: 'COLLATERAL', + type: TokenType.collateral, token: erc20.address, }; } @@ -123,18 +124,16 @@ for (const withCollateral of [true, false]) { await expectBalance(remote, owner, totalSupply); }); - it('should allow for local transfers', async () => { - // do not test underlying ERC20 collateral functionality - if (withCollateral) { - return; - } - - 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); - }); + // 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( diff --git a/test/erc721.test.ts b/test/erc721.test.ts index 45761833d..151d77b88 100644 --- a/test/erc721.test.ts +++ b/test/erc721.test.ts @@ -17,7 +17,8 @@ import { utils } from '@hyperlane-xyz/utils'; import { HypERC721CollateralConfig, HypERC721Config, - TokenConfig, + SyntheticConfig, + TokenType, } from '../src/config'; import { HypERC721Contracts } from '../src/contracts'; import { HypERC721Deployer } from '../src/deploy'; @@ -27,6 +28,8 @@ import { ERC721__factory, HypERC721, HypERC721Collateral, + HypERC721URICollateral, + HypERC721URIStorage, } from '../src/types'; const localChain = 'test1'; @@ -40,216 +43,238 @@ const tokenId3 = 30; const tokenId4 = 40; const testInterchainGasPayment = 123456789; -const tokenConfig: TokenConfig = { - type: 'SYNTHETIC', - name: 'HypERC721', - symbol: 'HYP', - totalSupply, -}; +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, - }, -}; + 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; + let contracts: Record; + let local: HypERC721 | HypERC721Collateral | HypERC721URICollateral; + let remote: HypERC721 | HypERC721Collateral | HypERC721URIStorage; -for (const withCollateral of [true, false]) { - describe(`HypERC721${withCollateral ? 'Collateral' : ''}`, async () => { - let owner: SignerWithAddress; - let recipient: SignerWithAddress; - let core: TestCoreApp; - let deployer: HypERC721Deployer; - let contracts: Record; - let local: HypERC721 | HypERC721Collateral; - let remote: HypERC721 | HypERC721Collateral; + beforeEach(async () => { + [owner, recipient] = await ethers.getSigners(); + const multiProvider = getTestMultiProvider(owner); - 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, + })); - 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, + }; + } - let erc721: ERC721 | undefined; - if (withCollateral) { - erc721 = await new ERC721Test__factory(owner).deploy( - tokenConfig.name, - tokenConfig.symbol, - tokenConfig.totalSupply, + deployer = new HypERC721Deployer( + multiProvider, + configWithTokenInfo, + core, ); - configWithTokenInfo.test1 = { - ...configWithTokenInfo.test1, - type: '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); - } + 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 () => { - const initializeTx = withCollateral - ? (local as HypERC721Collateral).initialize( - ethers.constants.AddressZero, - ethers.constants.AddressZero, - ethers.constants.AddressZero, - ) - : (local as HypERC721).initialize( - ethers.constants.AddressZero, - ethers.constants.AddressZero, - ethers.constants.AddressZero, - 0, - '', - '', - ); - await expect(initializeTx).to.be.revertedWith( - 'Initializable: contract is already initialized', - ); - }); + it('should not be initializable again', async () => { + const initializeTx = withCollateral + ? (local as HypERC721Collateral).initialize( + ethers.constants.AddressZero, + ethers.constants.AddressZero, + ethers.constants.AddressZero, + ) + : (local as HypERC721).initialize( + ethers.constants.AddressZero, + 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); - }); + 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); + }); - it('should allow for local transfers', async () => { // 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) { - await expect( - (local as HypERC721).transferFrom( + 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'); - } - await expect( - local.transferRemote( + }); + + it('should allow for remote transfers', async () => { + await local.transferRemote( remoteDomain, utils.addressToBytes32(recipient.address), - invalidTokenId, - ), - ).to.be.revertedWith('ERC721: invalid token ID'); - }); + tokenId2, + ); - 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 expectBalance(local, recipient, 0); - await expectBalance(local, owner, totalSupply - 1); - await expectBalance(remote, recipient, 0); - await expectBalance(remote, owner, 0); + await core.processMessages(); - 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); - 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(''); - it('should prevent remote transfer of unowned id', async () => { - const revertReason = withCollateral - ? 'ERC721: transfer from incorrect owner' - : '!owner'; - await expect( - local - .connect(recipient) - .transferRemote( + await local.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'); - }); + await expect(remoteUri.tokenURI(tokenId2)).to.be.revertedWith(''); - 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); + 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('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 (