Skip to content

Commit bacb570

Browse files
Amxxernestognwfrangiojames-toussaintericglau
authored
ERC-7786 based crosschain bridge for ERC-20 tokens (#5914)
Co-authored-by: Ernesto García <[email protected]> Co-authored-by: Francisco Giordano <[email protected]> Co-authored-by: James Toussaint <[email protected]> Co-authored-by: Eric Lau <[email protected]>
1 parent 93ca624 commit bacb570

File tree

15 files changed

+632
-6
lines changed

15 files changed

+632
-6
lines changed

.changeset/clean-worlds-end.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+
`ERC20Crosschain`: Added an ERC-20 extension to embed an ERC-7786 based crosschain bridge directly in the token contract.

.changeset/grumpy-cats-brake.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+
`CrosschainLinked`: Added a new helper contract to facilitate communication between a contract on one chain and counterparts on remote chains through ERC-7786 gateways.

.changeset/new-socks-deny.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+
`BridgeERC20Core`, `BridgeERC20` and `BridgeERC7802`: Added bridge contracts to handle crosschain movements of ERC-20 (and ERC-7802) tokens.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.26;
4+
5+
import {IERC7786GatewaySource} from "../interfaces/draft-IERC7786.sol";
6+
import {InteroperableAddress} from "../utils/draft-InteroperableAddress.sol";
7+
import {Bytes} from "../utils/Bytes.sol";
8+
import {ERC7786Recipient} from "./ERC7786Recipient.sol";
9+
10+
/**
11+
* @dev Core bridging mechanism.
12+
*
13+
* This contract contains the logic to register and send messages to counterparts on remote chains using ERC-7786
14+
* gateways. It ensure received messages originate from a counterpart. This is the base of token bridges such as
15+
* {BridgeERC20Core}.
16+
*
17+
* Contracts that inherit from this contract can use the internal {_sendMessageToCounterpart} to send messages to their
18+
* counterpart on a foreign chain. They must override the {_processMessage} function to handle messages that have
19+
* been verified.
20+
*/
21+
abstract contract CrosschainLinked is ERC7786Recipient {
22+
using Bytes for bytes;
23+
using InteroperableAddress for bytes;
24+
25+
struct Link {
26+
address gateway;
27+
bytes counterpart; // Full InteroperableAddress (chain ref + address)
28+
}
29+
mapping(bytes chain => Link) private _links;
30+
31+
/**
32+
* @dev Emitted when a new link is registered.
33+
*
34+
* Note: the `counterpart` argument is a full InteroperableAddress (chain ref + address).
35+
*/
36+
event LinkRegistered(address gateway, bytes counterpart);
37+
38+
/**
39+
* @dev Reverted when trying to register a link for a chain that is already registered.
40+
*
41+
* Note: the `chain` argument is a "chain-only" InteroperableAddress (empty address).
42+
*/
43+
error LinkAlreadyRegistered(bytes chain);
44+
45+
constructor(Link[] memory links) {
46+
for (uint256 i = 0; i < links.length; ++i) {
47+
_setLink(links[i].gateway, links[i].counterpart, false);
48+
}
49+
}
50+
51+
/**
52+
* @dev Returns the ERC-7786 gateway used for sending and receiving cross-chain messages to a given chain.
53+
*
54+
* Note: The `chain` parameter is a "chain-only" InteroperableAddress (empty address) and the `counterpart` returns
55+
* the full InteroperableAddress (chain ref + address) that is on `chain`.
56+
*/
57+
function getLink(bytes memory chain) public view virtual returns (address gateway, bytes memory counterpart) {
58+
Link storage self = _links[chain];
59+
return (self.gateway, self.counterpart);
60+
}
61+
62+
/**
63+
* @dev Internal setter to change the ERC-7786 gateway and counterpart for a given chain. Called at construction.
64+
*
65+
* Note: The `counterpart` parameter is the full InteroperableAddress (chain ref + address).
66+
*/
67+
function _setLink(address gateway, bytes memory counterpart, bool allowOverride) internal virtual {
68+
// Sanity check, this should revert if gateway is not an ERC-7786 implementation. Note that since
69+
// supportsAttribute returns data, an EOA would fail that test (nothing returned).
70+
IERC7786GatewaySource(gateway).supportsAttribute(bytes4(0));
71+
72+
bytes memory chain = _extractChain(counterpart);
73+
if (allowOverride || _links[chain].gateway == address(0)) {
74+
_links[chain] = Link(gateway, counterpart);
75+
emit LinkRegistered(gateway, counterpart);
76+
} else {
77+
revert LinkAlreadyRegistered(chain);
78+
}
79+
}
80+
81+
/**
82+
* @dev Internal messaging function
83+
*
84+
* Note: The `chain` parameter is a "chain-only" InteroperableAddress (empty address).
85+
*/
86+
function _sendMessageToCounterpart(
87+
bytes memory chain,
88+
bytes memory payload,
89+
bytes[] memory attributes
90+
) internal virtual returns (bytes32) {
91+
(address gateway, bytes memory counterpart) = getLink(chain);
92+
return IERC7786GatewaySource(gateway).sendMessage(counterpart, payload, attributes);
93+
}
94+
95+
/// @inheritdoc ERC7786Recipient
96+
function _isAuthorizedGateway(
97+
address instance,
98+
bytes calldata sender
99+
) internal view virtual override returns (bool) {
100+
(address gateway, bytes memory router) = getLink(_extractChain(sender));
101+
return instance == gateway && sender.equal(router);
102+
}
103+
104+
function _extractChain(bytes memory self) private pure returns (bytes memory) {
105+
(bytes2 chainType, bytes memory chainReference, ) = self.parseV1();
106+
return InteroperableAddress.formatV1(chainType, chainReference, hex"");
107+
}
108+
}

contracts/crosschain/README.adoc

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,25 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/
55

66
This directory contains contracts for sending and receiving cross chain messages that follows the ERC-7786 standard.
77

8-
- {ERC7786Recipient}: generic ERC-7786 crosschain contract that receives messages from a trusted gateway
8+
* {CrosschainLinked}: helper to facilitate communication between a contract on one chain and counterparts on remote chains through ERC-7786 gateways.
9+
* {ERC7786Recipient}: generic ERC-7786 crosschain contract that receives messages from a trusted gateway.
10+
11+
Additionally there are multiple bridge constructions:
12+
13+
* {BridgeERC20Core}: Core bridging logic for crosschain ERC-20 transfer. Used by {BridgeERC20}, {BridgeERC7802} and {ERC20Crosschain},
14+
* {BridgeERC20}: Standalone bridge contract to connect an ERC-20 token contract with counterparts on remote chains,
15+
* {BridgeERC7802}: Standalone bridge contract to connect an ERC-7802 token contract with counterparts on remote chains.
916
1017
== Helpers
1118

19+
{{CrosschainLinked}}
20+
1221
{{ERC7786Recipient}}
22+
23+
== Bridges
24+
25+
{{BridgeERC20Core}}
26+
27+
{{BridgeERC20}}
28+
29+
{{BridgeERC7802}}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.26;
4+
5+
import {IERC20, SafeERC20} from "../../token/ERC20/utils/SafeERC20.sol";
6+
import {BridgeERC20Core} from "./BridgeERC20Core.sol";
7+
8+
/**
9+
* @dev This is a variant of {BridgeERC20Core} that implements the bridge logic for ERC-20 tokens that do not expose a
10+
* crosschain mint and burn mechanism. Instead, it takes custody of bridged assets.
11+
*/
12+
// slither-disable-next-line locked-ether
13+
abstract contract BridgeERC20 is BridgeERC20Core {
14+
using SafeERC20 for IERC20;
15+
16+
IERC20 private immutable _token;
17+
18+
constructor(IERC20 token_) {
19+
_token = token_;
20+
}
21+
22+
/// @dev Return the address of the ERC20 token this bridge operates on.
23+
function token() public view virtual returns (IERC20) {
24+
return _token;
25+
}
26+
27+
/// @dev "Locking" tokens is done by taking custody
28+
function _onSend(address from, uint256 amount) internal virtual override {
29+
token().safeTransferFrom(from, address(this), amount);
30+
}
31+
32+
/// @dev "Unlocking" tokens is done by releasing custody
33+
function _onReceive(address to, uint256 amount) internal virtual override {
34+
token().safeTransfer(to, amount);
35+
}
36+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.26;
4+
5+
import {InteroperableAddress} from "../../utils/draft-InteroperableAddress.sol";
6+
import {ERC7786Recipient} from "../ERC7786Recipient.sol";
7+
import {CrosschainLinked} from "../CrosschainLinked.sol";
8+
9+
/**
10+
* @dev Base contract for bridging ERC-20 between chains using an ERC-7786 gateway.
11+
*
12+
* In order to use this contract, two functions must be implemented to link it to the token:
13+
* * {_onSend}: called when a crosschain transfer is going out. Must take the sender tokens or revert.
14+
* * {_onReceive}: called when a crosschain transfer is coming in. Must give tokens to the receiver.
15+
*
16+
* This base contract is used by the {BridgeERC20}, which interfaces with legacy ERC-20 tokens, and {BridgeERC7802},
17+
* which interface with ERC-7802 to provide an approve-free user experience. It is also used by the {ERC20Crosschain}
18+
* extension, which embeds the bridge logic directly in the token contract.
19+
*/
20+
abstract contract BridgeERC20Core is CrosschainLinked {
21+
using InteroperableAddress for bytes;
22+
23+
event CrosschainERC20TransferSent(bytes32 indexed sendId, address indexed from, bytes to, uint256 amount);
24+
event CrosschainERC20TransferReceived(bytes32 indexed receiveId, bytes from, address indexed to, uint256 amount);
25+
26+
/**
27+
* @dev Transfer `amount` tokens to a crosschain receiver.
28+
*
29+
* Note: The `to` parameter is the full InteroperableAddress (chain ref + address).
30+
*/
31+
function crosschainTransfer(bytes memory to, uint256 amount) public virtual returns (bytes32) {
32+
return _crosschainTransfer(msg.sender, to, amount);
33+
}
34+
35+
/**
36+
* @dev Internal crosschain transfer function.
37+
*
38+
* Note: The `to` parameter is the full InteroperableAddress (chain ref + address).
39+
*/
40+
function _crosschainTransfer(address from, bytes memory to, uint256 amount) internal virtual returns (bytes32) {
41+
_onSend(from, amount);
42+
43+
(bytes2 chainType, bytes memory chainReference, bytes memory addr) = to.parseV1();
44+
bytes memory chain = InteroperableAddress.formatV1(chainType, chainReference, hex"");
45+
46+
bytes32 sendId = _sendMessageToCounterpart(
47+
chain,
48+
abi.encode(InteroperableAddress.formatEvmV1(block.chainid, from), addr, amount),
49+
new bytes[](0)
50+
);
51+
52+
emit CrosschainERC20TransferSent(sendId, from, to, amount);
53+
54+
return sendId;
55+
}
56+
57+
/// @inheritdoc ERC7786Recipient
58+
function _processMessage(
59+
address /*gateway*/,
60+
bytes32 receiveId,
61+
bytes calldata /*sender*/,
62+
bytes calldata payload
63+
) internal virtual override {
64+
// split payload
65+
(bytes memory from, bytes memory toBinary, uint256 amount) = abi.decode(payload, (bytes, bytes, uint256));
66+
address to = address(bytes20(toBinary));
67+
68+
_onReceive(to, amount);
69+
70+
emit CrosschainERC20TransferReceived(receiveId, from, to, amount);
71+
}
72+
73+
/// @dev Virtual function: implementation is required to handle token being burnt or locked on the source chain.
74+
function _onSend(address from, uint256 amount) internal virtual;
75+
76+
/// @dev Virtual function: implementation is required to handle token being minted or unlocked on the destination chain.
77+
function _onReceive(address to, uint256 amount) internal virtual;
78+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.26;
4+
5+
import {IERC7802} from "../../interfaces/draft-IERC7802.sol";
6+
import {BridgeERC20Core} from "./BridgeERC20Core.sol";
7+
8+
/**
9+
* @dev This is a variant of {BridgeERC20Core} that implements the bridge logic for ERC-7802 compliant tokens.
10+
*/
11+
// slither-disable-next-line locked-ether
12+
abstract contract BridgeERC7802 is BridgeERC20Core {
13+
IERC7802 private immutable _token;
14+
15+
constructor(IERC7802 token_) {
16+
_token = token_;
17+
}
18+
19+
/// @dev Return the address of the ERC20 token this bridge operates on.
20+
function token() public view virtual returns (IERC7802) {
21+
return _token;
22+
}
23+
24+
/// @dev "Locking" tokens using an ERC-7802 crosschain burn
25+
function _onSend(address from, uint256 amount) internal virtual override {
26+
token().crosschainBurn(from, amount);
27+
}
28+
29+
/// @dev "Unlocking" tokens using an ERC-7802 crosschain mint
30+
function _onReceive(address to, uint256 amount) internal virtual override {
31+
token().crosschainMint(to, amount);
32+
}
33+
}

contracts/mocks/token/ERC20BridgeableMock.sol

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ abstract contract ERC20BridgeableMock is ERC20Bridgeable {
1010
error OnlyTokenBridge();
1111
event OnlyTokenBridgeFnCalled(address caller);
1212

13-
constructor(address bridge) {
13+
constructor(address initialBridge) {
14+
_setBridge(initialBridge);
15+
}
16+
17+
function _setBridge(address bridge) internal {
1418
_bridge = bridge;
1519
}
1620

contracts/token/ERC20/README.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Additionally there are multiple custom extensions, including:
1919
* {ERC20Bridgeable}: compatibility with crosschain bridges through ERC-7802.
2020
* {ERC20Burnable}: destruction of own tokens.
2121
* {ERC20Capped}: enforcement of a cap to the total supply when minting tokens.
22+
* {ERC20Crosschain}: embedded {BridgeERC20Core} bridge, making the token crosschain through the use of ERC-7786 gateways.
2223
* {ERC20Pausable}: ability to pause token transfers.
2324
* {ERC20FlashMint}: token level support for flash loans through the minting and burning of ephemeral tokens (standardized as ERC-3156).
2425
* {ERC20Votes}: support for voting and vote delegation. xref:governance.adoc#token[See the governance guide for a minimal example (with the required overrides when combining ERC20 + ERC20Permit + ERC20Votes)].
@@ -57,6 +58,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel
5758

5859
{{ERC20Capped}}
5960

61+
{{ERC20Crosschain}}
62+
6063
{{ERC20Pausable}}
6164

6265
{{ERC20Votes}}

0 commit comments

Comments
 (0)