Skip to content

Commit 77b1d80

Browse files
Amxxcoderabbitai[bot]frangioernestognw
authored
ERC-7786 based crosschain bridge for ERC-721 tokens (#6259)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Francisco Giordano <fg@frang.io> Co-authored-by: Ernesto García <ernestognw@gmail.com>
1 parent b659750 commit 77b1d80

File tree

12 files changed

+639
-134
lines changed

12 files changed

+639
-134
lines changed

.changeset/fruity-coats-smash.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+
`BridgeNonFungibleCore` and `BridgeERC721`: Added bridge contracts to handle crosschain movements of ERC-721 tokens.

.changeset/tidy-turkeys-build.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+
`ERC721Crosschain`: Added an ERC-721 extension to embed an ERC-7786 based crosschain bridge directly in the token contract.

contracts/crosschain/README.adoc

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ This directory contains contracts for sending and receiving cross chain messages
1111
Additionally there are multiple bridge constructions:
1212

1313
* {BridgeFungible}: 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.
14+
* {BridgeNonFungible}: Core bridging logic for crosschain ERC-721 transfer. Used by {BridgeERC721} and {ERC721Crosschain},
1615
* {BridgeMultiToken}: Core bridging logic for crosschain ERC-1155 transfer. Used by {BridgeERC1155} and {ERC1155Crosschain},
16+
* {BridgeERC20}: Standalone bridge contract to connect an ERC-20 token contract with counterparts on remote chains,
17+
* {BridgeERC721}: Standalone bridge contract to connect an ERC-721 token contract with counterparts on remote chains,
1718
* {BridgeERC1155}: Standalone bridge contract to connect an ERC-1155 token contract with counterparts on remote chains,
19+
* {BridgeERC7802}: Standalone bridge contract to connect an ERC-7802 token contract with counterparts on remote chains.
1820
1921
== Helpers
2022

@@ -26,10 +28,14 @@ Additionally there are multiple bridge constructions:
2628

2729
{{BridgeFungible}}
2830

29-
{{BridgeERC20}}
30-
31-
{{BridgeERC7802}}
31+
{{BridgeNonFungible}}
3232

3333
{{BridgeMultiToken}}
3434

35+
{{BridgeERC20}}
36+
37+
{{BridgeERC721}}
38+
3539
{{BridgeERC1155}}
40+
41+
{{BridgeERC7802}}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.26;
4+
5+
import {IERC721} from "../../interfaces/IERC721.sol";
6+
import {IERC721Errors} from "../../interfaces/draft-IERC6093.sol";
7+
import {BridgeNonFungible} from "./abstract/BridgeNonFungible.sol";
8+
9+
/**
10+
* @dev This is a variant of {BridgeNonFungible} that implements the bridge logic for ERC-721 tokens that do not expose
11+
* a crosschain mint and burn mechanism. Instead, it takes custody of bridged assets.
12+
*/
13+
// slither-disable-next-line locked-ether
14+
abstract contract BridgeERC721 is BridgeNonFungible {
15+
IERC721 private immutable _token;
16+
17+
constructor(IERC721 token_) {
18+
_token = token_;
19+
}
20+
21+
/// @dev Return the address of the ERC721 token this bridge operates on.
22+
function token() public view virtual returns (IERC721) {
23+
return _token;
24+
}
25+
26+
/**
27+
* @dev Transfer `tokenId` from `from` (on this chain) to `to` (on a different chain).
28+
*
29+
* The `to` parameter is the full InteroperableAddress that references both the destination chain and the account
30+
* on that chain. Similarly to the underlying token's {ERC721-transferFrom} function, this function can be called
31+
* either by the token holder or by anyone that is approved by the token holder. It reuses the token's allowance
32+
* system, meaning that an account that is "approved for all" or "approved for tokenId" can perform the crosschain
33+
* transfer directly without having to take temporary custody of the token.
34+
*/
35+
function crosschainTransferFrom(address from, bytes memory to, uint256 tokenId) public virtual returns (bytes32) {
36+
// Permission is handled using the ERC721's allowance system. This check replicates `ERC721._isAuthorized`.
37+
address spender = _msgSender();
38+
require(
39+
from == spender || token().isApprovedForAll(from, spender) || token().getApproved(tokenId) == spender,
40+
IERC721Errors.ERC721InsufficientApproval(spender, tokenId)
41+
);
42+
43+
// This call verifies that `from` is the owner of `tokenId` (in `_onSend`), and the previous checks ensure
44+
// that `spender` is allowed to move tokenId on behalf of `from`.
45+
//
46+
// Perform the crosschain transfer and return the send id
47+
return _crosschainTransfer(from, to, tokenId);
48+
}
49+
50+
/// @dev "Locking" tokens is done by taking custody
51+
function _onSend(address from, uint256 tokenId) internal virtual override {
52+
// slither-disable-next-line arbitrary-send-erc20
53+
token().transferFrom(from, address(this), tokenId);
54+
}
55+
56+
/// @dev "Unlocking" tokens is done by releasing custody
57+
function _onReceive(address to, uint256 tokenId) internal virtual override {
58+
// slither-disable-next-line arbitrary-send-erc20
59+
token().transferFrom(address(this), to, tokenId);
60+
}
61+
}

contracts/crosschain/bridges/abstract/BridgeFungible.sol

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ import {CrosschainLinked} from "../../CrosschainLinked.sol";
1919
* extension, which embeds the bridge logic directly in the token contract.
2020
*/
2121
abstract contract BridgeFungible is Context, CrosschainLinked {
22-
using InteroperableAddress for bytes;
23-
22+
/// @dev Emitted when a crosschain ERC-20 transfer is sent.
2423
event CrosschainFungibleTransferSent(bytes32 indexed sendId, address indexed from, bytes to, uint256 amount);
24+
25+
/// @dev Emitted when a crosschain ERC-20 transfer is received.
2526
event CrosschainFungibleTransferReceived(bytes32 indexed receiveId, bytes from, address indexed to, uint256 amount);
2627

2728
/**
@@ -41,7 +42,7 @@ abstract contract BridgeFungible is Context, CrosschainLinked {
4142
function _crosschainTransfer(address from, bytes memory to, uint256 amount) internal virtual returns (bytes32) {
4243
_onSend(from, amount);
4344

44-
(bytes2 chainType, bytes memory chainReference, bytes memory addr) = to.parseV1();
45+
(bytes2 chainType, bytes memory chainReference, bytes memory addr) = InteroperableAddress.parseV1(to);
4546
bytes memory chain = InteroperableAddress.formatV1(chainType, chainReference, hex"");
4647

4748
bytes32 sendId = _sendMessageToCounterpart(
@@ -65,8 +66,8 @@ abstract contract BridgeFungible is Context, CrosschainLinked {
6566
// NOTE: Gateway is validated by {_isAuthorizedGateway} (implemented in {CrosschainLinked}). No need to check here.
6667

6768
// split payload
68-
(bytes memory from, bytes memory toBinary, uint256 amount) = abi.decode(payload, (bytes, bytes, uint256));
69-
address to = address(bytes20(toBinary));
69+
(bytes memory from, bytes memory toEvm, uint256 amount) = abi.decode(payload, (bytes, bytes, uint256));
70+
address to = address(bytes20(toEvm));
7071

7172
_onReceive(to, amount);
7273

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.26;
4+
5+
import {InteroperableAddress} from "../../../utils/draft-InteroperableAddress.sol";
6+
import {Context} from "../../../utils/Context.sol";
7+
import {ERC7786Recipient} from "../../ERC7786Recipient.sol";
8+
import {CrosschainLinked} from "../../CrosschainLinked.sol";
9+
10+
/**
11+
* @dev Base contract for bridging ERC-721 between chains using an ERC-7786 gateway.
12+
*
13+
* In order to use this contract, two functions must be implemented to link it to the token:
14+
* * {_onSend}: called when a crosschain transfer is going out. Must take the sender tokens or revert.
15+
* * {_onReceive}: called when a crosschain transfer is coming in. Must give tokens to the receiver.
16+
*
17+
* This base contract is used by the {BridgeERC721}, which interfaces with legacy ERC-721 tokens. It is also used by
18+
* the {ERC721Crosschain} extension, which embeds the bridge logic directly in the token contract.
19+
*/
20+
abstract contract BridgeNonFungible is Context, CrosschainLinked {
21+
/// @dev Emitted when a crosschain ERC-721 transfer is sent.
22+
event CrosschainNonFungibleTransferSent(bytes32 indexed sendId, address indexed from, bytes to, uint256 tokenId);
23+
24+
/// @dev Emitted when a crosschain ERC-721 transfer is received.
25+
event CrosschainNonFungibleTransferReceived(
26+
bytes32 indexed receiveId,
27+
bytes from,
28+
address indexed to,
29+
uint256 tokenId
30+
);
31+
32+
/**
33+
* @dev Internal crosschain transfer function.
34+
*
35+
* NOTE: The `to` parameter is the full InteroperableAddress (chain ref + address).
36+
*/
37+
function _crosschainTransfer(address from, bytes memory to, uint256 tokenId) internal virtual returns (bytes32) {
38+
_onSend(from, tokenId);
39+
40+
(bytes2 chainType, bytes memory chainReference, bytes memory addr) = InteroperableAddress.parseV1(to);
41+
bytes memory chain = InteroperableAddress.formatV1(chainType, chainReference, hex"");
42+
43+
bytes32 sendId = _sendMessageToCounterpart(
44+
chain,
45+
abi.encode(InteroperableAddress.formatEvmV1(block.chainid, from), addr, tokenId),
46+
new bytes[](0)
47+
);
48+
49+
emit CrosschainNonFungibleTransferSent(sendId, from, to, tokenId);
50+
51+
return sendId;
52+
}
53+
54+
/// @inheritdoc ERC7786Recipient
55+
function _processMessage(
56+
address /*gateway*/,
57+
bytes32 receiveId,
58+
bytes calldata /*sender*/,
59+
bytes calldata payload
60+
) internal virtual override {
61+
// split payload
62+
(bytes memory from, bytes memory toEvm, uint256 tokenId) = abi.decode(payload, (bytes, bytes, uint256));
63+
address to = address(bytes20(toEvm));
64+
65+
_onReceive(to, tokenId);
66+
67+
emit CrosschainNonFungibleTransferReceived(receiveId, from, to, tokenId);
68+
}
69+
70+
/// @dev Virtual function: implementation is required to handle token being burnt or locked on the source chain.
71+
function _onSend(address from, uint256 tokenId) internal virtual;
72+
73+
/// @dev Virtual function: implementation is required to handle token being minted or unlocked on the destination chain.
74+
function _onReceive(address to, uint256 tokenId) internal virtual;
75+
}

contracts/token/ERC721/README.adoc

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ OpenZeppelin Contracts provides implementations of all four interfaces:
2222
2323
Additionally there are a few of other extensions:
2424

25+
* {ERC721Burnable}: A way for token holders to burn their own tokens.
2526
* {ERC721Consecutive}: An implementation of https://eips.ethereum.org/EIPS/eip-2309[ERC-2309] for minting batches of tokens during construction, in accordance with ERC-721.
27+
* {ERC721Crosschain}: Embedded {BridgeNonFungible} bridge, making the token crosschain through the use of ERC-7786 gateways.
28+
* {ERC721Pausable}: A primitive to pause contract operation.
29+
* {ERC721Royalty}: A way to signal royalty information following ERC-2981.
2630
* {ERC721URIStorage}: A more flexible but more expensive way of storing metadata.
2731
* {ERC721Votes}: Support for voting and vote delegation.
28-
* {ERC721Royalty}: A way to signal royalty information following ERC-2981.
29-
* {ERC721Pausable}: A primitive to pause contract operation.
30-
* {ERC721Burnable}: A way for token holders to burn their own tokens.
3132
* {ERC721Wrapper}: Wrapper to create an ERC-721 backed by another ERC-721, with deposit and withdraw methods. Useful in conjunction with {ERC721Votes}.
3233
3334
NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC-721 (such as <<ERC721-_mint-address-uint256-,`_mint`>>) and expose them as external functions in the way they prefer.
@@ -48,18 +49,20 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel
4849

4950
== Extensions
5051

51-
{{ERC721Pausable}}
52-
5352
{{ERC721Burnable}}
5453

5554
{{ERC721Consecutive}}
5655

57-
{{ERC721URIStorage}}
56+
{{ERC721Crosschain}}
5857

59-
{{ERC721Votes}}
58+
{{ERC721Pausable}}
6059

6160
{{ERC721Royalty}}
6261

62+
{{ERC721URIStorage}}
63+
64+
{{ERC721Votes}}
65+
6366
{{ERC721Wrapper}}
6467

6568
== Utilities
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.26;
4+
5+
import {ERC721} from "../ERC721.sol";
6+
import {BridgeNonFungible} from "../../../crosschain/bridges/abstract/BridgeNonFungible.sol";
7+
8+
/**
9+
* @dev Extension of {ERC721} that makes it natively cross-chain using the ERC-7786 based {BridgeNonFungible}.
10+
*
11+
* This extension makes the token compatible with:
12+
* * {ERC721Crosschain} instances on other chains,
13+
* * {ERC721} instances on other chains that are bridged using {BridgeERC721},
14+
*/
15+
// slither-disable-next-line locked-ether
16+
abstract contract ERC721Crosschain is ERC721, BridgeNonFungible {
17+
/// @dev Crosschain variant of {transferFrom}, using the allowance system from the underlying ERC-721 token.
18+
function crosschainTransferFrom(address from, bytes memory to, uint256 tokenId) public virtual returns (bytes32) {
19+
// operator (_msgSender) permission over `from` is checked in `_onSend`
20+
return _crosschainTransfer(from, to, tokenId);
21+
}
22+
23+
/// @dev "Locking" tokens is achieved through burning
24+
function _onSend(address from, uint256 tokenId) internal virtual override {
25+
address previousOwner = _update(address(0), tokenId, _msgSender());
26+
if (previousOwner == address(0)) {
27+
revert ERC721NonexistentToken(tokenId);
28+
} else if (previousOwner != from) {
29+
revert ERC721IncorrectOwner(from, tokenId, previousOwner);
30+
}
31+
}
32+
33+
/// @dev "Unlocking" tokens is achieved through minting
34+
function _onReceive(address to, uint256 tokenId) internal virtual override {
35+
_mint(to, tokenId);
36+
}
37+
}

0 commit comments

Comments
 (0)