Skip to content
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

Open
wants to merge 69 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
b2eedbe
Strings: add toUint, toInt and hexToUint
Amxx Aug 28, 2024
efd2f30
codespell
Amxx Aug 28, 2024
bc42b25
Update contracts/utils/Strings.sol
Amxx Aug 29, 2024
07f4b44
Update .changeset/eighty-hounds-promise.md
Amxx Sep 2, 2024
40ba631
Update contracts/utils/Strings.sol
Amxx Sep 3, 2024
07ec518
Update Strings.sol
Amxx Sep 3, 2024
95fb0db
Apply suggestions from code review
Amxx Sep 3, 2024
f263819
Update contracts/utils/Strings.sol
Amxx Sep 3, 2024
f51fbe6
Update Strings.sol
Amxx Sep 3, 2024
52a301b
Fix value variable
cairoeth Sep 3, 2024
027859e
make return explicit
Amxx Sep 4, 2024
a91a999
branchless
Amxx Sep 4, 2024
86abf5a
Update contracts/utils/Strings.sol
Amxx Sep 5, 2024
6dca3cb
Update contracts/utils/Strings.sol
Amxx Sep 5, 2024
a7a6e9e
add try variants + use for governor proposal parsing
Amxx Sep 9, 2024
ec9a659
parseAddress
Amxx Sep 11, 2024
568dc7b
use string literal for 0x
Amxx Sep 17, 2024
0292c31
Apply suggestions from code review
Amxx Sep 17, 2024
aea4a14
add support for + prefix in parseInt
Amxx Sep 17, 2024
cf78a9f
Remove invalid "memory-safe" annotation.
Amxx Sep 17, 2024
26cec97
Merge branch 'master' into feature/parse-strings
Amxx Sep 18, 2024
3a7f904
Merge branch 'master' into feature/parse-strings
Amxx Oct 11, 2024
4d18729
Add Bytes.sol
Amxx Oct 11, 2024
c7a7c94
codespell
Amxx Oct 11, 2024
d6319e8
cleanup
Amxx Oct 11, 2024
b3bf461
Update .changeset/eighty-hounds-promise.md
Amxx Oct 11, 2024
2ab63b7
Update .changeset/rude-cougars-look.md
Amxx Oct 11, 2024
231b93b
optimization
Amxx Oct 11, 2024
24f1490
optimization
Amxx Oct 11, 2024
43f0dc1
testing
Amxx Oct 11, 2024
7b7c1fd
comment update
Amxx Oct 11, 2024
2abfa49
Update contracts/utils/Strings.sol
Amxx Oct 11, 2024
f433e6d
making unsafeReadBytesOffset private
Amxx Oct 11, 2024
27c7c0d
optimize
Amxx Oct 11, 2024
75e1e4c
Update contracts/utils/README.adoc
Amxx Oct 11, 2024
4f48757
Update contracts/governance/Governor.sol
Amxx Oct 11, 2024
1ec1e3f
rename parseHex to parseHexUint
Amxx Oct 11, 2024
53d72d7
fix tests
Amxx Oct 11, 2024
c5790f8
optimize
Amxx Oct 14, 2024
f6007b0
add CAIP2, CAIP10 and Bytes
Amxx Oct 11, 2024
b17f548
documentation
Amxx Oct 11, 2024
d679082
Bytes testing
Amxx Oct 11, 2024
117c10f
stateless + rename
Amxx Oct 11, 2024
54e1732
codespell
Amxx Oct 11, 2024
88ee5a4
use unchecked accesses
Amxx Oct 14, 2024
811d7c3
documentation
Amxx Oct 14, 2024
c54c431
Apply suggestions from code review
Amxx Oct 14, 2024
37f2363
Merge branch 'master' into features/caip-helpers
Amxx Oct 14, 2024
1acd441
Update Stateless.sol
Amxx Oct 14, 2024
c5d6081
refactor tests
Amxx Oct 14, 2024
ec54368
Adding ERCXXXX interfaces and helpers
Amxx Oct 14, 2024
d290223
fix pragma
Amxx Oct 14, 2024
6c1c61a
Merge branch 'master' into feature/crosschain
Amxx Oct 14, 2024
04f54c9
fix
Amxx Oct 14, 2024
75cc5da
remove some logic from ERCXXXXReceiver
Amxx Oct 15, 2024
1dfff54
doc
Amxx Oct 15, 2024
06f5493
rename ERCXXXX -> ERC7786
Amxx Oct 16, 2024
1b45dbd
up
Amxx Oct 16, 2024
b62f8b5
add changeset
Amxx Oct 16, 2024
5acc9ee
Apply suggestions from code review
Amxx Oct 16, 2024
0c67669
fixes & codespell
Amxx Oct 16, 2024
23ce2c3
Merge branch 'feature/crosschain' of https://github.com/Amxx/openzepp…
Amxx Oct 16, 2024
5fe2fb4
foundry solc version
Amxx Oct 16, 2024
5ad9ad4
Apply suggestions from code review
Amxx Oct 16, 2024
8f5c897
Update .changeset/khaki-keys-burn.md
Amxx Oct 16, 2024
f2370bf
Apply suggestions from code review
Amxx Oct 16, 2024
a158138
forbid passive mode with value
Amxx Oct 16, 2024
5ba182b
cross-chain consistency
Amxx Oct 16, 2024
1aea26e
Update draft-IERC7786.sol
Amxx Oct 17, 2024
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/khaki-keys-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`ERC-7786`: Add interfaces for the ERC-7786 gateways and receiver, and a receiver base contract.
Amxx marked this conversation as resolved.
Show resolved Hide resolved
60 changes: 60 additions & 0 deletions contracts/crosschain/draft-ERC7786Receiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// 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(address)`, 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.
Amxx marked this conversation as resolved.
Show resolved Hide resolved
*/
abstract contract ERC7786Receiver is IERC7786Receiver {
error ERC7786ReceiverInvalidGateway(address gateway);

/// @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
Copy link
Contributor

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 and gateway are zero https://github.com/ethereum/ERCs/pull/673/files#diff-07848a22c16b11582927b37d76b2c54a0617b1444b30d11bca783ac98eaed01fR150? or is that checked by the gateway?

Copy link
Collaborator Author

@Amxx Amxx Oct 16, 2024

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.

Copy link
Contributor

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.

} else if (_isKnownGateway(gateway)) {
// Passive mode
IERC7786GatewayDestinationPassive(gateway).validateReceivedMessage(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

receiveMessage is payable so it can receive value, but the value received is not passed to validateReceivedMessage, how can the passive gateway verify the msg.value received?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The 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 value parameter to validateReceivedMessage.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussion it feels like the thing is:

  • Value should only be part of active mode calls.
  • Passive mode should not support value, because value cannot be easily moved in a passive way
    • In that case we have a choice of doing require(msg.value == 0); in the passive mode, or to allow value to be passed by the relayer, without it being related to the semantics of the message

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would do require(msg.value == 0). In terms of the spec I think this check should be recommended but not mandatory.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 weither an address in a valid ERC-7786 gateway.
Amxx marked this conversation as resolved.
Show resolved Hide resolved
function _isKnownGateway(address instance) internal view virtual returns (bool);

/// @dev Virtual function that should contain the logic to execute when a crosschain message is received.
function _processMessage(
address gateway,
string calldata source,
string calldata sender,
bytes calldata payload,
bytes[] calldata attributes
) internal virtual;
}
3 changes: 3 additions & 0 deletions contracts/interfaces/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ are useful to interact with third party contracts that implement them.
- {IERC5805}
- {IERC6372}
- {IERC7674}
- {IERC7786}

== Detailed ABI

Expand Down Expand Up @@ -83,3 +84,5 @@ are useful to interact with third party contracts that implement them.
{{IERC6372}}

{{IERC7674}}

{{IERC7786}}
99 changes: 99 additions & 0 deletions contracts/interfaces/draft-IERC7786.sol
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
* not {MessageSent} event SHOULD be expected. If `outboxId` is not zero, then further (gateway specific, and non
Amxx marked this conversation as resolved.
Show resolved Hide resolved
* standardized) action is required.
*/
event MessageCreated(
bytes32 outboxId,
Copy link
Contributor

@frangio frangio Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be indexed. I know it's not in the spec but we should change that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we discussed it.

The MessageSent one is indexed (because we may want to activelly filter a particular MessageSent once we know the id. But for the MessageCreated part, we don't have a way to predict the value (at least not in general) so creating a is not that meaningfull IMO (hence why its not indexed)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My reasoning was that if you see a MessageSent event you may want to retrieve the matching MessageCreated.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sens

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 UnsuportedAttribute(bytes4 selector);
Amxx marked this conversation as resolved.
Show resolved Hide resolved

/// @dev Getter to check weither an attribute is supported.
Amxx marked this conversation as resolved.
Show resolved Hide resolved
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, this the
Amxx marked this conversation as resolved.
Show resolved Hide resolved
* 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should it not emit a MessageSent event if above it says that when returning zero the message must be sent.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is part of the ERC specification.

  • If the message requires post processing, a unique id is give to that message, otherwize 0 is used
  • The MessageSent is used when post processing is done, in that case we say which message was processed using that (non 0) id.
  • if no processing is needed, the message is sent automatically when its created. emitting MessageSent(0) would be useless (that is implicit by 0 being in MessageCreated) but also confusing (id 0 is not unique)

*
* If any of the `attributes` is not supported, this function SHOULD revert with an {UnsuportedAttribute} 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 implicitelly the caller of this method, which guarantees that no-one but the receiver can
Amxx marked this conversation as resolved.
Show resolved Hide resolved
* "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;
}
2 changes: 1 addition & 1 deletion contracts/mocks/Stateless.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;
pragma solidity ^0.8.25;

// We keep these imports and a dummy contract just to we can run the test suite after transpilation.

Expand Down
65 changes: 65 additions & 0 deletions contracts/mocks/crosschain/ERC7786GatewayMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.25;

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 UnsuportedAttribute(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);
}
}
29 changes: 29 additions & 0 deletions contracts/mocks/crosschain/ERC7786ReceiverMock.sol
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);
}
}
2 changes: 1 addition & 1 deletion contracts/utils/Bytes.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;
pragma solidity ^0.8.25;

import {Math} from "./math/Math.sol";

Expand Down
2 changes: 1 addition & 1 deletion contracts/utils/CAIP10.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;
pragma solidity ^0.8.25;

import {SafeCast} from "./math/SafeCast.sol";
import {Bytes} from "./Bytes.sol";
Expand Down
2 changes: 1 addition & 1 deletion contracts/utils/CAIP2.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;
pragma solidity ^0.8.25;

import {SafeCast} from "./math/SafeCast.sol";
import {Bytes} from "./Bytes.sol";
Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[profile.default]
solc_version = '0.8.24'
solc_version = '0.8.25'
evm_version = 'cancun'
optimizer = true
optimizer-runs = 200
Expand Down
4 changes: 2 additions & 2 deletions hardhat.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// ENVVAR
// - COMPILER: compiler version (default: 0.8.24)
// - COMPILER: compiler version (default: 0.8.25)
// - SRC: contracts folder to compile (default: contracts)
// - RUNS: number of optimization runs (default: 200)
// - IR: enable IR compilation (default: false)
Expand All @@ -18,7 +18,7 @@ const { argv } = require('yargs/yargs')()
compiler: {
alias: 'compileVersion',
type: 'string',
default: '0.8.24',
default: '0.8.25',
},
src: {
alias: 'source',
Expand Down
Loading
Loading