Smart Contract
FlowEVMBridge
A.1e4aa0b87d10b141.FlowEVMBridge
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