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