Smart Contract

FlowEVMBridge

A.1e4aa0b87d10b141.FlowEVMBridge

Valid From

114,569,068

Deployed

1w ago
Feb 16, 2026, 08:14:21 PM UTC

Dependents

11719 imports
1import Burner from 0xf233dcee88fe0abe
2import FungibleToken from 0xf233dcee88fe0abe
3import FungibleTokenMetadataViews from 0xf233dcee88fe0abe
4import NonFungibleToken from 0x1d7e57aa55817448
5import MetadataViews from 0x1d7e57aa55817448
6import CrossVMMetadataViews from 0x1d7e57aa55817448
7import ViewResolver from 0x1d7e57aa55817448
8
9import EVM from 0xe467b9dd11fa00df
10
11import IBridgePermissions from 0x1e4aa0b87d10b141
12import ICrossVM from 0x1e4aa0b87d10b141
13import IEVMBridgeNFTMinter from 0x1e4aa0b87d10b141
14import IEVMBridgeTokenMinter from 0x1e4aa0b87d10b141
15import IFlowEVMNFTBridge from 0x1e4aa0b87d10b141
16import IFlowEVMTokenBridge from 0x1e4aa0b87d10b141
17import CrossVMNFT from 0x1e4aa0b87d10b141
18import CrossVMToken from 0x1e4aa0b87d10b141
19import FlowEVMBridgeCustomAssociationTypes from 0x1e4aa0b87d10b141
20import FlowEVMBridgeCustomAssociations from 0x1e4aa0b87d10b141
21import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141
22import FlowEVMBridgeHandlerInterfaces from 0x1e4aa0b87d10b141
23import FlowEVMBridgeUtils from 0x1e4aa0b87d10b141
24import FlowEVMBridgeNFTEscrow from 0x1e4aa0b87d10b141
25import FlowEVMBridgeTokenEscrow from 0x1e4aa0b87d10b141
26import FlowEVMBridgeTemplates from 0x1e4aa0b87d10b141
27import SerializeMetadata from 0x1e4aa0b87d10b141
28
29/// The FlowEVMBridge contract is the main entrypoint for bridging NFT & FT assets between Flow & FlowEVM.
30///
31/// Before bridging, be sure to onboard the asset type which will configure the bridge to handle the asset. From there,
32/// the asset can be bridged between VMs via the COA as the entrypoint.
33///
34/// See also:
35/// - Code in context: https://github.com/onflow/flow-evm-bridge
36/// - FLIP #237: https://github.com/onflow/flips/pull/233
37///
38access(all)
39contract FlowEVMBridge : IFlowEVMNFTBridge, IFlowEVMTokenBridge {
40
41    /*************
42        Events
43    **************/
44
45    /// Emitted any time a new asset type is onboarded to the bridge
46    access(all)
47    event Onboarded(type: String, cadenceContractAddress: Address, evmContractAddress: String)
48    /// Denotes a defining contract was deployed to the bridge account
49    access(all)
50    event BridgeDefiningContractDeployed(
51        contractName: String,
52        assetName: String,
53        symbol: String,
54        isERC721: Bool,
55        evmContractAddress: String
56    )
57    /// Emitted whenever a bridged NFT is burned as a part of the bridging process. In the context of this contract,
58    /// this only occurs when an EVM-native ERC721 updates from a bridged NFT to their own custom Cadence NFT.
59    access(all)
60    event BridgedNFTBurned(type: String, id: UInt64, evmID: UInt256, uuid: UInt64, erc721Address: String)
61
62    /**************************
63        Public Onboarding
64    **************************/
65
66    /// Onboards a given asset by type to the bridge. Since we're onboarding by Cadence Type, the asset must be defined
67    /// in a third-party contract. Attempting to onboard a bridge-defined asset will result in an error as the asset has
68    /// already been onboarded to the bridge.
69    ///
70    /// @param type: The Cadence Type of the NFT to be onboarded
71    /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW
72    ///
73    access(all)
74    fun onboardByType(_ type: Type, feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}) {
75        pre {
76            !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused"
77            !FlowEVMBridgeConfig.isCadenceTypeBlocked(type):
78            "This Cadence Type ".concat(type.identifier).concat(" is currently blocked from being onboarded")
79            self.typeRequiresOnboarding(type) == true: "Onboarding is not needed for this type"
80            FlowEVMBridgeUtils.typeAllowsBridging(type):
81            "This Cadence Type ".concat(type.identifier).concat(" is currently opted-out of bridge onboarding")
82            FlowEVMBridgeUtils.isCadenceNative(type: type): "Only Cadence-native assets can be onboarded by Type"
83        }
84        /* Custom cross-VM Implementation check */
85        //
86        // Register as a custom cross-VM implementation if detected
87        if FlowEVMBridgeUtils.getEVMPointerView(forType: type) != nil {
88            self.registerCrossVMNFT(type: type, fulfillmentMinter: nil, feeProvider: feeProvider)
89            return
90        }
91
92        /* Provision fees */
93        //
94        // Withdraw from feeProvider and deposit to self
95        FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: FlowEVMBridgeConfig.onboardFee)
96
97        /* EVM setup */
98        //
99        // Deploy an EVM defining contract via the FlowBridgeFactory.sol contract
100        let onboardingValues = self.deployEVMContract(forAssetType: type)
101
102        /* Cadence escrow setup */
103        //
104        // Initialize bridge escrow for the asset based on its type
105        if type.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) {
106            FlowEVMBridgeNFTEscrow.initializeEscrow(
107                forType: type,
108                name: onboardingValues.name,
109                symbol: onboardingValues.symbol,
110                erc721Address: onboardingValues.evmContractAddress
111            )
112        } else if type.isSubtype(of: Type<@{FungibleToken.Vault}>()) {
113            let createVaultFunction = FlowEVMBridgeUtils.getCreateEmptyVaultFunction(forType: type)
114                ?? panic("Could not retrieve createEmptyVault function for the given type")
115            let vault <-createVaultFunction(type)
116            assert(
117                vault.getType() == type,
118                message: "Requested to onboard type=".concat(type.identifier).concat( "but contract returned type=").concat(vault.getType().identifier)
119            )
120            FlowEVMBridgeTokenEscrow.initializeEscrow(
121                with: <-vault,
122                name: onboardingValues.name,
123                symbol: onboardingValues.symbol,
124                decimals: onboardingValues.decimals!,
125                evmTokenAddress: onboardingValues.evmContractAddress
126            )
127        } else {
128            panic("Attempted to onboard unsupported type: ".concat(type.identifier))
129        }
130
131        /* Confirmation */
132        //
133        assert(
134            FlowEVMBridgeNFTEscrow.isInitialized(forType: type) || FlowEVMBridgeTokenEscrow.isInitialized(forType: type),
135            message: "Failed to initialize escrow for given type"
136        )
137
138        emit Onboarded(
139            type: type.identifier,
140            cadenceContractAddress: FlowEVMBridgeUtils.getContractAddress(fromType: type)!,
141            evmContractAddress: onboardingValues.evmContractAddress.toString()
142        )
143    }
144
145    /// Onboards a given EVM contract to the bridge. Since we're onboarding by EVM Address, the asset must be defined in
146    /// a third-party EVM contract. Attempting to onboard a bridge-defined asset will result in an error as onboarding
147    /// is not required.
148    ///
149    /// @param address: The EVMAddress of the ERC721 or ERC20 to be onboarded
150    /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW
151    ///
152    access(all)
153    fun onboardByEVMAddress(
154        _ address: EVM.EVMAddress,
155        feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
156    ) {
157        pre {
158            !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused"
159            !FlowEVMBridgeConfig.isEVMAddressBlocked(address):
160                "This EVM contract ".concat(address.toString()).concat(" is currently blocked from being onboarded")
161        }
162        /* Custom cross-VM Implementation check */
163        //
164        let cadenceAddr = FlowEVMBridgeUtils.getDeclaredCadenceAddressFromCrossVM(evmContract: address)
165        let cadenceType = FlowEVMBridgeUtils.getDeclaredCadenceTypeFromCrossVM(evmContract: address)
166        // Register as a custom cross-VM implementation if detected
167        if cadenceAddr != nil && cadenceType != nil {
168            self.registerCrossVMNFT(type: cadenceType!, fulfillmentMinter: nil, feeProvider: feeProvider)
169            return
170        }
171
172        /* Validate the EVM contract */
173        //
174        // Ensure the project has not opted out of bridge support
175        assert(
176            FlowEVMBridgeUtils.evmAddressAllowsBridging(address),
177            message: "This contract is not supported as defined by the project's development team"
178        )
179        assert(
180            self.evmAddressRequiresOnboarding(address) == true,
181            message: "Onboarding is not needed for this contract"
182        )
183
184        /* Provision fees */
185        //
186        // Withdraw fee from feeProvider and deposit
187        FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: FlowEVMBridgeConfig.onboardFee)
188
189        /* Setup Cadence-defining contract */
190        //
191        // Deploy a defining Cadence contract to the bridge account
192        self.deployDefiningContract(evmContractAddress: address)
193    }
194
195    /// Registers a custom cross-VM NFT implementation, allowing projects to integrate their Cadence & EVM contracts
196    /// such that the VM bridge facilitates movement between VMs as the integrated implementations.
197    ///
198    /// @param type: The NFT Type to register as cross-VM NFT
199    /// @param fulfillmentMinter: The optional NFTFulfillmentMinter Capability. This parameter is required for
200    ///     EVM-native NFTs
201    /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW
202    ///
203    access(all)
204    fun registerCrossVMNFT(
205        type: Type,
206        fulfillmentMinter: Capability<auth(FlowEVMBridgeCustomAssociationTypes.FulfillFromEVM) &{FlowEVMBridgeCustomAssociationTypes.NFTFulfillmentMinter}>?,
207        feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
208    ) {
209        pre {
210            FlowEVMBridgeUtils.typeAllowsBridging(type):
211            "This Cadence Type \(type.identifier) is currently opted-out of bridge onboarding"
212            type.isSubtype(of: Type<@{NonFungibleToken.NFT}>()):
213            "The provided Type \(type.identifier) is not an NFT - only NFTs can register as cross-VM"
214            !type.isSubtype(of: Type<@{FungibleToken.Vault}>()):
215            "The provided Type \(type.identifier) is also a FungibleToken Vault - only NFTs can register as cross-VM"
216            !FlowEVMBridgeConfig.isCadenceTypeBlocked(type):
217            "Type \(type.identifier) has been blocked from onboarding"
218            FlowEVMBridgeUtils.isCadenceNative(type: type):
219            "Attempting to register a bridge-deployed NFT - cannot update a bridge-defined asset. If updating your EVM "
220                .concat("contract's Cadence association, deploy your Cadence NFT contract and register using the newly defined Cadence type")
221            FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type) == nil:
222            "A custom association has already been declared for type \(type.identifier) with EVM address "
223                .concat(FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type)!.toString())
224                .concat(". Custom associations can only be declared once for any given Cadence Type or EVM contract")
225            fulfillmentMinter?.check() ?? true:
226            "NFTFulfillmentMinter Capability is invalid - Issue a new "
227                .concat("Capability<auth(FlowEVMBridgeCustomAssociationTypes.FulfillFromEVM) &{FlowEVMBridgeCustomAssociationTypes.NFTFulfillmentMinter}> and try again")
228            fulfillmentMinter != nil ? fulfillmentMinter!.borrow()!.getType().address! == type.address! : true:
229            "NFTFulfillmentMinter must be defined by a contract deployed to the registered type address \(type.address!) "
230                .concat(" but found defining address of \(fulfillmentMinter!.borrow()!.getType().address!)")
231        }
232        /* Provision fees */
233        //
234        // Withdraw fee from feeProvider and deposit
235        FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: FlowEVMBridgeConfig.onboardFee)
236
237        /* Get pointers from both contracts */
238        //
239        // Get the Cadence side EVMPointer
240        let evmPointer = FlowEVMBridgeUtils.getEVMPointerView(forType: type)
241            ?? panic("The CrossVMMetadataViews.EVMPointer is not supported by the type \(type.identifier).")
242        // EVM contract checks
243        assert(!FlowEVMBridgeConfig.isEVMAddressBlocked(evmPointer.evmContractAddress),
244            message: "Type \(type.identifier) has been blocked from onboarding.")
245        assert(
246            FlowEVMBridgeUtils.evmAddressAllowsBridging(evmPointer.evmContractAddress),
247            message: "The EVM contract \(evmPointer.evmContractAddress.toString()) developers have opted out of VM bridge integration."
248        )
249        assert(
250            FlowEVMBridgeCustomAssociations.getTypeAssociated(with: evmPointer.evmContractAddress) == nil,
251            message: "A custom association has already been declared for EVM address \(evmPointer.evmContractAddress.toString()) with Cadence Type "
252                .concat(FlowEVMBridgeCustomAssociations.getTypeAssociated(with: evmPointer.evmContractAddress)?.identifier ?? "<UNKNOWN>")
253                .concat(". Custom associations can only be declared once for any given Cadence Type or EVM contract")
254        )
255        assert(
256            FlowEVMBridgeUtils.isERC721(evmContractAddress: evmPointer.evmContractAddress)
257            && !FlowEVMBridgeUtils.isERC20(evmContractAddress: evmPointer.evmContractAddress),
258            message: "Cross-VM NFTs must be implemented as ERC721 exclusively, but detected an invalid EVM interface "
259                .concat("at EVM contract \(evmPointer.evmContractAddress.toString())")
260        )
261
262        // Get pointer on EVM side
263        let cadenceAddr = FlowEVMBridgeUtils.getDeclaredCadenceAddressFromCrossVM(evmContract: evmPointer.evmContractAddress)
264            ?? panic("Could not retrieve a Cadence address declaration from the EVM contract \(evmPointer.evmContractAddress.toString())")
265        let cadenceType = FlowEVMBridgeUtils.getDeclaredCadenceTypeFromCrossVM(evmContract: evmPointer.evmContractAddress)
266            ?? panic("Could not retrieve a Cadence Type declaration from the EVM contract \(evmPointer.evmContractAddress.toString())")
267
268        /* Pointer validation */
269        //
270        // Assert both point to each other
271        assert(
272            type.address == cadenceAddr,
273            message: "Mismatched Cadence Address pointers: \(type.address!.toString()) and \(cadenceAddr.toString())"
274        )
275        assert(
276            type == cadenceType,
277            message: "Mismatched type pointers: \(type.identifier) and \(cadenceType.identifier)"
278        )
279
280        /* Cross-VM conformance check */
281        //
282        // Check supportsInterface() for CrossVMBridgeERC721Fulfillment if NFT is Cadence-native
283        if evmPointer.nativeVM == CrossVMMetadataViews.VM.Cadence {
284            assert(FlowEVMBridgeUtils.supportsCadenceNativeNFTEVMInterfaces(evmContract: evmPointer.evmContractAddress),
285                message: "Corresponding EVM contract does not implement necessary EVM interfaces ICrossVMBridgeERC721Fulfillment "
286                    .concat("and/or ICrossVMBridgeCallable. All Cadence-native cross-VM NFTs must implement these interfaces and ")
287                    .concat("grant the bridge COA the ability to fulfill bridge requests moving NFTs into EVM."))
288            let designatedVMBridgeAddress = FlowEVMBridgeUtils.getVMBridgeAddressFromICrossVMBridgeCallable(evmContract: evmPointer.evmContractAddress)
289                ?? panic("Could not recover declared VM bridge address from EVM contract \(evmPointer.evmContractAddress.toString()). "
290                    .concat("Ensure the contract conforms to ICrossVMBridgeCallable and declare the vmBridgeAddress as \(FlowEVMBridgeUtils.getBridgeCOAEVMAddress().toString())"))
291            assert(designatedVMBridgeAddress.equals(FlowEVMBridgeUtils.getBridgeCOAEVMAddress()),
292                message: "ICrossVMBridgeCallable declared \(designatedVMBridgeAddress.toString())"
293                    .concat(" as vmBridgeAddress which must be declared as \(FlowEVMBridgeUtils.getBridgeCOAEVMAddress().toString())"))
294        }
295
296        /* Native VM consistency check */
297        //
298        // Assess if the NFT has been previously onboarded to the bridge
299        let legacyEVMAssoc = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: type)
300        let legacyCadenceAssoc = FlowEVMBridgeConfig.getLegacyTypeAssociated(with: evmPointer.evmContractAddress)
301        assert(legacyEVMAssoc == nil || legacyCadenceAssoc == nil,
302            message: "Both the EVM contract \(evmPointer.evmContractAddress.toString()) and the Cadence Type \(type.identifier) "
303                .concat("have already been onboarded to the VM bridge - one side of this association will have to be redeployed ")
304                .concat("and the declared association updated to a non-onboarded target in order to register as a custom cross-VM asset."))
305        // Ensure the native VM is consistent if the NFT has been previously onboarded via the permissionless path
306        if legacyEVMAssoc != nil {
307            assert(evmPointer.nativeVM == CrossVMMetadataViews.VM.Cadence,
308                message: "Attempting to register NFT \(type.identifier) as EVM-native after it has already been "
309                    .concat("onboarded as Cadence-native. This NFT must be configured as Cadence-native with an ERC721 ")
310                    .concat("implementing CrossVMBridgeERC721Fulfillment base contract allowing the bridge to fulfill ")
311                    .concat("NFTs moving into EVM"))
312        } else if legacyCadenceAssoc != nil {
313            assert(evmPointer.nativeVM == CrossVMMetadataViews.VM.EVM,
314                message: "Attempting to register NFT \(type.identifier) as Cadence-native after it has already been "
315                    .concat("onboarded as EVM-native. This NFT must be configured as EVM-native and provide an NFTFulfillmentMinter ")
316                    .concat("Capability so the bridge may fulfill NFTs moving into Cadence."))
317        }
318
319        FlowEVMBridgeCustomAssociations.saveCustomAssociation(
320            type: type,
321            evmContractAddress: evmPointer.evmContractAddress,
322            nativeVM: evmPointer.nativeVM,
323            updatedFromBridged: legacyEVMAssoc != nil || legacyCadenceAssoc != nil,
324            fulfillmentMinter: fulfillmentMinter
325        )
326
327        if !FlowEVMBridgeNFTEscrow.isInitialized(forType: type) {
328            let name = FlowEVMBridgeUtils.getName(evmContractAddress: evmPointer.evmContractAddress)
329            let symbol = FlowEVMBridgeUtils.getSymbol(evmContractAddress: evmPointer.evmContractAddress)
330            FlowEVMBridgeNFTEscrow.initializeEscrow(
331                forType: type,
332                name: name,
333                symbol: symbol,
334                erc721Address: evmPointer.evmContractAddress
335            )
336        }
337    }
338
339    /*************************
340        NFT Handling
341    **************************/
342
343    /// Public entrypoint to bridge NFTs from Cadence to EVM as ERC721.
344    ///
345    /// @param token: The NFT to be bridged
346    /// @param to: The NFT recipient in FlowEVM
347    /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW
348    ///
349    access(all)
350    fun bridgeNFTToEVM(
351        token: @{NonFungibleToken.NFT},
352        to: EVM.EVMAddress,
353        feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
354    ) {
355        pre {
356            !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused"
357            !token.isInstance(Type<@{FungibleToken.Vault}>()): "Mixed asset types are not yet supported"
358            self.typeRequiresOnboarding(token.getType()) == false: "NFT must first be onboarded"
359            FlowEVMBridgeConfig.isTypePaused(token.getType()) == false: "Bridging is currently paused for this NFT"
360        }
361        let bridgedAssoc = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: token.getType())
362        let customAssocByType = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: token.getType())
363        let customAssocByEVMAddr =  bridgedAssoc != nil ? FlowEVMBridgeCustomAssociations.getTypeAssociated(with: bridgedAssoc!) : nil
364        if bridgedAssoc != nil && customAssocByType == nil && customAssocByEVMAddr == nil {
365            // Common case - bridge-defined counterpart in non-native VM
366            return self.handleDefaultNFTToEVM(token: <-token, to: to, feeProvider: feeProvider)
367        } else if customAssocByType != nil && customAssocByEVMAddr == nil {
368            // NFT is registered as cross-VM
369            return self.handleCrossVMNFTToEVM(token: <-token, to: to, feeProvider: feeProvider)
370        } else if customAssocByType == nil && customAssocByEVMAddr != nil {
371            // Dealing with a bridge-defined NFT after a custom association has been configured
372            return self.handleUpdatedBridgedNFTToEVM(token: <-token, to: to, feeProvider: feeProvider)
373        }
374        // customAssocByType != nil && customAssocByEVMAddr != nil
375        panic("Unknown error encountered bridging NFT \(token.getType().identifier) with ID \(token.id) to EVM recipient \(to.toString())")
376    }
377
378    /// Entrypoint to bridge ERC721 from EVM to Cadence as NonFungibleToken.NFT
379    ///
380    /// @param owner: The EVM address of the NFT owner. Current ownership and successful transfer (via
381    ///     `protectedTransferCall`) is validated before the bridge request is executed.
382    /// @param calldata: Caller-provided approve() call, enabling contract COA to operate on NFT in EVM contract
383    /// @param id: The NFT ID to bridged
384    /// @param evmContractAddress: Address of the EVM address defining the NFT being bridged - also call target
385    /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW
386    /// @param protectedTransferCall: A function that executes the transfer of the NFT from the named owner to the
387    ///     bridge's COA. This function is expected to return a Result indicating the status of the transfer call.
388    ///
389    /// @returns The bridged NFT
390    ///
391    access(account)
392    fun bridgeNFTFromEVM(
393        owner: EVM.EVMAddress,
394        type: Type,
395        id: UInt256,
396        feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider},
397        protectedTransferCall: fun (EVM.EVMAddress): EVM.Result
398    ): @{NonFungibleToken.NFT} {
399        pre {
400            !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused"
401            !type.isSubtype(of: Type<@{FungibleToken.Vault}>()): "Mixed asset types are not yet supported"
402            self.typeRequiresOnboarding(type) == false: "NFT must first be onboarded"
403            FlowEVMBridgeConfig.isTypePaused(type) == false: "Bridging is currently paused for this NFT"
404        }
405        let bridgedAssoc = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: type)
406        let customAssocByType = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type)
407        let customAssocByEVMAddr =  bridgedAssoc != nil ? FlowEVMBridgeCustomAssociations.getTypeAssociated(with: bridgedAssoc!) : nil
408        // Initialize the internal handler method that will be used to move the NFT from EVM
409        var handler: (fun (EVM.EVMAddress, Type, UInt256, auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, fun (EVM.EVMAddress): EVM.Result): @{NonFungibleToken.NFT})? = nil
410        if bridgedAssoc != nil && customAssocByType == nil && customAssocByEVMAddr == nil {
411            // Common case - bridge-defined counterpart in non-native VM
412            handler = self.handleDefaultNFTFromEVM
413        } else if customAssocByType != nil && customAssocByEVMAddr == nil {
414            // NFT is registered as cross-VM
415            handler = self.handleCrossVMNFTFromEVM
416        } else if customAssocByType == nil && customAssocByEVMAddr != nil {
417            // Dealing with a bridge-defined NFT after a custom association has been configured
418            handler = self.handleUpdatedBridgedNFTFromEVM
419        } else {
420            // customAssocByType != nil && customAssocByEVMAddr != nil
421            panic("Unknown error encountered bridging NFT \(type.identifier) with ID \(id) from EVM owner \(owner.toString())")
422        }
423        // Return the bridged NFT, using the appropriate handler
424        return <- handler!(owner: owner, type: type, id: id, feeProvider: feeProvider, protectedTransferCall: protectedTransferCall)
425    }
426
427    /**************************
428        FT Handling
429    ***************************/
430
431    /// Public entrypoint to bridge FTs from Cadence to EVM as ERC20 tokens.
432    ///
433    /// @param vault: The fungible token Vault to be bridged
434    /// @param to: The fungible token recipient in EVM
435    /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW
436    ///
437    access(all)
438    fun bridgeTokensToEVM(
439        vault: @{FungibleToken.Vault},
440        to: EVM.EVMAddress,
441        feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
442    ) {
443        pre {
444            !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused"
445            !vault.isInstance(Type<@{NonFungibleToken.NFT}>()): "Mixed asset types are not yet supported"
446            self.typeRequiresOnboarding(vault.getType()) == false: "FungibleToken must first be onboarded"
447            FlowEVMBridgeConfig.isTypePaused(vault.getType()) == false: "Bridging is currently paused for this token"
448        }
449        /* Handle $FLOW requests via EVM interface & return */
450        //
451        let vaultType = vault.getType()
452
453        // Gather the vault balance before acting on the resource
454        let vaultBalance = vault.balance
455        // Initialize fee amount to 0.0 and assign as appropriate for how the token is handled
456        var feeAmount = 0.0
457
458        /* TokenHandler coverage */
459        //
460        // Some tokens pre-dating bridge require special case handling - borrow handler and passthrough to fulfill
461        if FlowEVMBridgeConfig.typeHasTokenHandler(vaultType) {
462            let handler = FlowEVMBridgeConfig.borrowTokenHandler(vaultType)
463                ?? panic("Could not retrieve handler for the given type")
464            handler.fulfillTokensToEVM(tokens: <-vault, to: to)
465
466            // Here we assume burning Vault in Cadence which doesn't require storage consumption
467            feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0)
468            FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: feeAmount)
469            return
470        }
471
472        /* Escrow or burn tokens depending on native environment */
473        //
474        // In most all other cases, if Cadence-native then tokens must be escrowed
475        if FlowEVMBridgeUtils.isCadenceNative(type: vaultType) {
476            // Lock the FT balance & calculate the extra used by the FT if any
477            let storageUsed = FlowEVMBridgeTokenEscrow.lockTokens(<-vault)
478            // Calculate the bridge fee on current rates
479            feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: storageUsed)
480        } else {
481            // Since not Cadence-native, bridge defines the token - burn the vault and calculate the fee
482            Burner.burn(<-vault)
483            feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0)
484        }
485
486        /* Provision fees */
487        //
488        // Withdraw fee amount from feeProvider and deposit
489        FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: feeAmount)
490
491        /* Gather identifying information */
492        //
493        // Does the bridge control the EVM contract associated with this type?
494        let associatedAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: vaultType)
495            ?? panic("No EVMAddress found for vault type")
496        // Convert the vault balance to a UInt256
497        let bridgeAmount = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(
498                vaultBalance,
499                erc20Address: associatedAddress
500            )
501        assert(bridgeAmount > UInt256(0), message: "Amount to bridge must be greater than 0")
502
503        // Determine if the EVM contract is bridge-owned - affects how tokens are transmitted to recipient
504        let isFactoryDeployed = FlowEVMBridgeUtils.isEVMContractBridgeOwned(evmContractAddress: associatedAddress)
505
506        /* Transmit tokens to recipient */
507        //
508        // Mint or transfer based on the bridge's EVM contract authority, making needed state assertions to confirm
509        if isFactoryDeployed {
510            FlowEVMBridgeUtils.mustMintERC20(to: to, amount: bridgeAmount, erc20Address: associatedAddress)
511        } else {
512            FlowEVMBridgeUtils.mustTransferERC20(to: to, amount: bridgeAmount, erc20Address: associatedAddress)
513        }
514    }
515
516    /// Entrypoint to bridge ERC20 tokens from EVM to Cadence as FungibleToken Vaults
517    ///
518    /// @param owner: The EVM address of the FT owner. Current ownership and successful transfer (via
519    ///     `protectedTransferCall`) is validated before the bridge request is executed.
520    /// @param calldata: Caller-provided approve() call, enabling contract COA to operate on FT in EVM contract
521    /// @param amount: The amount of tokens to be bridged
522    /// @param evmContractAddress: Address of the EVM address defining the FT being bridged - also call target
523    /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW
524    /// @param protectedTransferCall: A function that executes the transfer of the FT from the named owner to the
525    ///     bridge's COA. This function is expected to return a Result indicating the status of the transfer call.
526    ///
527    /// @returns The bridged fungible token Vault
528    ///
529    access(account)
530    fun bridgeTokensFromEVM(
531        owner: EVM.EVMAddress,
532        type: Type,
533        amount: UInt256,
534        feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider},
535        protectedTransferCall: fun (): EVM.Result
536    ): @{FungibleToken.Vault} {
537        pre {
538            !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused"
539            !type.isSubtype(of: Type<@{NonFungibleToken.Collection}>()): "Mixed asset types are not yet supported"
540            self.typeRequiresOnboarding(type) == false: "FungibleToken must first be onboarded"
541            FlowEVMBridgeConfig.isTypePaused(type) == false: "Bridging is currently paused for this token"
542        }
543        /* Provision fees */
544        //
545        // Withdraw from feeProvider and deposit to self
546        let feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0)
547        FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: feeAmount)
548
549        /* TokenHandler case coverage */
550        //
551        // Some tokens pre-dating bridge require special case handling. If such a case, fulfill via the related handler
552        if FlowEVMBridgeConfig.typeHasTokenHandler(type) {
553            //  - borrow handler and passthrough to fulfill
554            let handler = FlowEVMBridgeConfig.borrowTokenHandler(type)
555                ?? panic("Could not retrieve handler for the given type")
556            return <-handler.fulfillTokensFromEVM(
557                owner: owner,
558                type: type,
559                amount: amount,
560                protectedTransferCall: protectedTransferCall
561            )
562        }
563
564        /* Gather identifying information */
565        //
566        // Get the EVMAddress of the ERC20 contract associated with the type
567        let associatedAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: type)
568            ?? panic("No EVMAddress found for token type")
569        // Find the Cadence defining address and contract name
570        let definingAddress = FlowEVMBridgeUtils.getContractAddress(fromType: type)!
571        let definingContractName = FlowEVMBridgeUtils.getContractName(fromType: type)!
572        // Convert the amount to a ufix64 so the amount can be settled on the Cadence side
573        let ufixAmount = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(amount, erc20Address: associatedAddress)
574        assert(ufixAmount > 0.0, message: "Amount to bridge must be greater than 0")
575
576        /* Execute the transfer call and make needed state assertions */
577        //
578        FlowEVMBridgeUtils.mustEscrowERC20(
579            owner: owner,
580            amount: amount,
581            erc20Address: associatedAddress,
582            protectedTransferCall: protectedTransferCall
583        )
584
585        /* Bridge-defined tokens are minted in Cadence */
586        //
587        // If the Cadence Vault is bridge-defined, mint the tokens
588        if definingAddress == self.account.address {
589            let minter = getAccount(definingAddress).contracts.borrow<&{IEVMBridgeTokenMinter}>(name: definingContractName)!
590            return <- minter.mintTokens(amount: ufixAmount)
591        }
592
593        /* Cadence-native tokens are withdrawn from escrow */
594        //
595        // Confirm the EVM defining contract is bridge-owned before burning tokens
596        assert(
597            FlowEVMBridgeUtils.isEVMContractBridgeOwned(evmContractAddress: associatedAddress),
598            message: "Unexpected error bridging FT from EVM"
599        )
600        // Burn the EVM tokens that have now been transferred to the bridge in EVM
601        let burnResult: EVM.Result = FlowEVMBridgeUtils.call(
602            signature: "burn(uint256)",
603            targetEVMAddress: associatedAddress,
604            args: [amount],
605            gasLimit: FlowEVMBridgeConfig.gasLimit,
606            value: 0.0
607        )
608        assert(burnResult.status == EVM.Status.successful, message: "Burn of EVM tokens failed")
609
610        // Unlock from escrow and return
611        return <-FlowEVMBridgeTokenEscrow.unlockTokens(type: type, amount: ufixAmount)
612    }
613
614    /**************************
615        Public Getters
616    **************************/
617
618    /// Returns the EVM address associated with the provided type
619    ///
620    access(all)
621    view fun getAssociatedEVMAddress(with type: Type): EVM.EVMAddress? {
622        return FlowEVMBridgeConfig.getEVMAddressAssociated(with: type)
623    }
624
625    /// Retrieves the bridge contract's COA EVMAddress
626    ///
627    /// @returns The EVMAddress of the bridge contract's COA orchestrating actions in FlowEVM
628    ///
629    access(all)
630    view fun getBridgeCOAEVMAddress(): EVM.EVMAddress {
631        return FlowEVMBridgeUtils.borrowCOA().address()
632    }
633
634    /// Returns whether an asset needs to be onboarded to the bridge
635    ///
636    /// @param type: The Cadence Type of the asset
637    ///
638    /// @returns Whether the asset needs to be onboarded
639    ///
640    access(all)
641    view fun typeRequiresOnboarding(_ type: Type): Bool? {
642        if !FlowEVMBridgeUtils.isValidCadenceAsset(type: type) {
643            return nil
644        }
645        return FlowEVMBridgeConfig.getEVMAddressAssociated(with: type) == nil &&
646            !FlowEVMBridgeConfig.typeHasTokenHandler(type)
647    }
648
649    /// Returns whether an EVM-native asset needs to be onboarded to the bridge
650    ///
651    /// @param address: The EVMAddress of the asset
652    ///
653    /// @returns Whether the asset needs to be onboarded, nil if the defined asset is not supported by this bridge
654    ///
655    access(all)
656    fun evmAddressRequiresOnboarding(_ address: EVM.EVMAddress): Bool? {
657        // See if the bridge already has a known type associated with the given address
658        if FlowEVMBridgeConfig.getTypeAssociated(with: address) != nil {
659            return false
660        }
661        // Dealing with EVM-native asset, check if it's NFT or FT exclusively
662        if FlowEVMBridgeUtils.isValidEVMAsset(evmContractAddress: address) {
663            return true
664        }
665        // Not onboarded and not a valid asset, so return nil
666        return nil
667    }
668
669    /**************************
670        Internal Helpers
671    ***************************/
672
673    /// Deploys templated EVM contract via Solidity Factory contract supporting bridging of a given asset type
674    ///
675    /// @param forAssetType: The Cadence Type of the asset
676    ///
677    /// @returns The EVMAddress of the deployed contract
678    ///
679    access(self)
680    fun deployEVMContract(forAssetType: Type): FlowEVMBridgeUtils.EVMOnboardingValues {
681        pre {
682            FlowEVMBridgeUtils.isValidCadenceAsset(type: forAssetType):
683                "Asset type is not supported by the bridge"
684        }
685        let isNFT = forAssetType.isSubtype(of: Type<@{NonFungibleToken.NFT}>())
686
687        let onboardingValues = FlowEVMBridgeUtils.getCadenceOnboardingValues(forAssetType: forAssetType)
688
689        let deployedContractAddress = FlowEVMBridgeUtils.mustDeployEVMContract(
690                name: onboardingValues.name,
691                symbol: onboardingValues.symbol,
692                cadenceAddress: onboardingValues.contractAddress,
693                flowIdentifier: onboardingValues.identifier,
694                contractURI: onboardingValues.contractURI,
695                isERC721: isNFT
696            )
697
698        // Associate the deployed contract with the given type & return the deployed address
699        FlowEVMBridgeConfig.associateType(forAssetType, with: deployedContractAddress)
700        return FlowEVMBridgeUtils.EVMOnboardingValues(
701            evmContractAddress: deployedContractAddress,
702            name: onboardingValues.name,
703            symbol: onboardingValues.symbol,
704            decimals: isNFT ? nil : FlowEVMBridgeConfig.defaultDecimals,
705            contractURI: onboardingValues.contractURI,
706            cadenceContractName: FlowEVMBridgeUtils.getContractName(fromType: forAssetType)!,
707            isERC721: isNFT
708        )
709    }
710
711    /// Helper for deploying templated defining contract supporting EVM-native asset bridging to Cadence
712    /// Deploys either NFT or FT contract depending on the provided type
713    ///
714    /// @param evmContractAddress: The EVMAddress currently defining the asset to be bridged
715    ///
716    access(self)
717    fun deployDefiningContract(evmContractAddress: EVM.EVMAddress) {
718        // Gather identifying information about the EVM contract
719        let evmOnboardingValues = FlowEVMBridgeUtils.getEVMOnboardingValues(evmContractAddress: evmContractAddress)
720
721        // Get Cadence code from template & deploy to the bridge account
722        let cadenceCode: [UInt8] = FlowEVMBridgeTemplates.getBridgedAssetContractCode(
723                evmOnboardingValues.cadenceContractName,
724                isERC721: evmOnboardingValues.isERC721
725            ) ?? panic("Problem retrieving code for Cadence-defining contract")
726        if evmOnboardingValues.isERC721 {
727            self.account.contracts.add(
728                name: evmOnboardingValues.cadenceContractName,
729                code: cadenceCode,
730                evmOnboardingValues.name,
731                evmOnboardingValues.symbol,
732                evmContractAddress,
733                evmOnboardingValues.contractURI
734            )
735        } else {
736            self.account.contracts.add(
737                name: evmOnboardingValues.cadenceContractName,
738                code: cadenceCode,
739                evmOnboardingValues.name,
740                evmOnboardingValues.symbol,
741                evmOnboardingValues.decimals!,
742                evmContractAddress, evmOnboardingValues.contractURI
743            )
744        }
745
746        emit BridgeDefiningContractDeployed(
747            contractName: evmOnboardingValues.cadenceContractName,
748            assetName: evmOnboardingValues.name,
749            symbol: evmOnboardingValues.symbol,
750            isERC721: evmOnboardingValues.isERC721,
751            evmContractAddress: evmContractAddress.toString()
752        )
753    }
754
755    /// Escrows the provided NFT and withdraws the bridging fee on the basis of a base fee + storage fee
756    ///
757    access(self)
758    fun escrowNFTAndWithdrawFee(
759        token: @{NonFungibleToken.NFT},
760        from: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
761    ) {
762        // Lock the NFT & calculate the storage used by the NFT
763        let storageUsed = FlowEVMBridgeNFTEscrow.lockNFT(<-token)
764        // Calculate the bridge fee on current rates
765        let feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: storageUsed)
766        // Withdraw fee from feeProvider and deposit
767        FlowEVMBridgeUtils.depositFee(from, feeAmount: feeAmount)
768    }
769
770    /// Handle permissionlessly onboarded NFTs where the bridge deployed and manages the non-native contract
771    ///
772    access(self)
773    fun handleDefaultNFTToEVM(
774        token: @{NonFungibleToken.NFT},
775        to: EVM.EVMAddress,
776        feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
777    ) {
778        /* Gather identifying information */
779        //
780        let tokenType = token.getType()
781        let tokenID = token.id
782        let evmID = CrossVMNFT.getEVMID(from: &token as &{NonFungibleToken.NFT}) ?? UInt256(token.id)
783
784        /* Metadata assignment */
785        //
786        // Grab the URI from the NFT if available
787        var uri: String = ""
788        var symbol: String = ""
789        // Default to project-specified URI
790        if let metadata = token.resolveView(Type<MetadataViews.EVMBridgedMetadata>()) as! MetadataViews.EVMBridgedMetadata? {
791            uri = metadata.uri.uri()
792            symbol = metadata.symbol
793        } else {
794            // Otherwise, serialize the NFT
795            uri = SerializeMetadata.serializeNFTMetadataAsURI(&token as &{NonFungibleToken.NFT})
796        }
797
798        /* Secure NFT in escrow & deposit calculated fees */
799        //
800        // Withdraw fee from feeProvider and deposit
801        self.escrowNFTAndWithdrawFee(token: <-token, from: feeProvider)
802
803        /* Determine EVM handling */
804        //
805        // Does the bridge control the EVM contract associated with this type?
806        let associatedAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: tokenType)
807            ?? panic("No EVMAddress found for token type")
808        let isFactoryDeployed = FlowEVMBridgeUtils.isEVMContractBridgeOwned(evmContractAddress: associatedAddress)
809
810        /* Third-party controlled ERC721 handling */
811        //
812        // Not bridge-controlled, transfer existing ownership
813        if !isFactoryDeployed {
814            FlowEVMBridgeUtils.mustSafeTransferERC721(erc721Address: associatedAddress, to: to, id: evmID)
815            return
816        }
817
818        /* Bridge-owned ERC721 handling */
819        //
820        // Check if the ERC721 exists in the EVM contract - determines if bridge mints or transfers
821        let exists = FlowEVMBridgeUtils.erc721Exists(erc721Address: associatedAddress, id: evmID)
822        if exists {
823            // Transfer the existing NFT & update the URI to reflect current metadata
824            FlowEVMBridgeUtils.mustSafeTransferERC721(erc721Address: associatedAddress, to: to, id: evmID)
825            FlowEVMBridgeUtils.mustUpdateTokenURI(erc721Address: associatedAddress, id: evmID, uri: uri)
826        } else {
827            // Otherwise mint with current URI
828            FlowEVMBridgeUtils.mustSafeMintERC721(erc721Address: associatedAddress, to: to, id: evmID, uri: uri)
829        }
830        // Update the bridged ERC721 symbol if different than the Cadence-defined EVMBridgedMetadata.symbol
831        if symbol.length > 0 && symbol != FlowEVMBridgeUtils.getSymbol(evmContractAddress: associatedAddress) {
832            FlowEVMBridgeUtils.tryUpdateSymbol(associatedAddress, symbol: symbol)
833        }
834        
835    }
836
837    /// Handler to move registered cross-VM NFTs to EVM
838    ///
839    access(self)
840    fun handleCrossVMNFTToEVM(
841        token: @{NonFungibleToken.NFT},
842        to: EVM.EVMAddress,
843        feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}) {
844        let evmPointer = FlowEVMBridgeCustomAssociations.getEVMPointerAsRegistered(forType: token.getType())
845            ?? panic("Could not find custom association for cross-VM NFT \(token.getType().identifier) with id \(token.id). "
846                .concat("Ensure this NFT has been registered as a cross-VM."))
847        return evmPointer.nativeVM == CrossVMMetadataViews.VM.Cadence ?
848            self.handleCadenceNativeCrossVMNFTToEVM(token: <-token, to: to, feeProvider: feeProvider) :
849            self.handleEVMNativeCrossVMNFTToEVM(token: <-token, to: to, feeProvider: feeProvider)
850    }
851
852    /// Handler to move registered cross-VM Cadence-native NFTs to EVM
853    ///
854    access(self)
855    fun handleCadenceNativeCrossVMNFTToEVM(
856        token: @{NonFungibleToken.NFT},
857        to: EVM.EVMAddress,
858        feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
859    ) {
860        let type = token.getType()
861        let id = UInt256(token.id)
862
863        // Check on permissionlessly onboarded association & bridged token existence
864        if let bridgedERC721 = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: type) {
865            // Burn bridged ERC721 if exists - will be replaced by custom ERC721 implementation
866            if FlowEVMBridgeUtils.erc721Exists(erc721Address: bridgedERC721, id: id) {
867                FlowEVMBridgeUtils.mustBurnERC721(erc721Address: bridgedERC721, id: id)
868            }
869        }
870        // Make ICrossVMBridgeERC721Fulfillment.fulfillToEVM call, passing any metadata resolved by the NFT allowing
871        // the ERC721 implementation to update metadata if needed. The base CrossVMBridgeERC721Fulfillment contract
872        // checks for existence and mints if needed or transfers from vm bridge escrow, following a mint/escrow
873        // pattern.
874        let customERC721 = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type)!
875        let data = CrossVMMetadataViews.getEVMBytesMetadata(&token as &{ViewResolver.Resolver})
876        FlowEVMBridgeUtils.mustFulfillNFTToEVM(erc721Address: customERC721, to: to, id: id, maybeBytes: data?.bytes)
877
878        // Escrow the NFT & charge the bridge fee
879        self.escrowNFTAndWithdrawFee(token: <-token, from: feeProvider)
880    }
881
882    /// Handler to move cross-VM EVM-native NFTs to EVM
883    ///
884    access(self)
885    fun handleEVMNativeCrossVMNFTToEVM(
886        token: @{NonFungibleToken.NFT},
887        to: EVM.EVMAddress,
888        feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
889    ) {
890        if !FlowEVMBridgeUtils.isCadenceNative(type: token.getType()) {
891            // Bridge-defined token means this is a bridged token - passthrough to appropriate handler method
892            return self.handleUpdatedBridgedNFTToEVM(token: <-token, to: to, feeProvider: feeProvider)
893        }
894        let type = token.getType()
895        let id = UInt256(token.id)
896        let customERC721 = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: token.getType())!
897
898        // Escrow the NFT & charge the bridge fee
899        self.escrowNFTAndWithdrawFee(token: <-token, from: feeProvider)
900
901        // Transfer the ERC721 from escrow to the named recipient
902        FlowEVMBridgeUtils.mustSafeTransferERC721(erc721Address: customERC721, to: to, id: id)
903    }
904
905    /// Handler to move NFTs to EVM that were once bridge-defined but were later updated to a registered custom
906    /// cross-VM implementation
907    ///
908    access(self)
909    fun handleUpdatedBridgedNFTToEVM(
910        token: @{NonFungibleToken.NFT},
911        to: EVM.EVMAddress,
912        feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
913    ) {
914        pre {
915            !FlowEVMBridgeUtils.isCadenceNative(type: token.getType()):
916            "Expected a bridge-defined NFT but was provided NFT of type \(token.getType().identifier)"
917        }
918        let bridgedAssociation = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: token.getType())!
919        let updatedCadenceAssociation = FlowEVMBridgeCustomAssociations.getTypeAssociated(with: bridgedAssociation)
920            ?? panic("Could not find a custom cross-VM association for NFT \(token.getType().identifier) #\(token.id). "
921                .concat("The handleUpdatedBridgedNFTToEVM route is intended for bridged Cadence NFTs associated with ")
922                .concat(" ERC721 contracts that have registered as a custom cross-VM NFT collection."))
923        let tokenRef = (&token as &{NonFungibleToken.NFT}) as! &{CrossVMNFT.EVMNFT}
924        let evmID = tokenRef.evmID
925        let bridgedToken <- token as! @{CrossVMNFT.EVMNFT}
926        emit BridgedNFTBurned(
927            type: bridgedToken.getType().identifier,
928            id: bridgedToken.id,
929            evmID: bridgedToken.evmID,
930            uuid: bridgedToken.uuid,
931            erc721Address: bridgedAssociation.toString()
932        )
933        Burner.burn(<-bridgedToken)
934        // Transfer the ERC721 from escrow to the named recipient
935        FlowEVMBridgeUtils.mustSafeTransferERC721(erc721Address: bridgedAssociation, to: to, id: evmID)
936    }
937
938    /// Handle permissionlessly onboarded NFTs where the bridge deployed and manages the non-native contract
939    ///
940    access(self)
941    fun handleDefaultNFTFromEVM(
942        owner: EVM.EVMAddress,
943        type: Type,
944        id: UInt256,
945        feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider},
946        protectedTransferCall: fun (EVM.EVMAddress): EVM.Result
947    ): @{NonFungibleToken.NFT} {
948        /* Provision fee */
949        //
950        // Withdraw from feeProvider and deposit to self
951        let feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0)
952        FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: feeAmount)
953
954        /* Execute escrow transfer */
955        //
956        // Get the EVMAddress of the ERC721 contract associated with the type
957        let associatedAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: type)
958            ?? panic("No EVMAddress found for token type")
959        // Execute the transfer call and make needed state assertions to confirm escrow from named owner
960        FlowEVMBridgeUtils.mustEscrowERC721(
961            owner: owner,
962            id: id,
963            erc721Address: associatedAddress,
964            protectedTransferCall: protectedTransferCall
965        )
966
967        /* Gather identifying info */
968        //
969        // Derive the defining Cadence contract name & address & attempt to borrow it as IEVMBridgeNFTMinter
970        let contractName = FlowEVMBridgeUtils.getContractName(fromType: type)!
971        let contractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: type)!
972        let nftContract = getAccount(contractAddress).contracts.borrow<&{IEVMBridgeNFTMinter}>(name: contractName)
973        // Get the token URI from the ERC721 contract
974        let uri = FlowEVMBridgeUtils.getTokenURI(evmContractAddress: associatedAddress, id: id)
975
976        /* Unlock escrowed NFTs */
977        //
978        // If the NFT is currently locked, unlock and return
979        if let cadenceID = FlowEVMBridgeNFTEscrow.getLockedCadenceID(type: type, evmID: id) {
980            let nft <- FlowEVMBridgeNFTEscrow.unlockNFT(type: type, id: cadenceID)
981
982            // If the NFT is bridge-defined, update the URI from the source ERC721 contract
983            if self.account.address == FlowEVMBridgeUtils.getContractAddress(fromType: type) {
984                nftContract!.updateTokenURI(evmID: id, newURI: uri)
985            }
986
987            return <-nft
988        }
989
990        /* Mint bridge-defined NFT */
991        //
992        // Ensure the NFT is bridge-defined
993        assert(self.account.address == contractAddress, message: "Unexpected error bridging NFT from EVM")
994
995        // We expect the NFT to be minted in Cadence as it is bridge-defined
996        let nft <- nftContract!.mintNFT(id: id, tokenURI: uri)
997        return <-nft
998    }
999
1000    /// Handler to move registered cross-VM NFTs from EVM
1001    ///
1002    access(self)
1003    fun handleCrossVMNFTFromEVM(
1004        owner: EVM.EVMAddress,
1005        type: Type,
1006        id: UInt256,
1007        feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider},
1008        protectedTransferCall: fun (EVM.EVMAddress): EVM.Result
1009    ): @{NonFungibleToken.NFT} {
1010        let evmPointer = FlowEVMBridgeCustomAssociations.getEVMPointerAsRegistered(forType: type)
1011            ?? panic("Could not find custom association for cross-VM NFT \(type.identifier) with id \(id). "
1012                .concat("Ensure this NFT has been registered as a cross-VM."))
1013        if evmPointer.nativeVM == CrossVMMetadataViews.VM.Cadence {
1014            return <- self.handleCadenceNativeCrossVMNFTFromEVM(
1015                owner: owner,
1016                type: type,
1017                id: id,
1018                feeProvider: feeProvider,
1019                protectedTransferCall: protectedTransferCall
1020            )
1021        } else { // EVM-native case as there are only two possible VMs
1022            return <- self.handleEVMNativeCrossVMNFTFromEVM(
1023                owner: owner,
1024                type: type,
1025                id: id,
1026                feeProvider: feeProvider,
1027                protectedTransferCall: protectedTransferCall
1028            )
1029        }
1030    }
1031
1032    /// Handler to move registered Cadence-native cross-VM NFTs from EVM
1033    ///
1034    access(self)
1035    fun handleCadenceNativeCrossVMNFTFromEVM(
1036        owner: EVM.EVMAddress,
1037        type: Type,
1038        id: UInt256,
1039        feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider},
1040        protectedTransferCall: fun (EVM.EVMAddress): EVM.Result
1041    ): @{NonFungibleToken.NFT} {
1042        pre {
1043            FlowEVMBridgeUtils.isCadenceNative(type: type):
1044            "Attempting to move bridge-defined NFT type \(type.identifier) from EVM as Cadence-native via handleCadenceNativeCrossVMNFTFromEVM"
1045        }
1046        let configInfo = FlowEVMBridgeCustomAssociations.getCustomConfigInfo(forType: type)!
1047        let customERC721 = configInfo.evmPointer.evmContractAddress
1048        let bridgedAssociation = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: type)
1049        let bridgedTokenExists = bridgedAssociation != nil ? FlowEVMBridgeUtils.erc721Exists(erc721Address: bridgedAssociation!, id: id) : false
1050        if configInfo.updatedFromBridged && bridgedTokenExists {
1051            let bridgedTokenOwner = FlowEVMBridgeUtils.ownerOf(id: id, evmContractAddress: bridgedAssociation!)!
1052            if bridgedTokenOwner.equals(owner) {
1053                FlowEVMBridgeUtils.mustEscrowERC721(
1054                    owner: owner,
1055                    id: id,
1056                    erc721Address: bridgedAssociation!,
1057                    protectedTransferCall: protectedTransferCall
1058                )
1059            } else if bridgedTokenOwner.equals(customERC721) {
1060                // Bridged token owned by custom ERC721 - treat as OpenZeppelin's ERC721Wrapper, escrow & unwrap
1061                FlowEVMBridgeUtils.mustEscrowERC721(
1062                    owner: owner,
1063                    id: id,
1064                    erc721Address: customERC721,
1065                    protectedTransferCall: protectedTransferCall
1066                )
1067                FlowEVMBridgeUtils.mustUnwrapERC721(
1068                    id: id,
1069                    erc721WrapperAddress: customERC721,
1070                    underlyingEVMAddress: bridgedAssociation!
1071                )
1072            } else {
1073                // Bridged token not wrapped nor owned by caller - could not determine owner
1074                panic("Bridged ERC721 \(bridgedAssociation!.toString()) ID \(id) still exists after \(type.identifier) "
1075                    .concat("was updated to associate with ERC721 \(customERC721.toString()), but the bridged token is ")
1076                    .concat("neither wrapped nor owned by caller \(owner.toString()). Could not determine owner."))
1077            }
1078            // Burn the bridged ERC721, taking the bridged representation out of circulation in favor of custom ERC721
1079            FlowEVMBridgeUtils.mustBurnERC721(erc721Address: bridgedAssociation!, id: id)
1080        } else {
1081            FlowEVMBridgeUtils.mustEscrowERC721(
1082                owner: owner,
1083                id: id,
1084                erc721Address: customERC721,
1085                protectedTransferCall: protectedTransferCall
1086            )
1087        }
1088        // Cadence-native NFTs must be in escrow, so unlock & return
1089        return <-FlowEVMBridgeNFTEscrow.unlockNFT(
1090            type: type,
1091            id: FlowEVMBridgeNFTEscrow.getLockedCadenceID(type: type, evmID: id)!
1092        )
1093    }
1094
1095    /// Handler to move registered cross-VM EVM-native NFTs from EVM
1096    ///
1097    access(self)
1098    fun handleEVMNativeCrossVMNFTFromEVM(
1099        owner: EVM.EVMAddress,
1100        type: Type,
1101        id: UInt256,
1102        feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider},
1103        protectedTransferCall: fun (EVM.EVMAddress): EVM.Result
1104    ): @{NonFungibleToken.NFT} {
1105        pre {
1106            id <= UInt256(UInt64.max):
1107            "NFT ID \(id) is greater than the maximum Cadence ID \(UInt64.max) - cannot fulfill this NFT from EVM"
1108        }
1109        var _type = type
1110        let erc721Address = FlowEVMBridgeConfig.getEVMAddressAssociated(with: type)!
1111
1112        // Burn if NFT is found to be bridge-defined as it's to be replaced by the registered custom cross-VM NFT
1113        if !FlowEVMBridgeUtils.isCadenceNative(type: type) {
1114            // Find and assign the updated custom Cadence NFT Type associated with the EVM-native ERC721
1115            _type = FlowEVMBridgeConfig.getTypeAssociated(with: erc721Address)!
1116
1117            // Burn the bridged NFT token if it's locked
1118            if let cadenceID = FlowEVMBridgeNFTEscrow.getLockedCadenceID(type: type, evmID: id) {
1119                let bridgedToken <- FlowEVMBridgeNFTEscrow.unlockNFT(type: type, id: cadenceID) as! @{CrossVMNFT.EVMNFT}
1120                emit BridgedNFTBurned(
1121                    type: bridgedToken.getType().identifier,
1122                    id: bridgedToken.id,
1123                    evmID: bridgedToken.evmID,
1124                    uuid: bridgedToken.uuid,
1125                    erc721Address: erc721Address.toString()
1126                )
1127                Burner.burn(<-bridgedToken)
1128            }
1129        }
1130
1131        FlowEVMBridgeUtils.mustEscrowERC721(owner: owner, id: id, erc721Address: erc721Address, protectedTransferCall: protectedTransferCall)
1132
1133        if FlowEVMBridgeNFTEscrow.isLocked(type: type, id: UInt64(id)) {
1134            // Unlock the NFT from escrow
1135            return <-FlowEVMBridgeNFTEscrow.unlockNFT(type: _type, id: UInt64(id))
1136        } else {
1137            // Otherwise, fulfill via configured NFTFulfillmentMinter
1138            return <- FlowEVMBridgeCustomAssociations.fulfillNFTFromEVM(forType: _type, id: id)
1139        }
1140    }
1141
1142    access(self)
1143    fun handleUpdatedBridgedNFTFromEVM(
1144        owner: EVM.EVMAddress,
1145        type: Type,
1146        id: UInt256,
1147        feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider},
1148        protectedTransferCall: fun (EVM.EVMAddress): EVM.Result
1149    ): @{NonFungibleToken.NFT} {
1150        pre {
1151            !FlowEVMBridgeUtils.isCadenceNative(type: type): // expect this type to be bridge-defined
1152            "Expected a bridge-defined NFT but was provided NFT of type \(type.identifier)"
1153            id < UInt256(UInt64.max):
1154            "Requested ID \(id) exceeds UIn64.max - Cross-VM NFT IDs must be within UInt64 range across Cadence & EVM implementations"
1155        }
1156        // Assign the legacy and custom associations
1157        let bridgedAssoc = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: type)!
1158        let updatedTypeAssoc = FlowEVMBridgeConfig.getTypeAssociated(with: bridgedAssoc)!
1159
1160        // Confirm custom association is EVM-native
1161        let configInfo = FlowEVMBridgeCustomAssociations.getCustomConfigInfo(forType: updatedTypeAssoc)!
1162        assert(configInfo.evmPointer.nativeVM == CrossVMMetadataViews.VM.EVM,
1163            message: "Expected native VM for ERC721 \(bridgedAssoc.toString()) associated with NFT type \(type.identifier) to be EVM-native")
1164
1165        FlowEVMBridgeUtils.mustEscrowERC721(owner: owner, id: id, erc721Address: bridgedAssoc, protectedTransferCall: protectedTransferCall)
1166
1167        // Check if originally associated bridged token is in escrow, burning if so
1168        if let lockedCadenceID = FlowEVMBridgeNFTEscrow.getLockedCadenceID(type: type, evmID: id) {
1169            let bridgedToken <- FlowEVMBridgeNFTEscrow.unlockNFT(type: type, id: lockedCadenceID) as! @{CrossVMNFT.EVMNFT}
1170            emit BridgedNFTBurned(
1171                type: bridgedToken.getType().identifier,
1172                id: bridgedToken.id,
1173                evmID: bridgedToken.evmID,
1174                uuid: bridgedToken.uuid,
1175                erc721Address: bridgedAssoc.toString()
1176            )
1177            Burner.burn(<-bridgedToken)
1178        }
1179        // Either unlock if locked or fulfill via configured NFTFulfillmentMinter
1180        if FlowEVMBridgeNFTEscrow.isLocked(type: updatedTypeAssoc, id: UInt64(id)) {
1181            return <- FlowEVMBridgeNFTEscrow.unlockNFT(type: updatedTypeAssoc, id: UInt64(id))
1182        } else {
1183            return <- FlowEVMBridgeCustomAssociations.fulfillNFTFromEVM(forType: updatedTypeAssoc, id: id)
1184        }
1185    }
1186}
1187