-
Notifications
You must be signed in to change notification settings - Fork 12.3k
ERC-7786 based crosschain bridge for ERC-20 tokens #5914
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
Changes from 44 commits
4a33948
fdb2e77
3651638
f2abf9f
ab9643f
d3e2223
9b015d3
b486a81
50be369
4cf52d0
bd4095c
e6e5d79
c0c421c
a051b91
c938d68
69d43a7
d39c18e
3cc4acf
610362d
e0a3fd0
434ee70
c2a6d40
42b450c
b8cc512
984b897
6a8e8f9
172b02d
0673a8a
aef394e
7df0623
f88e636
509163f
4f97141
79fff25
4811775
deb6870
7f3c7e5
02078c3
9215f69
7a9339f
6521f01
99bf379
43da2d0
7a040dc
771068d
3c922a3
ff790ef
63ef13b
fc0066e
123eb19
b5331da
941db67
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 | ||
| --- | ||
|
|
||
| `ERC20Crosschain`: Added an ERC-20 extension to embed an ERC-7786 based crosschain bridge directly in the token contract. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| 'openzeppelin-solidity': minor | ||
| --- | ||
|
|
||
| `CrosschainLinks`: Added a new helper contract to facilitate communication between a contract on one chain and counterparts on remote chains through ERC-7786 gateways. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| 'openzeppelin-solidity': minor | ||
| --- | ||
|
|
||
| `BridgeERC20Core`, `BridgeERC20` and `BridgeERC7802`: Added bridge contracts to handle crosschain movements of ERC-20 (and ERC-7802) tokens. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| pragma solidity ^0.8.26; | ||
|
|
||
| import {IERC7786GatewaySource} from "../interfaces/draft-IERC7786.sol"; | ||
| import {InteroperableAddress} from "../utils/draft-InteroperableAddress.sol"; | ||
| import {Bytes} from "../utils/Bytes.sol"; | ||
| import {ERC7786Recipient} from "./ERC7786Recipient.sol"; | ||
|
|
||
| /** | ||
| * @dev Core bridging mechanism. | ||
| * | ||
| * This contract contains the logic to register and send messages to counterparts on remote chains using ERC-7786 | ||
| * gateways. It ensure received messages originate from a counterpart. This is the base of token bridges such as | ||
| * {BridgeERC20Core}. | ||
| * | ||
| * Contract that inherit from this contract can use the internal {_sendMessageToCounterpart} to send messages to their | ||
| * counterpart on a foreign chain. They must override the {_processMessage} function to handle the message that have | ||
| * been verified. | ||
| */ | ||
| abstract contract CrosschainLinks is ERC7786Recipient { | ||
| using Bytes for bytes; | ||
| using InteroperableAddress for bytes; | ||
|
|
||
| struct Link { | ||
| address gateway; | ||
| bytes counterpart; // Full InteroperableAddress (chain ref + address) | ||
| } | ||
| mapping(bytes chain => Link) private _links; | ||
|
|
||
| /** | ||
| * @dev Emitted when a new link is registered. | ||
| * | ||
| * Note: the `counterpart` argument is a full InteroperableAddress (chain ref + address). | ||
| */ | ||
| event LinkRegistered(address gateway, bytes counterpart); | ||
|
|
||
| /** | ||
| * @dev Reverted when trying to register a link for a chain that is already registered. | ||
| * | ||
| * Note: the `chain` argument is a "chain-only" InteroperableAddress (empty address). | ||
| */ | ||
| error LinkAlreadyRegistered(bytes chain); | ||
|
|
||
| constructor(Link[] memory links) { | ||
| for (uint256 i = 0; i < links.length; ++i) { | ||
| _setLink(links[i].gateway, links[i].counterpart, false); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @dev Returns the ERC-7786 gateway used for sending and receiving cross-chain messages to a given chain | ||
| * | ||
| * Note: The `chain` parameter is a "chain-only" InteroperableAddress (empty address) and the `counterpart` return | ||
| * the full InteroperableAddress (chain ref + address) that is on `chain`. | ||
| */ | ||
| function getLink(bytes memory chain) public view virtual returns (address gateway, bytes memory counterpart) { | ||
| Link storage self = _links[chain]; | ||
| return (self.gateway, self.counterpart); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Internal setter to change the ERC-7786 gateway and counterpart for a given chain. Called at construction. | ||
| * | ||
| * Note: The `counterpart` parameter is the full InteroperableAddress (chain ref + address). | ||
| */ | ||
| function _setLink(address gateway, bytes memory counterpart, bool allowOverride) internal virtual { | ||
| // Sanity check, this should revert if gateway is not an ERC-7786 implementation. Note that since | ||
| // supportsAttribute returns data, an EOA would fail that test (nothing returned). | ||
| IERC7786GatewaySource(gateway).supportsAttribute(bytes4(0)); | ||
|
|
||
| bytes memory chain = _extractChain(counterpart); | ||
| if (allowOverride || _links[chain].gateway == address(0)) { | ||
| _links[chain] = Link(gateway, counterpart); | ||
| emit LinkRegistered(gateway, counterpart); | ||
| } else { | ||
| revert LinkAlreadyRegistered(chain); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @dev Internal messaging function | ||
| * | ||
| * Note: The `chain` parameter is a"chain-only" InteroperableAddress (empty address). | ||
Amxx marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| */ | ||
| function _sendMessageToCounterpart( | ||
| bytes memory chain, | ||
| bytes memory payload, | ||
| bytes[] memory attributes | ||
| ) internal virtual returns (bytes32) { | ||
| (address gateway, bytes memory counterpart) = getLink(chain); | ||
| return IERC7786GatewaySource(gateway).sendMessage(counterpart, payload, attributes); | ||
| } | ||
|
|
||
| /// @inheritdoc ERC7786Recipient | ||
| function _isAuthorizedGateway( | ||
| address instance, | ||
| bytes calldata sender | ||
| ) internal view virtual override returns (bool) { | ||
| (address gateway, bytes memory router) = getLink(_extractChain(sender)); | ||
| return instance == gateway && sender.equal(router); | ||
| } | ||
|
|
||
| function _extractChain(bytes memory self) private pure returns (bytes memory) { | ||
| (bytes2 chainType, bytes memory chainReference, ) = self.parseV1(); | ||
| return InteroperableAddress.formatV1(chainType, chainReference, hex""); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| pragma solidity ^0.8.26; | ||
|
|
||
| import {IERC20, SafeERC20} from "../../token/ERC20/utils/SafeERC20.sol"; | ||
| import {BridgeERC20Core} from "./BridgeERC20Core.sol"; | ||
|
|
||
| /** | ||
| * @dev This is a variant of {BridgeERC20Core} that implements the bridge logic for ERC-20 tokens that do not expose mint | ||
| * and burn mechanism. Instead it takes custody of bridged assets. | ||
Amxx marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| */ | ||
| // slither-disable-next-line locked-ether | ||
| abstract contract BridgeERC20 is BridgeERC20Core { | ||
| using SafeERC20 for IERC20; | ||
|
|
||
| IERC20 private immutable _token; | ||
|
|
||
| constructor(IERC20 token_) { | ||
| _token = token_; | ||
| } | ||
|
|
||
| /// @dev Return the address of the ERC20 token this bridge operates on. | ||
| function token() public view virtual returns (IERC20) { | ||
| return _token; | ||
| } | ||
|
|
||
| /// @dev "Locking" tokens is done by taking custody | ||
| function _onSend(address from, uint256 amount) internal virtual override { | ||
| token().safeTransferFrom(from, address(this), amount); | ||
| } | ||
|
|
||
| /// @dev "Unlocking" tokens is done by releasing custody | ||
| function _onReceive(address to, uint256 amount) internal virtual override { | ||
| token().safeTransfer(to, amount); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| pragma solidity ^0.8.26; | ||
|
|
||
| import {InteroperableAddress} from "../../utils/draft-InteroperableAddress.sol"; | ||
| import {ERC7786Recipient} from "../ERC7786Recipient.sol"; | ||
| import {CrosschainLinks} from "../CrosschainLinks.sol"; | ||
|
|
||
| /** | ||
| * @dev Base contract for bridging ERC-20 between chains using an ERC-7786 gateway. | ||
| * | ||
| * In order to use this contract, two functions must be implemented to link it to the token: | ||
| * * {_onSend}: called when a crosschain transfer is going out. Must take the sender tokens or revert. | ||
| * * {_onReceive}: called when a crosschain transfer is coming it. Must give tokens to the receiver. | ||
Amxx marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * | ||
| * This base contract is used by the {BridgeERC20}, which interfaces with legacy ERC-20 tokens, and {BridgeERC7802}, | ||
| * which interface with ERC-7802 to provide an approve-free user experience. It is also used by the {ERC20Crosschain} | ||
| * extension, which embeds the bridge logic directly in the token contract. | ||
| */ | ||
| abstract contract BridgeERC20Core is CrosschainLinks { | ||
| using InteroperableAddress for bytes; | ||
|
|
||
| event CrossChainTransferSent(bytes32 indexed sendId, address indexed from, bytes to, uint256 amount); | ||
| event CrossChainTransferReceived(bytes32 indexed receiveId, bytes from, address indexed to, uint256 amount); | ||
Amxx marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * @dev Transfer `amount` tokens to a crosschain receiver. | ||
| * | ||
| * Note: The `to` parameter is the full InteroperableAddress (chain ref + address). | ||
| */ | ||
| function crosschainTransfer(bytes memory to, uint256 amount) public virtual returns (bytes32) { | ||
| return _crosschainTransfer(msg.sender, to, amount); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Internal crosschain transfer function. | ||
| * | ||
| * Note: The `to` parameter is the full InteroperableAddress (chain ref + address). | ||
| */ | ||
| function _crosschainTransfer(address from, bytes memory to, uint256 amount) internal virtual returns (bytes32) { | ||
| _onSend(from, amount); | ||
|
|
||
| (bytes2 chainType, bytes memory chainReference, bytes memory addr) = to.parseV1(); | ||
| bytes memory chain = InteroperableAddress.formatV1(chainType, chainReference, hex""); | ||
|
|
||
| bytes32 sendId = _sendMessageToCounterpart( | ||
| chain, | ||
| abi.encode(InteroperableAddress.formatEvmV1(block.chainid, from), addr, amount), | ||
| new bytes[](0) | ||
| ); | ||
|
|
||
| emit CrossChainTransferSent(sendId, from, to, amount); | ||
|
|
||
| return sendId; | ||
| } | ||
|
|
||
| /// @inheritdoc ERC7786Recipient | ||
| function _processMessage( | ||
| address /*gateway*/, | ||
| bytes32 receiveId, | ||
| bytes calldata /*sender*/, | ||
| bytes calldata payload | ||
| ) internal virtual override { | ||
| // split payload | ||
| (bytes memory from, bytes memory toBinary, uint256 amount) = abi.decode(payload, (bytes, bytes, uint256)); | ||
| address to = address(bytes20(toBinary)); | ||
|
|
||
| _onReceive(to, amount); | ||
|
|
||
| emit CrossChainTransferReceived(receiveId, from, to, amount); | ||
| } | ||
|
|
||
| /// @dev Virtual function: implementation is required to handle token being burnt or locked on the source chain. | ||
| function _onSend(address from, uint256 amount) internal virtual; | ||
|
|
||
| /// @dev Virtual function: implementation is required to handle token being minted or unlocked on the destination chain. | ||
| function _onReceive(address to, uint256 amount) internal virtual; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| pragma solidity ^0.8.26; | ||
|
|
||
| import {IERC7802} from "../../interfaces/draft-IERC7802.sol"; | ||
| import {BridgeERC20Core} from "./BridgeERC20Core.sol"; | ||
|
|
||
| /** | ||
| * @dev This is a variant of {BridgeERC20Core} that implements the bridge logic for ERC-7802 compliant tokens. | ||
| */ | ||
| // slither-disable-next-line locked-ether | ||
| abstract contract BridgeERC7802 is BridgeERC20Core { | ||
| IERC7802 private immutable _token; | ||
|
|
||
| constructor(IERC7802 token_) { | ||
| _token = token_; | ||
| } | ||
|
|
||
| /// @dev Return the address of the ERC20 token this bridge operates on. | ||
| function token() public view virtual returns (IERC7802) { | ||
| return _token; | ||
| } | ||
|
|
||
| /// @dev "Locking" tokens using an ERC-7802 crosschain burn | ||
| function _onSend(address from, uint256 amount) internal virtual override { | ||
| token().crosschainBurn(from, amount); | ||
| } | ||
|
|
||
| /// @dev "Unlocking" tokens using an ERC-7802 crosschain mint | ||
| function _onReceive(address to, uint256 amount) internal virtual override { | ||
| token().crosschainMint(to, amount); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| pragma solidity ^0.8.26; | ||
|
|
||
| import {ERC20} from "../ERC20.sol"; | ||
| import {BridgeERC20Core} from "../../../crosschain/bridges/BridgeERC20Core.sol"; | ||
|
|
||
| /** | ||
| * @dev Extension of {ERC20} that makes it natively cross-chain using the ERC-7786 based {BridgeERC20Core}. | ||
| * | ||
| * This extension makes the token compatible with: | ||
| * * {ERC20Crosschain} instances on other chains, | ||
| * * {ERC20} instances on other chains that are bridged using {BridgeERC20}, | ||
| * * {ERC20Bridgeable} instances on other chains that are bridged using {BridgeERC7802}. | ||
Amxx marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * | ||
| * It is mostly equivalent to inheriting from both {ERC20Bridgeable} and {BridgeERC7802}, and configuring then such | ||
Amxx marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * that: | ||
| * * `token` (on the {BridgeERC7802} side) is `address(this)`, | ||
| * * `_checkTokenBridge` (on the {ERC20Bridgeable} side) is implemented such that it only accepts calls self-calls. | ||
Amxx marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| */ | ||
| // slither-disable-next-line locked-ether | ||
| abstract contract ERC20Crosschain is ERC20, BridgeERC20Core { | ||
| /// @dev TransferFrom variant of {crosschainTransferFrom}, using ERC20 allowance from the sender to the caller. | ||
Amxx marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| function crosschainTransferFrom(address from, bytes memory to, uint256 amount) public virtual returns (bytes32) { | ||
| _spendAllowance(from, msg.sender, amount); | ||
| return _crosschainTransfer(from, to, amount); | ||
| } | ||
|
||
|
|
||
| /// @dev "Locking" tokens is achieved through burning | ||
| function _onSend(address from, uint256 amount) internal virtual override { | ||
| _burn(from, amount); | ||
| } | ||
|
|
||
| /// @dev "Unlocking" tokens is achieved through minting | ||
| function _onReceive(address to, uint256 amount) internal virtual override { | ||
| _mint(to, amount); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.