Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
4a33948
Migrate ERC7786Receiver from community
Amxx Aug 28, 2025
fdb2e77
add documentation
Amxx Aug 28, 2025
3651638
Apply suggestion from @ernestognw
ernestognw Aug 28, 2025
f2abf9f
Update .changeset/silent-zebras-press.md
Amxx Aug 28, 2025
ab9643f
Apply suggestions from code review
Amxx Aug 28, 2025
d3e2223
rename ERC7786Receiver into ERC7786Recipient
Amxx Aug 28, 2025
9b015d3
Update .changeset/silent-zebras-press.md
Amxx Aug 29, 2025
b486a81
Add CrosschainBridgeERC20, CrosschainBridgeERC20Custodial, Crosschain…
Amxx Aug 29, 2025
50be369
refactor bridges
Amxx Aug 29, 2025
4cf52d0
comments
Amxx Aug 29, 2025
bd4095c
remove the "with attributes" variant of crosschainTransfer + testing
Amxx Sep 1, 2025
e6e5d79
update Bridge: use chain specific gateway
Amxx Sep 1, 2025
c0c421c
refactor permission
Amxx Sep 4, 2025
a051b91
Merge branch 'crosschain/erc7786receiver' into crosschain/erc20bridge
Amxx Sep 4, 2025
c938d68
calldata
Amxx Sep 4, 2025
69d43a7
Merge branch 'crosschain/erc7786receiver' into crosschain/erc20bridge
Amxx Sep 4, 2025
d39c18e
prevent message replay at the receiver level
Amxx Sep 4, 2025
3cc4acf
Merge branch 'crosschain/erc7786receiver' into crosschain/erc20bridge
Amxx Sep 4, 2025
610362d
update to match new ERC7786Recipient
Amxx Sep 4, 2025
e0a3fd0
Update contracts/crosschain/bridges/BridgeCore.sol
Amxx Sep 9, 2025
434ee70
Merge branch 'master' into crosschain/erc20bridge
Amxx Sep 30, 2025
c2a6d40
Apply suggestions from code review
Amxx Oct 3, 2025
42b450c
Merge branch 'master' into crosschain/erc20bridge
Amxx Oct 3, 2025
b8cc512
Merge remote-tracking branch 'amxx/crosschain/erc20bridge' into cross…
Amxx Oct 3, 2025
984b897
fix tests
Amxx Oct 3, 2025
6a8e8f9
pragma consistency
Amxx Oct 9, 2025
172b02d
pragma consistency
Amxx Oct 9, 2025
0673a8a
Merge branch 'master' into crosschain/erc20bridge
Amxx Oct 10, 2025
aef394e
Apply suggestions from code review
Amxx Nov 6, 2025
7df0623
rename BridgeCore.link -> getLink
Amxx Nov 6, 2025
f88e636
rename BridgeCore._sendMessage -> _sendMessageToRemote
Amxx Nov 6, 2025
509163f
fix BridgeERC20 doc
Amxx Nov 6, 2025
4f97141
rename BridgeCore -> CrosschainLinks
Amxx Nov 6, 2025
79fff25
rename BridgeERC20Bridgeable -> BridgeERC7802
Amxx Nov 6, 2025
4811775
rename BridgeERC20 -> BridgeERC20Core
Amxx Nov 6, 2025
deb6870
rename BridgeERC20Custodial -> BridgeERC20
Amxx Nov 6, 2025
7f3c7e5
coverage
Amxx Nov 6, 2025
02078c3
rename _lock/_unlock -> _onSend/_onReceive
Amxx Nov 6, 2025
9215f69
overall rename: remote -> counterpart
Amxx Nov 6, 2025
7a9339f
document ERC20Crosschain
Amxx Nov 6, 2025
6521f01
documentation
Amxx Nov 6, 2025
99bf379
Document bytes objects that are interoperable addresses
Amxx Nov 6, 2025
43da2d0
try fix slither locked-ether
Amxx Nov 6, 2025
7a040dc
add changesets
Amxx Nov 7, 2025
771068d
Update contracts/crosschain/CrosschainLinks.sol
Amxx Nov 10, 2025
3c922a3
Rename CrosschainLinks -> CrosschainLinked
Amxx Nov 25, 2025
ff790ef
move file to finish renaming
Amxx Nov 25, 2025
63ef13b
Apply suggestions from code review
Amxx Nov 26, 2025
fc0066e
rename events
Amxx Nov 27, 2025
123eb19
Update contracts/crosschain/CrosschainLinked.sol
Amxx Nov 27, 2025
b5331da
Update contracts/crosschain/README.adoc
Amxx Nov 27, 2025
941db67
Apply suggestions from code review
Amxx Nov 27, 2025
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
84 changes: 84 additions & 0 deletions contracts/crosschain/bridges/BridgeCore.sol
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"");
}
}
70 changes: 70 additions & 0 deletions contracts/crosschain/bridges/BridgeERC20.sol
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);
}

/// @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;
}
32 changes: 32 additions & 0 deletions contracts/crosschain/bridges/BridgeERC20Bridgeable.sol
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);
}
}
35 changes: 35 additions & 0 deletions contracts/crosschain/bridges/BridgeERC20Custodial.sol
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_;
}

/// @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);
}
}
6 changes: 5 additions & 1 deletion contracts/mocks/token/ERC20BridgeableMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ abstract contract ERC20BridgeableMock is ERC20Bridgeable {
error OnlyTokenBridge();
event OnlyTokenBridgeFnCalled(address caller);

constructor(address bridge) {
constructor(address initialBridge) {
_setBridge(initialBridge);
}

function _setBridge(address bridge) internal {
_bridge = bridge;
}

Expand Down
24 changes: 24 additions & 0 deletions contracts/token/ERC20/extensions/ERC20Crosschain.sol
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);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I'd remove this, to reduce surface, and because it's not standardized so I don't expect it to be used.

Copy link
Collaborator Author

@Amxx Amxx Nov 6, 2025

Choose a reason for hiding this comment

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

IMO its a nice to have.

Not generic, but if a projects knows its there on their own tokens, they might leverage it trhough their own UI/SDK

Copy link
Contributor

Choose a reason for hiding this comment

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

There's an easy workaround by doing transferFrom followed by crosschainTransfer. It's not a super strong opinion but I still feel I'd rather remove it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

its an easy work around, but even with multicall, that would still involves two storage update (for the balance of the intermediary account).


/// @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);
}
}
Loading
Loading