-
Notifications
You must be signed in to change notification settings - Fork 11.8k
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
ERC-7786 interfaces and receiver #5255
base: master
Are you sure you want to change the base?
Changes from all commits
b2eedbe
efd2f30
bc42b25
07f4b44
40ba631
07ec518
95fb0db
f263819
f51fbe6
52a301b
027859e
a91a999
86abf5a
6dca3cb
a7a6e9e
ec9a659
568dc7b
0292c31
aea4a14
cf78a9f
26cec97
3a7f904
4d18729
c7a7c94
d6319e8
b3bf461
2ab63b7
231b93b
24f1490
43f0dc1
7b7c1fd
2abfa49
f433e6d
27c7c0d
75e1e4c
4f48757
1ec1e3f
53d72d7
c5790f8
f6007b0
b17f548
d679082
117c10f
54e1732
88ee5a4
811d7c3
c54c431
37f2363
1acd441
c5d6081
ec54368
d290223
6c1c61a
04f54c9
75cc5da
1dfff54
06f5493
1b45dbd
b62f8b5
5acc9ee
0c67669
23ce2c3
5fe2fb4
5ad9ad4
8f5c897
f2370bf
a158138
5ba182b
1aea26e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'openzeppelin-solidity': minor | ||
--- | ||
|
||
`draft-IERC7786` and `draft-ERC7786Receiver`: Add interfaces for the ERC-7786 gateways and receiver, and a receiver base contract. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.0; | ||
|
||
import {IERC7786GatewayDestinationPassive, IERC7786Receiver} 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 in both active and passive mode for | ||
* communication with (one or multiple) destination gateways. This contract leaves two function 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 ERC7786Receiver is IERC7786Receiver { | ||
error ERC7786ReceiverInvalidGateway(address gateway); | ||
error ERC7786ReceivePassiveModeValue(); | ||
|
||
/// @inheritdoc IERC7786Receiver | ||
function receiveMessage( | ||
address gateway, | ||
bytes calldata gatewayMessageKey, | ||
string calldata source, | ||
string calldata sender, | ||
bytes calldata payload, | ||
bytes[] calldata attributes | ||
) public payable virtual { | ||
if (_isKnownGateway(msg.sender)) { | ||
// Active mode | ||
// no extra check | ||
} else if (_isKnownGateway(gateway)) { | ||
// Passive mode | ||
if (msg.value != 0) revert ERC7786ReceivePassiveModeValue(); | ||
IERC7786GatewayDestinationPassive(gateway).validateReceivedMessage( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In passive mode, the value is not sent by the relayer. Value (if any) must be sent by the gateway. So if you expect value with a message, and if you operate in passive mode, you should have a receive function. When you do the validate, the gateway should send you the value using that receive function. How the receiver detect that value was received is a great question. I think it's more a question for the ERC than for the implementation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One answer is that any value should come with a corresponding attribute, and that is how it would be visible from the processMessage (instead of using msg.value) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we want to make payable compatible with passive mode we would have to add a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After discussion it feels like the thing is:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a check |
||
gatewayMessageKey, | ||
source, | ||
sender, | ||
payload, | ||
attributes | ||
); | ||
} else { | ||
revert ERC7786ReceiverInvalidGateway(gateway); | ||
} | ||
_processMessage(gateway, source, sender, payload, attributes); | ||
} | ||
|
||
/// @dev Virtual getter that returns whether an address in 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, | ||
string calldata source, | ||
string calldata sender, | ||
bytes calldata payload, | ||
bytes[] calldata attributes | ||
) internal virtual; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.0; | ||
|
||
/** | ||
* @dev Interface for ERC-7786 source gateways. | ||
* | ||
* See ERC-7786 for more details | ||
*/ | ||
interface IERC7786GatewaySource { | ||
/** | ||
* @dev Event emitted when a message is created. If `outboxId` is zero, no further processing is necessary, and | ||
* no {MessageSent} event SHOULD be expected. If `outboxId` is not zero, then further (gateway specific, and non | ||
* standardized) action is required. | ||
*/ | ||
event MessageCreated( | ||
bytes32 indexed outboxId, | ||
string sender, // CAIP-10 account ID | ||
string receiver, // CAIP-10 account ID | ||
bytes payload, | ||
bytes[] attributes | ||
); | ||
|
||
/** | ||
* @dev This event is emitted when a message, for which the {MessageCreated} event contains an non zero `outboxId`, | ||
* received the required post processing actions, and was thus sent to the destination chain. | ||
*/ | ||
event MessageSent(bytes32 indexed outboxId); | ||
|
||
/// @dev This error is thrown when a message creation fails because of an unsupported attribute being specified. | ||
error UnsupportedAttribute(bytes4 selector); | ||
|
||
/// @dev Getter to check whether an attribute is supported or not. | ||
function supportsAttribute(bytes4 selector) external view returns (bool); | ||
arr00 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* @dev Endpoint for creating a new message. If the message requires further (gateway specific) processing before | ||
* it can be sent to the destination chain, then a non-zero `outboxId` must be returned. Otherwise, the | ||
* message MUST be sent and this function must return 0. | ||
* | ||
* * MUST emit a {MessageCreated} event. | ||
* * SHOULD NOT emit a {MessageSent} event. | ||
Comment on lines
+41
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why should it not emit a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is part of the ERC specification.
|
||
* | ||
* If any of the `attributes` is not supported, this function SHOULD revert with an {UnsupportedAttribute} error. | ||
* Other errors SHOULD revert with errors not specified in ERC-7786. | ||
*/ | ||
function sendMessage( | ||
string calldata destination, // CAIP-2 chain ID | ||
string calldata receiver, // CAIP-10 account ID | ||
bytes calldata payload, | ||
bytes[] calldata attributes | ||
) external payable returns (bytes32 outboxId); | ||
} | ||
|
||
/** | ||
* @dev Interface for ERC-7786 destination gateways operating in passive mode. | ||
* | ||
* See ERC-7786 for more details | ||
*/ | ||
interface IERC7786GatewayDestinationPassive { | ||
error InvalidMessageKey(bytes messageKey); | ||
|
||
/** | ||
* @dev Endpoint for checking the validity of a message that is being relayed in passive mode. The message | ||
* receiver is implicitly the caller of this method, which guarantees that no-one but the receiver can | ||
* "consume" the message. This function MUST implement replay protection, meaning that if called multiple time | ||
* for same message, all but the first calls MUST revert. | ||
* | ||
* NOTE: implementing this interface is OPTIONAL. Some destination gateway MAY only support active mode. | ||
*/ | ||
function validateReceivedMessage( | ||
bytes calldata messageKey, | ||
string calldata source, | ||
string calldata sender, | ||
bytes calldata payload, | ||
bytes[] calldata attributes | ||
) external; | ||
} | ||
|
||
/** | ||
* @dev Interface for the ERC-7786 client contracts (receiver). | ||
* | ||
* See ERC-7786 for more details | ||
*/ | ||
interface IERC7786Receiver { | ||
/** | ||
* @dev Endpoint for receiving cross-chain message. | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* | ||
* This function may be called directly by the gateway (active mode) or by a third party (passive mode). | ||
*/ | ||
function receiveMessage( | ||
address gateway, | ||
bytes calldata gatewayMessageKey, | ||
string calldata source, | ||
string calldata sender, | ||
bytes calldata payload, | ||
bytes[] calldata attributes | ||
) external payable; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.24; | ||
|
||
import {IERC7786GatewaySource, IERC7786GatewayDestinationPassive, IERC7786Receiver} from "../../interfaces/draft-IERC7786.sol"; | ||
import {BitMaps} from "../../utils/structs/BitMaps.sol"; | ||
import {Strings} from "../../utils/Strings.sol"; | ||
import {CAIP2} from "../../utils/CAIP2.sol"; | ||
import {CAIP10} from "../../utils/CAIP10.sol"; | ||
|
||
contract ERC7786GatewayMock is IERC7786GatewaySource, IERC7786GatewayDestinationPassive { | ||
using BitMaps for BitMaps.BitMap; | ||
using Strings for *; | ||
|
||
BitMaps.BitMap private _outbox; | ||
bool private _activeMode; | ||
|
||
function _setActive(bool newActiveMode) internal { | ||
_activeMode = newActiveMode; | ||
} | ||
|
||
function supportsAttribute(bytes4 /*selector*/) public pure returns (bool) { | ||
return false; | ||
} | ||
|
||
function sendMessage( | ||
string calldata destination, // CAIP-2 chain ID | ||
string calldata receiver, // CAIP-10 account ID | ||
bytes calldata payload, | ||
bytes[] calldata attributes | ||
) public payable returns (bytes32) { | ||
string memory source = CAIP2.local(); | ||
string memory sender = msg.sender.toChecksumHexString(); | ||
|
||
require(destination.equal(source), "This mock only supports local messages"); | ||
for (uint256 i = 0; i < attributes.length; ++i) { | ||
bytes4 selector = bytes4(attributes[i][0:4]); | ||
if (!supportsAttribute(selector)) revert UnsupportedAttribute(selector); | ||
} | ||
|
||
if (_activeMode) { | ||
address target = Strings.parseAddress(receiver); | ||
IERC7786Receiver(target).receiveMessage(address(this), new bytes(0), source, sender, payload, attributes); | ||
} else { | ||
_outbox.set(uint256(keccak256(abi.encode(source, sender, receiver, payload, attributes)))); | ||
} | ||
|
||
emit MessageCreated(0, CAIP10.format(source, sender), CAIP10.format(source, receiver), payload, attributes); | ||
return 0; | ||
} | ||
|
||
function validateReceivedMessage( | ||
bytes calldata /*messageKey*/, // this mock doesn't use a messageKey | ||
string calldata source, | ||
string calldata sender, | ||
bytes calldata payload, | ||
bytes[] calldata attributes | ||
) public { | ||
uint256 digest = uint256( | ||
keccak256(abi.encode(source, sender, msg.sender.toChecksumHexString(), payload, attributes)) | ||
); | ||
require(_outbox.get(digest), "invalid message"); | ||
_outbox.unset(digest); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.0; | ||
|
||
import {ERC7786Receiver} from "../../crosschain/draft-ERC7786Receiver.sol"; | ||
|
||
contract ERC7786ReceiverMock is ERC7786Receiver { | ||
address private immutable _gateway; | ||
|
||
event MessageReceived(address gateway, string source, string sender, bytes payload, bytes[] attributes); | ||
|
||
constructor(address gateway_) { | ||
_gateway = gateway_; | ||
} | ||
|
||
function _isKnownGateway(address instance) internal view virtual override returns (bool) { | ||
return instance == _gateway; | ||
} | ||
|
||
function _processMessage( | ||
address gateway, | ||
string calldata source, | ||
string calldata sender, | ||
bytes calldata payload, | ||
bytes[] calldata attributes | ||
) internal virtual override { | ||
emit MessageReceived(gateway, source, sender, payload, attributes); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
const { ethers } = require('hardhat'); | ||
const { expect } = require('chai'); | ||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); | ||
|
||
const { getLocalCAIP } = require('../helpers/chains'); | ||
const payload = require('../helpers/random').generators.hexBytes(128); | ||
const attributes = []; | ||
|
||
async function fixture() { | ||
const [sender, notAGateway] = await ethers.getSigners(); | ||
const { caip2, toCaip10 } = await getLocalCAIP(); | ||
|
||
const gateway = await ethers.deployContract('$ERC7786GatewayMock'); | ||
const receiver = await ethers.deployContract('$ERC7786ReceiverMock', [gateway]); | ||
|
||
return { sender, notAGateway, gateway, receiver, caip2, toCaip10 }; | ||
} | ||
|
||
// NOTE: here we are only testing the receiver. Failures of the gateway itself (invalid attributes, ...) are out of scope. | ||
describe('ERC7786Receiver', function () { | ||
beforeEach(async function () { | ||
Object.assign(this, await loadFixture(fixture)); | ||
}); | ||
|
||
describe('active mode', function () { | ||
beforeEach(async function () { | ||
await this.gateway.$_setActive(true); | ||
}); | ||
|
||
it('nominal workflow', async function () { | ||
await expect(this.gateway.connect(this.sender).sendMessage(this.caip2, this.receiver.target, payload, attributes)) | ||
.to.emit(this.gateway, 'MessageCreated') | ||
.withArgs(ethers.ZeroHash, this.toCaip10(this.sender), this.toCaip10(this.receiver), payload, attributes) | ||
.to.emit(this.receiver, 'MessageReceived') | ||
.withArgs(this.gateway, this.caip2, this.sender.address, payload, attributes); | ||
}); | ||
}); | ||
|
||
describe('passive mode', function () { | ||
beforeEach(async function () { | ||
await this.gateway.$_setActive(false); | ||
}); | ||
|
||
it('nominal workflow', async function () { | ||
await expect(this.gateway.connect(this.sender).sendMessage(this.caip2, this.receiver.target, payload, attributes)) | ||
.to.emit(this.gateway, 'MessageCreated') | ||
.withArgs(ethers.ZeroHash, this.toCaip10(this.sender), this.toCaip10(this.receiver), payload, attributes) | ||
.to.not.emit(this.receiver, 'MessageReceived'); | ||
|
||
await expect( | ||
this.receiver.receiveMessage(this.gateway, '0x', this.caip2, this.sender.address, payload, attributes), | ||
) | ||
.to.emit(this.receiver, 'MessageReceived') | ||
.withArgs(this.gateway, this.caip2, this.sender.address, payload, attributes); | ||
}); | ||
|
||
it('invalid message', async function () { | ||
await this.gateway.connect(this.sender).sendMessage(this.caip2, this.receiver.target, payload, attributes); | ||
|
||
// Altering the message (in this case, changing the sender's address) | ||
// Here the error is actually triggered by the gateway itself. | ||
await expect( | ||
this.receiver.receiveMessage(this.gateway, '0x', this.caip2, this.notAGateway, payload, attributes), | ||
).to.be.revertedWith('invalid message'); | ||
}); | ||
|
||
it('invalid gateway', async function () { | ||
await expect( | ||
this.receiver.receiveMessage(this.notAGateway, '0x', this.caip2, this.sender.address, payload, attributes), | ||
) | ||
.to.be.revertedWithCustomError(this.receiver, 'ERC7786ReceiverInvalidGateway') | ||
.withArgs(this.notAGateway); | ||
}); | ||
|
||
it('with value', async function () { | ||
await expect( | ||
this.receiver.receiveMessage(this.gateway, '0x', this.caip2, this.sender.address, payload, attributes, { | ||
value: 1n, | ||
}), | ||
).to.be.revertedWithCustomError(this.receiver, 'ERC7786ReceivePassiveModeValue'); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this check that
gatewayMessageKey
andgateway
are zero https://github.com/ethereum/ERCs/pull/673/files#diff-07848a22c16b11582927b37d76b2c54a0617b1444b30d11bca783ac98eaed01fR150? or is that checked by the gateway?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I remember the ERC says that in active mode the fields should be empty, not that must be empty. The idea is that we should not expect them to be populated (in fact we don't use these values in active mode)... but we should also not fail if they contain something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I specified it as "SHOULD" so this implementation is compliant. But it's worth considering if it should be "MUST". I don't see any concrete issue with allowing any values and ignoring them.