Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 5 additions & 14 deletions contracts/crosschain/ERC7786Recipient.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
pragma solidity ^0.8.20;

import {IERC7786Recipient} from "../interfaces/draft-IERC7786.sol";
import {BitMaps} from "../utils/structs/BitMaps.sol";

/**
* @dev Base implementation of an ERC-7786 compliant cross-chain message receiver.
Expand All @@ -18,16 +17,14 @@ import {BitMaps} from "../utils/structs/BitMaps.sol";
*
* * {_processMessage}, the internal function that will be called with any message that has been validated.
*
* This contract implements replay protection, meaning that if two messages are received from the same gateway with the
* same `receiveId`, then the second one will NOT be executed, regardless of the result of {_isAuthorizedGateway}.
* ERC-7786 requires the gateway to ensure messages are not delivered more than once. Therefore, we don't need to keep
* track of the processed receiveId.
*
* @custom:stateless
*/
abstract contract ERC7786Recipient is IERC7786Recipient {
using BitMaps for BitMaps.BitMap;

mapping(address gateway => BitMaps.BitMap) private _received;

/// @dev Error thrown if the gateway is not authorized to send messages to this contract on behalf of the sender.
error ERC7786RecipientUnauthorizedGateway(address gateway, bytes sender);
error ERC7786RecipientMessageAlreadyProcessed(address gateway, bytes32 receiveId);

/// @inheritdoc IERC7786Recipient
function receiveMessage(
Expand All @@ -40,12 +37,6 @@ abstract contract ERC7786Recipient is IERC7786Recipient {
revert ERC7786RecipientUnauthorizedGateway(msg.sender, sender);
}

// Prevent duplicate execution
if (_received[msg.sender].get(uint256(receiveId))) {
revert ERC7786RecipientMessageAlreadyProcessed(msg.sender, receiveId);
}
_received[msg.sender].set(uint256(receiveId));

_processMessage(msg.sender, receiveId, sender, payload);

return IERC7786Recipient.receiveMessage.selector;
Expand Down
19 changes: 0 additions & 19 deletions test/crosschain/BridgeERC20.behavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,25 +104,6 @@ function shouldBehaveLikeBridgeERC20({ chainAIsCustodial = false, chainBIsCustod
.to.be.revertedWithCustomError(this.bridgeA, 'ERC7786RecipientUnauthorizedGateway')
.withArgs(this.gateway, this.chain.toErc7930(invalid));
});

it('cannot replay message', async function () {
const [from, to] = this.accounts;

const id = ethers.ZeroHash;
const payload = this.encodePayload(from, to, amount);

// first time works
await expect(
this.bridgeA.connect(this.gatewayAsEOA).receiveMessage(id, this.chain.toErc7930(this.bridgeB), payload),
).to.emit(this.bridgeA, 'CrosschainFungibleTransferReceived');

// second time fails
await expect(
this.bridgeA.connect(this.gatewayAsEOA).receiveMessage(id, this.chain.toErc7930(this.bridgeB), payload),
)
.to.be.revertedWithCustomError(this.bridgeA, 'ERC7786RecipientMessageAlreadyProcessed')
.withArgs(this.gateway, id);
});
});

describe('reconfiguration', function () {
Expand Down
20 changes: 1 addition & 19 deletions test/crosschain/ERC7786Recipient.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');

const { getLocalChain } = require('../helpers/chains');
const { impersonate } = require('../helpers/account');
const { generators } = require('../helpers/random');

const value = 42n;
Expand Down Expand Up @@ -36,7 +35,7 @@ describe('ERC7786Recipient', function () {
.withArgs(this.gateway, ethers.toBeHex(1n, 32n), this.toErc7930(this.sender), payload, value);
});

it('receive multiple similar messages (with different receiveIds)', async function () {
it('receive multiple similar messages', async function () {
for (let i = 1n; i < 5n; ++i) {
await expect(
this.gateway.connect(this.sender).sendMessage(this.toErc7930(this.receiver), payload, attributes, { value }),
Expand All @@ -46,23 +45,6 @@ describe('ERC7786Recipient', function () {
}
});

it('multiple use of the same receiveId', async function () {
const gatewayAsEOA = await impersonate(this.gateway.target);
const receiveId = ethers.toBeHex(1n, 32n);

await expect(
this.receiver.connect(gatewayAsEOA).receiveMessage(receiveId, this.toErc7930(this.sender), payload, { value }),
)
.to.emit(this.receiver, 'MessageReceived')
.withArgs(this.gateway, receiveId, this.toErc7930(this.sender), payload, value);

await expect(
this.receiver.connect(gatewayAsEOA).receiveMessage(receiveId, this.toErc7930(this.sender), payload, { value }),
)
.to.be.revertedWithCustomError(this.receiver, 'ERC7786RecipientMessageAlreadyProcessed')
.withArgs(this.gateway, receiveId);
});

it('unauthorized call', async function () {
await expect(
this.receiver.connect(this.notAGateway).receiveMessage(ethers.ZeroHash, this.toErc7930(this.sender), payload),
Expand Down
Loading