Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
---

`ERC7786Receiver`: ERC-7786 generic crosschain message receiver contract.
43 changes: 43 additions & 0 deletions contracts/crosschain/ERC7786Recipient.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

import {IERC7786Recipient} from "../interfaces/draft-IERC7786.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:
*
* * {_isKnownGateway}, 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.
*/
abstract contract ERC7786Recipient is IERC7786Recipient {
error ERC7786RecipientInvalidGateway(address gateway);

/// @inheritdoc IERC7786Recipient
function receiveMessage(
bytes32 receiveId,
bytes calldata sender, // Binary Interoperable Address
bytes calldata payload
) public payable virtual returns (bytes4) {
require(_isKnownGateway(msg.sender), ERC7786RecipientInvalidGateway(msg.sender));
_processMessage(msg.sender, receiveId, sender, payload);
return IERC7786Recipient.receiveMessage.selector;
}

/// @dev Virtual getter that returns whether an address is a valid ERC-7786 gateway.
function _isKnownGateway(address instance) 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.

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

{{IERC7786Recipient}}
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;
}
}
28 changes: 28 additions & 0 deletions contracts/mocks/crosschain/ERC7786RecipientMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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 _isKnownGateway(address instance) internal view virtual override returns (bool) {
return instance == _gateway;
}

function _processMessage(
address gateway,
bytes32 receiveId,
bytes calldata sender,
bytes calldata payload
) internal virtual override {
emit MessageReceived(gateway, receiveId, sender, payload, msg.value);
}
}
45 changes: 45 additions & 0 deletions test/crosschain/ERC7786Recipient.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');

const { getLocalChain } = require('../helpers/chains');
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('unauthorized call', async function () {
await expect(
this.receiver.connect(this.notAGateway).receiveMessage(ethers.ZeroHash, this.toErc7930(this.sender), payload),
)
.to.be.revertedWithCustomError(this.receiver, 'ERC7786RecipientInvalidGateway')
.withArgs(this.notAGateway);
});
});