-
Notifications
You must be signed in to change notification settings - Fork 12.2k
Migrate ERC7786Recipient from community #5904
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Amxx
wants to merge
15
commits into
OpenZeppelin:master
Choose a base branch
from
Amxx:crosschain/erc7786receiver
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
4a33948
Migrate ERC7786Receiver from community
Amxx fdb2e77
add documentation
Amxx 3651638
Apply suggestion from @ernestognw
ernestognw f2abf9f
Update .changeset/silent-zebras-press.md
Amxx ab9643f
Apply suggestions from code review
Amxx d3e2223
rename ERC7786Receiver into ERC7786Recipient
Amxx 9b015d3
Update .changeset/silent-zebras-press.md
Amxx c0c421c
refactor permission
Amxx c938d68
calldata
Amxx d39c18e
prevent message replay at the receiver level
Amxx 9f63e43
change custom error
Amxx 4701a6d
Merge branch 'master' into crosschain/erc7786receiver
Amxx 7be8e78
update
Amxx 1c78bc3
Merge branch 'crosschain/erc7786receiver' of https://github.com/Amxx/…
Amxx 4a12837
add details about sender in _isAuthorizedGateway
Amxx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.27; | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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) { | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 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; | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
= Cross chain interoperability | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
[.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}} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// @inheritdoc IERC7786GatewaySource | ||
function sendMessage( | ||
bytes calldata recipient, | ||
bytes calldata payload, | ||
bytes[] calldata attributes | ||
) public payable virtual returns (bytes32 sendId) { | ||
// attributes are not supported | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (attributes.length > 0) { | ||
revert UnsupportedAttribute(bytes4(attributes[0])); | ||
} | ||
|
||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// parse recipient | ||
(bool success, uint256 chainid, address target) = recipient.tryParseEvmV1Calldata(); | ||
require(success && chainid == block.chainid, InvalidDestination()); | ||
|
||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 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; | ||
sourcery-ai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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_; | ||
} | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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)); | ||
}); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.