Smart Contract

FlowEVMBridgeUtils

A.1e4aa0b87d10b141.FlowEVMBridgeUtils

Valid From

114,568,854

Deployed

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

Dependents

12157 imports
1import NonFungibleToken from 0x1d7e57aa55817448
2import FungibleToken from 0xf233dcee88fe0abe
3import MetadataViews from 0x1d7e57aa55817448
4import CrossVMMetadataViews from 0x1d7e57aa55817448
5import FungibleTokenMetadataViews from 0xf233dcee88fe0abe
6import ViewResolver from 0x1d7e57aa55817448
7import FlowToken from 0x1654653399040a61
8import FlowStorageFees from 0xe467b9dd11fa00df
9
10import EVM from 0xe467b9dd11fa00df
11
12import SerializeMetadata from 0x1e4aa0b87d10b141
13import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141
14import CrossVMNFT from 0x1e4aa0b87d10b141
15import IBridgePermissions from 0x1e4aa0b87d10b141
16
17/// This contract serves as a source of utility methods leveraged by FlowEVMBridge contracts
18//
19access(all)
20contract FlowEVMBridgeUtils {
21
22    /// Address of the bridge factory Solidity contract
23    access(self)
24    var bridgeFactoryEVMAddress: EVM.EVMAddress
25    /// Delimeter used to derive contract names
26    access(self)
27    let delimiter: String
28    /// Mapping containing contract name prefixes
29    access(self)
30    let contractNamePrefixes: {Type: {String: String}}
31
32    /****************
33        Constructs
34    *****************/
35
36    /// Struct used to preserve and pass around multiple values relating to Cadence asset onboarding
37    ///
38    access(all) struct CadenceOnboardingValues {
39        access(all) let contractAddress: Address
40        access(all) let name: String
41        access(all) let symbol: String
42        access(all) let identifier: String
43        access(all) let contractURI: String
44
45        init(
46            contractAddress: Address,
47            name: String,
48            symbol: String,
49            identifier: String,
50            contractURI: String
51        ) {
52            self.contractAddress = contractAddress
53            self.name = name
54            self.symbol = symbol
55            self.identifier = identifier
56            self.contractURI = contractURI
57        }
58    }
59
60    /// Struct used to preserve and pass around multiple values preventing the need to make multiple EVM calls
61    /// during EVM asset onboarding
62    ///
63    access(all) struct EVMOnboardingValues {
64        access(all) let evmContractAddress: EVM.EVMAddress
65        access(all) let name: String
66        access(all) let symbol: String
67        access(all) let decimals: UInt8?
68        access(all) let contractURI: String?
69        access(all) let cadenceContractName: String
70        access(all) let isERC721: Bool
71
72        init(
73            evmContractAddress: EVM.EVMAddress,
74            name: String,
75            symbol: String,
76            decimals: UInt8?,
77            contractURI: String?,
78            cadenceContractName: String,
79            isERC721: Bool
80        ) {
81            self.evmContractAddress = evmContractAddress
82            self.name = name
83            self.symbol = symbol
84            self.decimals = decimals
85            self.contractURI = contractURI
86            self.cadenceContractName = cadenceContractName
87            self.isERC721 = isERC721
88        }
89    }
90
91    /**************************
92        Public Bridge Utils
93     **************************/
94
95    /// Retrieves the bridge factory contract address
96    ///
97    /// @returns The EVMAddress of the bridge factory contract in EVM
98    ///
99    access(all)
100    view fun getBridgeFactoryEVMAddress(): EVM.EVMAddress {
101        return self.bridgeFactoryEVMAddress
102    }
103
104    /// Calculates the fee bridge fee based on the given storage usage + the current base fee.
105    ///
106    /// @param used: The amount of storage used by the asset
107    ///
108    /// @return The calculated fee amount
109    ///
110    access(all)
111    view fun calculateBridgeFee(bytes used: UInt64): UFix64 {
112        let megabytesUsed = FlowStorageFees.convertUInt64StorageBytesToUFix64Megabytes(used)
113        let storageFee = FlowStorageFees.storageCapacityToFlow(megabytesUsed)
114        return storageFee + FlowEVMBridgeConfig.baseFee
115    }
116
117    /// Returns whether the given type is allowed to be bridged as defined by the IBridgePermissions contract interface.
118    /// If the type's defining contract does not implement IBridgePermissions, the method returns true as the bridge
119    /// operates permissionlessly by default. Otherwise, the result of {IBridgePermissions}.allowsBridging() is returned
120    ///
121    /// @param type: The Type of the asset to check
122    ///
123    /// @return true if the type is allowed to be bridged, false otherwise
124    ///
125    access(all)
126    view fun typeAllowsBridging(_ type: Type): Bool {
127        let contractAddress = self.getContractAddress(fromType: type)
128            ?? panic("Could not construct contract address from type identifier: ".concat(type.identifier))
129        let contractName = self.getContractName(fromType: type)
130            ?? panic("Could not construct contract name from type identifier: ".concat(type.identifier))
131        if let bridgePermissions = getAccount(contractAddress).contracts.borrow<&{IBridgePermissions}>(name: contractName) {
132            return bridgePermissions.allowsBridging()
133        }
134        return true
135    }
136
137    /// Returns whether the given address has opted out of enabling bridging for its defined assets
138    ///
139    /// @param address: The EVM contract address to check
140    ///
141    /// @return false if the address has opted out of enabling bridging, true otherwise
142    ///
143    access(all)
144    fun evmAddressAllowsBridging(_ address: EVM.EVMAddress): Bool {
145        let callResult = self.dryCall(
146            signature: "allowsBridging()",
147            targetEVMAddress: address,
148            args: [],
149            gasLimit: FlowEVMBridgeConfig.gasLimit,
150            value: 0.0
151        )
152        // Contract doesn't support the method - proceed permissionlessly
153        if callResult.status != EVM.Status.successful {
154            return true
155        }
156        // Contract is IBridgePermissions - return the result
157        let decodedResult = EVM.decodeABI(types: [Type<Bool>()], data: callResult.data) as! [AnyStruct]
158        return (decodedResult.length == 1 && decodedResult[0] as! Bool) == true ? true : false
159    }
160
161    /// Identifies if an asset is Cadence- or EVM-native, defined by whether a bridge contract defines it or not
162    ///
163    /// @param type: The Type of the asset to check
164    ///
165    /// @return True if the asset is Cadence-native, false if it is EVM-native
166    ///
167    access(all)
168    view fun isCadenceNative(type: Type): Bool {
169        let definingAddress = self.getContractAddress(fromType: type)
170            ?? panic("Could not construct address from type identifier: ".concat(type.identifier))
171        return definingAddress != self.account.address
172    }
173
174    /// Identifies if an asset is a type that is defined by a bridge-owned Cadence contract. For NFTs, this would
175    /// indicate that the NFT is a bridged representation of a corresponding ERC721. For a Vault, this would
176    /// indicate that the Vault is a bridged representation of a corresponding ERC20.
177    ///
178    /// @param type: The Type of the asset to check
179    ///
180    /// @return True if the asset is bridge-defined, false if another Cadence contract defines the type. Reverts if the
181    ///     type is a primitive type that is not defined by a Cadence contract.
182    ///
183    access(all)
184    view fun isBridgeDefined(type: Type): Bool {
185        let definingAddress = self.getContractAddress(fromType: type)
186            ?? panic("Could not construct address from type identifier: ".concat(type.identifier))
187        return definingAddress == self.account.address
188    }
189
190    /// Identifies if an asset is Cadence- or EVM-native, defined by whether a bridge-owned contract defines it or not.
191    /// Reverts on EVM call failure.
192    ///
193    /// @param type: The Type of the asset to check
194    ///
195    /// @return True if the asset is EVM-native, false if it is Cadence-native
196    ///
197    access(all)
198    fun isEVMNative(evmContractAddress: EVM.EVMAddress): Bool {
199        return self.isEVMContractBridgeOwned(evmContractAddress: evmContractAddress) == false
200    }
201
202    /// Determines if the given EVM contract address was deployed by the bridge by querying the factory contract
203    /// Reverts on EVM call failure.
204    ///
205    /// @param evmContractAddress: The EVM contract address to check
206    ///
207    /// @return True if the contract was deployed by the bridge, false otherwise
208    ///
209    access(all)
210    fun isEVMContractBridgeOwned(evmContractAddress: EVM.EVMAddress): Bool {
211        // Ask the bridge factory if the given contract address was deployed by the bridge
212        let callResult = self.dryCall(
213                signature: "isBridgeDeployed(address)",
214                targetEVMAddress: self.bridgeFactoryEVMAddress,
215                args: [evmContractAddress],
216                gasLimit: FlowEVMBridgeConfig.gasLimit,
217                value: 0.0
218            )
219
220        assert(callResult.status == EVM.Status.successful, message: "Call to bridge factory failed")
221        let decodedResult = EVM.decodeABI(types: [Type<Bool>()], data: callResult.data)
222        assert(decodedResult.length == 1, message: "Invalid response length")
223
224        return decodedResult[0] as! Bool
225    }
226
227    /// Identifies if an asset is ERC721. Reverts on EVM call failure.
228    ///
229    /// @param evmContractAddress: The EVM contract address to check
230    ///
231    /// @return True if the asset is an ERC721, false otherwise
232    ///
233    access(all)
234    fun isERC721(evmContractAddress: EVM.EVMAddress): Bool {
235        let callResult = self.dryCall(
236            signature: "isERC721(address)",
237            targetEVMAddress: self.bridgeFactoryEVMAddress,
238            args: [evmContractAddress],
239            gasLimit: FlowEVMBridgeConfig.gasLimit,
240            value: 0.0
241        )
242
243        assert(callResult.status == EVM.Status.successful, message: "Call to bridge factory failed")
244        let decodedResult = EVM.decodeABI(types: [Type<Bool>()], data: callResult.data)
245        assert(decodedResult.length == 1, message: "Invalid response length")
246
247        return decodedResult[0] as! Bool
248    }
249
250    /// Identifies if an asset is ERC20 as far as is possible without true EVM type introspection. Reverts on EVM call
251    /// failure.
252    ///
253    /// @param evmContractAddress: The EVM contract address to check
254    ///
255    /// @return true if the asset is an ERC20, false otherwise
256    ///
257    access(all)
258    fun isERC20(evmContractAddress: EVM.EVMAddress): Bool {
259        let callResult = self.dryCall(
260            signature: "isERC20(address)",
261            targetEVMAddress: self.bridgeFactoryEVMAddress,
262            args: [evmContractAddress],
263            gasLimit: FlowEVMBridgeConfig.gasLimit,
264            value: 0.0
265        )
266
267        assert(callResult.status == EVM.Status.successful, message: "Call to bridge factory failed")
268        let decodedResult = EVM.decodeABI(types: [Type<Bool>()], data: callResult.data)
269        assert(decodedResult.length == 1, message: "Invalid response length")
270
271        return decodedResult[0] as! Bool
272    }
273
274    /// Returns whether the contract address is either an ERC721 or ERC20 exclusively. Reverts on EVM call failure.
275    ///
276    /// @param evmContractAddress: The EVM contract address to check
277    ///
278    /// @return True if the contract is either an ERC721 or ERC20, false otherwise
279    ///
280    access(all)
281    fun isValidEVMAsset(evmContractAddress: EVM.EVMAddress): Bool {
282        let callResult = self.dryCall(
283            signature: "isValidAsset(address)",
284            targetEVMAddress: self.bridgeFactoryEVMAddress,
285            args: [evmContractAddress],
286            gasLimit: FlowEVMBridgeConfig.gasLimit,
287            value: 0.0
288        )
289        let decodedResult = EVM.decodeABI(types: [Type<Bool>()], data: callResult.data)
290        assert(decodedResult.length == 1, message: "Invalid response length")
291        return decodedResult[0] as! Bool
292    }
293
294    /// Returns whether the given type is either an NFT or FT exclusively
295    ///
296    /// @param type: The Type of the asset to check
297    ///
298    /// @return True if the type is either an NFT or FT, false otherwise
299    ///
300    access(all)
301    view fun isValidCadenceAsset(type: Type): Bool {
302        let isCadenceNFT = type.isSubtype(of: Type<@{NonFungibleToken.NFT}>())
303        let isCadenceFungibleToken = type.isSubtype(of: Type<@{FungibleToken.Vault}>())
304        return isCadenceNFT != isCadenceFungibleToken
305    }
306
307    /// Retrieves the bridge contract's COA EVMAddress
308    ///
309    /// @returns The EVMAddress of the bridge contract's COA orchestrating actions in FlowEVM
310    ///
311    access(all)
312    view fun getBridgeCOAEVMAddress(): EVM.EVMAddress {
313        return self.borrowCOA().address()
314    }
315
316    /// Retrieves the relevant information for onboarding a Cadence asset to the bridge. This method is used to
317    /// retrieve the name, symbol, contract address, and contract URI for a given Cadence asset type. These values
318    /// are used to then deploy a corresponding EVM contract. If EVMBridgedMetadata is supported by the asset's
319    /// defining contract, the values are retrieved from that view. Otherwise, the values are derived from other
320    /// common metadata views.
321    ///
322    /// @param forAssetType: The Type of the asset to retrieve onboarding values for
323    ///
324    /// @return The CadenceOnboardingValues struct containing the asset's name, symbol, identifier, contract address,
325    ///     and contract URI
326    ///
327    access(all)
328    fun getCadenceOnboardingValues(forAssetType: Type): CadenceOnboardingValues {
329        pre {
330            self.isValidCadenceAsset(type: forAssetType): "This type is not a supported Flow asset type."
331        }
332        // If not an NFT, assumed to be fungible token.
333        let isNFT = forAssetType.isSubtype(of: Type<@{NonFungibleToken.NFT}>())
334
335        // Retrieve the Cadence type's defining contract name, address, & its identifier
336        var name = self.getContractName(fromType: forAssetType)
337            ?? panic("Could not contract name from type: ".concat(forAssetType.identifier))
338        let identifier = forAssetType.identifier
339        let cadenceAddress = self.getContractAddress(fromType: forAssetType)
340            ?? panic("Could not derive contract address for token type: ".concat(identifier))
341        // Initialize asset symbol which will be assigned later
342        // based on presence of asset-defined metadata
343        var symbol: String? = nil
344        // Borrow the ViewResolver to attempt to resolve the EVMBridgedMetadata view
345        let viewResolver = getAccount(cadenceAddress).contracts.borrow<&{ViewResolver}>(name: name)!
346        var contractURI = ""
347
348        // Try to resolve the EVMBridgedMetadata
349        let bridgedMetadata = viewResolver.resolveContractView(
350                resourceType: forAssetType,
351                viewType: Type<MetadataViews.EVMBridgedMetadata>()
352            ) as! MetadataViews.EVMBridgedMetadata?
353        // Default to project-defined URI if available
354        if bridgedMetadata != nil {
355            name = bridgedMetadata!.name
356            symbol = bridgedMetadata!.symbol
357            contractURI = bridgedMetadata!.uri.uri()
358        } else {
359            if isNFT {
360                // Otherwise, serialize collection-level NFTCollectionDisplay
361                if let collectionDisplay = viewResolver.resolveContractView(
362                    resourceType: forAssetType,
363                    viewType: Type<MetadataViews.NFTCollectionDisplay>()
364                ) as! MetadataViews.NFTCollectionDisplay? {
365                    name = collectionDisplay.name
366                    let serializedDisplay = SerializeMetadata.serializeFromDisplays(nftDisplay: nil, collectionDisplay: collectionDisplay)!
367                    contractURI = "data:application/json;utf8,{".concat(serializedDisplay).concat("}")
368                }
369                if symbol == nil {
370                    symbol = SerializeMetadata.deriveSymbol(fromString: name)
371                }
372            } else {
373                let ftDisplay = viewResolver.resolveContractView(
374                    resourceType: forAssetType,
375                    viewType: Type<FungibleTokenMetadataViews.FTDisplay>()
376                ) as! FungibleTokenMetadataViews.FTDisplay?
377                if ftDisplay != nil {
378                    name = ftDisplay!.name
379                    symbol = ftDisplay!.symbol
380                }
381                if contractURI.length == 0 && ftDisplay != nil {
382                    let serializedDisplay = SerializeMetadata.serializeFTDisplay(ftDisplay!)
383                    contractURI = "data:application/json;utf8,{".concat(serializedDisplay).concat("}")
384                }
385            }
386        }
387
388        return CadenceOnboardingValues(
389            contractAddress: cadenceAddress,
390            name: name,
391            symbol: symbol!,
392            identifier: identifier,
393            contractURI: contractURI
394        )
395    }
396
397    /// Retrieves identifying information about an EVM contract related to bridge onboarding.
398    ///
399    /// @param evmContractAddress: The EVM contract address to retrieve onboarding values for
400    ///
401    /// @return The EVMOnboardingValues struct containing the asset's name, symbol, decimals, contractURI, and
402    ///    Cadence contract name as well as whether the asset is an ERC721
403    ///
404    access(all)
405    fun getEVMOnboardingValues(evmContractAddress: EVM.EVMAddress): EVMOnboardingValues {
406        // Retrieve the EVM contract's name, symbol, and contractURI
407        let name: String = self.getName(evmContractAddress: evmContractAddress)
408        let symbol: String = self.getSymbol(evmContractAddress: evmContractAddress)
409        let contractURI = self.getContractURI(evmContractAddress: evmContractAddress)
410        // Default to 18 decimals for ERC20s
411        var decimals: UInt8 = FlowEVMBridgeConfig.defaultDecimals
412
413        // Derive Cadence contract name
414        let isERC721: Bool = self.isERC721(evmContractAddress: evmContractAddress)
415        var cadenceContractName: String = ""
416        if isERC721 {
417            // Assert the contract is not mixed asset
418            let isERC20 = self.isERC20(evmContractAddress: evmContractAddress)
419            assert(!isERC20, message: "Contract is mixed asset and is not currently supported by the bridge")
420            // Derive the contract name from the ERC721 contract
421            cadenceContractName = self.deriveBridgedNFTContractName(from: evmContractAddress)
422        } else {
423            // Otherwise, treat as ERC20
424            let isERC20 = self.isERC20(evmContractAddress: evmContractAddress)
425            assert(
426                isERC20,
427                message: "Contract ".concat(evmContractAddress.toString()).concat("defines an asset that is not currently supported by the bridge")
428            )
429            cadenceContractName = self.deriveBridgedTokenContractName(from: evmContractAddress)
430            decimals = self.getTokenDecimals(evmContractAddress: evmContractAddress)
431        }
432
433        return EVMOnboardingValues(
434            evmContractAddress: evmContractAddress,
435            name: name,
436            symbol: symbol,
437            decimals: decimals,
438            contractURI: contractURI,
439            cadenceContractName: cadenceContractName,
440            isERC721: isERC721
441        )
442    }
443
444    /// Retrieves the EVMPointer view from a given type's defining contract if the view is supported.
445    /// NOTE: This does not guarantee the association is valid, only that the defining Cadence contract declares
446    /// the association.
447    ///
448    /// @param from: The type for which to retrieve the EVMPointer view
449    ///
450    /// @return The resolved EVMPointer view for the given type or nil if the view is unsupported
451    ///
452    access(all)
453    fun getEVMPointerView(forType: Type): CrossVMMetadataViews.EVMPointer? {
454        let contractAddress = forType.address!
455        let contractName = forType.contractName!
456        if let viewResolver = getAccount(contractAddress).contracts.borrow<&{ViewResolver}>(name: contractName) {
457            return viewResolver.resolveContractView(
458                resourceType: forType,
459                viewType: Type<CrossVMMetadataViews.EVMPointer>()
460            ) as? CrossVMMetadataViews.EVMPointer? ?? nil
461        }
462        return nil
463    }
464
465    /************************
466        EVM Call Wrappers
467     ************************/
468
469    /// Retrieves the NFT/FT name from the given EVM contract address - applies for both ERC20 & ERC721.
470    /// Reverts on EVM call failure.
471    ///
472    /// @param evmContractAddress: The EVM contract address to retrieve the name from
473    ///
474    /// @return the name of the asset
475    ///
476    access(all)
477    fun getName(evmContractAddress: EVM.EVMAddress): String {
478        let callResult = self.dryCall(
479            signature: "name()",
480            targetEVMAddress: evmContractAddress,
481            args: [],
482            gasLimit: FlowEVMBridgeConfig.gasLimit,
483            value: 0.0
484        )
485
486        assert(callResult.status == EVM.Status.successful, message: "Call for EVM asset name failed")
487        let decodedResult = EVM.decodeABI(types: [Type<String>()], data: callResult.data) as! [AnyStruct]
488        assert(decodedResult.length == 1, message: "Invalid response length")
489
490        return decodedResult[0] as! String
491    }
492
493    /// Retrieves the NFT/FT symbol from the given EVM contract address - applies for both ERC20 & ERC721
494    /// Reverts on EVM call failure.
495    ///
496    /// @param evmContractAddress: The EVM contract address to retrieve the symbol from
497    ///
498    /// @return the symbol of the asset
499    ///
500    access(all)
501    fun getSymbol(evmContractAddress: EVM.EVMAddress): String {
502        let callResult = self.dryCall(
503            signature: "symbol()",
504            targetEVMAddress: evmContractAddress,
505            args: [],
506            gasLimit: FlowEVMBridgeConfig.gasLimit,
507            value: 0.0
508        )
509        assert(callResult.status == EVM.Status.successful, message: "Call for EVM asset symbol failed")
510        let decodedResult = EVM.decodeABI(types: [Type<String>()], data: callResult.data) as! [AnyStruct]
511        assert(decodedResult.length == 1, message: "Invalid response length")
512        return decodedResult[0] as! String
513    }
514
515    /// Retrieves the tokenURI for the given NFT ID from the given EVM contract address. Reverts on EVM call failure.
516    /// Reverts on EVM call failure.
517    ///
518    /// @param evmContractAddress: The EVM contract address to retrieve the tokenURI from
519    /// @param id: The ID of the NFT for which to retrieve the tokenURI value
520    ///
521    /// @return the tokenURI of the ERC721
522    ///
523    access(all)
524    fun getTokenURI(evmContractAddress: EVM.EVMAddress, id: UInt256): String {
525        let callResult = self.dryCall(
526            signature: "tokenURI(uint256)",
527            targetEVMAddress: evmContractAddress,
528            args: [id],
529            gasLimit: FlowEVMBridgeConfig.gasLimit,
530            value: 0.0
531        )
532
533        assert(callResult.status == EVM.Status.successful, message: "Call to EVM for tokenURI failed")
534        let decodedResult = EVM.decodeABI(types: [Type<String>()], data: callResult.data) as! [AnyStruct]
535        assert(decodedResult.length == 1, message: "Invalid response length")
536
537        return decodedResult[0] as! String
538    }
539
540    /// Retrieves the contract URI from the given EVM contract address. Returns nil on EVM call failure.
541    ///
542    /// @param evmContractAddress: The EVM contract address to retrieve the contractURI from
543    ///
544    /// @return the contract's contractURI
545    ///
546    access(all)
547    fun getContractURI(evmContractAddress: EVM.EVMAddress): String? {
548        let callResult = self.dryCall(
549            signature: "contractURI()",
550            targetEVMAddress: evmContractAddress,
551            args: [],
552            gasLimit: FlowEVMBridgeConfig.gasLimit,
553            value: 0.0
554        )
555        if callResult.status != EVM.Status.successful {
556            return nil
557        }
558        let decodedResult = EVM.decodeABI(types: [Type<String>()], data: callResult.data) as! [AnyStruct]
559        return decodedResult.length == 1 ? decodedResult[0] as! String : nil
560    }
561
562    /// Retrieves the number of decimals for a given ERC20 contract address. Reverts on EVM call failure.
563    ///
564    /// @param evmContractAddress: The ERC20 contract address to retrieve the token decimals from
565    ///
566    /// @return the token decimals of the ERC20
567    ///
568    access(all)
569    fun getTokenDecimals(evmContractAddress: EVM.EVMAddress): UInt8 {
570        let callResult = self.dryCall(
571                signature: "decimals()",
572                targetEVMAddress: evmContractAddress,
573                args: [],
574                gasLimit: FlowEVMBridgeConfig.gasLimit,
575                value: 0.0
576            )
577
578        assert(callResult.status == EVM.Status.successful, message: "Call for EVM asset decimals failed")
579        let decodedResult = EVM.decodeABI(types: [Type<UInt8>()], data: callResult.data) as! [AnyStruct]
580        assert(decodedResult.length == 1, message: "Invalid response length")
581
582        return decodedResult[0] as! UInt8
583    }
584
585    /// Determines if the provided owner address is either the owner or approved for the NFT in the ERC721 contract
586    /// Reverts on EVM call failure.
587    ///
588    /// @param ofNFT: The ID of the NFT to query
589    /// @param owner: The owner address to query
590    /// @param evmContractAddress: The ERC721 contract address to query
591    ///
592    /// @return true if the owner is either the owner or approved for the NFT, false otherwise
593    ///
594    access(all)
595    fun isOwnerOrApproved(ofNFT: UInt256, owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): Bool {
596        return self.isOwner(ofNFT: ofNFT, owner: owner, evmContractAddress: evmContractAddress) ||
597            self.isApproved(ofNFT: ofNFT, owner: owner, evmContractAddress: evmContractAddress)
598    }
599
600    /// Returns whether the given owner is the owner of the given NFT. Reverts on EVM call failure.
601    ///
602    /// @param ofNFT: The ID of the NFT to query
603    /// @param owner: The owner address to query
604    /// @param evmContractAddress: The ERC721 contract address to query
605    ///
606    /// @return true if the owner is in fact the owner of the NFT, false otherwise
607    ///
608    access(all)
609    fun isOwner(ofNFT: UInt256, owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): Bool {
610        return self.ownerOf(id: ofNFT, evmContractAddress: evmContractAddress)?.equals(owner) ?? false
611    }
612
613    /// Returns the owner of a given ERC721 token
614    ///
615    /// @param id: The ID of the NFT to query
616    /// @param evmContractAddress: The ERC721 contract address to query
617    ///
618    /// @return The current owner's EVM address or nil if the `ownerOf` call is unsuccessful
619    /// 
620    access(all)
621    fun ownerOf(id: UInt256, evmContractAddress: EVM.EVMAddress): EVM.EVMAddress? {
622        let callResult = self.dryCall(
623                signature: "ownerOf(uint256)",
624                targetEVMAddress: evmContractAddress,
625                args: [id],
626                gasLimit: FlowEVMBridgeConfig.gasLimit,
627                value: 0.0
628            )
629        if callResult.status == EVM.Status.failed {
630            return nil
631        }
632        let decodedCallResult = EVM.decodeABI(types: [Type<EVM.EVMAddress>()], data: callResult.data)
633        return decodedCallResult.length == 1 ? decodedCallResult[0] as! EVM.EVMAddress : nil
634    }
635
636    /// Returns whether the given owner is approved for the given NFT. Reverts on EVM call failure.
637    ///
638    /// @param ofNFT: The ID of the NFT to query
639    /// @param owner: The owner address to query
640    /// @param evmContractAddress: The ERC721 contract address to query
641    ///
642    /// @return true if the owner is in fact approved for the NFT, false otherwise
643    ///
644    access(all)
645    fun isApproved(ofNFT: UInt256, owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): Bool {
646        let callResult = self.dryCall(
647            signature: "getApproved(uint256)",
648            targetEVMAddress: evmContractAddress,
649            args: [ofNFT],
650            gasLimit: FlowEVMBridgeConfig.gasLimit,
651            value: 0.0
652        )
653        assert(callResult.status == EVM.Status.successful, message: "Call to ERC721.getApproved(uint256) failed")
654        let decodedCallResult = EVM.decodeABI(types: [Type<EVM.EVMAddress>()], data: callResult.data)
655        if decodedCallResult.length == 1 {
656            let actualApproved = decodedCallResult[0] as! EVM.EVMAddress
657            return actualApproved.equals(owner)
658        }
659        return false
660    }
661
662    /// Returns whether the given ERC721 exists, assuming the ERC721 contract implements the `exists` method. While this
663    /// method is not part of the ERC721 standard, it is implemented in the bridge-deployed ERC721 implementation.
664    /// Reverts on EVM call failure.
665    ///
666    /// @param erc721Address: The EVM contract address of the ERC721 token
667    /// @param id: The ID of the ERC721 token to check
668    ///
669    /// @return true if the ERC721 token exists, false otherwise
670    ///
671    access(all)
672    fun erc721Exists(erc721Address: EVM.EVMAddress, id: UInt256): Bool {
673        let existsResponse = EVM.decodeABI(
674                types: [Type<Bool>()],
675                data: self.dryCall(
676                    signature: "exists(uint256)",
677                    targetEVMAddress: erc721Address,
678                    args: [id],
679                    gasLimit: FlowEVMBridgeConfig.gasLimit,
680                    value: 0.0
681                ).data,
682            )
683        assert(existsResponse.length == 1, message: "Invalid response length")
684        return existsResponse[0] as! Bool
685    }
686
687    /// Returns the ERC20 balance of the owner at the given ERC20 contract address. Reverts on EVM call failure.
688    ///
689    /// @param owner: The owner address to query
690    /// @param evmContractAddress: The ERC20 contract address to query
691    ///
692    /// @return The UInt256 balance of the owner at the ERC20 contract address. Callers may wish to convert the return
693    ///     value to a UFix64 via convertERC20AmountToCadenceAmount, though note there may be a loss of precision.
694    ///
695    access(all)
696    fun balanceOf(owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): UInt256 {
697        let callResult = self.dryCall(
698            signature: "balanceOf(address)",
699            targetEVMAddress: evmContractAddress,
700            args: [owner],
701            gasLimit: FlowEVMBridgeConfig.gasLimit,
702            value: 0.0
703        )
704        assert(callResult.status == EVM.Status.successful, message: "Call to ERC20.balanceOf(address) failed")
705        let decodedResult = EVM.decodeABI(types: [Type<UInt256>()], data: callResult.data) as! [AnyStruct]
706        assert(decodedResult.length == 1, message: "Invalid response length")
707        return decodedResult[0] as! UInt256
708    }
709
710    /// Determines if the owner has sufficient funds to bridge the given amount at the ERC20 contract address
711    /// Reverts on EVM call failure.
712    ///
713    /// @param amount: The amount to check if the owner has enough balance to cover
714    /// @param owner: The owner address to query
715    /// @param evmContractAddress: The ERC20 contract address to query
716    ///
717    /// @return true if the owner's balance >= amount, false otherwise
718    ///
719    access(all)
720    fun hasSufficientBalance(amount: UInt256, owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): Bool {
721        return self.balanceOf(owner: owner, evmContractAddress: evmContractAddress) >= amount
722    }
723
724    /// Retrieves the total supply of the ERC20 contract at the given EVM contract address. Reverts on EVM call failure.
725    ///
726    /// @param evmContractAddress: The EVM contract address to retrieve the total supply from
727    ///
728    /// @return the total supply of the ERC20
729    ///
730    access(all)
731    fun totalSupply(evmContractAddress: EVM.EVMAddress): UInt256 {
732        let callResult = self.dryCall(
733            signature: "totalSupply()",
734            targetEVMAddress: evmContractAddress,
735            args: [],
736            gasLimit: FlowEVMBridgeConfig.gasLimit,
737            value: 0.0
738        )
739        assert(callResult.status == EVM.Status.successful, message: "Call to ERC20.totalSupply() failed")
740        let decodedResult = EVM.decodeABI(types: [Type<UInt256>()], data: callResult.data) as! [AnyStruct]
741        assert(decodedResult.length == 1, message: "Invalid response length")
742        return decodedResult[0] as! UInt256
743    }
744
745    /// Converts the given amount of ERC20 tokens to the equivalent amount in FLOW tokens based on the ERC20s decimals
746    /// value. Note that may be some loss of decimal precision as UFix64 supports precision for 8 decimal places.
747    /// Reverts on EVM call failure.
748    ///
749    /// @param amount: The amount of ERC20 tokens to convert
750    /// @param erc20Address: The EVM contract address of the ERC20 token
751    ///
752    /// @return the equivalent amount in FLOW tokens as a UFix64
753    ///
754    access(all)
755    fun convertERC20AmountToCadenceAmount(_ amount: UInt256, erc20Address: EVM.EVMAddress): UFix64 {
756        return self.uint256ToUFix64(
757            value: amount,
758            decimals: self.getTokenDecimals(evmContractAddress: erc20Address)
759        )
760    }
761
762    /// Converts the given amount of Cadence fungible tokens to the equivalent amount in ERC20 tokens based on the
763    /// ERC20s decimals. Note that there may be some loss of decimal precision as UFix64 supports precision for 8
764    /// decimal places. Reverts on EVM call failure.
765    ///
766    /// @param amount: The amount of Cadence fungible tokens to convert
767    /// @param erc20Address: The EVM contract address of the ERC20 token
768    ///
769    /// @return the equivalent amount in ERC20 tokens as a UInt256
770    ///
771    access(all)
772    fun convertCadenceAmountToERC20Amount(_ amount: UFix64, erc20Address: EVM.EVMAddress): UInt256 {
773        return self.ufix64ToUInt256(value: amount, decimals: self.getTokenDecimals(evmContractAddress: erc20Address))
774    }
775
776    /// Gets the declared Cadence contract address declared by an EVM contract in conformance to the ICrossVM.sol
777    /// contract interface. Reverts if the EVM call is unsuccessful.
778    /// NOTE: Just because an EVM contract declares an association does not mean it it is valid!
779    ///
780    /// @param evmContract: The ICrossVM.sol conforming EVM contract from which to retrieve the declared Cadence
781    ///     contract address
782    ///
783    /// @return The resulting Cadence Address as declared associated by the provided EVM contract or nil if the call fails
784    ///
785    access(all)
786    fun getDeclaredCadenceAddressFromCrossVM(evmContract: EVM.EVMAddress): Address? {
787        let cadenceAddrRes = self.dryCall(
788            signature: "getCadenceAddress()",
789            targetEVMAddress: evmContract,
790            args: [],
791            gasLimit: FlowEVMBridgeConfig.gasLimit,
792            value: 0.0
793        )
794        if cadenceAddrRes.status != EVM.Status.successful {
795            return nil
796        }
797        let decodedCadenceAddr = EVM.decodeABI(types: [Type<String>()], data: cadenceAddrRes.data)
798        assert(decodedCadenceAddr.length == 1)
799        var cadenceAddrStr = decodedCadenceAddr[0] as! String
800        if cadenceAddrStr[1] != "x" {
801            cadenceAddrStr = "0x".concat(cadenceAddrStr)
802        }
803        return Address.fromString(cadenceAddrStr) ?? nil
804    }
805
806    /// Gets the declared Cadence Type declared by an EVM contract in conformance to the ICrossVM.sol contract
807    /// interface. Reverts if the EVM call is unsuccessful.
808    /// NOTE: Just because an EVM contract declares an association does not mean it it is valid!
809    ///
810    /// @param evmContract: The ICrossVM.sol conforming EVM contract from which to retrieve the declared Cadence
811    ///     Type
812    ///
813    /// @return The resulting Cadence Type as declared associated by the provided EVM contract or nil if the call fails
814    ///
815    ///
816    access(all)
817    fun getDeclaredCadenceTypeFromCrossVM(evmContract: EVM.EVMAddress): Type? {
818        let cadenceIdentifierRes = self.dryCall(
819            signature: "getCadenceIdentifier()",
820            targetEVMAddress: evmContract,
821            args: [],
822            gasLimit: FlowEVMBridgeConfig.gasLimit,
823            value: 0.0
824        )
825        if cadenceIdentifierRes.status != EVM.Status.successful {
826            return nil
827        }
828        let decodedCadenceIdentifier = EVM.decodeABI(types: [Type<String>()], data: cadenceIdentifierRes.data)
829        assert(decodedCadenceIdentifier.length == 1)
830        let cadenceIdentifier = decodedCadenceIdentifier[0] as! String
831        return CompositeType(cadenceIdentifier) ?? nil
832    }
833
834    /// Returns whether the provided EVM contract conforms to ICrossVMBridgeERC721Fulfillment.sol contract interface.
835    /// Doing so is one of two interfaces that must be implemented for Cadence-native cross-VM NFTs to be successfully
836    /// registered
837    ///
838    /// @param evmContract: The EVM contract to check for ICrossVMBridgeERC721 conformance
839    ///
840    /// @return True if conformance is found, false otherwise
841    ///
842    access(all)
843    fun supportsICrossVMBridgeERC721Fulfillment(evmContract: EVM.EVMAddress): Bool {
844        let interfaceID = EVM.EVMBytes4(value: "2e608d70".decodeHex().toConstantSized<[UInt8; 4]>()!)
845        let supportsRes = self.dryCall(
846            signature: "supportsInterface(bytes4)",
847            targetEVMAddress: evmContract,
848            args: [interfaceID],
849            gasLimit: FlowEVMBridgeConfig.gasLimit,
850            value: 0.0
851        )
852        if supportsRes.status != EVM.Status.successful {
853            return false
854        }
855        let decodedSupports = EVM.decodeABI(types: [Type<Bool>()], data: supportsRes.data)
856        if decodedSupports.length != 1 {
857            return false
858        }
859        return decodedSupports[0] as! Bool
860    }
861
862    /// Returns whether the provided EVM contract conforms to ICrossVMBridgeCallable.sol contract interface.
863    /// Doing so is one of two interfaces that must be implemented for Cadence-native cross-VM NFTs to be successfully
864    /// registered
865    ///
866    /// @param evmContract: The EVM contract to check for ICrossVMBridgeCallable conformance
867    ///
868    /// @return True if conformance is found, false otherwise
869    ///
870    access(all)
871    fun supportsICrossVMBridgeCallable(evmContract: EVM.EVMAddress): Bool {
872        let interfaceID = EVM.EVMBytes4(value: "b7f9a9ec".decodeHex().toConstantSized<[UInt8; 4]>()!)
873        let supportsRes = self.dryCall(
874            signature: "supportsInterface(bytes4)",
875            targetEVMAddress: evmContract,
876            args: [interfaceID],
877            gasLimit: FlowEVMBridgeConfig.gasLimit,
878            value: 0.0
879        )
880        if supportsRes.status != EVM.Status.successful {
881            return false
882        }
883        let decodedSupports = EVM.decodeABI(types: [Type<Bool>()], data: supportsRes.data)
884        if decodedSupports.length != 1 {
885            return false
886        }
887        return decodedSupports[0] as! Bool
888    }
889
890    /// Returns whether the provided EVM contract conforms to both ICrossVMBridgeERC721Fulfillment and
891    /// ICrossVMBridgeCallable Solidity contract interfaces
892    ///
893    /// @param evmContract: The EVM contract to check for conformance
894    ///
895    /// @return True if conformance is found, false otherwise
896    ///
897    access(all)
898    fun supportsCadenceNativeNFTEVMInterfaces(evmContract: EVM.EVMAddress): Bool {
899        return self.supportsICrossVMBridgeCallable(evmContract: evmContract)
900            && self.supportsICrossVMBridgeERC721Fulfillment(evmContract: evmContract)
901    }
902
903    /// Returns the VM Bridge address designated by the ICrossVMBridgeCallable conforming EVM contract. Reverts on call
904    /// failure.
905    ///
906    /// @param evmContract: The ICrossVMBridgeCallable EVM contract from which to retrieve the value
907    ///
908    /// @return The EVM address designated as the VM bridge address in the provided contract
909    ///
910    access(all)
911    fun getVMBridgeAddressFromICrossVMBridgeCallable(evmContract: EVM.EVMAddress): EVM.EVMAddress? {
912        let cadenceIdentifierRes = self.dryCall(
913            signature: "vmBridgeAddress()",
914            targetEVMAddress: evmContract,
915            args: [],
916            gasLimit: FlowEVMBridgeConfig.gasLimit,
917            value: 0.0
918        )
919        if cadenceIdentifierRes.status != EVM.Status.successful {
920            return nil
921        }
922        let decodedCadenceIdentifier = EVM.decodeABI(types: [Type<EVM.EVMAddress>()], data: cadenceIdentifierRes.data)
923        return decodedCadenceIdentifier.length == 1 ? decodedCadenceIdentifier[0] as! EVM.EVMAddress : nil
924    }
925
926    /************************
927        Derivation Utils
928     ************************/
929
930    /// Derives the StoragePath where the escrow locker is stored for a given Type of asset & returns. The given type
931    /// must be of an asset supported by the bridge.
932    ///
933    /// @param fromType: The type of the asset the escrow locker is being derived for
934    ///
935    /// @return The StoragePath associated with the type's escrow Locker, or nil if the type is not supported
936    ///
937    access(all)
938    view fun deriveEscrowStoragePath(fromType: Type): StoragePath? {
939        if !self.isValidCadenceAsset(type: fromType) {
940            return nil
941        }
942        var prefix = ""
943        if fromType.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) {
944            prefix = "flowEVMBridgeNFTEscrow"
945        } else if fromType.isSubtype(of: Type<@{FungibleToken.Vault}>()) {
946            prefix = "flowEVMBridgeTokenEscrow"
947        }
948        assert(prefix.length > 1, message: "Invalid prefix")
949        if let splitIdentifier = self.splitObjectIdentifier(identifier: fromType.identifier) {
950            let sourceContractAddress = Address.fromString("0x".concat(splitIdentifier[1]))!
951            let sourceContractName = splitIdentifier[2]
952            let resourceName = splitIdentifier[3]
953            return StoragePath(
954                identifier: prefix.concat(self.delimiter)
955                    .concat(sourceContractAddress.toString()).concat(self.delimiter)
956                    .concat(sourceContractName).concat(self.delimiter)
957                    .concat(resourceName)
958            ) ?? nil
959        }
960        return nil
961    }
962
963    /// Derives the Cadence contract name for a given EVM NFT of the form
964    /// EVMVMBridgedNFT_<0xCONTRACT_ADDRESS>
965    ///
966    /// @param from evmContract: The EVM contract address to derive the Cadence NFT contract name for
967    ///
968    /// @return The derived Cadence FT contract name
969    ///
970    access(all)
971    view fun deriveBridgedNFTContractName(from evmContract: EVM.EVMAddress): String {
972        return self.contractNamePrefixes[Type<@{NonFungibleToken.NFT}>()]!["bridged"]!
973            .concat(self.delimiter)
974            .concat(evmContract.toString())
975    }
976
977    /// Derives the Cadence contract name for a given EVM fungible token of the form
978    /// EVMVMBridgedToken_<0xCONTRACT_ADDRESS>
979    ///
980    /// @param from evmContract: The EVM contract address to derive the Cadence FT contract name for
981    ///
982    /// @return The derived Cadence FT contract name
983    ///
984    access(all)
985    view fun deriveBridgedTokenContractName(from evmContract: EVM.EVMAddress): String {
986        return self.contractNamePrefixes[Type<@{FungibleToken.Vault}>()]!["bridged"]!
987            .concat(self.delimiter)
988            .concat(evmContract.toString())
989    }
990
991    /****************
992        Math Utils
993     ****************/
994
995    /// Raises the base to the power of the exponent
996    ///
997    access(all)
998    view fun pow(base: UInt256, exponent: UInt8): UInt256 {
999        if exponent == 0 {
1000            return 1
1001        }
1002
1003        var r = base
1004        var exp: UInt8 = 1
1005        while exp < exponent {
1006            r = r * base
1007            exp = exp + 1
1008        }
1009
1010        return r
1011    }
1012
1013    /// Raises the fixed point base to the power of the exponent
1014    ///
1015    access(all)
1016    view fun ufixPow(base: UFix64, exponent: UInt8): UFix64 {
1017        if exponent == 0 {
1018            return 1.0
1019        }
1020
1021        var r = base
1022        var exp: UInt8 = 1
1023        while exp < exponent {
1024            r = r * base
1025            exp = exp + 1
1026        }
1027
1028        return r
1029    }
1030
1031    /// Converts a UFix64 to a UInt256
1032    //
1033    access(all)
1034    view fun ufix64ToUInt256(value: UFix64, decimals: UInt8): UInt256 {
1035        // Default to 10e8 scale, catching instances where decimals are less than default and scale appropriately
1036        let ufixScaleExp: UInt8 = decimals < 8 ? decimals : 8
1037        var ufixScale = self.ufixPow(base: 10.0, exponent: ufixScaleExp)
1038
1039        // Separate the fractional and integer parts of the UFix64
1040        let integer = UInt256(value)
1041        var fractional = (value % 1.0) * ufixScale
1042
1043        // Calculate the multiplier for integer and fractional parts
1044        var integerMultiplier: UInt256 = self.pow(base:10, exponent: decimals)
1045        let fractionalMultiplierExp: UInt8 = decimals < 8 ? 0 : decimals - 8
1046        var fractionalMultiplier: UInt256 = self.pow(base:10, exponent: fractionalMultiplierExp)
1047
1048        // Scale and sum the parts
1049        return integer * integerMultiplier + UInt256(fractional) * fractionalMultiplier
1050    }
1051
1052    /// Converts a UInt256 to a UFix64
1053    ///
1054    access(all)
1055    view fun uint256ToUFix64(value: UInt256, decimals: UInt8): UFix64 {
1056        // Calculate scale factors for the integer and fractional parts
1057        let absoluteScaleFactor = self.pow(base: 10, exponent: decimals)
1058
1059        // Separate the integer and fractional parts of the value
1060        let scaledValue = value / absoluteScaleFactor
1061        var fractional = value % absoluteScaleFactor
1062        // Scale the fractional part
1063        let scaledFractional = self.uint256FractionalToScaledUFix64Decimals(value: fractional, decimals: decimals)
1064
1065        // Ensure the parts do not exceed the max UFix64 value before conversion
1066        assert(
1067            scaledValue <= UInt256(UFix64.max),
1068            message: "Scaled integer value ".concat(value.toString()).concat(" exceeds max UFix64 value")
1069        )
1070        /// Check for the max value that can be converted to a UFix64 without overflowing
1071        assert(
1072            scaledValue == UInt256(UFix64.max) ? scaledFractional < 0.09551616 : true,
1073            message: "Scaled integer value ".concat(value.toString()).concat(" exceeds max UFix64 value")
1074        )
1075
1076        return UFix64(scaledValue) + scaledFractional
1077    }
1078
1079    /// Converts a UInt256 fractional value with the given decimal places to a scaled UFix64. Note that UFix64 has
1080    /// decimal precision of 8 places so converted values may lose precision and be rounded down.
1081    ///
1082    access(all)
1083    view fun uint256FractionalToScaledUFix64Decimals(value: UInt256, decimals: UInt8): UFix64 {
1084        pre {
1085            self.getNumberOfDigits(value) <= decimals: "Fractional digits exceed the defined decimal places"
1086        }
1087        post {
1088            result < 1.0: "Resulting scaled fractional exceeds 1.0"
1089        }
1090
1091        var fractional = value
1092        // Truncate fractional to the first 8 decimal places which is the max precision for UFix64
1093        if decimals >= 8 {
1094            fractional = fractional / self.pow(base: 10, exponent: decimals - 8)
1095        }
1096        // Return early if the truncated fractional part is now 0
1097        if fractional == 0 {
1098            return 0.0
1099        }
1100
1101        // Scale the fractional part
1102        let fractionalMultiplier = self.ufixPow(base: 0.1, exponent: decimals < 8 ? decimals : 8)
1103        return UFix64(fractional) * fractionalMultiplier
1104    }
1105
1106    /// Returns the value as a UInt64 if it fits, otherwise panics
1107    ///
1108    access(all)
1109    view fun uint256ToUInt64(value: UInt256): UInt64 {
1110        return value <= UInt256(UInt64.max) ? UInt64(value) : panic("Value too large to fit into UInt64")
1111    }
1112
1113    /// Returns the number of digits in the given UInt256
1114    ///
1115    access(all)
1116    view fun getNumberOfDigits(_ value: UInt256): UInt8 {
1117        var tmp = value
1118        var digits: UInt8 = 0
1119        while tmp > 0 {
1120            tmp = tmp / 10
1121            digits = digits + 1
1122        }
1123        return digits
1124    }
1125
1126    /***************************
1127        Type Identifier Utils
1128     ***************************/
1129
1130    /// Returns the contract address from the given Type
1131    ///
1132    /// @param fromType: The Type to extract the contract address from
1133    ///
1134    /// @return The defining contract's Address, or nil if the identifier does not have an associated Address
1135    ///
1136    access(all)
1137    view fun getContractAddress(fromType: Type): Address? {
1138        return fromType.address
1139    }
1140
1141    /// Returns the defining contract name from the given Type
1142    ///
1143    /// @param fromType: The Type to extract the contract name from
1144    ///
1145    /// @return The defining contract's name, or nil if the identifier does not have an associated contract name
1146    ///
1147    access(all)
1148    view fun getContractName(fromType: Type): String? {
1149        return fromType.contractName
1150    }
1151
1152    /// Returns the object's name from the given Type's identifier where the identifier is in the format
1153    /// of: A.<CONTRACT_ADDRESS_SANS_0x>.<CONTRACT_NAME>.<OBJECT_NAME>
1154    ///
1155    /// @param fromType: The Type to extract the object name from
1156    ///
1157    /// @return The object's name, or nil if the identifier does identify an object
1158    ///
1159    access(all)
1160    view fun getObjectName(fromType: Type): String? {
1161        if let identifierSplit = self.splitObjectIdentifier(identifier: fromType.identifier) {
1162            return identifierSplit[3]
1163        }
1164        return nil
1165    }
1166
1167    /// Splits the given identifier into its constituent parts defined by a delimiter of '".'"
1168    ///
1169    /// @param identifier: The identifier to split
1170    ///
1171    /// @return An array of the identifier's constituent parts, or nil if the identifier does not have 4 parts
1172    ///
1173    access(all)
1174    view fun splitObjectIdentifier(identifier: String): [String]? {
1175        let identifierSplit = identifier.split(separator: ".")
1176        return identifierSplit.length != 4 ? nil : identifierSplit
1177    }
1178
1179    /// Builds a composite type from the given identifier parts
1180    ///
1181    /// @param address: The defining contract address
1182    /// @param contractName: The defining contract name
1183    /// @param resourceName: The resource name
1184    ///
1185    access(all)
1186    view fun buildCompositeType(address: Address, contractName: String, resourceName: String): Type? {
1187        let addressStr = address.toString()
1188        let subtract0x = addressStr.slice(from: 2, upTo: addressStr.length)
1189        let identifier = "A".concat(".").concat(subtract0x).concat(".").concat(contractName).concat(".").concat(resourceName)
1190        return CompositeType(identifier)
1191    }
1192
1193    /**************************
1194        FungibleToken Utils
1195     **************************/
1196
1197    /// Returns the `createEmptyVault()` function from a Vault Type's defining contract or nil if either the Type is not
1198    access(all) fun getCreateEmptyVaultFunction(forType: Type): (fun (Type): @{FungibleToken.Vault})? {
1199        // We can only reasonably assume that the requested function is accessible from a FungibleToken contract
1200        if !forType.isSubtype(of: Type<@{FungibleToken.Vault}>()) {
1201            return nil
1202        }
1203        // Vault Types should guarantee that the following forced optionals are safe
1204        let contractAddress = self.getContractAddress(fromType: forType)!
1205        let contractName = self.getContractName(fromType: forType)!
1206        let tokenContract: &{FungibleToken} = getAccount(contractAddress).contracts.borrow<&{FungibleToken}>(
1207                name: contractName
1208            )!
1209        return tokenContract.createEmptyVault
1210    }
1211
1212    /******************************
1213        Bridge-Access Only Utils
1214     ******************************/
1215
1216    /// Deposits fees to the bridge account's FlowToken Vault - helps fund asset storage
1217    ///
1218    access(account)
1219    fun depositFee(_ feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, feeAmount: UFix64) {
1220        let vault = self.account.storage.borrow<&FlowToken.Vault>(from: /storage/flowTokenVault)
1221            ?? panic("Could not borrow FlowToken.Vault reference")
1222
1223        let feeVault <-feeProvider.withdraw(amount: feeAmount) as! @FlowToken.Vault
1224        assert(feeVault.balance == feeAmount, message: "Fee provider did not return the requested fee")
1225
1226        vault.deposit(from: <-feeVault)
1227    }
1228
1229    /// Enables other bridge contracts to orchestrate bridge operations from contract-owned COA
1230    ///
1231    access(account)
1232    view fun borrowCOA(): auth(EVM.Call, EVM.Withdraw) &EVM.CadenceOwnedAccount {
1233        return self.account.storage.borrow<auth(EVM.Call, EVM.Withdraw) &EVM.CadenceOwnedAccount>(
1234            from: FlowEVMBridgeConfig.coaStoragePath
1235        ) ?? panic("Could not borrow COA reference")
1236    }
1237
1238    /// Shared helper simplifying calls using the bridge account's COA
1239    ///
1240    access(account)
1241    fun call(
1242        signature: String,
1243        targetEVMAddress: EVM.EVMAddress,
1244        args: [AnyStruct],
1245        gasLimit: UInt64,
1246        value: UFix64
1247    ): EVM.Result {
1248        let calldata = EVM.encodeABIWithSignature(signature, args)
1249        let valueBalance = EVM.Balance(attoflow: 0)
1250        valueBalance.setFLOW(flow: value)
1251        return self.borrowCOA().call(
1252            to: targetEVMAddress,
1253            data: calldata,
1254            gasLimit: gasLimit,
1255            value: valueBalance
1256        )
1257    }
1258
1259    /// Shared helper simplifying dryCalls using the bridge account's COA. Note that `COA.dryCall` does not execute the
1260    /// call within EVM, serving solely as a mechanism for retrieving data from Flow-EVM environment.
1261    ///
1262    access(account)
1263    fun dryCall(
1264        signature: String,
1265        targetEVMAddress: EVM.EVMAddress,
1266        args: [AnyStruct],
1267        gasLimit: UInt64,
1268        value: UFix64
1269    ): EVM.Result {
1270        let calldata = EVM.encodeABIWithSignature(signature, args)
1271        let valueBalance = EVM.Balance(attoflow: 0)
1272        valueBalance.setFLOW(flow: value)
1273        return self.borrowCOA().dryCall(
1274            to: targetEVMAddress,
1275            data: calldata,
1276            gasLimit: gasLimit,
1277            value: valueBalance
1278        )
1279    }
1280
1281    /// Executes a safeTransferFrom call on the given ERC721 contract address, transferring the NFT from bridge escrow
1282    /// in EVM to the named recipient and asserting pre- and post-state changes.
1283    ///
1284    access(account)
1285    fun mustSafeTransferERC721(erc721Address: EVM.EVMAddress, to: EVM.EVMAddress, id: UInt256) {
1286        let bridgeCOAAddress = self.getBridgeCOAEVMAddress()
1287
1288        let bridgePreStatus = self.isOwner(ofNFT: id, owner: bridgeCOAAddress, evmContractAddress: erc721Address)
1289        let toPreStatus = self.isOwner(ofNFT: id, owner: to, evmContractAddress: erc721Address)
1290        assert(bridgePreStatus, message: "Bridge COA does not own ERC721 requesting to be transferred")
1291        assert(!toPreStatus, message: "Recipient already owns ERC721 attempting to be transferred")
1292
1293        let transferResult: EVM.Result = self.call(
1294            signature: "safeTransferFrom(address,address,uint256)",
1295            targetEVMAddress: erc721Address,
1296            args: [bridgeCOAAddress, to, id],
1297            gasLimit: FlowEVMBridgeConfig.gasLimit,
1298            value: 0.0
1299        )
1300        assert(
1301            transferResult.status == EVM.Status.successful,
1302            message: "safeTransferFrom call to ERC721 transferring NFT from escrow to bridge recipient failed"
1303        )
1304
1305        let bridgePostStatus = self.isOwner(ofNFT: id, owner: bridgeCOAAddress, evmContractAddress: erc721Address)
1306        let toPostStatus = self.isOwner(ofNFT: id, owner: to, evmContractAddress: erc721Address)
1307        assert(!bridgePostStatus, message: "ERC721 is still in escrow after transfer")
1308        assert(toPostStatus, message: "ERC721 was not successfully transferred to recipient from escrow")
1309    }
1310
1311    /// Executes a safeMint call on the given ERC721 contract address, minting an ERC721 to the named recipient and
1312    /// asserting pre- and post-state changes. Assumes the bridge COA has the authority to mint the NFT.
1313    ///
1314    access(account)
1315    fun mustSafeMintERC721(erc721Address: EVM.EVMAddress, to: EVM.EVMAddress, id: UInt256, uri: String) {
1316        let bridgeCOAAddress = self.getBridgeCOAEVMAddress()
1317
1318        let mintResult: EVM.Result = self.call(
1319            signature: "safeMint(address,uint256,string)",
1320            targetEVMAddress: erc721Address,
1321            args: [to, id, uri],
1322            gasLimit: FlowEVMBridgeConfig.gasLimit,
1323            value: 0.0
1324        )
1325        assert(mintResult.status == EVM.Status.successful, message: "Mint to bridge recipient failed")
1326
1327        let toPostStatus = self.isOwner(ofNFT: id, owner: to, evmContractAddress: erc721Address)
1328        assert(toPostStatus, message: "Recipient does not own the NFT after minting")
1329    }
1330
1331    /// Executes a safeMint call on the given ERC721 contract address, minting an ERC721 to the named recipient and
1332    /// asserting pre- and post-state changes. Assumes the bridge COA has the authority to mint the NFT.
1333    ///
1334    access(account)
1335    fun mustFulfillNFTToEVM(erc721Address: EVM.EVMAddress, to: EVM.EVMAddress, id: UInt256, maybeBytes: EVM.EVMBytes?) {
1336        let fulfillResult = self.call(
1337            signature: "fulfillToEVM(address,uint256,bytes)",
1338            targetEVMAddress: erc721Address,
1339            args: [to, id, maybeBytes ?? EVM.EVMBytes(value: [])],
1340            gasLimit: FlowEVMBridgeConfig.gasLimit,
1341            value: 0.0
1342        )
1343        assert(
1344            fulfillResult.status == EVM.Status.successful,
1345            message: "Fulfill ERC721 \(erc721Address.toString()) with id \(id) to \(to.toString()) failed with error code \(fulfillResult.errorCode): \(fulfillResult.errorMessage)"
1346        )
1347
1348        let toPostStatus = self.isOwner(ofNFT: id, owner: to, evmContractAddress: erc721Address)
1349        assert(toPostStatus, message: "Recipient does not own the NFT after minting")
1350    }
1351
1352    /// Executes updateTokenURI call on the given ERC721 contract address, updating the tokenURI of the NFT. This is
1353    /// not a standard ERC721 function, but is implemented in the bridge-deployed ERC721 implementation to enable
1354    /// synchronization of token metadata with Cadence NFT state on bridging.
1355    ///
1356    access(account)
1357    fun mustUpdateTokenURI(erc721Address: EVM.EVMAddress, id: UInt256, uri: String) {
1358        let bridgeCOAAddress = self.getBridgeCOAEVMAddress()
1359
1360        let updateResult: EVM.Result = self.call(
1361            signature: "updateTokenURI(uint256,string)",
1362            targetEVMAddress: erc721Address,
1363            args: [id, uri],
1364            gasLimit: FlowEVMBridgeConfig.gasLimit,
1365            value: 0.0
1366        )
1367        assert(updateResult.status == EVM.Status.successful, message: "URI update failed")
1368    }
1369
1370    /// Executes the provided method, assumed to be a protected transfer call, and confirms that the transfer was
1371    /// successful by validating the named owner is authorized to act on the NFT before the transfer, the transfer
1372    /// was successful, and the bridge COA owns the NFT after the protected transfer call.
1373    ///
1374    access(account)
1375    fun mustEscrowERC721(
1376        owner: EVM.EVMAddress,
1377        id: UInt256,
1378        erc721Address: EVM.EVMAddress,
1379        protectedTransferCall: fun (EVM.EVMAddress): EVM.Result
1380    ) {
1381        // Ensure the named owner is authorized to act on the NFT
1382        let isAuthorized = self.isOwnerOrApproved(ofNFT: id, owner: owner, evmContractAddress: erc721Address)
1383        assert(isAuthorized, message: "Named owner is not the owner of the ERC721")
1384
1385        // Call the protected transfer function which should execute a transfer call from the owner to escrow
1386        let transferResult = protectedTransferCall(erc721Address)
1387        assert(transferResult.status == EVM.Status.successful, message: "Transfer ERC721 to escrow via callback failed")
1388
1389        // Validate the NFT is now owned by the bridge COA, escrow the NFT
1390        let isEscrowed = self.isOwner(ofNFT: id, owner: self.getBridgeCOAEVMAddress(), evmContractAddress: erc721Address)
1391        assert(isEscrowed, message: "ERC721 was not successfully escrowed")
1392    }
1393
1394    /// Unwraps an ERC721 token, calling `ERC721Wrapper.withdrawTo(address,uint256[])` on the provided wrapper address
1395    /// and ensuring that the underlying ERC721 is owned by the bridge COA before returning.
1396    /// NOTE: This method relies on implementation of OpenZeppelin's `ERC721Wrapper` contract interface, reverting if
1397    /// the unwrap operation is unsuccessful.
1398    ///
1399    access(account)
1400    fun mustUnwrapERC721(
1401        id: UInt256,
1402        erc721WrapperAddress: EVM.EVMAddress,
1403        underlyingEVMAddress: EVM.EVMAddress
1404    ) {
1405        assert(
1406            self.isOwner(ofNFT: id, owner: erc721WrapperAddress, evmContractAddress: underlyingEVMAddress),
1407            message: "Attempting to unwrap \(underlyingEVMAddress.toString()) ID \(id), but token is not wrapped by \(erc721WrapperAddress.toString())"
1408        )
1409        let bridgeCOA = self.getBridgeCOAEVMAddress()
1410
1411        let unwrapResult: EVM.Result = self.call(
1412            signature: "withdrawTo(address,uint256[])",
1413            targetEVMAddress: erc721WrapperAddress,
1414            args: [bridgeCOA, [id]],
1415            gasLimit: FlowEVMBridgeConfig.gasLimit,
1416            value: 0.0
1417        )
1418        assert(
1419            unwrapResult.status == EVM.Status.successful,
1420            message: "Call to \(erc721WrapperAddress.toString()) ERC721Wrapper.withdrawTo(address,uint256[]) failed"
1421        )
1422
1423        assert(
1424            self.isOwner(ofNFT: id, owner: bridgeCOA, evmContractAddress: underlyingEVMAddress),
1425            message: "Unsuccessful escrow of wrapped ERC721 \(erc721WrapperAddress.toString()) wrapping underlying \(underlyingEVMAddress.toString()) ID \(id)"
1426        )
1427    }
1428
1429    /// Mints ERC20 tokens to the recipient and confirms that the recipient's balance was updated
1430    ///
1431    access(account)
1432    fun mustMintERC20(to: EVM.EVMAddress, amount: UInt256, erc20Address: EVM.EVMAddress) {
1433        let toPreBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address)
1434        // Mint tokens to the recipient
1435        let mintResult: EVM.Result = self.call(
1436            signature: "mint(address,uint256)",
1437            targetEVMAddress: erc20Address,
1438            args: [to, amount],
1439            gasLimit: FlowEVMBridgeConfig.gasLimit,
1440            value: 0.0
1441        )
1442        assert(mintResult.status == EVM.Status.successful, message: "Mint to bridge ERC20 contract failed")
1443        // Ensure bridge to recipient was succcessful
1444        let toPostBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address)
1445        assert(
1446            toPostBalance == toPreBalance + amount,
1447            message: "Recipient didn't receive minted ERC20 tokens during bridging"
1448        )
1449    }
1450
1451    /// Transfers ERC20 tokens to the recipient and confirms that the recipient's balance was incremented and the escrow
1452    /// balance was decremented by the requested amount.
1453    ///
1454    access(account)
1455    fun mustTransferERC20(to: EVM.EVMAddress, amount: UInt256, erc20Address: EVM.EVMAddress) {
1456        let bridgeCOAAddress = self.getBridgeCOAEVMAddress()
1457
1458        let toPreBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address)
1459        let escrowPreBalance = self.balanceOf(
1460            owner: bridgeCOAAddress,
1461            evmContractAddress: erc20Address
1462        )
1463
1464        // Transfer tokens to the recipient
1465        let transferResult: EVM.Result = self.call(
1466            signature: "transfer(address,uint256)",
1467            targetEVMAddress: erc20Address,
1468            args: [to, amount],
1469            gasLimit: FlowEVMBridgeConfig.gasLimit,
1470            value: 0.0
1471        )
1472        assert(transferResult.status == EVM.Status.successful, message: "transfer call to ERC20 contract failed")
1473
1474        // Ensure bridge to recipient was succcessful
1475        let toPostBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address)
1476        let escrowPostBalance = self.balanceOf(
1477            owner: bridgeCOAAddress,
1478            evmContractAddress: erc20Address
1479        )
1480        assert(
1481            toPostBalance == toPreBalance + amount,
1482            message: "Recipient's ERC20 balance did not increment by the requested amount after transfer from escrow"
1483        )
1484        assert(
1485            escrowPostBalance == escrowPreBalance - amount,
1486            message: "Escrow ERC20 balance did not decrement by the requested amount after transfer from escrow"
1487        )
1488    }
1489
1490    /// Executes the provided method, assumed to be a protected transfer call, and confirms that the transfer was
1491    /// successful by validating that the named owner's balance was decremented by the requested amount and the bridge
1492    /// escrow balance was incremented by the same amount.
1493    ///
1494    access(account)
1495    fun mustEscrowERC20(
1496        owner: EVM.EVMAddress,
1497        amount: UInt256,
1498        erc20Address: EVM.EVMAddress,
1499        protectedTransferCall: fun (): EVM.Result
1500    ) {
1501        // Ensure the caller is has sufficient balance to bridge the requested amount
1502        let hasSufficientBalance = self.hasSufficientBalance(
1503            amount: amount,
1504            owner: owner,
1505            evmContractAddress: erc20Address
1506        )
1507        assert(hasSufficientBalance, message: "Caller does not have sufficient balance to bridge requested tokens")
1508
1509        // Get the owner and escrow balances before transfer
1510        let ownerPreBalance = self.balanceOf(owner: owner, evmContractAddress: erc20Address)
1511        let bridgePreBalance = self.balanceOf(
1512                owner: self.getBridgeCOAEVMAddress(),
1513                evmContractAddress: erc20Address
1514            )
1515
1516        // Call the protected transfer function which should execute a transfer call from the owner to escrow
1517        let transferResult = protectedTransferCall()
1518        assert(transferResult.status == EVM.Status.successful, message: "Transfer via callback failed")
1519
1520        // Get the resulting balances after transfer
1521        let ownerPostBalance = self.balanceOf(owner: owner, evmContractAddress: erc20Address)
1522        let bridgePostBalance = self.balanceOf(
1523                owner: self.getBridgeCOAEVMAddress(),
1524                evmContractAddress: erc20Address
1525            )
1526
1527        // Confirm the transfer of the expected was successful in both sending owner and recipient escrow
1528        assert(ownerPostBalance == ownerPreBalance - amount, message: "Transfer to owner failed")
1529        assert(bridgePostBalance == bridgePreBalance + amount, message: "Transfer to bridge escrow failed")
1530    }
1531
1532    /// Executes a `burn(uint256)` call targeting the provided ERC721 contract address. Reverts if the call is
1533    /// unsuccessful
1534    ///
1535    access(account)
1536    fun mustBurnERC721(erc721Address: EVM.EVMAddress, id: UInt256) {
1537        let burnResult = FlowEVMBridgeUtils.call(
1538            signature: "burn(uint256)",
1539            targetEVMAddress: erc721Address,
1540            args: [id],
1541            gasLimit: FlowEVMBridgeConfig.gasLimit,
1542            value: 0.0
1543        )
1544        assert(burnResult.status == EVM.Status.successful,
1545            message: "0x\(erc721Address.toString()).burn(\(id)) failed with error code \(burnResult.errorCode) and message: \(burnResult.errorMessage)")
1546    }
1547
1548    /// Calls to the bridge factory to deploy an ERC721/ERC20 contract and returns the deployed contract address
1549    ///
1550    access(account)
1551    fun mustDeployEVMContract(
1552        name: String,
1553        symbol: String,
1554        cadenceAddress: Address,
1555        flowIdentifier: String,
1556        contractURI: String,
1557        isERC721: Bool
1558    ): EVM.EVMAddress {
1559        let deployerTag = isERC721 ? "ERC721" : "ERC20"
1560        let deployResult: EVM.Result = self.call(
1561            signature: "deploy(string,string,string,string,string,string)",
1562            targetEVMAddress: self.bridgeFactoryEVMAddress,
1563            args: [deployerTag, name, symbol, cadenceAddress.toString(), flowIdentifier, contractURI],
1564            gasLimit: FlowEVMBridgeConfig.gasLimit,
1565            value: 0.0
1566        )
1567        assert(deployResult.status == EVM.Status.successful, message: "EVM Token contract deployment failed")
1568        let decodedResult: [AnyStruct] = EVM.decodeABI(types: [Type<EVM.EVMAddress>()], data: deployResult.data)
1569        assert(decodedResult.length == 1, message: "Invalid response length")
1570        return decodedResult[0] as! EVM.EVMAddress
1571    }
1572
1573    /// Calls `setSymbol(string)` on the EVM contract as exposed on FlowEVMBridgedERC721 contracts, enabling Cadence
1574    /// NFTs to update their EVM symbol via EVMBridgedMetadata.symbol. The call's status is returned so conditional 
1575    /// execution can be handled on the caller's end.
1576    ///
1577    access(account)
1578    fun tryUpdateSymbol(_ evmContractAddress: EVM.EVMAddress, symbol: String): Bool {
1579        return self.call(
1580            signature: "setSymbol(string)",
1581            targetEVMAddress: evmContractAddress,
1582            args: [symbol],
1583            gasLimit: FlowEVMBridgeConfig.gasLimit,
1584            value: 0.0
1585        ).status == EVM.Status.successful
1586    }
1587
1588    init(bridgeFactoryAddressHex: String) {
1589        self.delimiter = "_"
1590        self.contractNamePrefixes = {
1591            Type<@{NonFungibleToken.NFT}>(): {
1592                "bridged": "EVMVMBridgedNFT"
1593            },
1594            Type<@{FungibleToken.Vault}>(): {
1595                "bridged": "EVMVMBridgedToken"
1596            }
1597        }
1598        self.bridgeFactoryEVMAddress = EVM.addressFromString(bridgeFactoryAddressHex.toLower())
1599    }
1600}
1601