Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/silent-zebras-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`ERC7786Recipient`: Generic ERC-7786 cross-chain message recipient contract.
69 changes: 69 additions & 0 deletions contracts/crosschain/ERC7786Recipient.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

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.
*
* This abstract contract exposes the `receiveMessage` function that is used for communication with (one or multiple)
* destination gateways. This contract leaves two functions unimplemented:
*
* * {_isAuthorizedGateway}, an internal getter used to verify whether an address is recognised by the contract as a
* valid ERC-7786 destination gateway. One or multiple gateway can be supported. Note that any malicious address for
* which this function returns true would be able to impersonate any account on any other chain sending any message.
*
* * {_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}.
*/
abstract contract ERC7786Recipient is IERC7786Recipient {
using BitMaps for BitMaps.BitMap;

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

error ERC7786RecipientUnauthorizedGateway(address gateway, bytes sender);
error ERC7786RecipientMessageAlreadyProcessed(address gateway, bytes32 receiveId);

/// @inheritdoc IERC7786Recipient
function receiveMessage(
bytes32 receiveId,
bytes calldata sender, // Binary Interoperable Address
bytes calldata payload
) external payable returns (bytes4) {
// Check authorization
if (!_isAuthorizedGateway(msg.sender, sender)) {
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;
}

/**
* @dev Virtual getter that returns whether an address is a valid ERC-7786 gateway for a given sender.
*
* The `sender` parameter is an interoperable address that include the source chain. The chain part can be
* extracted using the {InteroperableAddress} library to selectively authorize gateways based on the origin chain
* of a message.
*/
function _isAuthorizedGateway(address gateway, bytes calldata sender) internal view virtual returns (bool);

/// @dev Virtual function that should contain the logic to execute when a cross-chain message is received.
function _processMessage(
address gateway,
bytes32 receiveId,
bytes calldata sender,
bytes calldata payload
) internal virtual;
}
12 changes: 12 additions & 0 deletions contracts/crosschain/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
= Cross chain interoperability

[.readme-notice]
NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/crosschain

This directory contains contracts for sending and receiving cross chain messages that follows the ERC-7786 standard.

- {ERC7786Recipient}: generic ERC-7786 crosschain contract that receives messages from a trusted gateway

== Helpers

{{ERC7786Recipient}}
7 changes: 5 additions & 2 deletions contracts/interfaces/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ are useful to interact with third party contracts that implement them.
- {IERC6909TokenSupply}
- {IERC7674}
- {IERC7751}
- {IERC7786}
- {IERC7786GatewaySource}
- {IERC7786Recipient}
- {IERC7802}

== Detailed ABI
Expand Down Expand Up @@ -103,6 +104,8 @@ are useful to interact with third party contracts that implement them.

{{IERC7751}}

{{IERC7786}}
{{IERC7786GatewaySource}}

{{IERC7786Recipient}}

{{IERC7802}}
4 changes: 2 additions & 2 deletions contracts/interfaces/draft-IERC7786.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface IERC7786GatewaySource {
event MessageSent(
bytes32 indexed sendId,
bytes sender, // Binary Interoperable Address
bytes receiver, // Binary Interoperable Address
bytes recipient, // Binary Interoperable Address
bytes payload,
uint256 value,
bytes[] attributes
Expand Down Expand Up @@ -49,7 +49,7 @@ interface IERC7786GatewaySource {
*
* See ERC-7786 for more details
*/
interface IERC7786Receiver {
interface IERC7786Recipient {
/**
* @dev Endpoint for receiving cross-chain message.
*
Expand Down
56 changes: 56 additions & 0 deletions contracts/mocks/crosschain/ERC7786GatewayMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

import {IERC7786GatewaySource, IERC7786Recipient} from "../../interfaces/draft-IERC7786.sol";
import {InteroperableAddress} from "../../utils/draft-InteroperableAddress.sol";

abstract contract ERC7786GatewayMock is IERC7786GatewaySource {
using InteroperableAddress for bytes;

error InvalidDestination();
error ReceiverError();

uint256 private _lastReceiveId;

/// @inheritdoc IERC7786GatewaySource
function supportsAttribute(bytes4 /*selector*/) public view virtual returns (bool) {
return false;
}

/// @inheritdoc IERC7786GatewaySource
function sendMessage(
bytes calldata recipient,
bytes calldata payload,
bytes[] calldata attributes
) public payable virtual returns (bytes32 sendId) {
// attributes are not supported
if (attributes.length > 0) {
revert UnsupportedAttribute(bytes4(attributes[0]));
}

// parse recipient
(bool success, uint256 chainid, address target) = recipient.tryParseEvmV1Calldata();
require(success && chainid == block.chainid, InvalidDestination());

// perform call
bytes4 magic = IERC7786Recipient(target).receiveMessage{value: msg.value}(
bytes32(++_lastReceiveId),
InteroperableAddress.formatEvmV1(block.chainid, msg.sender),
payload
);
require(magic == IERC7786Recipient.receiveMessage.selector, ReceiverError());

// emit standard event
emit MessageSent(
bytes32(0),
InteroperableAddress.formatEvmV1(block.chainid, msg.sender),
recipient,
payload,
msg.value,
attributes
);

return 0;
}
}
31 changes: 31 additions & 0 deletions contracts/mocks/crosschain/ERC7786RecipientMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

import {ERC7786Recipient} from "../../crosschain/ERC7786Recipient.sol";

contract ERC7786RecipientMock is ERC7786Recipient {
address private immutable _gateway;

event MessageReceived(address gateway, bytes32 receiveId, bytes sender, bytes payload, uint256 value);

constructor(address gateway_) {
_gateway = gateway_;
}

function _isAuthorizedGateway(
address gateway,
bytes calldata /*sender*/
) internal view virtual override returns (bool) {
return gateway == _gateway;
}

function _processMessage(
address gateway,
bytes32 receiveId,
bytes calldata sender,
bytes calldata payload
) internal virtual override {
emit MessageReceived(gateway, receiveId, sender, payload, msg.value);
}
}
73 changes: 73 additions & 0 deletions test/crosschain/ERC7786Recipient.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const { ethers } = require('hardhat');
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;
const payload = generators.hexBytes(128);
const attributes = [];

async function fixture() {
const [sender, notAGateway] = await ethers.getSigners();
const { toErc7930 } = await getLocalChain();

const gateway = await ethers.deployContract('$ERC7786GatewayMock');
const receiver = await ethers.deployContract('$ERC7786RecipientMock', [gateway]);

return { sender, notAGateway, gateway, receiver, toErc7930 };
}

// NOTE: here we are only testing the receiver. Failures of the gateway itself (invalid attributes, ...) are out of scope.
describe('ERC7786Recipient', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});

it('receives gateway relayed messages', async function () {
await expect(
this.gateway.connect(this.sender).sendMessage(this.toErc7930(this.receiver), payload, attributes, { value }),
)
.to.emit(this.gateway, 'MessageSent')
.withArgs(ethers.ZeroHash, this.toErc7930(this.sender), this.toErc7930(this.receiver), payload, value, attributes)
.to.emit(this.receiver, 'MessageReceived')
.withArgs(this.gateway, ethers.toBeHex(1n, 32n), this.toErc7930(this.sender), payload, value);
});

it('receive multiple similar messages (with different receiveIds)', async function () {
for (let i = 1n; i < 5n; ++i) {
await expect(
this.gateway.connect(this.sender).sendMessage(this.toErc7930(this.receiver), payload, attributes, { value }),
)
.to.emit(this.receiver, 'MessageReceived')
.withArgs(this.gateway, ethers.toBeHex(i, 32n), this.toErc7930(this.sender), payload, value);
}
});

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),
)
.to.be.revertedWithCustomError(this.receiver, 'ERC7786RecipientUnauthorizedGateway')
.withArgs(this.notAGateway, this.toErc7930(this.sender));
});
});