|
|
@ -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 ( |
|
|
|