From ba60d21b8558113011e6a1b7e3748329c0d5def8 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 17 Jul 2026 15:54:32 +0200 Subject: [PATCH 1/2] Record all gateways in ERC7786OpenBridge outbox Fill the outbox with an entry for every gateway (including those that return a zero id) instead of leaving gaps, so OutboxDetails and the derived sendId reflect the complete set of gateways the message was posted to. --- contracts/crosschain/ERC7786OpenBridge.sol | 8 +- .../mocks/crosschain/ERC7786GatewayMock.sol | 10 ++- test/crosschain/ERC7786OpenBridge.test.js | 80 +++++++++++++------ 3 files changed, 67 insertions(+), 31 deletions(-) diff --git a/contracts/crosschain/ERC7786OpenBridge.sol b/contracts/crosschain/ERC7786OpenBridge.sol index b03f080b..fc4c99d1 100644 --- a/contracts/crosschain/ERC7786OpenBridge.sol +++ b/contracts/crosschain/ERC7786OpenBridge.sol @@ -113,11 +113,9 @@ contract ERC7786OpenBridge is IERC7786GatewaySource, IERC7786Recipient, Ownable, address gateway = _gateways.at(i); // send message bytes32 id = IERC7786GatewaySource(gateway).sendMessage(bridge, wrappedPayload, attributes); - // if ID, track it - if (id != bytes32(0)) { - outbox[i] = Outbox(gateway, id); - needsId = true; - } + // fill outbox + outbox[i] = Outbox(gateway, id); + needsId = needsId || id != bytes32(0); } if (needsId) { diff --git a/contracts/mocks/crosschain/ERC7786GatewayMock.sol b/contracts/mocks/crosschain/ERC7786GatewayMock.sol index 4c62ead4..dad742e8 100644 --- a/contracts/mocks/crosschain/ERC7786GatewayMock.sol +++ b/contracts/mocks/crosschain/ERC7786GatewayMock.sol @@ -10,6 +10,12 @@ contract ERC7786GatewayMock is IERC7786GatewaySource { using BitMaps for BitMaps.BitMap; using InteroperableAddress for *; + bytes32 public sendId; + + function setSendId(bytes32 _sendId) public { + sendId = _sendId; + } + function supportsAttribute(bytes4 /*selector*/) public pure returns (bool) { return false; } @@ -33,7 +39,7 @@ contract ERC7786GatewayMock is IERC7786GatewaySource { "Receiver error" ); - emit MessageSent(0, sender, recipient, payload, 0, attributes); - return 0; + emit MessageSent(sendId, sender, recipient, payload, 0, attributes); + return sendId; } } diff --git a/test/crosschain/ERC7786OpenBridge.test.js b/test/crosschain/ERC7786OpenBridge.test.js index 6c4d2f16..c4f32981 100644 --- a/test/crosschain/ERC7786OpenBridge.test.js +++ b/test/crosschain/ERC7786OpenBridge.test.js @@ -2,8 +2,7 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); - -const AxelarHelper = require('./axelar/AxelarHelper'); +const { getLocalChain } = require('@openzeppelin/contracts/test/helpers/chains'); const getAddress = account => ethers.getAddress(account.target ?? account.address ?? account); @@ -13,24 +12,15 @@ const M = 5; async function fixture() { const [owner, sender, ...accounts] = await ethers.getSigners(); + const chain = await getLocalChain(); const protocoles = await Promise.all( Array(M) .fill() - .map(() => AxelarHelper.deploy(owner)), + .map(() => ethers.deployContract('ERC7786GatewayMock')), ); - const { chain } = protocoles.at(0); - - const bridgeA = await ethers.deployContract('ERC7786OpenBridge', [ - owner, - protocoles.map(({ gatewayA }) => gatewayA), - N, - ]); - const bridgeB = await ethers.deployContract('ERC7786OpenBridge', [ - owner, - protocoles.map(({ gatewayB }) => gatewayB), - N, - ]); + const bridgeA = await ethers.deployContract('ERC7786OpenBridge', [owner, protocoles, N]); + const bridgeB = await ethers.deployContract('ERC7786OpenBridge', [owner, protocoles, N]); await bridgeA.registerRemoteBridge(chain.toErc7930(bridgeB)); await bridgeB.registerRemoteBridge(chain.toErc7930(bridgeA)); @@ -43,17 +33,13 @@ describe('ERC7786OpenBridge', function () { }); it('initial setup', async function () { - await expect(this.bridgeA.getGateways()).to.eventually.deep.equal( - this.protocoles.map(({ gatewayA }) => getAddress(gatewayA)), - ); + await expect(this.bridgeA.getGateways()).to.eventually.deep.equal(this.protocoles.map(getAddress)); await expect(this.bridgeA.getThreshold()).to.eventually.equal(N); await expect(this.bridgeA.getRemoteBridge(this.chain.erc7930)).to.eventually.equal( this.chain.toErc7930(this.bridgeB), ); - await expect(this.bridgeB.getGateways()).to.eventually.deep.equal( - this.protocoles.map(({ gatewayB }) => getAddress(gatewayB)), - ); + await expect(this.bridgeB.getGateways()).to.eventually.deep.equal(this.protocoles.map(getAddress)); await expect(this.bridgeB.getThreshold()).to.eventually.equal(N); await expect(this.bridgeB.getRemoteBridge(this.chain.erc7930)).to.eventually.equal( this.chain.toErc7930(this.bridgeA), @@ -136,9 +122,9 @@ describe('ERC7786OpenBridge', function () { ); // MessagePosted to all gateways on the A side and received from all gateways on the B side - for (const { gatewayA, gatewayB } of this.protocoles) { + for (const gateway of this.protocoles) { await expect(txPromise) - .to.emit(gatewayA, 'MessageSent') + .to.emit(gateway, 'MessageSent') .withArgs( ethers.ZeroHash, this.chain.toErc7930(this.bridgeA), @@ -148,7 +134,7 @@ describe('ERC7786OpenBridge', function () { anyValue, ) .to.emit(this.bridgeB, 'Received') - .withArgs(resultId, gatewayB); + .withArgs(resultId, gateway); } if (this.outcome) { @@ -175,4 +161,50 @@ describe('ERC7786OpenBridge', function () { } }); }); + + describe('outbox tracking', function () { + it('records every gateway in the outbox and emits OutboxDetails when a gateway returns a non-zero id', async function () { + const destination = await ethers.deployContract('$ERC7786RecipientMock', [this.bridgeB]); + const payload = ethers.randomBytes(128); + const attributes = []; + + const outbox = this.protocoles.map(gateway => [gateway.target, ethers.ZeroHash]); + const id1 = (outbox[0][1] = ethers.hexlify(ethers.randomBytes(32))); + const id2 = (outbox[2][1] = ethers.hexlify(ethers.randomBytes(32))); + + this.protocoles[0].setSendId(id1); + this.protocoles[2].setSendId(id2); + + // The outbox lists all gateways, including the one that returned a zero id (with its address, not address(0)) + const sendId = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(['(address,bytes32)[]'], [outbox])); + + await expect( + this.bridgeA.connect(this.sender).sendMessage(this.chain.toErc7930(destination), payload, attributes), + ) + .to.emit(this.bridgeA, 'MessageSent') + .withArgs(sendId, this.chain.toErc7930(this.sender), this.chain.toErc7930(destination), payload, 0n, []) + .to.emit(this.bridgeA, 'OutboxDetails') + .withArgs(sendId, outbox); + }); + + it('does not emit OutboxDetails and keeps a zero sendId when all gateways return zero', async function () { + const destination = await ethers.deployContract('$ERC7786RecipientMock', [this.bridgeB]); + const payload = ethers.randomBytes(128); + const attributes = []; + + await expect( + this.bridgeA.connect(this.sender).sendMessage(this.chain.toErc7930(destination), payload, attributes), + ) + .to.emit(this.bridgeA, 'MessageSent') + .withArgs( + ethers.ZeroHash, + this.chain.toErc7930(this.sender), + this.chain.toErc7930(destination), + payload, + 0n, + [], + ) + .to.not.emit(this.bridgeA, 'OutboxDetails'); + }); + }); }); From 874bb551c0f82f980514b55b0dd312c8c734658d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 17 Jul 2026 16:39:22 +0200 Subject: [PATCH 2/2] Update test/crosschain/ERC7786OpenBridge.test.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- test/crosschain/ERC7786OpenBridge.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/crosschain/ERC7786OpenBridge.test.js b/test/crosschain/ERC7786OpenBridge.test.js index c4f32981..b922bcd1 100644 --- a/test/crosschain/ERC7786OpenBridge.test.js +++ b/test/crosschain/ERC7786OpenBridge.test.js @@ -172,8 +172,8 @@ describe('ERC7786OpenBridge', function () { const id1 = (outbox[0][1] = ethers.hexlify(ethers.randomBytes(32))); const id2 = (outbox[2][1] = ethers.hexlify(ethers.randomBytes(32))); - this.protocoles[0].setSendId(id1); - this.protocoles[2].setSendId(id2); + await this.protocoles[0].setSendId(id1); + await this.protocoles[2].setSendId(id2); // The outbox lists all gateways, including the one that returned a zero id (with its address, not address(0)) const sendId = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(['(address,bytes32)[]'], [outbox]));