|
| 1 | +const { ethers } = require('hardhat'); |
| 2 | +const { expect } = require('chai'); |
| 3 | +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); |
| 4 | + |
| 5 | +const AxelarHelper = require('./axelar/AxelarHelper'); |
| 6 | + |
| 7 | +const keccak256AbiEncode = (types, values) => ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(types, values)); |
| 8 | +const sortBytes32 = values => values.sort((a, b) => (BigInt(a) < BigInt(b) ? -1 : 1)); |
| 9 | + |
| 10 | +async function fixture() { |
| 11 | + const [admin, ...accounts] = await ethers.getSigners(); |
| 12 | + |
| 13 | + const { chain, gatewayA, gatewayB } = await AxelarHelper.deploy(admin); |
| 14 | + |
| 15 | + // On chain A, we have a "legacy" token |
| 16 | + const bridgeA = await ethers.deployContract('$ERC7802Bridge'); |
| 17 | + const tokenA = await ethers.deployContract('$ERC20', ['Token A', 'TA']); |
| 18 | + |
| 19 | + // On chain B we have a bridgeable token |
| 20 | + const bridgeB = await ethers.deployContract('$ERC7802Bridge'); |
| 21 | + const tokenB = await ethers.deployContract('$ERC20BridgeableMock', ['Token B', 'TB', admin]); |
| 22 | + |
| 23 | + // Bridge side identifiers |
| 24 | + const chainAId = keccak256AbiEncode( |
| 25 | + ['bytes', 'bytes32', 'bytes32[]'], |
| 26 | + [ |
| 27 | + chain.toErc7930(tokenA), |
| 28 | + '0x0000000000000000000000000000000000000000000000000000000000000001', // custodial |
| 29 | + sortBytes32([keccak256AbiEncode(['bytes', 'bytes'], [chain.toErc7930(gatewayA), chain.toErc7930(bridgeB)])]), |
| 30 | + ], |
| 31 | + ); |
| 32 | + |
| 33 | + const chainBId = keccak256AbiEncode( |
| 34 | + ['bytes', 'bytes32', 'bytes32[]'], |
| 35 | + [ |
| 36 | + chain.toErc7930(tokenB), |
| 37 | + '0x0000000000000000000000000000000000000000000000000000000000000000', // crosschain |
| 38 | + sortBytes32([keccak256AbiEncode(['bytes', 'bytes'], [chain.toErc7930(gatewayB), chain.toErc7930(bridgeA)])]), |
| 39 | + ], |
| 40 | + ); |
| 41 | + |
| 42 | + // Bridge global identifier |
| 43 | + const id = ethers.solidityPackedKeccak256(['bytes32[]'], [sortBytes32([chainAId, chainBId])]); |
| 44 | + |
| 45 | + // Register bridge |
| 46 | + await expect( |
| 47 | + bridgeA.createBridge( |
| 48 | + tokenA, |
| 49 | + true, // is custodial |
| 50 | + [{ id: chainBId, gateway: gatewayA, remote: chain.toErc7930(bridgeB) }], // link to B + id of B |
| 51 | + ), |
| 52 | + ) |
| 53 | + .to.emit(bridgeA, 'NewBridge') |
| 54 | + .withArgs(id, tokenA); |
| 55 | + |
| 56 | + await expect( |
| 57 | + bridgeB.createBridge( |
| 58 | + tokenB, |
| 59 | + false, // is crosschain |
| 60 | + [{ id: chainAId, gateway: gatewayB, remote: chain.toErc7930(bridgeA) }], // link to B + id of B |
| 61 | + ), |
| 62 | + ) |
| 63 | + .to.emit(bridgeB, 'NewBridge') |
| 64 | + .withArgs(id, tokenB); |
| 65 | + |
| 66 | + // Get endpoint for that bridge |
| 67 | + const endpointA = await bridgeA.getBridgeEndpoint.staticCall(id); |
| 68 | + const endpointB = await bridgeB.getBridgeEndpoint.staticCall(id); |
| 69 | + |
| 70 | + // Whitelist |
| 71 | + await tokenB.connect(admin).grantRole(ethers.id('BRIDGE'), endpointB); |
| 72 | + |
| 73 | + return { admin, accounts, chain, tokenA, tokenB, gatewayA, gatewayB, bridgeA, bridgeB, endpointA, endpointB, id }; |
| 74 | +} |
| 75 | + |
| 76 | +describe('ERC7802Bridge', function () { |
| 77 | + beforeEach(async function () { |
| 78 | + Object.assign(this, await loadFixture(fixture)); |
| 79 | + }); |
| 80 | + |
| 81 | + it('initial setup', async function () { |
| 82 | + await expect(this.bridgeA.getBridgeToken(this.id)).to.eventually.deep.equal([this.tokenA.target, true]); |
| 83 | + await expect(this.bridgeA.getBridgeGateway(this.id, this.chain.erc7930)).to.eventually.equal(this.gatewayA); |
| 84 | + await expect(this.bridgeA.getBridgeRemote(this.id, this.chain.erc7930)).to.eventually.equal( |
| 85 | + this.chain.toErc7930(this.bridgeB), |
| 86 | + ); |
| 87 | + |
| 88 | + await expect(this.bridgeB.getBridgeToken(this.id)).to.eventually.deep.equal([this.tokenB.target, false]); |
| 89 | + await expect(this.bridgeB.getBridgeGateway(this.id, this.chain.erc7930)).to.eventually.equal(this.gatewayB); |
| 90 | + await expect(this.bridgeB.getBridgeRemote(this.id, this.chain.erc7930)).to.eventually.equal( |
| 91 | + this.chain.toErc7930(this.bridgeA), |
| 92 | + ); |
| 93 | + }); |
| 94 | + |
| 95 | + it('crosschain send', async function () { |
| 96 | + const [alice, bruce, chris] = this.accounts; |
| 97 | + const amount = 100n; |
| 98 | + |
| 99 | + await this.tokenA.$_mint(alice, amount); |
| 100 | + await this.tokenA.connect(alice).approve(this.endpointA, ethers.MaxUint256); |
| 101 | + |
| 102 | + await expect(this.tokenA.balanceOf(this.bridgeA)).to.eventually.equal(0n); |
| 103 | + await expect(this.tokenB.balanceOf(this.bridgeB)).to.eventually.equal(0n); |
| 104 | + await expect(this.tokenA.balanceOf(this.endpointA)).to.eventually.equal(0n); |
| 105 | + await expect(this.tokenA.balanceOf(this.endpointB)).to.eventually.equal(0n); |
| 106 | + |
| 107 | + // Alice sends tokens from chain A to Bruce on chain B. The 7802 custodian bridge on chain A takes ownership of Alice's tokens. |
| 108 | + await expect(this.bridgeA.connect(alice).send(this.id, this.chain.toErc7930(bruce), amount, [])) |
| 109 | + .to.emit(this.tokenA, 'Transfer') |
| 110 | + .withArgs(alice, this.endpointA, amount) // endpoint on chain A takes custody of the funds |
| 111 | + .to.emit(this.bridgeA, 'Sent') |
| 112 | + .withArgs(this.tokenA, alice, this.chain.toErc7930(bruce), amount) |
| 113 | + .to.emit(this.gatewayA, 'MessageSent') |
| 114 | + .to.emit(this.bridgeB, 'Received') |
| 115 | + .withArgs(this.tokenB, this.chain.toErc7930(alice), bruce, amount) |
| 116 | + .to.emit(this.tokenB, 'Transfer') |
| 117 | + .withArgs(ethers.ZeroAddress, bruce, amount); |
| 118 | + |
| 119 | + await expect(this.tokenA.balanceOf(this.bridgeA)).to.eventually.equal(0n); |
| 120 | + await expect(this.tokenB.balanceOf(this.bridgeB)).to.eventually.equal(0n); |
| 121 | + await expect(this.tokenA.balanceOf(this.endpointA)).to.eventually.equal(amount); // custody |
| 122 | + await expect(this.tokenA.balanceOf(this.endpointB)).to.eventually.equal(0n); |
| 123 | + |
| 124 | + // Bruce sends tokens from chain B to Chris on chain B. The 7802 custodian bridge on chain A releases ownership of the tokens to Chris. |
| 125 | + await expect(this.bridgeB.connect(bruce).send(this.id, this.chain.toErc7930(chris), amount, [])) |
| 126 | + .to.emit(this.tokenB, 'Transfer') |
| 127 | + .withArgs(bruce, ethers.ZeroAddress, amount) // bridge B burns the tokens |
| 128 | + .to.emit(this.bridgeB, 'Sent') |
| 129 | + .withArgs(this.tokenB, bruce, this.chain.toErc7930(chris), amount) |
| 130 | + .to.emit(this.gatewayB, 'MessageSent') |
| 131 | + .to.emit(this.bridgeA, 'Received') |
| 132 | + .withArgs(this.tokenA, this.chain.toErc7930(bruce), chris, amount) |
| 133 | + .to.emit(this.tokenA, 'Transfer') |
| 134 | + .withArgs(this.endpointA, chris, amount); |
| 135 | + |
| 136 | + await expect(this.tokenA.balanceOf(this.bridgeA)).to.eventually.equal(0n); |
| 137 | + await expect(this.tokenB.balanceOf(this.bridgeB)).to.eventually.equal(0n); |
| 138 | + await expect(this.tokenA.balanceOf(this.endpointA)).to.eventually.equal(0n); |
| 139 | + await expect(this.tokenA.balanceOf(this.endpointB)).to.eventually.equal(0n); |
| 140 | + }); |
| 141 | +}); |
0 commit comments