Skip to content
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

Add CrossVMBridgeFulfillment base contracts #168

Merged
merged 17 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from 13 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
2 changes: 1 addition & 1 deletion cadence/tests/serialize_metadata_tests.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ fun testSerializeNFTSucceeds() {

// Cadence dictionaries are not ordered by insertion order, so we need to check for both possible orderings
let expectedPrefix = "data:application/json;utf8,{\"name\": \"ExampleNFT\", \"description\": \"Example NFT Collection\", \"image\": \"https://flow.com/examplenft.jpg\", \"external_url\": \"https://example-nft.onflow.org\", "
let altSuffix1 = "\"attributes\": [{\"trait_type\": \"mintedBlock\", \"value\": \"".concat(heightString).concat("\"}, {\"trait_type\": \"foo\", \"value\": \"nil\"}]}")
let altSuffix1 = "\"attributes\": [{\"trait_type\": \"mintedBlock\", \"value\": \"".concat(heightString).concat("\"}, {\"trait_type\": \"foo\", \"value\": \"bar\"}]}")
let altSuffix2 = "\"attributes\": [{\"trait_type\": \"foo\", \"value\": \"nil\"}, {\"trait_type\": \"mintedBlock\", \"value\": \"".concat(heightString).concat("\"}]}")

let idsResult = executeScript(
Expand Down
65 changes: 65 additions & 0 deletions solidity/src/example-assets/CadenceNativeERC721.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
pragma solidity 0.8.24;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {CrossVMBridgeERC721Fulfillment} from "../interfaces/CrossVMBridgeERC721Fulfillment.sol";

/**
* @title CadenceNativeERC721
* @dev This contract is a minimal ERC721 implementation demonstrating the use of the
* CrossVMBridgeERC721Fulfillment base contract. Such ERC721 contracts are intended for use in
* cross-VM NFT implementations where projects deploy both a Cadence & Solidity definition with
* movement of individual NFTs facilitated by Flow's canonical VM bridge.
* In such cases, NFTs must be distributed in either Cadence or EVM - this is termed the NFT's
* "native" VM. When moving the NFT into the non-native VM, the bridge implements a mint/escrow
* pattern, minting if the NFT does not exist and unlocking from escrow if it does.
* The contract below demonstrates the Solidity implementation for a Cadence-native NFT. By
* implementing CrossVMBridgeERC721Fulfillment and correctly naming the vmBridgeAddress as the
* bridge's CadenceOwnedAccount EVM address, this ERC721 enables the bridge to execute the
* mint/escrow needed to fulfill bridge requests.
*
* For more information on cross-VM NFTs, see Flow's developer documentation as well as
* FLIP-318: https://github.com/onflow/flips/issues/318
*/
contract CadenceNativeERC721 is CrossVMBridgeERC721Fulfillment {

// included to test before & after fulfillment hooks
uint256 public beforeCounter;
uint256 public afterCounter;

constructor(
string memory name_,
string memory symbol_,
address _vmBridgeAddress
) CrossVMBridgeERC721Fulfillment(_vmBridgeAddress) ERC721(name_, symbol_) {}

/**
* @dev This hook executes before the fulfillment into EVM executes. It's overridden here as
* a simple demonstration and for testing; however, you might include your own validation or
* pre-processing.
*
* @param _to address of the pending token recipient
* @param _id the id of the token to be moved into EVM from Cadence
* @param _data any encoded metadata passed by the corresponding Cadence NFT at the time of
* bridging into EVM
*/
function _beforeFulfillment(address _to, uint256 _id, bytes memory _data) internal override {
beforeCounter += 1;
}

/**
* @dev This hook executes after the fulfillment into EVM executes. It's overridden here as
* a simple demonstration and for testing; however, you might include your own validation or
* post-processing. For instance, you may decode the bytes passed by the VM bridge at the
* time of bridging into EVM and update the token's metadata. Since you presumably control the
* corresponding Cadence implementation, what is passed to your at fulfillment is in your
* control by having your Cadence NFT resolve the `EVMBytesMetadata` view.
*
* @param _to address of the pending token recipient
* @param _id the id of the token to be moved into EVM from Cadence
* @param _data any encoded metadata passed by the corresponding Cadence NFT at the time of
* bridging into EVM
*/
function _afterFulfillment(address _to, uint256 _id, bytes memory _data) internal override {
afterCounter += 1;
}
}
57 changes: 57 additions & 0 deletions solidity/src/interfaces/CrossVMBridgeCallable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: Unlicense
pragma solidity 0.8.24;

import {ICrossVMBridgeCallable} from "./ICrossVMBridgeCallable.sol";
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";

/**
* @title CrossVMBridgeCallable
* @dev A base contract intended for use in implementations on Flow, allowing a contract to define
* access to the Cadence X EVM bridge on certain methods.
*/
abstract contract CrossVMBridgeCallable is ICrossVMBridgeCallable, Context, ERC165 {

address private _vmBridgeAddress;

/**
* @dev Sets the bridge EVM address such that only the bridge COA can call the privileged methods
*/
constructor(address vmBridgeAddress_) {
if (vmBridgeAddress_ == address(0)) {
revert CrossVMBridgeCallableZeroInitialization();
}
_vmBridgeAddress = vmBridgeAddress_;
}

/**
* @dev Modifier restricting access to the designated VM bridge EVM address
*/
modifier onlyVMBridge() {
_checkVMBridgeAddress();
_;
}

/**
* @dev Returns the designated VM bridge’s EVM address
*/
function vmBridgeAddress() public view virtual returns (address) {
return _vmBridgeAddress;
}

/**
* @dev Checks that msg.sender is the designated VM bridge address
*/
function _checkVMBridgeAddress() internal view virtual {
if (_vmBridgeAddress != _msgSender()) {
revert CrossVMBridgeCallableUnauthorizedAccount(_msgSender());
}
}

/**
* @dev Allows a caller to determine the contract conforms to the `ICrossVMFulfillment` interface
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165) returns (bool) {
return interfaceId == type(ICrossVMBridgeCallable).interfaceId || super.supportsInterface(interfaceId);
}
}
106 changes: 106 additions & 0 deletions solidity/src/interfaces/CrossVMBridgeERC721Fulfillment.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: Unlicense
pragma solidity 0.8.24;

import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ICrossVMBridgeERC721Fulfillment} from "./ICrossVMBridgeERC721Fulfillment.sol";
import {ICrossVMBridgeCallable} from "./CrossVMBridgeCallable.sol";
import {CrossVMBridgeCallable} from "./CrossVMBridgeCallable.sol";

/**
* @title CrossVMBridgeERC721Fulfillment
* @dev Related to https://github.com/onflow/flips/issues/318[FLIP-318] Cross VM NFT implementations
* on Flow in the context of Cadence-native NFTs. The following base contract should be implemented to
* integrate with the Flow VM bridge connecting Cadence & EVM implementations so that the canonical
* VM bridge may move the Cadence NFT into EVM in a mint/escrow pattern.
*/
abstract contract CrossVMBridgeERC721Fulfillment is ICrossVMBridgeERC721Fulfillment, CrossVMBridgeCallable, ERC721 {

/**
* Initializes the bridge EVM address such that only the bridge COA can call privileged methods
*/
constructor(address _vmBridgeAddress) CrossVMBridgeCallable(_vmBridgeAddress) {}

/**
* @dev Fulfills the bridge request, minting (if non-existent) or transferring (if escrowed) the
* token with the given ID to the provided address. For dynamic metadata handling between
* Cadence & EVM, implementations should override and assign metadata as encoded from Cadence
* side. If overriding, be sure to preserve the mint/escrow pattern as shown in the default
* implementation. See `_beforeFulfillment` and `_afterFulfillment` hooks to enable pre-and/or
* post-processing without the need to override this function.
*
* @param _to address of the token recipient
* @param _id the id of the token being moved into EVM from Cadence
* @param _data any encoded metadata passed by the corresponding Cadence NFT at the time of
* bridging into EVM
*/
function fulfillToEVM(address _to, uint256 _id, bytes memory _data) external onlyVMBridge {
_beforeFulfillment(_to, _id, _data); // hook allowing implementation to perform pre-fulfillment validation
if (_ownerOf(_id) == address(0)) {
_mint(_to, _id); // Doesn't exist, mint the token
} else {
// Should be escrowed under vm bridge - transfer from escrow to recipient
_requireEscrowed(_id);
safeTransferFrom(vmBridgeAddress(), _to, _id);
}
_afterFulfillment(_to, _id, _data); // hook allowing implementation to perform post-fulfillment processing
emit FulfilledToEVM(_to, _id);
}

/**
* @dev Returns whether the token is currently escrowed under custody of the designated VM bridge
*
* @param _id the ID of the token in question
*/
function isEscrowed(uint256 _id) public view returns (bool) {
return _ownerOf(_id) == vmBridgeAddress();
}

/**
* @dev Allows a caller to determine the contract conforms to implemented interfaces
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(CrossVMBridgeCallable, ERC721, IERC165) returns (bool) {
return interfaceId == type(ICrossVMBridgeERC721Fulfillment).interfaceId
|| interfaceId == type(ICrossVMBridgeCallable).interfaceId
|| super.supportsInterface(interfaceId);
}

/**
* @dev Internal method that reverts with FulfillmentFailedTokenNotEscrowed if the provided
* token is not escrowed with the assigned vm bridge address as owner.
*
* @param _id the token id that must be escrowed
*/
function _requireEscrowed(uint256 _id) internal view {
if (!isEscrowed(_id)) {
revert FulfillmentFailedTokenNotEscrowed(_id, vmBridgeAddress());
}
}

/**
* @dev This internal method is included as a step implementations can override and have
* executed in the default fullfillToEVM call.
*
* @param _to address of the pending token recipient
* @param _id the id of the token to be moved into EVM from Cadence
* @param _data any encoded metadata passed by the corresponding Cadence NFT at the time of
* bridging into EVM
*/
function _beforeFulfillment(address _to, uint256 _id, bytes memory _data) internal virtual {
// No-op by default, meant to be overridden by implementations
}

/**
* @dev This internal method is included as a step implementations can override and have
* executed in the default fullfillToEVM call.
*
* @param _to address of the pending token recipient
* @param _id the id of the token to be moved into EVM from Cadence
* @param _data any encoded metadata passed by the corresponding Cadence NFT at the time of
* bridging into EVM
*/
function _afterFulfillment(address _to, uint256 _id, bytes memory _data) internal virtual {
// No-op by default, meant to be overridden by implementations for things like processing
// and setting metadata
}
}
22 changes: 22 additions & 0 deletions solidity/src/interfaces/ICrossVMBridgeCallable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: Unlicense
pragma solidity 0.8.24;

import {Context} from "@openzeppelin/contracts/utils/Context.sol";

/**
* @title ICrossVMBridgeCallable
* @dev An interface intended for use by implementations on Flow EVM, allowing a contract to define
* access to the Cadence X EVM bridge on certain methods.
*/
interface ICrossVMBridgeCallable {

/// @dev Should encounter when the vmBridgeAddress is initialized to 0x0
error CrossVMBridgeCallableZeroInitialization();
/// @dev Should encounter when a VM bridge privileged method is triggered by unauthorized caller
error CrossVMBridgeCallableUnauthorizedAccount(address account);

/**
* @dev Returns the designated VM bridge’s EVM address
*/
function vmBridgeAddress() external view returns (address);
}
42 changes: 42 additions & 0 deletions solidity/src/interfaces/ICrossVMBridgeERC721Fulfillment.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
pragma solidity 0.8.24;

import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";

/**
* @title ICrossVMBridgeERC721Fulfillment
* @dev Related to https://github.com/onflow/flips/issues/318[FLIP-318] Cross VM NFT implementations
* on Flow in the context of Cadence-native NFTs. The following interface must be implemented to
* integrate with the Flow VM bridge connecting Cadence & EVM implementations so that the canonical
* VM bridge may move the Cadence NFT into EVM in a mint/escrow pattern.
*/
interface ICrossVMBridgeERC721Fulfillment is IERC165 {

// Encountered when attempting to fulfill a token that has been previously minted and is not
// escrowed in EVM under the VM bridge
error FulfillmentFailedTokenNotEscrowed(uint256 id, address escrowAddress);

// Emitted when an NFT is moved from Cadence into EVM
event FulfilledToEVM(address indexed recipient, uint256 indexed tokenId);

/**
* @dev Returns whether the token is currently escrowed under custody of the designated VM bridge
*
* @param _id the ID of the token in question
*/
function isEscrowed(uint256 _id) external view returns (bool);

/**
* @dev Fulfills the bridge request, minting (if non-existent) or transferring (if escrowed) the
* token with the given ID to the provided address. For dynamic metadata handling between
* Cadence & EVM, implementations should override and assign metadata as encoded from Cadence
* side. If overriding, be sure to preserve the mint/escrow pattern as shown in the default
* implementation. See `_beforeFulfillment` and `_afterFulfillment` hooks to enable pre-and/or
* post-processing without the need to override this function.
*
* @param _to address of the token recipient
* @param _id the id of the token being moved into EVM from Cadence
* @param _data any encoded metadata passed by the corresponding Cadence NFT at the time of
* bridging into EVM
*/
function fulfillToEVM(address _to, uint256 _id, bytes memory _data) external;
}
Loading
Loading