diff --git a/cadence/contracts/bridge/FlowEVMBridge.cdc b/cadence/contracts/bridge/FlowEVMBridge.cdc index eac6fb1f..27fc8037 100644 --- a/cadence/contracts/bridge/FlowEVMBridge.cdc +++ b/cadence/contracts/bridge/FlowEVMBridge.cdc @@ -3,6 +3,7 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "NonFungibleToken" import "MetadataViews" +import "CrossVMMetadataViews" import "ViewResolver" import "EVM" @@ -15,6 +16,7 @@ import "IFlowEVMNFTBridge" import "IFlowEVMTokenBridge" import "CrossVMNFT" import "CrossVMToken" +import "FlowEVMBridgeCustomAssociations" import "FlowEVMBridgeConfig" import "FlowEVMBridgeHandlerInterfaces" import "FlowEVMBridgeUtils" @@ -56,7 +58,6 @@ contract FlowEVMBridge : IFlowEVMNFTBridge, IFlowEVMTokenBridge { Public Onboarding **************************/ - /// Onboards a given asset by type to the bridge. Since we're onboarding by Cadence Type, the asset must be defined /// in a third-party contract. Attempting to onboard a bridge-defined asset will result in an error as the asset has /// already been onboarded to the bridge. @@ -69,10 +70,10 @@ contract FlowEVMBridge : IFlowEVMNFTBridge, IFlowEVMTokenBridge { pre { !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused" !FlowEVMBridgeConfig.isCadenceTypeBlocked(type): - "This Cadence Type ".concat(type.identifier).concat(" is currently blocked from being onboarded") + "This Cadence Type ".concat(type.identifier).concat(" is currently blocked from being onboarded") self.typeRequiresOnboarding(type) == true: "Onboarding is not needed for this type" FlowEVMBridgeUtils.typeAllowsBridging(type): - "This type is not supported as defined by the project's development team" + "This Cadence Type ".concat(type.identifier).concat(" is currently opted-out of bridge onboarding") FlowEVMBridgeUtils.isCadenceNative(type: type): "Only Cadence-native assets can be onboarded by Type" } /* Provision fees */ @@ -168,6 +169,81 @@ contract FlowEVMBridge : IFlowEVMNFTBridge, IFlowEVMTokenBridge { self.deployDefiningContract(evmContractAddress: address) } + access(all) + fun registerCrossVMNFT( + type: Type, + fulfillmentMinter: Capability? + ) { + pre { + FlowEVMBridgeUtils.typeAllowsBridging(type): + "This Cadence Type ".concat(type.identifier).concat(" is currently opted-out of bridge onboarding") + type.isSubtype(of: Type<@{NonFungibleToken.NFT}>()): + "The provided Type ".concat(type.identifier).concat(" is not an NFT - only NFTs can register as cross-VM") + !type.isSubtype(of: Type<@{FungibleToken.Vault}>()): + "The provided Type ".concat(type.identifier).concat(" is also a FungibleToken Vault - only NFTs can register as cross-VM") + FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type) == nil: + "A custom association has already been registered for type ".concat(type.identifier) + .concat(" with EVM contract ") + .concat(FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type)!.toString()) + !FlowEVMBridgeConfig.isCadenceTypeBlocked(type): + "Type ".concat(type.identifier).concat(" has been blocked from onboarding") + } + // Get the Cadence side EVMPointer + let evmPointer = FlowEVMBridgeUtils.getEVMPointer(forType: type) + ?? panic("The CrossVMMetadataViews.EVMPointer is not supported by the type ".concat(type.identifier)) + assert(!FlowEVMBridgeConfig.isEVMAddressBlocked(evmPointer.evmContractAddress), + message: "Type ".concat(type.identifier).concat(" has been blocked from onboarding")) + assert( + FlowEVMBridgeUtils.evmAddressAllowsBridging(evmPointer.evmContractAddress), + message: "This contract is not supported as defined by the project's development team" + ) + + // Get pointer on EVM side + let cadenceAddr = FlowEVMBridgeUtils.getCorrespondingCadenceAddressFromCrossVM(evmContract: evmPointer.evmContractAddress) + let cadenceType = FlowEVMBridgeUtils.getCorrespondingCadenceTypeFromCrossVM(evmContract: evmPointer.evmContractAddress) + + // Assert both point to each other + assert( + type.address == cadenceAddr, + message: "Mismatched Cadence Address pointers: ".concat(type.address!.toString()).concat(" and ").concat(cadenceAddr.toString()) + ) + assert( + type == cadenceType, + message: "Mistmatched type pointers: ".concat(type.identifier).concat(" and ").concat(cadenceType.identifier) + ) + + // if evm-native, check supportsInterface() for CrossVMBridgeERC721Fulfillment + if evmPointer.nativeVM == CrossVMMetadataViews.VM.Cadence { + assert( + FlowEVMBridgeUtils.supportsCadenceNativeNFTEVMInterfaces(evmContract: evmPointer.evmContractAddress), + message: + "Corresponding EVM contract does not implement necessary EVM interfaces ICrossVMBridgeERC721Fulfillment and/or ICrossVMBridgeCallable. " + .concat("All Cadence-native cross-VM NFTs must implement these interfaces and grant the bridge COA") + .concat(" the ability to fulfill bridge requests moving NFTs into EVM.") + ) + let designatedVMBridgeAddress = FlowEVMBridgeUtils.getVMBridgeAddressFromICrossVMBridgeCallable(evmContract: evmPointer.evmContractAddress) + assert( + designatedVMBridgeAddress.equals(FlowEVMBridgeUtils.getBridgeCOAEVMAddress()), + message: "ICrossVMBridgeCallable declared ".concat(designatedVMBridgeAddress.toString()) + .concat(" as vmBridgeAddress which must be declared as ") + .concat(FlowEVMBridgeUtils.getBridgeCOAEVMAddress().toString()) + ) + } + + // determine if onboarded via permissionless path + let updatedFromBridged = FlowEVMBridgeConfig.getEVMAddressAssociated(with: type) != nil + || FlowEVMBridgeConfig.getTypeAssociated(with: evmPointer.evmContractAddress) != nil + + // saveCustomAssociation + FlowEVMBridgeCustomAssociations.saveCustomAssociation( + type: type, + evmContractAddress: evmPointer.evmContractAddress, + nativeVM: evmPointer.nativeVM, + updatedFromBridged: updatedFromBridged, + fulfillmentMinter: fulfillmentMinter + ) + } + /************************* NFT Handling **************************/ diff --git a/cadence/contracts/bridge/FlowEVMBridgeConfig.cdc b/cadence/contracts/bridge/FlowEVMBridgeConfig.cdc index 912940c9..1879a12a 100644 --- a/cadence/contracts/bridge/FlowEVMBridgeConfig.cdc +++ b/cadence/contracts/bridge/FlowEVMBridgeConfig.cdc @@ -1,6 +1,7 @@ import "EVM" import "FlowEVMBridgeHandlerInterfaces" +import "FlowEVMBridgeCustomAssociations" /// This contract is used to store configuration information shared by FlowEVMBridge contracts /// @@ -116,10 +117,11 @@ contract FlowEVMBridgeConfig { /// access(all) view fun getEVMAddressAssociated(with type: Type): EVM.EVMAddress? { - if !self.typeHasTokenHandler(type) { - return self.registeredTypes[type]?.evmAddress + if self.typeHasTokenHandler(type) { + return self.borrowTokenHandler(type)!.getTargetEVMAddress() } - return self.borrowTokenHandler(type)!.getTargetEVMAddress() + let customAssociation = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type) + return customAssociation ?? self.registeredTypes[type]?.evmAddress } /// Retrieves the type associated with a given EVMAddress if it has been onboarded to the bridge @@ -127,7 +129,8 @@ contract FlowEVMBridgeConfig { access(all) view fun getTypeAssociated(with evmAddress: EVM.EVMAddress): Type? { let evmAddressHex = evmAddress.toString() - return self.evmAddressHexToType[evmAddressHex] + let customAssociation = FlowEVMBridgeCustomAssociations.getTypeAssociated(with: evmAddress) + return customAssociation ?? self.evmAddressHexToType[evmAddressHex] } /// Returns whether the given EVMAddress is currently blocked from onboarding to the bridge diff --git a/cadence/contracts/bridge/FlowEVMBridgeCustomAssociations.cdc b/cadence/contracts/bridge/FlowEVMBridgeCustomAssociations.cdc new file mode 100644 index 00000000..96f65981 --- /dev/null +++ b/cadence/contracts/bridge/FlowEVMBridgeCustomAssociations.cdc @@ -0,0 +1,231 @@ +import "NonFungibleToken" +import "CrossVMMetadataViews" +import "EVM" + +/// The FlowEVMBridgeCustomAssociations is tasked with preserving custom associations between Cadence assets and their +/// EVM implementations. These associations should be validated before `saveCustomAssociation` is called by +/// leveraging the interfaces outlined in FLIP-318 (https://github.com/onflow/flips/issues/318) to ensure that the +/// declared association is valid and that neither implementation is bridge-defined. +/// +access(all) contract FlowEVMBridgeCustomAssociations { + + /// Stored associations indexed by Cadence Type + access(self) let associationsConfig: @{Type: CustomConfig} + /// Reverse lookup indexed on serialized EVM contract address + access(self) let associationsByEVMAddress: {String: Type} + + /// Event emitted whenever a custom association is established + access(all) event CustomAssociationEstablished( + type: Type, + evmContractAddress: String, + nativeVMRawValue: UInt8, + updatedFromBridged: Bool, + fulfillmentMinterType: String?, + fulfillmentMinterOrigin: Address?, + fulfillmentMinterCapID: UInt64?, + fulfillmentMinterUUID: UInt64?, + configUUID: UInt64 + ) + + access(all) + view fun getEVMAddressAssociated(with type: Type): EVM.EVMAddress? { + return self.associationsConfig[type]?.getEVMContractAddress() ?? nil + } + + access(all) + view fun getTypeAssociated(with evmAddress: EVM.EVMAddress): Type? { + return self.associationsByEVMAddress[evmAddress.toString()] + } + + access(all) + fun getEVMPointerAsRegistered(forType: Type): CrossVMMetadataViews.EVMPointer? { + if let config = &self.associationsConfig[forType] as &CustomConfig? { + return CrossVMMetadataViews.EVMPointer( + cadenceType: config.getCadenceType(), + cadenceContractAddress: config.getCadenceType().address!, + evmContractAddress: config.getEVMContractAddress(), + nativeVM: config.getNativeVM() + ) + } + return nil + } + + /// Allows the bridge contracts to preserve a custom association. Will revert if a custom association already exists + /// + /// @param type: The Cadence Type of the associated asset. + /// @param evmContractAddress: The EVM address defining the EVM implementation of the associated asset. + /// @param nativeVM: The VM in which the asset is distributed by the project. The bridge will mint/escrow in the non-native + /// VM environment. + /// @param updatedFromBridged: Whether the asset was originally onboarded to the bridge via permissionless + /// onboarding. In other words, whether there was first a bridge-defined implementation of the underlying asset. + /// @param fulfillmentMinter: An authorized Capability allowing the bridge to fulfill bridge requests moving the + /// underlying asset from EVM. Required if the asset is EVM-native. + /// + access(account) + fun saveCustomAssociation( + type: Type, + evmContractAddress: EVM.EVMAddress, + nativeVM: CrossVMMetadataViews.VM, + updatedFromBridged: Bool, + fulfillmentMinter: Capability? + ) { + pre { + self.associationsConfig[type] == nil: + "Type ".concat(type.identifier).concat(" already has a custom association with ") + .concat(self.borrowCustomConfig(forType: type)!.evmContractAddress.toString()) + self.associationsByEVMAddress[evmContractAddress.toString()] == nil: + "EVM Address ".concat(evmContractAddress.toString()).concat(" already has a custom association with ") + .concat(self.borrowCustomConfig(forType: type)!.type.identifier) + fulfillmentMinter?.check() ?? true: + "The NFTFulfillmentMinter Capability issued from ".concat(fulfillmentMinter!.address.toString()) + .concat(" is invalid. Ensure the Capability is properly issued and active.") + } + let config <- create CustomConfig( + type: type, + evmContractAddress: evmContractAddress, + nativeVM: nativeVM, + updatedFromBridged: updatedFromBridged, + fulfillmentMinter: fulfillmentMinter + ) + emit CustomAssociationEstablished( + type: type, + evmContractAddress: evmContractAddress.toString(), + nativeVMRawValue: nativeVM.rawValue, + updatedFromBridged: updatedFromBridged, + fulfillmentMinterType: fulfillmentMinter != nil ? fulfillmentMinter!.borrow()!.getType().identifier : nil, + fulfillmentMinterOrigin: fulfillmentMinter?.address ?? nil, + fulfillmentMinterCapID: fulfillmentMinter?.id ?? nil, + fulfillmentMinterUUID: fulfillmentMinter != nil ? fulfillmentMinter!.borrow()!.uuid : nil, + configUUID: config.uuid + ) + self.associationsByEVMAddress[config.evmContractAddress.toString()] = type + self.associationsConfig[type] <-! config + } + + access(all) entitlement FulfillFromEVM + + /// Resource interface used by EVM-native NFT collections allowing for the fulfillment of NFTs from EVM into Cadence + /// + access(all) resource interface NFTFulfillmentMinter { + /// Getter for the type of NFT that's fulfilled by this implementation + /// + access(all) + view fun getFulfilledType(): Type + + /// Called by the VM bridge when moving NFTs from EVM into Cadence if the NFT is not in escrow. Since such NFTs + /// are EVM-native, they are distributed in EVM. On the Cadence side, those NFTs are handled by a mint & escrow + /// pattern. On moving to EVM, the NFTs are minted if not in escrow at the time of bridging. + /// + /// @param id: The id of the token being fulfilled from EVM + /// + /// @return The NFT fulfilled from EVM as its Cadence implementation + /// + access(FulfillFromEVM) + fun fulfillFromEVM(id: UInt256): @{NonFungibleToken.NFT} { + pre { + id < UInt256(UInt64.max): + "The requested ID ".concat(id.toString()) + .concat(" exceeds the maximum assignable Cadence NFT ID ").concat(UInt64.max.toString()) + } + post { + UInt256(result.id) == id: + "Resulting NFT ID ".concat(result.id.toString()) + .concat(" does not match requested ID ").concat(id.toString()) + result.getType() == self.getFulfilledType(): + "Expected ".concat(self.getFulfilledType().identifier).concat(" but fulfilled ") + .concat(result.getType().identifier) + } + } + } + + /// Resource containing all relevant information for the VM bridge to fulfill bridge requests. This is a resource + /// instead of a struct to ensure contained Capabilities cannot be copied + /// + access(all) resource CustomConfig { + /// The Cadence Type of the associated asset. + access(all) let type: Type + /// The EVM address defining the EVM implementation of the associated asset. + access(all) let evmContractAddress: EVM.EVMAddress + /// The VM in which the asset is distributed by the project. The bridge will mint/escrow in the non-native + /// VM environment. + access(all) let nativeVM: CrossVMMetadataViews.VM + /// Whether the asset was originally onboarded to the bridge via permissionless onboarding. In other words, + /// whether there was first a bridge-defined implementation of the underlying asset. + access(all) let updatedFromBridged: Bool + /// An authorized Capability allowing the bridge to fulfill bridge requests moving the underlying asset from + /// EVM. Required if the asset is EVM-native. + access(self) let fulfillmentMinter: Capability? + + init( + type: Type, + evmContractAddress: EVM.EVMAddress, + nativeVM: CrossVMMetadataViews.VM, + updatedFromBridged: Bool, + fulfillmentMinter: Capability? + ) { + pre { + nativeVM == CrossVMMetadataViews.VM.EVM ? fulfillmentMinter != nil : true: + "EVM-native NFTs must provide an NFTFulfillmentMinter Capability." + fulfillmentMinter?.check() ?? true: + "Invalid NFTFulfillmentMinter Capability provided. Ensure the Capability is properly issued and active." + fulfillmentMinter != nil ? fulfillmentMinter!.borrow()!.getFulfilledType() == type : true: + "NFTFulfillmentMinter fulfills ".concat(fulfillmentMinter!.borrow()!.getFulfilledType().identifier) + .concat(" but expected ").concat(type.identifier) + } + self.type = type + self.evmContractAddress = evmContractAddress + self.nativeVM = nativeVM + self.updatedFromBridged = updatedFromBridged + self.fulfillmentMinter = fulfillmentMinter + } + + access(all) + view fun check(): Bool? { + return self.fulfillmentMinter?.check() ?? nil + } + + access(all) + view fun getCadenceType(): Type { + return self.type + } + + access(all) + view fun getEVMContractAddress(): EVM.EVMAddress { + return self.evmContractAddress + } + + access(all) + view fun getNativeVM(): CrossVMMetadataViews.VM { + return self.nativeVM + } + + access(all) + view fun isUpdatedFromBridged(): Bool { + return self.updatedFromBridged + } + + access(account) + view fun borrowFulfillmentMinter(): auth(FulfillFromEVM) &{NFTFulfillmentMinter} { + pre { + self.fulfillmentMinter != nil: + "CustomConfig for type ".concat(self.type.identifier) + .concat(" was not assigned a NFTFulfillmentMinter.") + } + return self.fulfillmentMinter!.borrow() + ?? panic("NFTFulfillmentMinter for type ".concat(self.type.identifier).concat(" is now invalid.")) + } + } + + /// Returns a reference to the CustomConfig if it exists, nil otherwise + /// + access(self) + view fun borrowCustomConfig(forType: Type): &CustomConfig? { + return &self.associationsConfig[forType] + } + + + init() { + self.associationsConfig <- {} + self.associationsByEVMAddress = {} + } +} diff --git a/cadence/contracts/bridge/FlowEVMBridgeUtils.cdc b/cadence/contracts/bridge/FlowEVMBridgeUtils.cdc index b1295ff1..1e689735 100644 --- a/cadence/contracts/bridge/FlowEVMBridgeUtils.cdc +++ b/cadence/contracts/bridge/FlowEVMBridgeUtils.cdc @@ -1,6 +1,7 @@ import "NonFungibleToken" import "FungibleToken" import "MetadataViews" +import "CrossVMMetadataViews" import "FungibleTokenMetadataViews" import "ViewResolver" import "FlowToken" @@ -424,6 +425,25 @@ contract FlowEVMBridgeUtils { ) } + /// Retrieves the EVMPointer view from a given type's defining contract if the view is supported + /// + /// @param from: The type for which to retrieve the EVMPointer view + /// + /// @return The resolved EVMPointer view for the given type or nil if the view is unsupported + /// + access(all) + fun getEVMPointer(forType: Type): CrossVMMetadataViews.EVMPointer? { + let contractAddress = forType.address! + let contractName = forType.contractName! + if let viewResolver = getAccount(contractAddress).contracts.borrow<&{ViewResolver}>(name: contractName) { + return viewResolver.resolveContractView( + resourceType: forType, + viewType: Type() + ) as? CrossVMMetadataViews.EVMPointer? ?? nil + } + return nil + } + /************************ EVM Call Wrappers ************************/ @@ -725,6 +745,148 @@ contract FlowEVMBridgeUtils { return self.ufix64ToUInt256(value: amount, decimals: self.getTokenDecimals(evmContractAddress: erc20Address)) } + /// Gets the corresponding Cadence contract address declared by an EVM contract in conformance to the ICrossVM.sol + /// contract interface. Reverts if the EVM call is unsuccessful. + /// NOTE: Just because an EVM contract declares an association does not mean it it is valid! + /// + /// @param evmContract: The ICrossVM.sol conforming EVM contract from which to retrieve the corresponding Cadence + /// contract address + /// + /// @return The resulting Cadence Address as declared associated by the provided EVM contract + /// + access(all) + fun getCorrespondingCadenceAddressFromCrossVM(evmContract: EVM.EVMAddress): Address { + let cadenceAddrRes = self.call( + signature: "getCadenceAddress()", + targetEVMAddress: evmContract, + args: [], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + assert(cadenceAddrRes.status == EVM.Status.successful) + let decodedCadenceAddr = EVM.decodeABI(types: [Type()], data: cadenceAddrRes.data) + assert(decodedCadenceAddr.length == 1) + var cadenceAddrStr = decodedCadenceAddr[0] as! String + if cadenceAddrStr[1] != "x" { + cadenceAddrStr = "0x".concat(cadenceAddrStr) + } + return Address.fromString(cadenceAddrStr) ?? panic("Could not construct Address from EVM contract's associated Cadence address ".concat(cadenceAddrStr)) + } + + /// Gets the corresponding Cadence Type declared by an EVM contract in conformance to the ICrossVM.sol contract + /// interface. Reverts if the EVM call is unsuccessful. + /// NOTE: Just because an EVM contract declares an association does not mean it it is valid! + /// + /// @param evmContract: The ICrossVM.sol conforming EVM contract from which to retrieve the corresponding Cadence + /// Type + /// + /// @return The resulting Cadence Type as declared associated by the provided EVM contract + /// + access(all) + fun getCorrespondingCadenceTypeFromCrossVM(evmContract: EVM.EVMAddress): Type { + let cadenceIdentifierRes = self.call( + signature: "getCadenceIdentifier()", + targetEVMAddress: evmContract, + args: [], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + assert(cadenceIdentifierRes.status == EVM.Status.successful) + let decodedCadenceIdentifier = EVM.decodeABI(types: [Type()], data: cadenceIdentifierRes.data) + assert(decodedCadenceIdentifier.length == 1) + let cadenceIdentifier = decodedCadenceIdentifier[0] as! String + return CompositeType(cadenceIdentifier) ?? panic("Could not construct Address from EVM contract's associated Cadence address ".concat(cadenceIdentifier)) + } + + /// Returns whether the provided EVM contract conforms to ICrossVMBridgeERC721Fulfillment.sol contract interface. + /// Doing so is one of two interfaces that must be implemented for Cadence-native cross-VM NFTs to be successfully + /// registered + /// + /// @param evmContract: The EVM contract to check for ICrossVMBridgeERC721 conformance + /// + /// @return True if conformance is found, false otherwise + /// + access(all) + fun supportsICrossVMBridgeERC721Fulfillment(evmContract: EVM.EVMAddress): Bool { + let supportsRes = self.call( + signature: "supportsInterface(bytes4)", + targetEVMAddress: evmContract, + args: [EVM.EVMBytes4(value: 0x2e608d7.toBigEndianBytes().toConstantSized<[UInt8; 4]>()!)], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + if supportsRes.status != EVM.Status.successful { + return false + } + let decodedSupports = EVM.decodeABI(types: [Type()], data: supportsRes.data) + if decodedSupports.length != 1 { + return false + } + return decodedSupports[0] as! Bool + } + + /// Returns whether the provided EVM contract conforms to ICrossVMBridgeCallable.sol contract interface. + /// Doing so is one of two interfaces that must be implemented for Cadence-native cross-VM NFTs to be successfully + /// registered + /// + /// @param evmContract: The EVM contract to check for ICrossVMBridgeCallable conformance + /// + /// @return True if conformance is found, false otherwise + /// + access(all) + fun supportsICrossVMBridgeCallable(evmContract: EVM.EVMAddress): Bool { + let supportsRes = self.call( + signature: "supportsInterface(bytes4)", + targetEVMAddress: evmContract, + args: [EVM.EVMBytes4(value: 0xb7f9a9ec.toBigEndianBytes().toConstantSized<[UInt8; 4]>()!)], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + if supportsRes.status != EVM.Status.successful { + return false + } + let decodedSupports = EVM.decodeABI(types: [Type()], data: supportsRes.data) + if decodedSupports.length != 1 { + return false + } + return decodedSupports[0] as! Bool + } + + /// Returns whether the provided EVM contract conforms to both ICrossVMBridgeERC721Fulfillment and + /// ICrossVMBridgeCallable Solidity contract interfaces + /// + /// @param evmContract: The EVM contract to check for conformance + /// + /// @return True if conformance is found, false otherwise + /// + access(all) + fun supportsCadenceNativeNFTEVMInterfaces(evmContract: EVM.EVMAddress): Bool { + return self.supportsICrossVMBridgeCallable(evmContract: evmContract) + && self.supportsICrossVMBridgeCallable(evmContract: evmContract) + } + + /// Returns the VM Bridge address designated by the ICrossVMBridgeCallable conforming EVM contract. Reverts on call + /// failure. + /// + /// @param evmContract: The ICrossVMBridgeCallable EVM contract from which to retrieve the value + /// + /// @return The EVM address designated as the VM bridge address in the provided contract + /// + access(all) + fun getVMBridgeAddressFromICrossVMBridgeCallable(evmContract: EVM.EVMAddress): EVM.EVMAddress { + let cadenceIdentifierRes = self.call( + signature: "vmBridgeAddress()", + targetEVMAddress: evmContract, + args: [], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + assert(cadenceIdentifierRes.status == EVM.Status.successful) + let decodedCadenceIdentifier = EVM.decodeABI(types: [Type()], data: cadenceIdentifierRes.data) + assert(decodedCadenceIdentifier.length == 1) + return decodedCadenceIdentifier[0] as! EVM.EVMAddress + } + /************************ Derivation Utils ************************/ diff --git a/cadence/contracts/example-assets/cross-vm-nfts/ExampleEVMNativeNFT.cdc b/cadence/contracts/example-assets/cross-vm-nfts/ExampleEVMNativeNFT.cdc new file mode 100644 index 00000000..7bffd602 --- /dev/null +++ b/cadence/contracts/example-assets/cross-vm-nfts/ExampleEVMNativeNFT.cdc @@ -0,0 +1,471 @@ +/* +* +* This is an example implementation of a Flow Non-Fungible Token +* using the V2 standard. +* It is not part of the official standard but it assumed to be +* similar to how many NFTs would implement the core functionality. +* +* This contract does not implement any sophisticated classification +* system for its NFTs. It defines a simple NFT with minimal metadata. +* +*/ + +import "NonFungibleToken" +import "ViewResolver" +import "MetadataViews" +import "CrossVMMetadataViews" + +import "EVM" + +import "ICrossVM" +import "ICrossVMAsset" +import "FlowEVMBridgeCustomAssociations" + +access(all) contract ExampleEVMNativeNFT: NonFungibleToken, ICrossVM, ICrossVMAsset { + + access(self) let evmContractAddress: EVM.EVMAddress + access(self) let name: String + access(self) let symbol: String + + /// Path where the fulfillment minter should be stored + /// The standard paths for the collection are stored in the collection resource type + access(all) let FulfillmentMinterStoragePath: StoragePath + + /* ICrossVMAsset conformance */ + + /// Returns the name of the asset + access(all) view fun getName(): String { + return self.name + } + /// Returns the symbol of the asset + access(all) view fun getSymbol(): String { + return self.symbol + } + + /* ICrossVM conformance */ + + /// Returns the associated EVM contract address + access(all) view fun getEVMContractAddress(): EVM.EVMAddress { + return self.evmContractAddress + } + + /* Custom Getters */ + + /// Returns the token URI for the provided ERC721 id, treating the corresponding ERC721 as the source of truth + access(all) fun tokenURI(id: UInt256): String { + let tokenURIRes = self.call( + signature: "tokenURI(uint256)", + targetEVMAddress: self.getEVMContractAddress(), + args: [id], + gasLimit: 100_000, + value: 0.0 + ) + assert( + tokenURIRes.status == EVM.Status.successful, + message: "Error calling ERC721.tokenURI(uint256) with message: ".concat(tokenURIRes.errorMessage) + ) + let decodedURIData = EVM.decodeABI(types: [Type()], data: tokenURIRes.data) + assert( + decodedURIData.length == 1, + message: "Unexpected tokenURI(uint256) return length of ".concat(decodedURIData.length.toString()) + ) + return decodedURIData[0] as! String + } + + /// Returns the token URI for the provided ERC721 id, treating the corresponding ERC721 as the source of truth + access(all) fun contractURI(): String { + let contractURIRes = self.call( + signature: "contractURI()", + targetEVMAddress: self.getEVMContractAddress(), + args: [], + gasLimit: 100_000, + value: 0.0 + ) + assert( + contractURIRes.status == EVM.Status.successful, + message: "Error calling ERC721.contractURI(uint256) with message: ".concat(contractURIRes.errorMessage) + ) + let decodedURIData = EVM.decodeABI(types: [Type()], data: contractURIRes.data) + assert( + decodedURIData.length == 1, + message: "Unexpected contractURI(uint256) return length of ".concat(decodedURIData.length.toString()) + ) + return decodedURIData[0] as! String + } + + /* --- Internal Helpers --- */ + + access(self) fun call( + signature: String, + targetEVMAddress: EVM.EVMAddress, + args: [AnyStruct], + gasLimit: UInt64, + value: UFix64 + ): EVM.Result { + let calldata = EVM.encodeABIWithSignature(signature, args) + let valueBalance = EVM.Balance(attoflow: 0) + valueBalance.setFLOW(flow: value) + return self.borrowCOA().call( + to: targetEVMAddress, + data: calldata, + gasLimit: gasLimit, + value: valueBalance + ) + } + + access(self) view fun borrowCOA(): auth(EVM.Owner) &EVM.CadenceOwnedAccount { + return self.account.storage.borrow( + from: /storage/evm + ) ?? panic("Could not borrow CadenceOwnedAccount (COA) from /storage/evm. " + .concat("Ensure this account has a COA configured to successfully call into EVM.")) + } + + /// We choose the name NFT here, but this type can have any name now + /// because the interface does not require it to have a specific name any more + access(all) resource NFT: NonFungibleToken.NFT, ViewResolver.Resolver { + + access(all) let id: UInt64 + + init( + erc721ID: UInt256 + ) { + pre { + erc721ID <= UInt256(UInt64.max): + "Provided EVM ID ".concat(erc721ID.toString()) + .concat(" exceeds the assignable Cadence ID of UInt64.max ").concat(UInt64.max.toString()) + } + self.id = UInt64(erc721ID) + } + + /// createEmptyCollection creates an empty Collection + /// and returns it to the caller so that they can own NFTs + /// @{NonFungibleToken.Collection} + access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { + return <-ExampleEVMNativeNFT.createEmptyCollection(nftType: Type<@ExampleEVMNativeNFT.NFT>()) + } + + access(all) view fun getViews(): [Type] { + return [ + Type(), + Type(), + Type(), + Type(), + Type(), + Type(), + Type(), + Type() + ] + } + + access(all) fun resolveView(_ view: Type): AnyStruct? { + switch view { + case Type(): + let collectionDisplay = (ExampleEVMNativeNFT.resolveContractView(resourceType: self.getType(), viewType: Type()) as! MetadataViews.NFTCollectionDisplay?)! + return MetadataViews.Display( + name: ExampleEVMNativeNFT.getName().concat(" #").concat(self.id.toString()), + description: collectionDisplay.description, + thumbnail: MetadataViews.HTTPFile( + url: "https://example-nft.flow.com/nft/".concat(self.id.toString()) + ) + ) + case Type(): + return MetadataViews.Serial( + self.id + ) + case Type(): + return MetadataViews.ExternalURL("https://example-nft.flow.com/".concat(self.id.toString())) + case Type(): + return ExampleEVMNativeNFT.resolveContractView(resourceType: Type<@ExampleEVMNativeNFT.NFT>(), viewType: Type()) + case Type(): + return ExampleEVMNativeNFT.resolveContractView(resourceType: Type<@ExampleEVMNativeNFT.NFT>(), viewType: Type()) + case Type(): + // retrieve the token URI from the ERC721 as source of truth + let uri = ExampleEVMNativeNFT.tokenURI(id: UInt256(self.id)) + + return MetadataViews.EVMBridgedMetadata( + name: ExampleEVMNativeNFT.getName(), + symbol: ExampleEVMNativeNFT.getSymbol(), + uri: MetadataViews.URI(baseURI: nil, value: uri) + ) + case Type(): + return CrossVMMetadataViews.EVMPointer( + cadenceType: self.getType(), + cadenceContractAddress: self.getType().address!, + evmContractAddress: ExampleEVMNativeNFT.getEVMContractAddress(), + nativeVM: CrossVMMetadataViews.VM.EVM + ) + } + return nil + } + } + + access(all) resource Collection: NonFungibleToken.Collection { + /// dictionary of NFT conforming tokens + /// NFT is a resource type with an `UInt64` ID field + access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}} + + access(all) var storagePath: StoragePath + access(all) var publicPath: PublicPath + + init () { + self.ownedNFTs <- {} + let identifier = "cadenceExampleEVMNativeNFTCollection" + self.storagePath = StoragePath(identifier: identifier)! + self.publicPath = PublicPath(identifier: identifier)! + } + + /// getSupportedNFTTypes returns a list of NFT types that this receiver accepts + access(all) view fun getSupportedNFTTypes(): {Type: Bool} { + let supportedTypes: {Type: Bool} = {} + supportedTypes[Type<@ExampleEVMNativeNFT.NFT>()] = true + return supportedTypes + } + + /// Returns whether or not the given type is accepted by the collection + /// A collection that can accept any type should just return true by default + access(all) view fun isSupportedNFTType(type: Type): Bool { + if type == Type<@ExampleEVMNativeNFT.NFT>() { + return true + } else { + return false + } + } + + /// withdraw removes an NFT from the collection and moves it to the caller + access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} { + let token <- self.ownedNFTs.remove(key: withdrawID) + ?? panic("Could not withdraw an NFT with the provided ID from the collection") + + return <-token + } + + /// deposit takes a NFT and adds it to the collections dictionary + /// and adds the ID to the id array + access(all) fun deposit(token: @{NonFungibleToken.NFT}) { + let token <- token as! @ExampleEVMNativeNFT.NFT + + // add the new token to the dictionary which removes the old one + let oldToken <- self.ownedNFTs[token.id] <- token + + destroy oldToken + } + + /// getIDs returns an array of the IDs that are in the collection + access(all) view fun getIDs(): [UInt64] { + return self.ownedNFTs.keys + } + + /// Gets the amount of NFTs stored in the collection + access(all) view fun getLength(): Int { + return self.ownedNFTs.keys.length + } + + access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? { + return (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?) + } + + /// Borrow the view resolver for the specified NFT ID + access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? { + if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? { + return nft as &{ViewResolver.Resolver} + } + return nil + } + + /// createEmptyCollection creates an empty Collection of the same type + /// and returns it to the caller + /// @return A an empty collection of the same type + access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { + return <-ExampleEVMNativeNFT.createEmptyCollection(nftType: Type<@ExampleEVMNativeNFT.NFT>()) + } + } + + /// createEmptyCollection creates an empty Collection for the specified NFT type + /// and returns it to the caller so that they can own NFTs + access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} { + return <- create Collection() + } + + /// Function that returns all the Metadata Views implemented by a Non Fungible Token + /// + /// @param resourceType: The Type of the relevant NFT defined in this contract. + /// @return An array of Types defining the implemented views. This value will be used by + /// developers to know which parameter to pass to the resolveView() method. + /// + access(all) view fun getContractViews(resourceType: Type?): [Type] { + return [ + Type(), + Type(), + Type(), + Type() + ] + } + + /// Function that resolves a metadata view for this contract. + /// + /// @param resourceType: The Type of the relevant NFT defined in this contract. + /// @param view: The Type of the desired view. + /// @return A structure representing the requested view. + /// + access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { + switch viewType { + case Type(): + let collectionData = MetadataViews.NFTCollectionData( + storagePath: /storage/cadenceExampleEVMNativeNFTCollection, + publicPath: /public/cadenceExampleEVMNativeNFTCollection, + publicCollection: Type<&ExampleEVMNativeNFT.Collection>(), + publicLinkedType: Type<&ExampleEVMNativeNFT.Collection>(), + createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} { + return <-ExampleEVMNativeNFT.createEmptyCollection(nftType: Type<@ExampleEVMNativeNFT.NFT>()) + }) + ) + return collectionData + case Type(): + let media = MetadataViews.Media( + file: MetadataViews.HTTPFile( + url: "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg" + ), + mediaType: "image/svg+xml" + ) + return MetadataViews.NFTCollectionDisplay( + name: "The Example EVM-Native NFT Collection", + description: "This collection is used as an example to help you develop your next EVM-native cross-VM Flow NFT.", + externalURL: MetadataViews.ExternalURL("https://example-nft.flow.com"), + squareImage: media, + bannerImage: media, + socials: { + "twitter": MetadataViews.ExternalURL("https://twitter.com/flow_blockchain") + } + ) + case Type(): + // retrieve the contract URI from the ERC721 as source of truth + let uri = ExampleEVMNativeNFT.contractURI() + return MetadataViews.EVMBridgedMetadata( + name: ExampleEVMNativeNFT.getName(), + symbol: ExampleEVMNativeNFT.getSymbol(), + uri: MetadataViews.URI(baseURI: nil, value: uri) + ) + case Type(): + return CrossVMMetadataViews.EVMPointer( + cadenceType: Type<@ExampleEVMNativeNFT.NFT>(), + cadenceContractAddress: self.account.address, + evmContractAddress: self.getEVMContractAddress(), + nativeVM: CrossVMMetadataViews.VM.EVM + ) + } + return nil + } + + /* FlowEVMBridgeCustomAssociations.NFTFulfillmentMinter Conformance */ + + /// Resource that allows the bridge to mint Cadence NFTs as needed when fulfilling movement of + /// EVM-native ERC721 tokens from Flow EVM. + /// + access(all) resource NFTMinter : FlowEVMBridgeCustomAssociations.NFTFulfillmentMinter { + + /// Getter for the type of NFT that's fulfilled by this implementation + /// + access(all) view fun getFulfilledType(): Type { + return Type<@ExampleEVMNativeNFT.NFT>() + } + + /// Called by the VM bridge when moving NFTs from EVM into Cadence if the NFT is not in escrow. Since such NFTs + /// are EVM-native, they are distributed in EVM. On the Cadence side, those NFTs are handled by a mint & escrow + /// pattern. On moving to EVM, the NFTs are minted if not in escrow at the time of bridging. + /// + /// @param id: The id of the token being fulfilled from EVM + /// + /// @return The NFT fulfilled from EVM as its Cadence implementation + /// + access(FlowEVMBridgeCustomAssociations.FulfillFromEVM) + fun fulfillFromEVM(id: UInt256): @{NonFungibleToken.NFT} { + return <- create NFT(erc721ID: id) + } + } + + /// Contract initialization + /// + /// @param erc721Bytecode: The bytecode for the ERC721 contract which is deployed via this contract account's + /// CadenceOwnedAccount. Any account can deploy the corresponding ERC721 contract, but it's done here for + /// demonstration and ease of EVM contract assignment. + /// + init(erc721Bytecode: String) { + + // Set the named paths + self.FulfillmentMinterStoragePath = /storage/cadenceExampleEVMNativeNFTFulfillmentMinter + + // Create a Collection resource and save it to storage + let collection <- create Collection() + let defaultStoragePath = collection.storagePath + let defaultPublicPath = collection.publicPath + self.account.storage.save(<-collection, to: defaultStoragePath) + + // Create a public capability for the collection + let collectionCap = self.account.capabilities.storage.issue<&ExampleEVMNativeNFT.Collection>(defaultStoragePath) + self.account.capabilities.publish(collectionCap, at: defaultPublicPath) + + // Create a Minter resource and save it to storage + let minter <- create NFTMinter() + self.account.storage.save(<-minter, to: self.FulfillmentMinterStoragePath) + + // Configure a COA so this contract can call into EVM + if self.account.storage.type(at: /storage/evm) != Type<@EVM.CadenceOwnedAccount>() { + self.account.storage.save(<-EVM.createCadenceOwnedAccount(), to: /storage/evm) + let coaCap = self.account.capabilities.storage.issue<&EVM.CadenceOwnedAccount>(/storage/evm) + self.account.capabilities.publish(coaCap, at: /public/evm) + } + let coa = self.account.storage.borrow( + from: /storage/evm + )! + + // Append the constructor args to the provided contract bytecode + let cadenceAddressStr = self.account.address.toString() + let cadenceIdentifier = Type<@ExampleEVMNativeNFT.NFT>().identifier + let encodedConstructorArgs = EVM.encodeABI([cadenceAddressStr, cadenceIdentifier]) + let finalBytecode = erc721Bytecode.decodeHex().concat(encodedConstructorArgs) + + // Deploy the provided EVM contract, passing the defined value of FLOW on init + let deployResult = coa.deploy( + code: finalBytecode, + gasLimit: 15_000_000, + value: EVM.Balance(attoflow: 0) + ) + assert( + deployResult.status == EVM.Status.successful, + message: "ERC721 deployment failed with message: ".concat(deployResult.errorMessage) + ) + + self.evmContractAddress = deployResult.deployedContract! + + // Assign name & symbol based on ERC721 contract + let nameRes = coa.call( + to: self.evmContractAddress, + data: EVM.encodeABIWithSignature("name()", []), + gasLimit: 100_000, + value: EVM.Balance(attoflow: 0) + ) + let symbolRes = coa.call( + to: self.evmContractAddress, + data: EVM.encodeABIWithSignature("symbol()", []), + gasLimit: 100_000, + value: EVM.Balance(attoflow: 0) + ) + assert( + nameRes.status == EVM.Status.successful, + message: "Error on ERC721.name() call with message: ".concat(nameRes.errorMessage) + ) + assert( + symbolRes.status == EVM.Status.successful, + message: "Error on ERC721.symbol() call with message: ".concat(symbolRes.errorMessage) + ) + + let decodedNameData = EVM.decodeABI(types: [Type()], data: nameRes.data) + let decodedSymbolData = EVM.decodeABI(types: [Type()], data: symbolRes.data) + assert(decodedNameData.length == 1, message: "Unexpected name() return length of ".concat(decodedNameData.length.toString())) + assert(decodedSymbolData.length == 1, message: "Unexpected symbol() return length of ".concat(decodedSymbolData.length.toString())) + + self.name = decodedNameData[0] as! String + self.symbol = decodedSymbolData[0] as! String + } +} + \ No newline at end of file diff --git a/cadence/tests/flow_evm_bridge_evm_native_nft_tests.cdc b/cadence/tests/flow_evm_bridge_evm_native_nft_tests.cdc new file mode 100644 index 00000000..74aa2a65 --- /dev/null +++ b/cadence/tests/flow_evm_bridge_evm_native_nft_tests.cdc @@ -0,0 +1,418 @@ +import Test +import BlockchainHelpers + +import "MetadataViews" +import "EVM" +import "ExampleEVMNativeNFT" + +import "test_helpers.cdc" + +access(all) let serviceAccount = Test.serviceAccount() +access(all) let bridgeAccount = Test.getAccount(0x0000000000000007) +access(all) let exampleEVMNativeNFTAccount = Test.getAccount(0x0000000000000008) +access(all) let alice = Test.createAccount() +access(all) let bob = Test.createAccount() + +// ExampleEVMNativeNFT +access(all) let exampleEVMNativeNFTIdentifier = "A.0000000000000008.ExampleEVMNativeNFT.NFT" +access(all) var mintedNFTID: UInt64 = 0 + +// Bridge-related EVM contract values +access(all) var registryAddressHex: String = "" +access(all) var erc20DeployerAddressHex: String = "" +access(all) var erc721DeployerAddressHex: String = "" + +// ERC721 values +access(all) var erc721AddressHex: String = "" +access(all) let erc721ID: UInt256 = 42 + +// Fee initialization values +access(all) let expectedOnboardFee = 1.0 +access(all) let expectedBaseFee = 0.001 + +// Test height snapshot for test state resets +access(all) var snapshot: UInt64 = 0 + +access(all) +fun setup() { + // TEMPORARY: Only included until emulator auto-deploys CrossVMMetadataViews + var err = Test.deployContract( + name: "CrossVMMetadataViews", + path: "../../imports/631e88ae7f1d7c20/CrossVMMetadataViews.cdc", + arguments: [] + ) + // Deploy supporting util contracts + err = Test.deployContract( + name: "ArrayUtils", + path: "../contracts/utils/ArrayUtils.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "StringUtils", + path: "../contracts/utils/StringUtils.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "ScopedFTProviders", + path: "../contracts/utils/ScopedFTProviders.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "Serialize", + path: "../contracts/utils/Serialize.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "SerializeMetadata", + path: "../contracts/utils/SerializeMetadata.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + // Transfer bridge account some $FLOW + transferFlow(signer: serviceAccount, recipient: bridgeAccount.address, amount: 10_000.0) + // Configure bridge account with a COA + createCOA(signer: bridgeAccount, fundingAmount: 1_000.0) + + err = Test.deployContract( + name: "IBridgePermissions", + path: "../contracts/bridge/interfaces/IBridgePermissions.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "ICrossVM", + path: "../contracts/bridge/interfaces/ICrossVM.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "ICrossVMAsset", + path: "../contracts/bridge/interfaces/ICrossVMAsset.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "CrossVMNFT", + path: "../contracts/bridge/interfaces/CrossVMNFT.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "CrossVMToken", + path: "../contracts/bridge/interfaces/CrossVMToken.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FlowEVMBridgeHandlerInterfaces", + path: "../contracts/bridge/interfaces/FlowEVMBridgeHandlerInterfaces.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FlowEVMBridgeCustomAssociations", + path: "../contracts/bridge/FlowEVMBridgeCustomAssociations.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FlowEVMBridgeConfig", + path: "../contracts/bridge/FlowEVMBridgeConfig.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + // Deploy registry + let registryDeploymentResult = executeTransaction( + "../transactions/evm/deploy.cdc", + [getRegistryBytecode(), UInt64(15_000_000), 0.0], + bridgeAccount + ) + Test.expect(registryDeploymentResult, Test.beSucceeded()) + // Deploy ERC20Deployer + let erc20DeployerDeploymentResult = executeTransaction( + "../transactions/evm/deploy.cdc", + [getERC20DeployerBytecode(), UInt64(15_000_000), 0.0], + bridgeAccount + ) + Test.expect(erc20DeployerDeploymentResult, Test.beSucceeded()) + // Deploy ERC721Deployer + let erc721DeployerDeploymentResult = executeTransaction( + "../transactions/evm/deploy.cdc", + [getERC721DeployerBytecode(), UInt64(15_000_000), 0.0], + bridgeAccount + ) + Test.expect(erc721DeployerDeploymentResult, Test.beSucceeded()) + // Assign contract addresses + var evts = Test.eventsOfType(Type()) + Test.assertEqual(5, evts.length) + registryAddressHex = getEVMAddressHexFromEvents(evts, idx: 2) + erc20DeployerAddressHex = getEVMAddressHexFromEvents(evts, idx: 3) + erc721DeployerAddressHex = getEVMAddressHexFromEvents(evts, idx: 4) + + // Deploy factory + let deploymentResult = executeTransaction( + "../transactions/evm/deploy.cdc", + [getCompiledFactoryBytecode(), UInt64(15_000_000), 0.0], + bridgeAccount + ) + Test.expect(deploymentResult, Test.beSucceeded()) + // Assign the factory contract address + evts = Test.eventsOfType(Type()) + Test.assertEqual(6, evts.length) + let factoryAddressHex = getEVMAddressHexFromEvents(evts, idx: 5) + Test.assertEqual(factoryAddressHex.length, 40) + + err = Test.deployContract( + name: "FlowEVMBridgeUtils", + path: "../contracts/bridge/FlowEVMBridgeUtils.cdc", + arguments: [factoryAddressHex] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "FlowEVMBridgeResolver", + path: "../contracts/bridge/FlowEVMBridgeResolver.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "FlowEVMBridgeHandlers", + path: "../contracts/bridge/FlowEVMBridgeHandlers.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + /* Integrate EVM bridge contract */ + + // Set factory as registrar in registry + let setRegistrarResult = executeTransaction( + "../transactions/bridge/admin/evm/set_registrar.cdc", + [registryAddressHex], + bridgeAccount + ) + Test.expect(setRegistrarResult, Test.beSucceeded()) + // Set registry as registry in factory + let setRegistryResult = executeTransaction( + "../transactions/bridge/admin/evm/set_deployment_registry.cdc", + [registryAddressHex], + bridgeAccount + ) + Test.expect(setRegistryResult, Test.beSucceeded()) + // Set factory as delegatedDeployer in erc20Deployer + var setDelegatedDeployerResult = executeTransaction( + "../transactions/bridge/admin/evm/set_delegated_deployer.cdc", + [erc20DeployerAddressHex], + bridgeAccount + ) + Test.expect(setDelegatedDeployerResult, Test.beSucceeded()) + // Set factory as delegatedDeployer in erc721Deployer + setDelegatedDeployerResult = executeTransaction( + "../transactions/bridge/admin/evm/set_delegated_deployer.cdc", + [erc721DeployerAddressHex], + bridgeAccount + ) + Test.expect(setDelegatedDeployerResult, Test.beSucceeded()) + // add erc20Deployer under "ERC20" tag to factory + var addDeployerResult = executeTransaction( + "../transactions/bridge/admin/evm/add_deployer.cdc", + ["ERC20", erc20DeployerAddressHex], + bridgeAccount + ) + Test.expect(addDeployerResult, Test.beSucceeded()) + // add erc721Deployer under "ERC721" tag to factory + addDeployerResult = executeTransaction( + "../transactions/bridge/admin/evm/add_deployer.cdc", + ["ERC721", erc721DeployerAddressHex], + bridgeAccount + ) + Test.expect(addDeployerResult, Test.beSucceeded()) + + /* End EVM bridge integration txns */ + + err = Test.deployContract( + name: "FlowEVMBridgeNFTEscrow", + path: "../contracts/bridge/FlowEVMBridgeNFTEscrow.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FlowEVMBridgeTokenEscrow", + path: "../contracts/bridge/FlowEVMBridgeTokenEscrow.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FlowEVMBridgeTemplates", + path: "../contracts/bridge/FlowEVMBridgeTemplates.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + // Commit bridged NFT code + let bridgedNFTChunkResult = executeTransaction( + "../transactions/bridge/admin/templates/upsert_contract_code_chunks.cdc", + ["bridgedNFT", getBridgedNFTCodeChunks()], + bridgeAccount + ) + Test.expect(bridgedNFTChunkResult, Test.beSucceeded()) + // Commit bridged Token code + let bridgedTokenChunkResult = executeTransaction( + "../transactions/bridge/admin/templates/upsert_contract_code_chunks.cdc", + ["bridgedToken", getBridgedTokenCodeChunks()], + bridgeAccount + ) + Test.expect(bridgedNFTChunkResult, Test.beSucceeded()) + + err = Test.deployContract( + name: "IEVMBridgeNFTMinter", + path: "../contracts/bridge/interfaces/IEVMBridgeNFTMinter.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "IEVMBridgeTokenMinter", + path: "../contracts/bridge/interfaces/IEVMBridgeTokenMinter.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "IFlowEVMNFTBridge", + path: "../contracts/bridge/interfaces/IFlowEVMNFTBridge.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "IFlowEVMTokenBridge", + path: "../contracts/bridge/interfaces/IFlowEVMTokenBridge.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FlowEVMBridge", + path: "../contracts/bridge/FlowEVMBridge.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FlowEVMBridgeAccessor", + path: "../contracts/bridge/FlowEVMBridgeAccessor.cdc", + arguments: [serviceAccount.address] + ) + Test.expect(err, Test.beNil()) + + let claimAccessorResult = executeTransaction( + "../transactions/bridge/admin/evm-integration/claim_accessor_capability_and_save_router.cdc", + ["FlowEVMBridgeAccessor", bridgeAccount.address], + serviceAccount + ) + Test.expect(claimAccessorResult, Test.beSucceeded()) + + // Configure example ERC20 account with a COA + transferFlow(signer: serviceAccount, recipient: exampleEVMNativeNFTAccount.address, amount: 1_000.0) + + err = Test.deployContract( + name: "ExampleEVMNativeNFT", + path: "../contracts/example-assets/cross-vm-nfts/ExampleEVMNativeNFT.cdc", + arguments: [getEVMNativeERC721Bytecode()] + ) + Test.expect(err, Test.beNil()) + erc721AddressHex = ExampleEVMNativeNFT.getEVMContractAddress().toString() + + // Configure metadata views for bridged NFTS & FTs + let setBridgedNFTDisplayViewResult = executeTransaction( + "../transactions/bridge/admin/metadata/set_bridged_nft_display_view.cdc", + [ + "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg", // thumbnailURI + Type().identifier, // thumbnailFileTypeIdentifier + nil // ipfsFilePath + ], + bridgeAccount + ) + Test.expect(setBridgedNFTDisplayViewResult, Test.beSucceeded()) + + let socialsDict: {String: String} = {} + let setBridgedNFTCollectionDisplayResult = executeTransaction( + "../transactions/bridge/admin/metadata/set_bridged_nft_collection_display_view.cdc", + [ + "https://port.flow.com", // externalURL + "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg", // squareImageURI + Type().identifier, // squareImageFileTypeIdentifier + nil, // squareImageIPFSFilePath + "image/svg+xml", // squareImageMediaType + "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg", // bannerImageURI + Type().identifier, // bannerImageFileTypeIdentifier + nil, // bannerImageIPFSFilePath + "image/svg+xml", // bannerImageMediaType + socialsDict // socialsDict + ], + bridgeAccount + ) + Test.expect(setBridgedNFTCollectionDisplayResult, Test.beSucceeded()) + + let setFTDisplayResult = executeTransaction( + "../transactions/bridge/admin/metadata/set_bridged_ft_display_view.cdc", + [ + "https://port.flow.com", // externalURL + "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg", // logoURI + Type().identifier, // logoFileTypeIdentifier + nil, // logoIPFSFilePath + "image/svg+xml", // logoMediaType + socialsDict // socialsDict + ], + bridgeAccount + ) + Test.expect(setFTDisplayResult, Test.beSucceeded()) + + // Unpause the bridge + updateBridgePauseStatus(signer: bridgeAccount, pause: false) +} + +access(all) +fun testRegisterCrossVMNFTSucceeds() { + registerCrossVMNFT( + signer: exampleEVMNativeNFTAccount, + nftTypeIdentifier: exampleEVMNativeNFTIdentifier, + fulfillmentMinterPath: ExampleEVMNativeNFT.FulfillmentMinterStoragePath, + beFailed: false + ) + let associatedEVMAddress = getAssociatedEVMAddressHex(with: exampleEVMNativeNFTIdentifier) + Test.assertEqual(erc721AddressHex, associatedEVMAddress) + let associatedType = getTypeAssociated(with: erc721AddressHex) + Test.assertEqual(exampleEVMNativeNFTIdentifier, associatedType) +} + +access(all) +fun testBridgeERC721FromEVMSucceeds() { + // create tmp account + // fund account + // create COA in account + // mint the ERC721 from the right account to the tmp account COA + // assert COA is ownerOf + // bridge from EVM + // assert on events + // assert EVM NFT is in escrow under bridge COA + // ensure signer has the bridged NFT in their collection + // assert metadata values from Cadence NFT +} + +access(all) +fun testBridgeNFTToEVMSucceeds() { + // create tmp account + // fund account + // create COA in account + // mint the ERC721 from the right account to the tmp account COA + // assert on ownerOf + // bridge from EVM + // assert on events + // assert EVM NFT is in escrow under bridge COA + // ensure signer has the bridged NFT in their collection + // assert metadata values from Cadence NFT + // bridge to EVM + // assert on events +} \ No newline at end of file diff --git a/cadence/tests/flow_evm_bridge_handler_tests.cdc b/cadence/tests/flow_evm_bridge_handler_tests.cdc index 0405790b..8b7ecd47 100644 --- a/cadence/tests/flow_evm_bridge_handler_tests.cdc +++ b/cadence/tests/flow_evm_bridge_handler_tests.cdc @@ -36,8 +36,14 @@ access(all) var snapshot: UInt64 = 0 access(all) fun setup() { - // Deploy supporting util contracts + // TEMPORARY: Only included until emulator auto-deploys CrossVMMetadataViews var err = Test.deployContract( + name: "CrossVMMetadataViews", + path: "../../imports/631e88ae7f1d7c20/CrossVMMetadataViews.cdc", + arguments: [] + ) + // Deploy supporting util contracts + err = Test.deployContract( name: "ArrayUtils", path: "../contracts/utils/ArrayUtils.cdc", arguments: [] @@ -109,6 +115,12 @@ fun setup() { arguments: [] ) Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FlowEVMBridgeCustomAssociations", + path: "../contracts/bridge/FlowEVMBridgeCustomAssociations.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) err = Test.deployContract( name: "FlowEVMBridgeConfig", path: "../contracts/bridge/FlowEVMBridgeConfig.cdc", diff --git a/cadence/tests/flow_evm_bridge_tests.cdc b/cadence/tests/flow_evm_bridge_tests.cdc index 2a4649d9..e612b0f8 100644 --- a/cadence/tests/flow_evm_bridge_tests.cdc +++ b/cadence/tests/flow_evm_bridge_tests.cdc @@ -57,8 +57,14 @@ access(all) var snapshot: UInt64 = 0 access(all) fun setup() { - // Deploy supporting util contracts + // TEMPORARY: Only included until emulator auto-deploys CrossVMMetadataViews var err = Test.deployContract( + name: "CrossVMMetadataViews", + path: "../../imports/631e88ae7f1d7c20/CrossVMMetadataViews.cdc", + arguments: [] + ) + // Deploy supporting util contracts + err = Test.deployContract( name: "ArrayUtils", path: "../contracts/utils/ArrayUtils.cdc", arguments: [] @@ -130,6 +136,12 @@ fun setup() { arguments: [] ) Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FlowEVMBridgeCustomAssociations", + path: "../contracts/bridge/FlowEVMBridgeCustomAssociations.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) err = Test.deployContract( name: "FlowEVMBridgeConfig", path: "../contracts/bridge/FlowEVMBridgeConfig.cdc", diff --git a/cadence/tests/flow_evm_bridge_utils_tests.cdc b/cadence/tests/flow_evm_bridge_utils_tests.cdc index bd89374a..4184ab2d 100644 --- a/cadence/tests/flow_evm_bridge_utils_tests.cdc +++ b/cadence/tests/flow_evm_bridge_utils_tests.cdc @@ -10,8 +10,14 @@ access(all) let bridgeAccount = Test.getAccount(0x0000000000000007) access(all) fun setup() { - // Deploy supporting util contracts + // TEMPORARY: Only included until emulator auto-deploys CrossVMMetadataViews var err = Test.deployContract( + name: "CrossVMMetadataViews", + path: "../../imports/631e88ae7f1d7c20/CrossVMMetadataViews.cdc", + arguments: [] + ) + // Deploy supporting util contracts + err = Test.deployContract( name: "ArrayUtils", path: "../contracts/utils/ArrayUtils.cdc", arguments: [] @@ -83,6 +89,12 @@ fun setup() { arguments: [] ) Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FlowEVMBridgeCustomAssociations", + path: "../contracts/bridge/FlowEVMBridgeCustomAssociations.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) err = Test.deployContract( name: "FlowEVMBridgeConfig", path: "../contracts/bridge/FlowEVMBridgeConfig.cdc", diff --git a/cadence/tests/flow_evm_wflow_handler_tests.cdc b/cadence/tests/flow_evm_wflow_handler_tests.cdc index ad2e5fb5..876ce11a 100644 --- a/cadence/tests/flow_evm_wflow_handler_tests.cdc +++ b/cadence/tests/flow_evm_wflow_handler_tests.cdc @@ -38,8 +38,14 @@ access(all) var snapshot: UInt64 = 0 access(all) fun setup() { - // Deploy supporting util contracts + // TEMPORARY: Only included until emulator auto-deploys CrossVMMetadataViews var err = Test.deployContract( + name: "CrossVMMetadataViews", + path: "../../imports/631e88ae7f1d7c20/CrossVMMetadataViews.cdc", + arguments: [] + ) + // Deploy supporting util contracts + err = Test.deployContract( name: "ArrayUtils", path: "../contracts/utils/ArrayUtils.cdc", arguments: [] @@ -111,6 +117,12 @@ fun setup() { arguments: [] ) Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FlowEVMBridgeCustomAssociations", + path: "../contracts/bridge/FlowEVMBridgeCustomAssociations.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) err = Test.deployContract( name: "FlowEVMBridgeConfig", path: "../contracts/bridge/FlowEVMBridgeConfig.cdc", @@ -270,7 +282,6 @@ fun testDeployWFLOWSucceeds() { let evts = Test.eventsOfType(Type()) Test.assertEqual(5, evts.length) wflowAddressHex = getEVMAddressHexFromEvents(evts, idx: 4) - log("WFLOW Address: ".concat(wflowAddressHex)) } access(all) diff --git a/cadence/tests/scripts/get_associated_type_identifier.cdc b/cadence/tests/scripts/get_associated_type_identifier.cdc new file mode 100644 index 00000000..bfdcf367 --- /dev/null +++ b/cadence/tests/scripts/get_associated_type_identifier.cdc @@ -0,0 +1,16 @@ +import "EVM" + +import "FlowEVMBridgeConfig" + +/// Returns the Cadence Type associated with the given EVM address (as its hex String) +/// +/// @param evmAddressHex: The hex-encoded address of the EVM contract as a String +/// +/// @return The Cadence Type associated with the EVM address or nil if the address is not onboarded. `nil` may also be +/// returned if the address is not a valid EVM address. +/// +access(all) +fun main(addressHex: String): String? { + let address = EVM.addressFromString(addressHex) + return FlowEVMBridgeConfig.getTypeAssociated(with: address)?.identifier ?? nil +} diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 4d6e43d0..ad887c73 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -78,6 +78,8 @@ access(all) let bridgedTokenCodeChunks = [ "2e5661756c743e28292c20776974683a2073656c662e65766d546f6b656e436f6e747261637441646472657373290a2020202020202020466c6f7745564d427269646765546f6b656e457363726f772e696e697469616c697a65457363726f77280a202020202020202020202020776974683a203c2d637265617465205661756c742862616c616e63653a20302e30292c0a2020202020202020202020206e616d653a206e616d652c0a20202020202020202020202073796d626f6c3a2073796d626f6c2c0a202020202020202020202020646563696d616c733a20646563696d616c732c0a20202020202020202020202065766d546f6b656e416464726573733a2073656c662e65766d546f6b656e436f6e7472616374416464726573730a2020202020202020290a202020207d0a7d0a" ] +access(all) let evmNativeERC721Bytecode = "60806040523480156200001157600080fd5b50604051620018253803806200182583398101604081905262000034916200021b565b336040518060400160405280600f81526020016e45564d4e617469766545524337323160881b815250604051806040016040528060078152602001661155935613541360ca1b81525081600090816200008e919062000316565b5060016200009d828262000316565b5050506001600160a01b038116620000cf57604051631e4fbdf760e01b81526000600482015260240160405180910390fd5b620000da8162000101565b506007620000e9838262000316565b506008620000f8828262000316565b505050620003e2565b600680546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b634e487b7160e01b600052604160045260246000fd5b600082601f8301126200017b57600080fd5b81516001600160401b038082111562000198576200019862000153565b604051601f8301601f19908116603f01168101908282118183101715620001c357620001c362000153565b8160405283815260209250866020858801011115620001e157600080fd5b600091505b83821015620002055785820183015181830184015290820190620001e6565b6000602085830101528094505050505092915050565b600080604083850312156200022f57600080fd5b82516001600160401b03808211156200024757600080fd5b620002558683870162000169565b935060208501519150808211156200026c57600080fd5b506200027b8582860162000169565b9150509250929050565b600181811c908216806200029a57607f821691505b602082108103620002bb57634e487b7160e01b600052602260045260246000fd5b50919050565b601f82111562000311576000816000526020600020601f850160051c81016020861015620002ec5750805b601f850160051c820191505b818110156200030d57828155600101620002f8565b5050505b505050565b81516001600160401b0381111562000332576200033262000153565b6200034a8162000343845462000285565b84620002c1565b602080601f831160018114620003825760008415620003695750858301515b600019600386901b1c1916600185901b1785556200030d565b600085815260208120601f198616915b82811015620003b35788860151825594840194600190910190840162000392565b5085821015620003d25787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b61143380620003f26000396000f3fe608060405234801561001057600080fd5b506004361061012c5760003560e01c80638da5cb5b116100ad578063b88d4fde11610071578063b88d4fde1461025f578063c87b56dd14610272578063e8a3d48514610285578063e985e9c51461028d578063f2fde38b146102a057600080fd5b80638da5cb5b1461021857806395d89b411461022957806397d9a15914610231578063a144819414610239578063a22cb4651461024c57600080fd5b806323b872dd116100f457806323b872dd146101b657806342842e0e146101c95780636352211e146101dc57806370a08231146101ef578063715018a61461021057600080fd5b806301ffc9a71461013157806306fdde0314610159578063081812fc1461016e578063095ea7b3146101995780631a622896146101ae575b600080fd5b61014461013f366004610df0565b6102b3565b60405190151581526020015b60405180910390f35b610161610305565b6040516101509190610e5d565b61018161017c366004610e70565b610397565b6040516001600160a01b039091168152602001610150565b6101ac6101a7366004610ea5565b6103c0565b005b6101616103cf565b6101ac6101c4366004610ecf565b6103de565b6101ac6101d7366004610ecf565b61046e565b6101816101ea366004610e70565b61048e565b6102026101fd366004610f0b565b610499565b604051908152602001610150565b6101ac6104e1565b6006546001600160a01b0316610181565b6101616104f5565b610161610504565b6101ac610247366004610ea5565b610513565b6101ac61025a366004610f26565b610525565b6101ac61026d366004610f78565b610530565b610161610280366004610e70565b610547565b6101616105af565b61014461029b366004611054565b6105f7565b6101ac6102ae366004610f0b565b610625565b60006001600160e01b031982166380ac58cd60e01b14806102e457506001600160e01b03198216635b5e139f60e01b145b806102ff57506301ffc9a760e01b6001600160e01b03198316145b92915050565b60606000805461031490611087565b80601f016020809104026020016040519081016040528092919081815260200182805461034090611087565b801561038d5780601f106103625761010080835404028352916020019161038d565b820191906000526020600020905b81548152906001019060200180831161037057829003601f168201915b5050505050905090565b60006103a282610663565b506000828152600460205260409020546001600160a01b03166102ff565b6103cb82823361069c565b5050565b60606007805461031490611087565b6001600160a01b03821661040d57604051633250574960e11b8152600060048201526024015b60405180910390fd5b600061041a8383336106a9565b9050836001600160a01b0316816001600160a01b031614610468576040516364283d7b60e01b81526001600160a01b0380861660048301526024820184905282166044820152606401610404565b50505050565b61048983838360405180602001604052806000815250610530565b505050565b60006102ff82610663565b60006001600160a01b0382166104c5576040516322718ad960e21b815260006004820152602401610404565b506001600160a01b031660009081526003602052604090205490565b6104e96107a2565b6104f360006107cf565b565b60606001805461031490611087565b60606008805461031490611087565b61051b6107a2565b6103cb8282610821565b6103cb33838361083b565b61053b8484846103de565b610468848484846108da565b606061055282610663565b50600061055d610a03565b9050600081511161057d57604051806020016040528060008152506105a8565b8061058784610a23565b6040516020016105989291906110c1565b6040516020818303038152906040525b9392505050565b6060600060405180610280016040528061024881526020016111b661024891399050806040516020016105e291906110f0565b60405160208183030381529060405291505090565b6001600160a01b03918216600090815260056020908152604080832093909416825291909152205460ff1690565b61062d6107a2565b6001600160a01b03811661065757604051631e4fbdf760e01b815260006004820152602401610404565b610660816107cf565b50565b6000818152600260205260408120546001600160a01b0316806102ff57604051637e27328960e01b815260048101849052602401610404565b6104898383836001610ab6565b6000828152600260205260408120546001600160a01b03908116908316156106d6576106d6818486610bbc565b6001600160a01b03811615610714576106f3600085600080610ab6565b6001600160a01b038116600090815260036020526040902080546000190190555b6001600160a01b03851615610743576001600160a01b0385166000908152600360205260409020805460010190555b60008481526002602052604080822080546001600160a01b0319166001600160a01b0389811691821790925591518793918516917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef91a4949350505050565b6006546001600160a01b031633146104f35760405163118cdaa760e01b8152336004820152602401610404565b600680546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b6103cb828260405180602001604052806000815250610c20565b6001600160a01b03821661086d57604051630b61174360e31b81526001600160a01b0383166004820152602401610404565b6001600160a01b03838116600081815260056020908152604080832094871680845294825291829020805460ff191686151590811790915591519182527f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31910160405180910390a3505050565b6001600160a01b0383163b1561046857604051630a85bd0160e11b81526001600160a01b0384169063150b7a029061091c903390889087908790600401611135565b6020604051808303816000875af1925050508015610957575060408051601f3d908101601f1916820190925261095491810190611172565b60015b6109c0573d808015610985576040519150601f19603f3d011682016040523d82523d6000602084013e61098a565b606091505b5080516000036109b857604051633250574960e11b81526001600160a01b0385166004820152602401610404565b805181602001fd5b6001600160e01b03198116630a85bd0160e11b146109fc57604051633250574960e11b81526001600160a01b0385166004820152602401610404565b5050505050565b606060405180606001604052806026815260200161119060269139905090565b60606000610a3083610c37565b600101905060008167ffffffffffffffff811115610a5057610a50610f62565b6040519080825280601f01601f191660200182016040528015610a7a576020820181803683370190505b5090508181016020015b600019016f181899199a1a9b1b9c1cb0b131b232b360811b600a86061a8153600a8504945084610a8457509392505050565b8080610aca57506001600160a01b03821615155b15610b8c576000610ada84610663565b90506001600160a01b03831615801590610b065750826001600160a01b0316816001600160a01b031614155b8015610b195750610b1781846105f7565b155b15610b425760405163a9fbf51f60e01b81526001600160a01b0384166004820152602401610404565b8115610b8a5783856001600160a01b0316826001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560405160405180910390a45b505b5050600090815260046020526040902080546001600160a01b0319166001600160a01b0392909216919091179055565b610bc7838383610d0f565b610489576001600160a01b038316610bf557604051637e27328960e01b815260048101829052602401610404565b60405163177e802f60e01b81526001600160a01b038316600482015260248101829052604401610404565b610c2a8383610d75565b61048960008484846108da565b60008072184f03e93ff9f4daa797ed6e38ed64bf6a1f0160401b8310610c765772184f03e93ff9f4daa797ed6e38ed64bf6a1f0160401b830492506040015b6d04ee2d6d415b85acef81000000008310610ca2576d04ee2d6d415b85acef8100000000830492506020015b662386f26fc100008310610cc057662386f26fc10000830492506010015b6305f5e1008310610cd8576305f5e100830492506008015b6127108310610cec57612710830492506004015b60648310610cfe576064830492506002015b600a83106102ff5760010192915050565b60006001600160a01b03831615801590610d6d5750826001600160a01b0316846001600160a01b03161480610d495750610d4984846105f7565b80610d6d57506000828152600460205260409020546001600160a01b038481169116145b949350505050565b6001600160a01b038216610d9f57604051633250574960e11b815260006004820152602401610404565b6000610dad838360006106a9565b90506001600160a01b03811615610489576040516339e3563760e11b815260006004820152602401610404565b6001600160e01b03198116811461066057600080fd5b600060208284031215610e0257600080fd5b81356105a881610dda565b60005b83811015610e28578181015183820152602001610e10565b50506000910152565b60008151808452610e49816020860160208601610e0d565b601f01601f19169290920160200192915050565b6020815260006105a86020830184610e31565b600060208284031215610e8257600080fd5b5035919050565b80356001600160a01b0381168114610ea057600080fd5b919050565b60008060408385031215610eb857600080fd5b610ec183610e89565b946020939093013593505050565b600080600060608486031215610ee457600080fd5b610eed84610e89565b9250610efb60208501610e89565b9150604084013590509250925092565b600060208284031215610f1d57600080fd5b6105a882610e89565b60008060408385031215610f3957600080fd5b610f4283610e89565b915060208301358015158114610f5757600080fd5b809150509250929050565b634e487b7160e01b600052604160045260246000fd5b60008060008060808587031215610f8e57600080fd5b610f9785610e89565b9350610fa560208601610e89565b925060408501359150606085013567ffffffffffffffff80821115610fc957600080fd5b818701915087601f830112610fdd57600080fd5b813581811115610fef57610fef610f62565b604051601f8201601f19908116603f0116810190838211818310171561101757611017610f62565b816040528281528a602084870101111561103057600080fd5b82602086016020830137600060208483010152809550505050505092959194509250565b6000806040838503121561106757600080fd5b61107083610e89565b915061107e60208401610e89565b90509250929050565b600181811c9082168061109b57607f821691505b6020821081036110bb57634e487b7160e01b600052602260045260246000fd5b50919050565b600083516110d3818460208801610e0d565b8351908301906110e7818360208801610e0d565b01949350505050565b7f646174613a6170706c69636174696f6e2f6a736f6e3b757466382c000000000081526000825161112881601b850160208701610e0d565b91909101601b0192915050565b6001600160a01b038581168252841660208201526040810183905260806060820181905260009061116890830184610e31565b9695505050505050565b60006020828403121561118457600080fd5b81516105a881610dda56fe68747470733a2f2f6578616d706c652d6e66742e666c6f772e636f6d2f746f6b656e5552492f7b226e616d65223a2022546865204578616d706c652045564d2d4e6174697665204e465420436f6c6c656374696f6e222c226465736372697074696f6e223a20225468697320636f6c6c656374696f6e206973207573656420617320616e206578616d706c6520746f2068656c7020796f7520646576656c6f7020796f7572206e6578742045564d2d6e61746976652063726f73732d564d20466c6f77204e46542e222c22696d616765223a202268747470733a2f2f6173736574732e776562736974652d66696c65732e636f6d2f3566363239346330633761386364643634336231633832302f3566363239346330633761386364613535636231633933365f466c6f775f576f72646d61726b2e737667222c2262616e6e65725f696d616765223a202268747470733a2f2f6173736574732e776562736974652d66696c65732e636f6d2f3566363239346330633761386364643634336231633832302f3566363239346330633761386364613535636231633933365f466c6f775f576f72646d61726b2e737667222c2266656174757265645f696d616765223a202268747470733a2f2f6173736574732e776562736974652d66696c65732e636f6d2f3566363239346330633761386364643634336231633832302f3566363239346330633761386364613535636231633933365f466c6f775f576f72646d61726b2e737667222c2265787465726e616c5f6c696e6b223a202268747470733a2f2f6578616d706c652d6e66742e666c6f772e636f6d222c22636f6c6c61626f7261746f7273223a205b5d7da2646970667358221220f9ad0fe0c5849b8f050dd96760ae52e0cbec71dfbe229f132c74aa541bbf05f464736f6c63430008180033" + /* --- Bytecode Getters --- */ access(all) @@ -125,6 +127,11 @@ fun getBridgedTokenCodeChunks(): [String] { return bridgedTokenCodeChunks } +access(all) +fun getEVMNativeERC721Bytecode(): String { + return evmNativeERC721Bytecode +} + /* --- Event Value Helpers --- */ access(all) @@ -192,6 +199,16 @@ fun getBridgeCOAAddressHex(): String { return addressHex } +access(all) +fun getTypeAssociated(with evmAddress: String): String { + var associatedTypeResult = _executeScript( + "./scripts/get_associated_type_identifier.cdc", + [evmAddress] + ) + Test.expect(associatedTypeResult, Test.beSucceeded()) + return associatedTypeResult.returnValue as! String? ?? panic("Problem getting associated Type as String") +} + access(all) fun getAssociatedEVMAddressHex(with typeIdentifier: String): String { var associatedEVMAddressResult = _executeScript( @@ -520,4 +537,19 @@ fun bridgeTokensFromEVM( // TODO: Add event assertions on bridge events. We can't currently import the event types to do this // so state assertions beyond call scope will need to suffice for now +} + +access(all) +fun registerCrossVMNFT( + signer: Test.TestAccount, + nftTypeIdentifier: String, + fulfillmentMinterPath: StoragePath?, + beFailed: Bool +) { + let registerResult = _executeTransaction( + "../transactions/bridge/onboarding/register_cross_vm_nft.cdc", + [nftTypeIdentifier, fulfillmentMinterPath], + signer + ) + Test.expect(registerResult, beFailed ? Test.beFailed() : Test.beSucceeded()) } \ No newline at end of file diff --git a/cadence/transactions/bridge/onboarding/register_cross_vm_nft.cdc b/cadence/transactions/bridge/onboarding/register_cross_vm_nft.cdc new file mode 100644 index 00000000..d023b377 --- /dev/null +++ b/cadence/transactions/bridge/onboarding/register_cross_vm_nft.cdc @@ -0,0 +1,41 @@ +import "FlowEVMBridgeCustomAssociations" +import "FlowEVMBridge" + +/// This transaction will register an NFT type as a custom cross-VM NFT. The Cadence contract must implement the +/// CrossVMMetadata.EVMPointer view and the corresponding ERC721 must implement ICrossVM interface such that the Type +/// points to the EVM contract and vice versa. If the NFT is EVM-native, a +/// FlowEVMBridgeCustomAssociations.NFTFulfillmentMinter Capability must be provided, allowing the bridge to fulfill +/// requests moving the ERC721 from EVM into Cadence. +/// +/// See FLIP-318 for more information on implementing custom cross-VM NFTs: https://github.com/onflow/flips/issues/318 +/// +/// @param nftTypeIdentifer: The type identifier of the NFT being registered as a custom cross-VM implementation +/// @param fulfillmentMinterPath: The StoragePath where the NFTFulfillmentMinter is stored +/// +transaction(nftTypeIdentifier: String, fulfillmentMinterPath: StoragePath?) { + + let nftType: Type + let fulfillmentMinterCap: Capability? + + prepare(signer: auth(BorrowValue, StorageCapabilities) &Account) { + self.nftType = CompositeType(nftTypeIdentifier) ?? panic("Could not construct type from identifier ".concat(nftTypeIdentifier)) + if fulfillmentMinterPath != nil { + assert( + signer.storage.type(at: fulfillmentMinterPath!) != nil, + message: "There was no resource found at provided path ".concat(fulfillmentMinterPath!.toString()) + ) + self.fulfillmentMinterCap = signer.capabilities.storage + .issue( + fulfillmentMinterPath! + ) + } else { + self.fulfillmentMinterCap = nil + } + } + + execute { + FlowEVMBridge.registerCrossVMNFT(type: self.nftType, fulfillmentMinter: self.fulfillmentMinterCap) + } + + // post {} - TODO: assert the association has been updated +} \ No newline at end of file diff --git a/flow.json b/flow.json index c7de7c9c..18061a13 100644 --- a/flow.json +++ b/flow.json @@ -41,6 +41,12 @@ "testing": "0000000000000008" } }, + "ExampleEVMNativeNFT": { + "source": "./cadence/contracts/example-assets/cross-vm-nfts/ExampleEVMNativeNFT.cdc", + "aliases": { + "testing": "0000000000000008" + } + }, "ExampleToken": { "source": "./cadence/contracts/example-assets/ExampleToken.cdc", "aliases": { @@ -67,6 +73,13 @@ "testnet": "dfc20aee650fcbdf" } }, + "FlowEVMBridgeCustomAssociations": { + "source": "./cadence/contracts/bridge/FlowEVMBridgeCustomAssociations.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testing": "0000000000000007" + } + }, "FlowEVMBridgeConfig": { "source": "./cadence/contracts/bridge/FlowEVMBridgeConfig.cdc", "aliases": { @@ -250,7 +263,7 @@ }, "dependencies": { "Burner": { - "source": "mainnet://f233dcee88fe0abe.Burner", + "source": "testnet://9a0766d93b6608b7.Burner", "hash": "71af18e227984cd434a3ad00bb2f3618b76482842bae920ee55662c37c8bf331", "aliases": { "emulator": "f8d6e0586b0a20c7", @@ -258,9 +271,18 @@ "testnet": "9a0766d93b6608b7" } }, + "CrossVMMetadataViews": { + "source": "testnet://631e88ae7f1d7c20.CrossVMMetadataViews", + "hash": "2857b50a3253f1c2414c151d75e4e961b280f4dfafa7d7b0a34a143790074a41", + "aliases": { + "mainnet": "631e88ae7f1d7c20", + "testnet": "631e88ae7f1d7c20", + "testing": "0000000000000001" + } + }, "EVM": { - "source": "mainnet://e467b9dd11fa00df.EVM", - "hash": "5c69921fa06088b477e2758e122636b39d3d3eb5316807c206c5680d9ac74c7e", + "source": "testnet://8c5303eaa26202d6.EVM", + "hash": "ecdb0041c47d1c6a56682e357677d49a6c8ae2e90cbe12e08c72bd64636759df", "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -268,8 +290,8 @@ } }, "FlowStorageFees": { - "source": "mainnet://e467b9dd11fa00df.FlowStorageFees", - "hash": "e38d8a95f6518b8ff46ce57dfa37b4b850b3638f33d16333096bc625b6d9b51a", + "source": "testnet://8c5303eaa26202d6.FlowStorageFees", + "hash": "a4dbe988fcaa61db479b437579b3a470d8f8ce11be08827111522f19a50fdb07", "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -277,8 +299,8 @@ } }, "FlowToken": { - "source": "mainnet://1654653399040a61.FlowToken", - "hash": "cefb25fd19d9fc80ce02896267eb6157a6b0df7b1935caa8641421fe34c0e67a", + "source": "testnet://7e60df042a9c0868.FlowToken", + "hash": "a7b219cf8596c1116aa219bb31535faa79ebf5e02d16fa594acd0398057674e1", "aliases": { "emulator": "0ae53cb6e3f42a79", "mainnet": "1654653399040a61", @@ -286,8 +308,8 @@ } }, "FungibleToken": { - "source": "mainnet://f233dcee88fe0abe.FungibleToken", - "hash": "050328d01c6cde307fbe14960632666848d9b7ea4fef03ca8c0bbfb0f2884068", + "source": "testnet://9a0766d93b6608b7.FungibleToken", + "hash": "b0f9da73434113e5b304e4bbf41e0da7e47d6e805a6733c05cf547a7767ea819", "aliases": { "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", @@ -295,8 +317,8 @@ } }, "FungibleTokenMetadataViews": { - "source": "mainnet://f233dcee88fe0abe.FungibleTokenMetadataViews", - "hash": "dff704a6e3da83997ed48bcd244aaa3eac0733156759a37c76a58ab08863016a", + "source": "testnet://9a0766d93b6608b7.FungibleTokenMetadataViews", + "hash": "085d02742eb50e6200cf2a4ad4551857313513afa7205c1e85f5966547a56df3", "aliases": { "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", @@ -304,8 +326,8 @@ } }, "MetadataViews": { - "source": "mainnet://1d7e57aa55817448.MetadataViews", - "hash": "10a239cc26e825077de6c8b424409ae173e78e8391df62750b6ba19ffd048f51", + "source": "testnet://631e88ae7f1d7c20.MetadataViews", + "hash": "d467bbde7bcf59dafbe5d472b44dcc6018ca53697baf1aea523f8ff145455eee", "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -313,8 +335,8 @@ } }, "NonFungibleToken": { - "source": "mainnet://1d7e57aa55817448.NonFungibleToken", - "hash": "b63f10e00d1a814492822652dac7c0574428a200e4c26cb3c832c4829e2778f0", + "source": "testnet://631e88ae7f1d7c20.NonFungibleToken", + "hash": "ac40c5a3ec05884ae48cb52ebf680deebb21e8a0143cd7d9b1dc88b0f107e088", "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -322,7 +344,7 @@ } }, "ViewResolver": { - "source": "mainnet://1d7e57aa55817448.ViewResolver", + "source": "testnet://631e88ae7f1d7c20.ViewResolver", "hash": "374a1994046bac9f6228b4843cb32393ef40554df9bd9907a702d098a2987bde", "aliases": { "emulator": "f8d6e0586b0a20c7", diff --git a/solidity/src/example-assets/CadenceNativeERC721.sol b/solidity/src/example-assets/cross-vm-nfts/CadenceNativeERC721.sol similarity index 93% rename from solidity/src/example-assets/CadenceNativeERC721.sol rename to solidity/src/example-assets/cross-vm-nfts/CadenceNativeERC721.sol index 4958a955..6b56975a 100644 --- a/solidity/src/example-assets/CadenceNativeERC721.sol +++ b/solidity/src/example-assets/cross-vm-nfts/CadenceNativeERC721.sol @@ -1,11 +1,11 @@ pragma solidity 0.8.24; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {CrossVMBridgeERC721Fulfillment} from "../interfaces/CrossVMBridgeERC721Fulfillment.sol"; +import {CrossVMBridgeERC721Fulfillment} from "../../interfaces/CrossVMBridgeERC721Fulfillment.sol"; /** - * @title CadenceNativeERC721 - * @dev This contract is a minimal ERC721 implementation demonstrating the use of the + * @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. diff --git a/solidity/src/example-assets/cross-vm-nfts/EVMNativeERC721.sol b/solidity/src/example-assets/cross-vm-nfts/EVMNativeERC721.sol new file mode 100644 index 00000000..9d89aea7 --- /dev/null +++ b/solidity/src/example-assets/cross-vm-nfts/EVMNativeERC721.sol @@ -0,0 +1,65 @@ +pragma solidity 0.8.24; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {ICrossVM} from "../../interfaces/ICrossVM.sol"; + +/** + * @title EVMNativeERC721 + * @dev This contract is a minimal ERC721 implementation demonstrating a simple EVM-native cross-VM + * NFT implementations where projects deploy both a Cadence & Solidity definition. 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 an EVM-native NFT. This token's + * corresponding example Cadence implementation can be seen as ExampleEVMNativeNFT.cdc in Flow's VM + * Bridge repo: https://github.com/onflow/flow-evm-bridge + * + * 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 EVMNativeERC721 is ERC721, Ownable, ICrossVM { + + string cadenceAddress; + string cadenceIdentifier; + + constructor( + string memory cadenceAddress_, + string memory cadenceIdentifier_ + + ) ERC721("EVMNativeERC721", "EVMXMPL") Ownable(msg.sender) { + cadenceAddress = cadenceAddress_; + cadenceIdentifier = cadenceIdentifier_; + } + + function getCadenceAddress() external view returns (string memory) { + return cadenceAddress; + } + + function getCadenceIdentifier() external view returns (string memory) { + return cadenceIdentifier; + } + + function safeMint(address to, uint256 tokenId) public onlyOwner { + _safeMint(to, tokenId); + } + + function _baseURI() internal pure override returns (string memory) { + return "https://example-nft.flow.com/tokenURI/"; + } + + function contractURI() public pure returns (string memory) { + // schema based on OpenSea's contractURI() guidance: https://docs.opensea.io/docs/contract-level-metadata + string memory json = '{' + '"name": "The Example EVM-Native NFT Collection",' + '"description": "This collection is used as an example to help you develop your next EVM-native cross-VM Flow NFT.",' + '"image": "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg",' + '"banner_image": "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg",' + '"featured_image": "https://assets.website-files.com/5f6294c0c7a8cdd643b1c820/5f6294c0c7a8cda55cb1c936_Flow_Wordmark.svg",' + '"external_link": "https://example-nft.flow.com",' + '"collaborators": []' + '}'; + return string.concat('data:application/json;utf8,', json); + } +} \ No newline at end of file diff --git a/solidity/test/CrossVMBridgeERC721Fulfillment.t.sol b/solidity/test/CrossVMBridgeERC721Fulfillment.t.sol index 27db6cac..da4c7f68 100644 --- a/solidity/test/CrossVMBridgeERC721Fulfillment.t.sol +++ b/solidity/test/CrossVMBridgeERC721Fulfillment.t.sol @@ -8,7 +8,7 @@ import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {ICrossVMBridgeCallable} from "../src/interfaces/ICrossVMBridgeCallable.sol"; import {ICrossVMBridgeERC721Fulfillment} from "../src/interfaces/ICrossVMBridgeERC721Fulfillment.sol"; import {ICrossVMBridgeERC721Fulfillment} from "../src/interfaces/ICrossVMBridgeERC721Fulfillment.sol"; -import {CadenceNativeERC721} from "../src/example-assets/CadenceNativeERC721.sol"; +import {CadenceNativeERC721} from "../src/example-assets/cross-vm-nfts/CadenceNativeERC721.sol"; contract CrossVMBridgeERC721FulfillmentTest is Test { CadenceNativeERC721 internal erc721Impl;