Skip to content

Commit 2a8e5a9

Browse files
committed
add tests
1 parent 44ea7b7 commit 2a8e5a9

File tree

3 files changed

+165
-16
lines changed

3 files changed

+165
-16
lines changed

contracts/crosschain/ERC7802Bridge.sol

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,12 @@ contract ERC7802Bridge is IERC7786Receiver {
3131

3232
event Sent(address token, address from, bytes to, uint256 amount);
3333
event Received(address token, bytes from, address to, uint256 amount);
34-
35-
// event GatewayRegistered(address indexed gateway, bytes chain);
36-
// event RemoteBridgeRegistered(address indexed token, bytes chain);
34+
event NewBridge(bytes32 indexed bridgeId, address indexed token);
35+
event NewBridgeLink(bytes32 indexed bridgeId, address gateway, bytes remote);
3736

3837
error ERC7802BridgeInvalidBidgeId(bytes32 bridgeId);
3938
error ERC7802BridgeMissingGateway(bytes32 bridgeId, bytes chain);
4039
error ERC7802BridgeMissingRemote(bytes32 bridgeId, bytes chain);
41-
// error ERC7802BridgeImproperChainIdentifier(bytes chain);
4240
error ERC7802BridgeDuplicate();
4341
error ERC7802BridgeInvalidGateway();
4442
error ERC7802BridgeInvalidSender();
@@ -74,6 +72,8 @@ contract ERC7802Bridge is IERC7786Receiver {
7472
bytes32[] memory ids = new bytes32[](foreign.length + 1);
7573
bytes32[] memory links = new bytes32[](foreign.length);
7674
for (uint256 i = 0; i < foreign.length; ++i) {
75+
require(foreign[i].gateway != address(0));
76+
require(foreign[i].remote.length > 0);
7777
ids[i] = foreign[i].id;
7878
links[i] = keccak256(
7979
abi.encode(InteroperableAddress.formatEvmV1(block.chainid, foreign[i].gateway), foreign[i].remote)
@@ -87,18 +87,23 @@ contract ERC7802Bridge is IERC7786Receiver {
8787
)
8888
);
8989

90-
bytes32 bridgeId = keccak256(abi.encode(Arrays.sort(ids)));
90+
bytes32 bridgeId = keccak256(abi.encodePacked(Arrays.sort(ids)));
9191

9292
// Should we check for collision. I don't think that is necessary
9393
BridgeMetadata storage details = _bridges[bridgeId];
9494
details.token = token;
9595
details.isCustodial = isCustodial;
9696

97+
emit NewBridge(bridgeId, token);
98+
9799
for (uint256 i = 0; i < foreign.length; ++i) {
98100
(bytes2 chainType, bytes memory chainReference, ) = foreign[i].remote.parseV1();
99101
bytes memory chain = InteroperableAddress.formatV1(chainType, chainReference, "");
102+
require(details.gateway[chain] == address(0));
100103
details.gateway[chain] = foreign[i].gateway;
101104
details.remote[chain] = foreign[i].remote;
105+
106+
emit NewBridgeLink(bridgeId, foreign[i].gateway, foreign[i].remote);
102107
}
103108

104109
return bridgeId;

contracts/mocks/token/ERC20BridgeableMock.sol

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,29 @@
22

33
pragma solidity ^0.8.20;
44

5-
import {ERC20, ERC20Bridgeable} from "../../token/ERC20/extensions/ERC20Bridgeable.sol";
5+
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
6+
import {ERC20Bridgeable} from "../../token/ERC20/extensions/ERC20Bridgeable.sol";
67

7-
abstract contract ERC20BridgeableMock is ERC20Bridgeable {
8-
address bridge;
8+
abstract contract ERC20BridgeableMock is ERC20Bridgeable, AccessControl {
9+
bytes32 public constant BRIDGE_ROLE = keccak256("BRIDGE");
10+
11+
event OnlyTokenBridgeFnCalled(address caller);
912

1013
error OnlyTokenBridge();
1114

12-
event OnlyTokenBridgeFnCalled(address caller);
15+
constructor(address admin) {
16+
_grantRole(DEFAULT_ADMIN_ROLE, admin);
17+
}
1318

14-
constructor(address bridge_) {
15-
bridge = bridge_;
19+
function supportsInterface(
20+
bytes4 interfaceId
21+
) public view virtual override(AccessControl, ERC20Bridgeable) returns (bool) {
22+
return super.supportsInterface(interfaceId);
1623
}
1724

1825
function onlyTokenBridgeFn() external onlyTokenBridge {
1926
emit OnlyTokenBridgeFnCalled(msg.sender);
2027
}
2128

22-
function _checkTokenBridge(address sender) internal view override {
23-
if (sender != bridge) {
24-
revert OnlyTokenBridge();
25-
}
26-
}
29+
function _checkTokenBridge(address sender) internal view override onlyRole(BRIDGE_ROLE) {}
2730
}

test/crosschain/ERC7802Bridge.test.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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

Comments
 (0)