Skip to content

Commit 49882a3

Browse files
Amxxernestognw
andauthored
Migrate ERC7786Recipient from community (#5904)
Co-authored-by: Ernesto García <[email protected]>
1 parent 0fc8e4b commit 49882a3

File tree

8 files changed

+253
-4
lines changed

8 files changed

+253
-4
lines changed

.changeset/silent-zebras-press.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`ERC7786Recipient`: Generic ERC-7786 cross-chain message recipient contract.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
4+
5+
import {IERC7786Recipient} from "../interfaces/draft-IERC7786.sol";
6+
import {BitMaps} from "../utils/structs/BitMaps.sol";
7+
8+
/**
9+
* @dev Base implementation of an ERC-7786 compliant cross-chain message receiver.
10+
*
11+
* This abstract contract exposes the `receiveMessage` function that is used for communication with (one or multiple)
12+
* destination gateways. This contract leaves two functions unimplemented:
13+
*
14+
* * {_isAuthorizedGateway}, an internal getter used to verify whether an address is recognised by the contract as a
15+
* valid ERC-7786 destination gateway. One or multiple gateway can be supported. Note that any malicious address for
16+
* which this function returns true would be able to impersonate any account on any other chain sending any message.
17+
*
18+
* * {_processMessage}, the internal function that will be called with any message that has been validated.
19+
*
20+
* This contract implements replay protection, meaning that if two messages are received from the same gateway with the
21+
* same `receiveId`, then the second one will NOT be executed, regardless of the result of {_isAuthorizedGateway}.
22+
*/
23+
abstract contract ERC7786Recipient is IERC7786Recipient {
24+
using BitMaps for BitMaps.BitMap;
25+
26+
mapping(address gateway => BitMaps.BitMap) private _received;
27+
28+
error ERC7786RecipientUnauthorizedGateway(address gateway, bytes sender);
29+
error ERC7786RecipientMessageAlreadyProcessed(address gateway, bytes32 receiveId);
30+
31+
/// @inheritdoc IERC7786Recipient
32+
function receiveMessage(
33+
bytes32 receiveId,
34+
bytes calldata sender, // Binary Interoperable Address
35+
bytes calldata payload
36+
) external payable returns (bytes4) {
37+
// Check authorization
38+
if (!_isAuthorizedGateway(msg.sender, sender)) {
39+
revert ERC7786RecipientUnauthorizedGateway(msg.sender, sender);
40+
}
41+
42+
// Prevent duplicate execution
43+
if (_received[msg.sender].get(uint256(receiveId))) {
44+
revert ERC7786RecipientMessageAlreadyProcessed(msg.sender, receiveId);
45+
}
46+
_received[msg.sender].set(uint256(receiveId));
47+
48+
_processMessage(msg.sender, receiveId, sender, payload);
49+
50+
return IERC7786Recipient.receiveMessage.selector;
51+
}
52+
53+
/**
54+
* @dev Virtual getter that returns whether an address is a valid ERC-7786 gateway for a given sender.
55+
*
56+
* The `sender` parameter is an interoperable address that include the source chain. The chain part can be
57+
* extracted using the {InteroperableAddress} library to selectively authorize gateways based on the origin chain
58+
* of a message.
59+
*/
60+
function _isAuthorizedGateway(address gateway, bytes calldata sender) internal view virtual returns (bool);
61+
62+
/// @dev Virtual function that should contain the logic to execute when a cross-chain message is received.
63+
function _processMessage(
64+
address gateway,
65+
bytes32 receiveId,
66+
bytes calldata sender,
67+
bytes calldata payload
68+
) internal virtual;
69+
}

contracts/crosschain/README.adoc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
= Cross chain interoperability
2+
3+
[.readme-notice]
4+
NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/crosschain
5+
6+
This directory contains contracts for sending and receiving cross chain messages that follows the ERC-7786 standard.
7+
8+
- {ERC7786Recipient}: generic ERC-7786 crosschain contract that receives messages from a trusted gateway
9+
10+
== Helpers
11+
12+
{{ERC7786Recipient}}

contracts/interfaces/README.adoc

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ are useful to interact with third party contracts that implement them.
4646
- {IERC6909TokenSupply}
4747
- {IERC7674}
4848
- {IERC7751}
49-
- {IERC7786}
49+
- {IERC7786GatewaySource}
50+
- {IERC7786Recipient}
5051
- {IERC7802}
5152

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

104105
{{IERC7751}}
105106

106-
{{IERC7786}}
107+
{{IERC7786GatewaySource}}
108+
109+
{{IERC7786Recipient}}
107110

108111
{{IERC7802}}

contracts/interfaces/draft-IERC7786.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ interface IERC7786GatewaySource {
1515
event MessageSent(
1616
bytes32 indexed sendId,
1717
bytes sender, // Binary Interoperable Address
18-
bytes receiver, // Binary Interoperable Address
18+
bytes recipient, // Binary Interoperable Address
1919
bytes payload,
2020
uint256 value,
2121
bytes[] attributes
@@ -49,7 +49,7 @@ interface IERC7786GatewaySource {
4949
*
5050
* See ERC-7786 for more details
5151
*/
52-
interface IERC7786Receiver {
52+
interface IERC7786Recipient {
5353
/**
5454
* @dev Endpoint for receiving cross-chain message.
5555
*
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
4+
5+
import {IERC7786GatewaySource, IERC7786Recipient} from "../../interfaces/draft-IERC7786.sol";
6+
import {InteroperableAddress} from "../../utils/draft-InteroperableAddress.sol";
7+
8+
abstract contract ERC7786GatewayMock is IERC7786GatewaySource {
9+
using InteroperableAddress for bytes;
10+
11+
error InvalidDestination();
12+
error ReceiverError();
13+
14+
uint256 private _lastReceiveId;
15+
16+
/// @inheritdoc IERC7786GatewaySource
17+
function supportsAttribute(bytes4 /*selector*/) public view virtual returns (bool) {
18+
return false;
19+
}
20+
21+
/// @inheritdoc IERC7786GatewaySource
22+
function sendMessage(
23+
bytes calldata recipient,
24+
bytes calldata payload,
25+
bytes[] calldata attributes
26+
) public payable virtual returns (bytes32 sendId) {
27+
// attributes are not supported
28+
if (attributes.length > 0) {
29+
revert UnsupportedAttribute(bytes4(attributes[0]));
30+
}
31+
32+
// parse recipient
33+
(bool success, uint256 chainid, address target) = recipient.tryParseEvmV1Calldata();
34+
require(success && chainid == block.chainid, InvalidDestination());
35+
36+
// perform call
37+
bytes4 magic = IERC7786Recipient(target).receiveMessage{value: msg.value}(
38+
bytes32(++_lastReceiveId),
39+
InteroperableAddress.formatEvmV1(block.chainid, msg.sender),
40+
payload
41+
);
42+
require(magic == IERC7786Recipient.receiveMessage.selector, ReceiverError());
43+
44+
// emit standard event
45+
emit MessageSent(
46+
bytes32(0),
47+
InteroperableAddress.formatEvmV1(block.chainid, msg.sender),
48+
recipient,
49+
payload,
50+
msg.value,
51+
attributes
52+
);
53+
54+
return 0;
55+
}
56+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
4+
5+
import {ERC7786Recipient} from "../../crosschain/ERC7786Recipient.sol";
6+
7+
contract ERC7786RecipientMock is ERC7786Recipient {
8+
address private immutable _gateway;
9+
10+
event MessageReceived(address gateway, bytes32 receiveId, bytes sender, bytes payload, uint256 value);
11+
12+
constructor(address gateway_) {
13+
_gateway = gateway_;
14+
}
15+
16+
function _isAuthorizedGateway(
17+
address gateway,
18+
bytes calldata /*sender*/
19+
) internal view virtual override returns (bool) {
20+
return gateway == _gateway;
21+
}
22+
23+
function _processMessage(
24+
address gateway,
25+
bytes32 receiveId,
26+
bytes calldata sender,
27+
bytes calldata payload
28+
) internal virtual override {
29+
emit MessageReceived(gateway, receiveId, sender, payload, msg.value);
30+
}
31+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
const { ethers } = require('hardhat');
2+
const { expect } = require('chai');
3+
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
4+
5+
const { getLocalChain } = require('../helpers/chains');
6+
const { impersonate } = require('../helpers/account');
7+
const { generators } = require('../helpers/random');
8+
9+
const value = 42n;
10+
const payload = generators.hexBytes(128);
11+
const attributes = [];
12+
13+
async function fixture() {
14+
const [sender, notAGateway] = await ethers.getSigners();
15+
const { toErc7930 } = await getLocalChain();
16+
17+
const gateway = await ethers.deployContract('$ERC7786GatewayMock');
18+
const receiver = await ethers.deployContract('$ERC7786RecipientMock', [gateway]);
19+
20+
return { sender, notAGateway, gateway, receiver, toErc7930 };
21+
}
22+
23+
// NOTE: here we are only testing the receiver. Failures of the gateway itself (invalid attributes, ...) are out of scope.
24+
describe('ERC7786Recipient', function () {
25+
beforeEach(async function () {
26+
Object.assign(this, await loadFixture(fixture));
27+
});
28+
29+
it('receives gateway relayed messages', async function () {
30+
await expect(
31+
this.gateway.connect(this.sender).sendMessage(this.toErc7930(this.receiver), payload, attributes, { value }),
32+
)
33+
.to.emit(this.gateway, 'MessageSent')
34+
.withArgs(ethers.ZeroHash, this.toErc7930(this.sender), this.toErc7930(this.receiver), payload, value, attributes)
35+
.to.emit(this.receiver, 'MessageReceived')
36+
.withArgs(this.gateway, ethers.toBeHex(1n, 32n), this.toErc7930(this.sender), payload, value);
37+
});
38+
39+
it('receive multiple similar messages (with different receiveIds)', async function () {
40+
for (let i = 1n; i < 5n; ++i) {
41+
await expect(
42+
this.gateway.connect(this.sender).sendMessage(this.toErc7930(this.receiver), payload, attributes, { value }),
43+
)
44+
.to.emit(this.receiver, 'MessageReceived')
45+
.withArgs(this.gateway, ethers.toBeHex(i, 32n), this.toErc7930(this.sender), payload, value);
46+
}
47+
});
48+
49+
it('multiple use of the same receiveId', async function () {
50+
const gatewayAsEOA = await impersonate(this.gateway.target);
51+
const receiveId = ethers.toBeHex(1n, 32n);
52+
53+
await expect(
54+
this.receiver.connect(gatewayAsEOA).receiveMessage(receiveId, this.toErc7930(this.sender), payload, { value }),
55+
)
56+
.to.emit(this.receiver, 'MessageReceived')
57+
.withArgs(this.gateway, receiveId, this.toErc7930(this.sender), payload, value);
58+
59+
await expect(
60+
this.receiver.connect(gatewayAsEOA).receiveMessage(receiveId, this.toErc7930(this.sender), payload, { value }),
61+
)
62+
.to.be.revertedWithCustomError(this.receiver, 'ERC7786RecipientMessageAlreadyProcessed')
63+
.withArgs(this.gateway, receiveId);
64+
});
65+
66+
it('unauthorized call', async function () {
67+
await expect(
68+
this.receiver.connect(this.notAGateway).receiveMessage(ethers.ZeroHash, this.toErc7930(this.sender), payload),
69+
)
70+
.to.be.revertedWithCustomError(this.receiver, 'ERC7786RecipientUnauthorizedGateway')
71+
.withArgs(this.notAGateway, this.toErc7930(this.sender));
72+
});
73+
});

0 commit comments

Comments
 (0)