Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/fruity-coats-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`BridgeERC721Core` and `BridgeERC721`: Added bridge contracts to handle crosschain movements of ERC-721 tokens.
5 changes: 5 additions & 0 deletions .changeset/tidy-turkeys-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`ERC721Crosschain`: Added an ERC-721 extension to embed an ERC-7786 based crosschain bridge directly in the token contract.
8 changes: 7 additions & 1 deletion contracts/crosschain/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Additionally there are multiple bridge constructions:
* {BridgeERC20Core}: Core bridging logic for crosschain ERC-20 transfer. Used by {BridgeERC20}, {BridgeERC7802} and {ERC20Crosschain},
* {BridgeERC20}: Standalone bridge contract to connect an ERC-20 token contract with counterparts on remote chains,
* {BridgeERC7802}: Standalone bridge contract to connect an ERC-7802 token contract with counterparts on remote chains.
* {BridgeERC721Core}: Core bridging logic for crosschain ERC-721 transfer. Used by {BridgeERC721} and {ERC721Crosschain},
* {BridgeERC721}: Standalone bridge contract to connect an ERC-721 token contract with counterparts on remote chains,

== Helpers

Expand All @@ -26,4 +28,8 @@ Additionally there are multiple bridge constructions:

{{BridgeERC20}}

{{BridgeERC7802}}
{{BridgeERC7802}}

{{BridgeERC721Core}}

{{BridgeERC721}}
63 changes: 63 additions & 0 deletions contracts/crosschain/bridges/BridgeERC721.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.26;

import {IERC721} from "../../interfaces/IERC721.sol";
import {IERC721Errors} from "../../interfaces/draft-IERC6093.sol";
import {BridgeERC721Core} from "./BridgeERC721Core.sol";

/**
* @dev This is a variant of {BridgeERC721Core} that implements the bridge logic for ERC-721 tokens that do not expose
* a crosschain mint and burn mechanism. Instead, it takes custody of bridged assets.
*/
// slither-disable-next-line locked-ether
abstract contract BridgeERC721 is BridgeERC721Core {
IERC721 private immutable _token;

error BridgeERC721Unauthorized(address caller);

constructor(IERC721 token_) {
_token = token_;
}

/// @dev Return the address of the ERC721 token this bridge operates on.
function token() public view virtual returns (IERC721) {
return _token;
}

/**
* @dev Transfer `tokenId` from `from` (on this chain) to `to` (on a different chain).
*
* The `to` parameter is the full InteroperableAddress that references both the destination chain and the account
. on that chain. Similarly to the underlying token's {ERC721-transferFrom} function, this function can be called
* either by the token holder or by anyone that is approved by the token holder. It reuses the token's allowance
* system, meaning that an account that is "approved for all" or "approved for tokenId" can perform the crosschain
* transfer directly without having to take temporary custody of the token.
*/
function crosschainTransferFrom(address from, bytes memory to, uint256 tokenId) public virtual returns (bytes32) {
// Permission is handled using the ERC721's allowance system. This check replicates `ERC721._isAuthorized`.
address spender = _msgSender();
require(
from == spender || token().isApprovedForAll(from, spender) || token().getApproved(tokenId) == spender,
IERC721Errors.ERC721InsufficientApproval(spender, tokenId)
);

// This call verifies that `from` is the owner of `tokenId` (in `_onSend`), and the previous checks ensure
// that `spender` is allowed to move tokenId on behalf of `from`.
//
// Perform the crosschain transfer and return the send id
return _crosschainTransfer(from, to, tokenId);
}

/// @dev "Locking" tokens is done by taking custody
function _onSend(address from, uint256 tokenId) internal virtual override {
// slither-disable-next-line arbitrary-send-erc20
token().transferFrom(from, address(this), tokenId);
}

/// @dev "Unlocking" tokens is done by releasing custody
function _onReceive(address to, uint256 tokenId) internal virtual override {
// slither-disable-next-line arbitrary-send-erc20
token().transferFrom(address(this), to, tokenId);
}
}
69 changes: 69 additions & 0 deletions contracts/crosschain/bridges/BridgeERC721Core.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.26;

import {InteroperableAddress} from "../../utils/draft-InteroperableAddress.sol";
import {Context} from "../../utils/Context.sol";
import {ERC7786Recipient} from "../ERC7786Recipient.sol";
import {CrosschainLinked} from "../CrosschainLinked.sol";

/**
* @dev Base contract for bridging ERC-721 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 in. Must give tokens to the receiver.
*
* This base contract is used by the {BridgeERC721}, which interfaces with legacy ERC-721 tokens. It is also used by
* the {ERC721Crosschain} extension, which embeds the bridge logic directly in the token contract.
*/
abstract contract BridgeERC721Core is Context, CrosschainLinked {
using InteroperableAddress for bytes;

event CrosschainERC721TransferSent(bytes32 indexed sendId, address indexed from, bytes to, uint256 tokenId);
event CrosschainERC721TransferReceived(bytes32 indexed receiveId, bytes from, address indexed to, uint256 tokenId);

/**
* @dev Internal crosschain transfer function.
*
* Note: The `to` parameter is the full InteroperableAddress (chain ref + address).
*/
function _crosschainTransfer(address from, bytes memory to, uint256 tokenId) internal virtual returns (bytes32) {
_onSend(from, tokenId);

(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, tokenId),
new bytes[](0)
);

emit CrosschainERC721TransferSent(sendId, from, to, tokenId);

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 tokenId) = abi.decode(payload, (bytes, bytes, uint256));
address to = address(bytes20(toBinary));

_onReceive(to, tokenId);

emit CrosschainERC721TransferReceived(receiveId, from, to, tokenId);
}

/// @dev Virtual function: implementation is required to handle token being burnt or locked on the source chain.
function _onSend(address from, uint256 tokenId) internal virtual;

/// @dev Virtual function: implementation is required to handle token being minted or unlocked on the destination chain.
function _onReceive(address to, uint256 tokenId) internal virtual;
}
17 changes: 10 additions & 7 deletions contracts/token/ERC721/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ OpenZeppelin Contracts provides implementations of all four interfaces:

Additionally there are a few of other extensions:

* {ERC721Burnable}: A way for token holders to burn their own tokens.
* {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.
* {ERC721Crosschain}: Embedded {BridgeERC721Core} bridge, making the token crosschain through the use of ERC-7786 gateways.
* {ERC721Pausable}: A primitive to pause contract operation.
* {ERC721Royalty}: A way to signal royalty information following ERC-2981.
* {ERC721URIStorage}: A more flexible but more expensive way of storing metadata.
* {ERC721Votes}: Support for voting and vote delegation.
* {ERC721Royalty}: A way to signal royalty information following ERC-2981.
* {ERC721Pausable}: A primitive to pause contract operation.
* {ERC721Burnable}: A way for token holders to burn their own tokens.
* {ERC721Wrapper}: Wrapper to create an ERC-721 backed by another ERC-721, with deposit and withdraw methods. Useful in conjunction with {ERC721Votes}.

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.
Expand All @@ -48,18 +49,20 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel

== Extensions

{{ERC721Pausable}}

{{ERC721Burnable}}

{{ERC721Consecutive}}

{{ERC721URIStorage}}
{{ERC721Crosschain}}

{{ERC721Votes}}
{{ERC721Pausable}}

{{ERC721Royalty}}

{{ERC721URIStorage}}

{{ERC721Votes}}

{{ERC721Wrapper}}

== Utilities
Expand Down
37 changes: 37 additions & 0 deletions contracts/token/ERC721/extensions/ERC721Crosschain.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.26;

import {ERC721} from "../ERC721.sol";
import {BridgeERC721Core} from "../../../crosschain/bridges/BridgeERC721Core.sol";

/**
* @dev Extension of {ERC721} that makes it natively cross-chain using the ERC-7786 based {BridgeERC721Core}.
*
* This extension makes the token compatible with:
* * {ERC721Crosschain} instances on other chains,
* * {ERC721} instances on other chains that are bridged using {BridgeERC721},
*/
// slither-disable-next-line locked-ether
abstract contract ERC721Crosschain is ERC721, BridgeERC721Core {
/// @dev Crosschain variant of {transferFrom}, using the allowance system from the underlying ERC-721 token.
function crosschainTransferFrom(address from, bytes memory to, uint256 tokenId) public virtual returns (bytes32) {
// operator (_msgSender) permission over `from` is checked in `_onSend`
return _crosschainTransfer(from, to, tokenId);
}

/// @dev "Locking" tokens is achieved through burning
function _onSend(address from, uint256 tokenId) internal virtual override {
address previousOwner = _update(address(0), tokenId, _msgSender());
if (previousOwner == address(0)) {
revert ERC721NonexistentToken(tokenId);
} else if (previousOwner != from) {
revert ERC721IncorrectOwner(from, tokenId, previousOwner);
}
}

/// @dev "Unlocking" tokens is achieved through minting
function _onReceive(address to, uint256 tokenId) internal virtual override {
_mint(to, tokenId);
}
}
Loading
Loading