-
Notifications
You must be signed in to change notification settings - Fork 12.2k
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
base: master
Are you sure you want to change the base?
Changes from 22 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
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,84 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.24; | ||
|
||
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``` | ||
* {BridgeERC20}. | ||
* | ||
* Contract that inherit from this contract can use the internal {_sendMessage} 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 BridgeCore is ERC7786Recipient { | ||
using Bytes for bytes; | ||
using InteroperableAddress for bytes; | ||
|
||
struct Link { | ||
address gateway; | ||
bytes remote; | ||
} | ||
mapping(bytes chain => Link) private _links; | ||
|
||
event RemoteRegistered(address gateway, bytes remote); | ||
|
||
error RemoteAlreadyRegistered(bytes chain); | ||
|
||
constructor(Link[] memory links) { | ||
for (uint256 i = 0; i < links.length; ++i) { | ||
_setLink(links[i].gateway, links[i].remote, false); | ||
} | ||
} | ||
|
||
/// @dev Returns the ERC-7786 gateway used for sending and receiving cross-chain messages to a given chain | ||
function link(bytes memory chain) public view virtual returns (address gateway, bytes memory remote) { | ||
Link storage self = _links[chain]; | ||
return (self.gateway, self.remote); | ||
} | ||
|
||
/// @dev Internal setter to change the ERC-7786 gateway and remote for a given chain. Called at construction. | ||
function _setLink(address gateway, bytes memory remote, 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(remote); | ||
if (allowOverride || _links[chain].gateway == address(0)) { | ||
_links[chain] = Link(gateway, remote); | ||
emit RemoteRegistered(gateway, remote); | ||
} else { | ||
revert RemoteAlreadyRegistered(chain); | ||
} | ||
} | ||
|
||
/// @dev Internal messaging function. | ||
function _sendMessage( | ||
bytes memory chain, | ||
bytes memory payload, | ||
bytes[] memory attributes | ||
) internal virtual returns (bytes32) { | ||
(address gateway, bytes memory remote) = link(chain); | ||
return IERC7786GatewaySource(gateway).sendMessage(remote, payload, attributes); | ||
} | ||
|
||
/// @inheritdoc ERC7786Recipient | ||
function _isAuthorizedGateway( | ||
address instance, | ||
bytes calldata sender | ||
) internal view virtual override returns (bool) { | ||
(address gateway, bytes memory router) = link(_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,70 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// SPDX-License-Identifier: MIT | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
pragma solidity ^0.8.24; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import {InteroperableAddress} from "../../utils/draft-InteroperableAddress.sol"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import {ERC7786Recipient} from "../ERC7786Recipient.sol"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import {BridgeCore} from "./BridgeCore.sol"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @dev Base contract for bridging ERC-20 between chains using an ERC-7786 gateway. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* In order to use this contract, two function must be implemented to link it to the token: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* * {lock}: called when a crosschain transfer is going out. Must take the sender tokens or revert. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* * {unlock}: called when a crosschain transfer is coming it. Must give tokens to the receiver. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* This base contract is used by the {BridgeERC20Custodial}, which interfaces with legacy ERC-20 tokens, and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* {BridgeERC20Bridgeable}, 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 BridgeERC20 is BridgeCore { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// @dev Transfer `amount` tokens to a crosschain receiver. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
function crosschainTransfer(bytes memory to, uint256 amount) public virtual returns (bytes32) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return _crosschainTransfer(msg.sender, to, amount); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// @dev Internal crosschain transfer function. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
function _crosschainTransfer(address from, bytes memory to, uint256 amount) internal virtual returns (bytes32) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
_lock(from, amount); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(bytes2 chainType, bytes memory chainReference, bytes memory addr) = to.parseV1(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
bytes memory chain = InteroperableAddress.formatV1(chainType, chainReference, hex""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
bytes32 sendId = _sendMessage( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
_unlock(to, amount); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
emit CrossChainTransferReceived(receiveId, from, to, amount); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+49
to
+63
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. 🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chainVerify address extraction safety. The payload decoding and address extraction at line 58 using Consider validating that toBinary is exactly 20 bytes for EVM addresses, or document the expected format and ensure remote bridges comply. 🏁 Script executed: #!/bin/bash
# Description: Verify how addresses are encoded in the sending logic and decoded here
# Find formatEvmV1 implementation to understand the address encoding
ast-grep --pattern $'function formatEvmV1($$$) {
$$$
}'
# Check if there are any address length validations in the codebase
rg -n "bytes20|address.*length" --type solidity -C 2 Length of output: 1274 🏁 Script executed: #!/bin/bash
sed -n '1,200p' contracts/crosschain/bridges/BridgeERC20.sol Length of output: 3066 🏁 Script executed: #!/bin/bash
# Show parseV1 implementation in InteroperableAddress
grep -R "function parseV1" -n contracts/utils/draft-InteroperableAddress.sol -C5 Length of output: 989 🏁 Script executed: #!/bin/bash
grep -R "function tryParseV1" -n contracts/utils/draft-InteroperableAddress.sol -C5 Length of output: 1070 🏁 Script executed: #!/bin/bash
# Display the body of tryParseV1 to inspect how `addr` is extracted
sed -n '90,150p' contracts/utils/draft-InteroperableAddress.sol Length of output: 3077 Enforce 20-byte EVM address in (bytes memory from, bytes memory toBinary, uint256 amount) = abi.decode(payload, (bytes, bytes, uint256));
+ require(toBinary.length == 20, "BridgeERC20: invalid address length");
address to = address(bytes20(toBinary)); This guarantees the 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
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. 🧩 Analysis chain🏁 Script executed:
Length of output: 363 🏁 Script executed:
Length of output: 752 @Amxx, thank you for the reference. I've reviewed the discussion, and you're absolutely right. As you explained in that thread, adding a The best-effort approach with My suggestion to add the length validation was inappropriate for this cross-chain bridging context. Would you like me to resolve this comment thread? 🧠 Learnings used
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// @dev Virtual function: implementation is required to handle token being burnt or locked on the source chain. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
function _lock(address from, uint256 amount) internal virtual; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// @dev Virtual function: implementation is required to handle token being minted or unlocked on the destination chain. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
function _unlock(address to, uint256 amount) internal virtual; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.24; | ||
|
||
import {IERC7802} from "../../interfaces/draft-IERC7802.sol"; | ||
import {BridgeERC20} from "./BridgeERC20.sol"; | ||
|
||
/** | ||
* @dev This is a variant of {BridgeERC20} that implements the bridge logic for ERC-7802 compliant tokens. | ||
*/ | ||
abstract contract BridgeERC20Bridgeable is BridgeERC20 { | ||
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 _lock(address from, uint256 amount) internal virtual override { | ||
token().crosschainBurn(from, amount); | ||
} | ||
|
||
/// @dev "Unlocking" tokens using an ERC-7802 crosschain mint | ||
function _unlock(address to, uint256 amount) internal virtual override { | ||
token().crosschainMint(to, amount); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,35 @@ | ||||||||||||||||||||
// SPDX-License-Identifier: MIT | ||||||||||||||||||||
|
||||||||||||||||||||
pragma solidity ^0.8.24; | ||||||||||||||||||||
|
||||||||||||||||||||
import {IERC20, SafeERC20} from "../../token/ERC20/utils/SafeERC20.sol"; | ||||||||||||||||||||
import {BridgeERC20} from "./BridgeERC20.sol"; | ||||||||||||||||||||
|
||||||||||||||||||||
/** | ||||||||||||||||||||
* @dev This is a variant of {BridgeERC20} that implements the bridge logic for ERC-20 tokens that do not expose mint | ||||||||||||||||||||
* and burn mechanism. Instead it takes custody of bridged assets. | ||||||||||||||||||||
*/ | ||||||||||||||||||||
abstract contract BridgeERC20Custodial is BridgeERC20 { | ||||||||||||||||||||
using SafeERC20 for IERC20; | ||||||||||||||||||||
|
||||||||||||||||||||
IERC20 private immutable _token; | ||||||||||||||||||||
|
||||||||||||||||||||
constructor(IERC20 token_) { | ||||||||||||||||||||
_token = token_; | ||||||||||||||||||||
} | ||||||||||||||||||||
Comment on lines
+17
to
+19
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. Validate token address in constructor. The constructor does not validate that Apply this diff to add validation: + error InvalidToken();
+
constructor(IERC20 token_) {
+ if (address(token_) == address(0)) revert InvalidToken();
_token = token_;
} 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||
|
||||||||||||||||||||
/// @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 _lock(address from, uint256 amount) internal virtual override { | ||||||||||||||||||||
token().safeTransferFrom(from, address(this), amount); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
/// @dev "Unlocking" tokens is done by releasing custody | ||||||||||||||||||||
function _unlock(address to, uint256 amount) internal virtual override { | ||||||||||||||||||||
token().safeTransfer(to, amount); | ||||||||||||||||||||
} | ||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.24; | ||
|
||
import {ERC20} from "../ERC20.sol"; | ||
import {BridgeERC20} from "../../../crosschain/bridges/BridgeERC20.sol"; | ||
|
||
abstract contract ERC20Crosschain is ERC20, BridgeERC20 { | ||
/// @dev TransferFrom variant of {crosschainTransferFrom}, using ERC20 allowance from the sender to the caller. | ||
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 _lock(address from, uint256 amount) internal virtual override { | ||
_burn(from, amount); | ||
} | ||
|
||
/// @dev "Unlocking" tokens is achieved through minting | ||
function _unlock(address to, uint256 amount) internal virtual override { | ||
_mint(to, amount); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.
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.
Unsafe address extraction from bytes.
The conversion
address(bytes20(toBinary))
assumestoBinary
is exactly 20 bytes in the expected position. However,addr
from the sender could be in various interoperable address formats (not just raw EVM addresses). This creates a mismatch:addr
directly fromparseV1()
outputbytes20
If
toBinary
is not a raw 20-byte EVM address or has encoding wrapper bytes, this will either revert or extract the wrong address.Consider using a proper parser or validating that
toBinary
is an EVM address format before casting:Or ensure that the sending side always encodes a raw 20-byte address rather than a full interoperable address structure.
🤖 Prompt for AI Agents
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.
The origin of the message has been verified, so we have a guarantee that
bytes memory toBinary
was encoded on line 40 and corresponds toaddr
that was parsed on line 35.So we have a guarantee that this is the "address" part of the
to
ERC-7930 parameter of_crosschainTransfer
.What if this is not properly formated? Reverting here means the the tokens were locked/burn on the source chain, and never minted on this side, creating an inconsistency. Not doing the check means we mint to whatever match the
to
parameter as best as possible. If the token is administered, there may be a way to recover these funds using a force transfer.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.
Note:
toBinary
is NOT a full ERC-7930 address. Its only the address section, so we shouldn't parse it.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.
@Amxx, you're absolutely right. I apologize for the misleading suggestion. In a cross-chain bridge context, reverting on malformed addresses would create an irrecoverable inconsistency where tokens are locked/burned on the source chain but never minted on the destination chain.
The current best-effort approach is the correct design:
toBinary
is guaranteed to come from a trusted source (line 40 encoding)to
parameter, that's user error at the point of initiating the transferThe current implementation correctly prioritizes cross-chain consistency over local validation. Thank you for the clarification!
✏️ Learnings added
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.
@Amxx, thank you for the clarification! I understand now—
toBinary
is just the raw address bytes extracted from the ERC-7930 address structure (theaddr
component fromparseV1()
), not a complete ERC-7930 formatted address. That's why the direct cast toaddress(bytes20(toBinary))
is appropriate here without needing to parse it again.I appreciate you taking the time to ensure I understood the data structure correctly.
🧠 Learnings used