@flowtyio/flow-contracts 0.1.0-beta.9 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +1 -1
  2. package/contracts/FungibleTokenSwitchboard.cdc +360 -0
  3. package/contracts/MetadataViews.cdc +79 -6
  4. package/contracts/NonFungibleToken.cdc +17 -10
  5. package/contracts/capability-cache/CapabilityCache.cdc +97 -0
  6. package/contracts/dapper/TopShot.cdc +323 -259
  7. package/contracts/dapper/TopShotLocking.cdc +41 -15
  8. package/contracts/dapper/offers/DapperOffersV2.cdc +46 -43
  9. package/contracts/dapper/offers/OffersV2.cdc +40 -56
  10. package/contracts/dapper/offers/Resolver.cdc +20 -13
  11. package/contracts/emerald-city/FLOAT.cdc +259 -254
  12. package/contracts/evm/CrossVMNFT.cdc +50 -0
  13. package/contracts/evm/EVM.cdc +851 -0
  14. package/contracts/evm/FlowEVMBridgeConfig.cdc +454 -0
  15. package/contracts/evm/FlowEVMBridgeHandlerInterfaces.cdc +163 -0
  16. package/contracts/evm/FlowEVMBridgeHandlers.cdc +230 -0
  17. package/contracts/evm/FlowEVMBridgeUtils.cdc +1303 -0
  18. package/contracts/evm/IBridgePermissions.cdc +19 -0
  19. package/contracts/evm/ICrossVM.cdc +12 -0
  20. package/contracts/evm/ICrossVMAsset.cdc +19 -0
  21. package/contracts/evm/Serialize.cdc +141 -0
  22. package/contracts/evm/SerializeMetadata.cdc +221 -0
  23. package/contracts/example/ExampleNFT.cdc +2 -2
  24. package/contracts/find/FindViews.cdc +357 -353
  25. package/contracts/flow-utils/ScopedFTProviders.cdc +5 -2
  26. package/contracts/flow-utils/ScopedNFTProviders.cdc +6 -2
  27. package/contracts/flowty-drops/ContractManager.cdc +73 -0
  28. package/contracts/flowty-drops/DropFactory.cdc +75 -0
  29. package/contracts/flowty-drops/DropTypes.cdc +282 -0
  30. package/contracts/flowty-drops/FlowtyActiveCheckers.cdc +113 -0
  31. package/contracts/flowty-drops/FlowtyAddressVerifiers.cdc +64 -0
  32. package/contracts/flowty-drops/FlowtyDrops.cdc +461 -0
  33. package/contracts/flowty-drops/FlowtyPricers.cdc +48 -0
  34. package/contracts/flowty-drops/initializers/ContractBorrower.cdc +14 -0
  35. package/contracts/flowty-drops/initializers/ContractInitializer.cdc +7 -0
  36. package/contracts/flowty-drops/initializers/OpenEditionInitializer.cdc +57 -0
  37. package/contracts/flowty-drops/nft/BaseCollection.cdc +97 -0
  38. package/contracts/flowty-drops/nft/BaseNFT.cdc +107 -0
  39. package/contracts/flowty-drops/nft/ContractFactory.cdc +13 -0
  40. package/contracts/flowty-drops/nft/ContractFactoryTemplate.cdc +48 -0
  41. package/contracts/flowty-drops/nft/NFTMetadata.cdc +140 -0
  42. package/contracts/flowty-drops/nft/OpenEditionNFT.cdc +42 -0
  43. package/contracts/flowty-drops/nft/OpenEditionTemplate.cdc +54 -0
  44. package/contracts/flowty-drops/nft/UniversalCollection.cdc +29 -0
  45. package/contracts/fungible-token-router/FungibleTokenRouter.cdc +103 -0
  46. package/contracts/hybrid-custody/CapabilityDelegator.cdc +28 -26
  47. package/contracts/hybrid-custody/CapabilityFactory.cdc +20 -18
  48. package/contracts/hybrid-custody/CapabilityFilter.cdc +41 -24
  49. package/contracts/hybrid-custody/HybridCustody.cdc +303 -242
  50. package/contracts/hybrid-custody/factories/FTAllFactory.cdc +16 -4
  51. package/contracts/hybrid-custody/factories/FTBalanceFactory.cdc +16 -4
  52. package/contracts/hybrid-custody/factories/FTProviderFactory.cdc +17 -5
  53. package/contracts/hybrid-custody/factories/FTReceiverBalanceFactory.cdc +16 -4
  54. package/contracts/hybrid-custody/factories/FTReceiverFactory.cdc +16 -4
  55. package/contracts/hybrid-custody/factories/FTVaultFactory.cdc +46 -0
  56. package/contracts/hybrid-custody/factories/NFTCollectionFactory.cdc +45 -0
  57. package/contracts/hybrid-custody/factories/NFTCollectionPublicFactory.cdc +16 -4
  58. package/contracts/hybrid-custody/factories/NFTProviderAndCollectionFactory.cdc +22 -0
  59. package/contracts/hybrid-custody/factories/NFTProviderFactory.cdc +16 -4
  60. package/contracts/lost-and-found/LostAndFound.cdc +21 -17
  61. package/contracts/tokens/USDCFlow.cdc +232 -0
  62. package/flow.json +278 -7
  63. package/package.json +1 -1
  64. package/contracts/hybrid-custody/factories/NFTProviderAndCollectionPublicFactory.cdc +0 -10
@@ -0,0 +1,1303 @@
1
+ import "NonFungibleToken"
2
+ import "FungibleToken"
3
+ import "MetadataViews"
4
+ import "FungibleTokenMetadataViews"
5
+ import "ViewResolver"
6
+ import "FlowToken"
7
+ import "FlowStorageFees"
8
+
9
+ import "EVM"
10
+
11
+ import "SerializeMetadata"
12
+ import "FlowEVMBridgeConfig"
13
+ import "CrossVMNFT"
14
+ import "IBridgePermissions"
15
+
16
+ /// This contract serves as a source of utility methods leveraged by FlowEVMBridge contracts
17
+ //
18
+ access(all)
19
+ contract FlowEVMBridgeUtils {
20
+
21
+ /// Address of the bridge factory Solidity contract
22
+ access(self)
23
+ var bridgeFactoryEVMAddress: EVM.EVMAddress
24
+ /// Delimeter used to derive contract names
25
+ access(self)
26
+ let delimiter: String
27
+ /// Mapping containing contract name prefixes
28
+ access(self)
29
+ let contractNamePrefixes: {Type: {String: String}}
30
+
31
+ /****************
32
+ Constructs
33
+ *****************/
34
+
35
+ /// Struct used to preserve and pass around multiple values relating to Cadence asset onboarding
36
+ ///
37
+ access(all) struct CadenceOnboardingValues {
38
+ access(all) let contractAddress: Address
39
+ access(all) let name: String
40
+ access(all) let symbol: String
41
+ access(all) let identifier: String
42
+ access(all) let contractURI: String
43
+
44
+ init(
45
+ contractAddress: Address,
46
+ name: String,
47
+ symbol: String,
48
+ identifier: String,
49
+ contractURI: String
50
+ ) {
51
+ self.contractAddress = contractAddress
52
+ self.name = name
53
+ self.symbol = symbol
54
+ self.identifier = identifier
55
+ self.contractURI = contractURI
56
+ }
57
+ }
58
+
59
+ /// Struct used to preserve and pass around multiple values preventing the need to make multiple EVM calls
60
+ /// during EVM asset onboarding
61
+ ///
62
+ access(all) struct EVMOnboardingValues {
63
+ access(all) let evmContractAddress: EVM.EVMAddress
64
+ access(all) let name: String
65
+ access(all) let symbol: String
66
+ access(all) let decimals: UInt8?
67
+ access(all) let contractURI: String?
68
+ access(all) let cadenceContractName: String
69
+ access(all) let isERC721: Bool
70
+
71
+ init(
72
+ evmContractAddress: EVM.EVMAddress,
73
+ name: String,
74
+ symbol: String,
75
+ decimals: UInt8?,
76
+ contractURI: String?,
77
+ cadenceContractName: String,
78
+ isERC721: Bool
79
+ ) {
80
+ self.evmContractAddress = evmContractAddress
81
+ self.name = name
82
+ self.symbol = symbol
83
+ self.decimals = decimals
84
+ self.contractURI = contractURI
85
+ self.cadenceContractName = cadenceContractName
86
+ self.isERC721 = isERC721
87
+ }
88
+ }
89
+
90
+ /**************************
91
+ Public Bridge Utils
92
+ **************************/
93
+
94
+ /// Retrieves the bridge factory contract address
95
+ ///
96
+ /// @returns The EVMAddress of the bridge factory contract in EVM
97
+ ///
98
+ access(all)
99
+ view fun getBridgeFactoryEVMAddress(): EVM.EVMAddress {
100
+ return self.bridgeFactoryEVMAddress
101
+ }
102
+
103
+ /// Calculates the fee bridge fee based on the given storage usage + the current base fee.
104
+ ///
105
+ /// @param used: The amount of storage used by the asset
106
+ ///
107
+ /// @return The calculated fee amount
108
+ ///
109
+ access(all)
110
+ view fun calculateBridgeFee(bytes used: UInt64): UFix64 {
111
+ let megabytesUsed = FlowStorageFees.convertUInt64StorageBytesToUFix64Megabytes(used)
112
+ let storageFee = FlowStorageFees.storageCapacityToFlow(megabytesUsed)
113
+ return storageFee + FlowEVMBridgeConfig.baseFee
114
+ }
115
+
116
+ /// Returns whether the given type is allowed to be bridged as defined by the IBridgePermissions contract interface.
117
+ /// If the type's defining contract does not implement IBridgePermissions, the method returns true as the bridge
118
+ /// operates permissionlessly by default. Otherwise, the result of {IBridgePermissions}.allowsBridging() is returned
119
+ ///
120
+ /// @param type: The Type of the asset to check
121
+ ///
122
+ /// @return true if the type is allowed to be bridged, false otherwise
123
+ ///
124
+ access(all)
125
+ view fun typeAllowsBridging(_ type: Type): Bool {
126
+ let contractAddress = self.getContractAddress(fromType: type)
127
+ ?? panic("Could not construct contract address from type identifier: ".concat(type.identifier))
128
+ let contractName = self.getContractName(fromType: type)
129
+ ?? panic("Could not construct contract name from type identifier: ".concat(type.identifier))
130
+ if let bridgePermissions = getAccount(contractAddress).contracts.borrow<&{IBridgePermissions}>(name: contractName) {
131
+ return bridgePermissions.allowsBridging()
132
+ }
133
+ return true
134
+ }
135
+
136
+ /// Returns whether the given address has opted out of enabling bridging for its defined assets
137
+ ///
138
+ /// @param address: The EVM contract address to check
139
+ ///
140
+ /// @return false if the address has opted out of enabling bridging, true otherwise
141
+ ///
142
+ access(all)
143
+ fun evmAddressAllowsBridging(_ address: EVM.EVMAddress): Bool {
144
+ let callResult = self.call(
145
+ signature: "allowsBridging()",
146
+ targetEVMAddress: address,
147
+ args: [],
148
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
149
+ value: 0.0
150
+ )
151
+ // Contract doesn't support the method - proceed permissionlessly
152
+ if callResult.status != EVM.Status.successful {
153
+ return true
154
+ }
155
+ // Contract is IBridgePermissions - return the result
156
+ let decodedResult = EVM.decodeABI(types: [Type<Bool>()], data: callResult.data) as! [AnyStruct]
157
+ return (decodedResult.length == 1 && decodedResult[0] as! Bool) == true ? true : false
158
+ }
159
+
160
+ /// Identifies if an asset is Cadence- or EVM-native, defined by whether a bridge contract defines it or not
161
+ ///
162
+ /// @param type: The Type of the asset to check
163
+ ///
164
+ /// @return True if the asset is Cadence-native, false if it is EVM-native
165
+ ///
166
+ access(all)
167
+ view fun isCadenceNative(type: Type): Bool {
168
+ let definingAddress = self.getContractAddress(fromType: type)
169
+ ?? panic("Could not construct address from type identifier: ".concat(type.identifier))
170
+ return definingAddress != self.account.address
171
+ }
172
+
173
+ /// Identifies if an asset is Cadence- or EVM-native, defined by whether a bridge-owned contract defines it or not.
174
+ /// Reverts on EVM call failure.
175
+ ///
176
+ /// @param type: The Type of the asset to check
177
+ ///
178
+ /// @return True if the asset is EVM-native, false if it is Cadence-native
179
+ ///
180
+ access(all)
181
+ fun isEVMNative(evmContractAddress: EVM.EVMAddress): Bool {
182
+ return self.isEVMContractBridgeOwned(evmContractAddress: evmContractAddress) == false
183
+ }
184
+
185
+ /// Determines if the given EVM contract address was deployed by the bridge by querying the factory contract
186
+ /// Reverts on EVM call failure.
187
+ ///
188
+ /// @param evmContractAddress: The EVM contract address to check
189
+ ///
190
+ /// @return True if the contract was deployed by the bridge, false otherwise
191
+ ///
192
+ access(all)
193
+ fun isEVMContractBridgeOwned(evmContractAddress: EVM.EVMAddress): Bool {
194
+ // Ask the bridge factory if the given contract address was deployed by the bridge
195
+ let callResult = self.call(
196
+ signature: "isBridgeDeployed(address)",
197
+ targetEVMAddress: self.bridgeFactoryEVMAddress,
198
+ args: [evmContractAddress],
199
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
200
+ value: 0.0
201
+ )
202
+
203
+ assert(callResult.status == EVM.Status.successful, message: "Call to bridge factory failed")
204
+ let decodedResult = EVM.decodeABI(types: [Type<Bool>()], data: callResult.data)
205
+ assert(decodedResult.length == 1, message: "Invalid response length")
206
+
207
+ return decodedResult[0] as! Bool
208
+ }
209
+
210
+ /// Identifies if an asset is ERC721. Reverts on EVM call failure.
211
+ ///
212
+ /// @param evmContractAddress: The EVM contract address to check
213
+ ///
214
+ /// @return True if the asset is an ERC721, false otherwise
215
+ ///
216
+ access(all)
217
+ fun isERC721(evmContractAddress: EVM.EVMAddress): Bool {
218
+ let callResult = self.call(
219
+ signature: "isERC721(address)",
220
+ targetEVMAddress: self.bridgeFactoryEVMAddress,
221
+ args: [evmContractAddress],
222
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
223
+ value: 0.0
224
+ )
225
+
226
+ assert(callResult.status == EVM.Status.successful, message: "Call to bridge factory failed")
227
+ let decodedResult = EVM.decodeABI(types: [Type<Bool>()], data: callResult.data)
228
+ assert(decodedResult.length == 1, message: "Invalid response length")
229
+
230
+ return decodedResult[0] as! Bool
231
+ }
232
+
233
+ /// Identifies if an asset is ERC20 as far as is possible without true EVM type introspection. Reverts on EVM call
234
+ /// failure.
235
+ ///
236
+ /// @param evmContractAddress: The EVM contract address to check
237
+ ///
238
+ /// @return true if the asset is an ERC20, false otherwise
239
+ ///
240
+ access(all)
241
+ fun isERC20(evmContractAddress: EVM.EVMAddress): Bool {
242
+ let callResult = self.call(
243
+ signature: "isERC20(address)",
244
+ targetEVMAddress: self.bridgeFactoryEVMAddress,
245
+ args: [evmContractAddress],
246
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
247
+ value: 0.0
248
+ )
249
+
250
+ assert(callResult.status == EVM.Status.successful, message: "Call to bridge factory failed")
251
+ let decodedResult = EVM.decodeABI(types: [Type<Bool>()], data: callResult.data)
252
+ assert(decodedResult.length == 1, message: "Invalid response length")
253
+
254
+ return decodedResult[0] as! Bool
255
+ }
256
+
257
+ /// Returns whether the contract address is either an ERC721 or ERC20 exclusively. Reverts on EVM call failure.
258
+ ///
259
+ /// @param evmContractAddress: The EVM contract address to check
260
+ ///
261
+ /// @return True if the contract is either an ERC721 or ERC20, false otherwise
262
+ ///
263
+ access(all)
264
+ fun isValidEVMAsset(evmContractAddress: EVM.EVMAddress): Bool {
265
+ let callResult = self.call(
266
+ signature: "isValidAsset(address)",
267
+ targetEVMAddress: self.bridgeFactoryEVMAddress,
268
+ args: [evmContractAddress],
269
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
270
+ value: 0.0
271
+ )
272
+ let decodedResult = EVM.decodeABI(types: [Type<Bool>()], data: callResult.data)
273
+ assert(decodedResult.length == 1, message: "Invalid response length")
274
+ return decodedResult[0] as! Bool
275
+ }
276
+
277
+ /// Returns whether the given type is either an NFT or FT exclusively
278
+ ///
279
+ /// @param type: The Type of the asset to check
280
+ ///
281
+ /// @return True if the type is either an NFT or FT, false otherwise
282
+ ///
283
+ access(all)
284
+ view fun isValidCadenceAsset(type: Type): Bool {
285
+ let isCadenceNFT = type.isSubtype(of: Type<@{NonFungibleToken.NFT}>())
286
+ let isCadenceFungibleToken = type.isSubtype(of: Type<@{FungibleToken.Vault}>())
287
+ return isCadenceNFT != isCadenceFungibleToken
288
+ }
289
+
290
+ /// Retrieves the bridge contract's COA EVMAddress
291
+ ///
292
+ /// @returns The EVMAddress of the bridge contract's COA orchestrating actions in FlowEVM
293
+ ///
294
+ access(all)
295
+ view fun getBridgeCOAEVMAddress(): EVM.EVMAddress {
296
+ return self.borrowCOA().address()
297
+ }
298
+
299
+ /// Retrieves the relevant information for onboarding a Cadence asset to the bridge. This method is used to
300
+ /// retrieve the name, symbol, contract address, and contract URI for a given Cadence asset type. These values
301
+ /// are used to then deploy a corresponding EVM contract. If EVMBridgedMetadata is supported by the asset's
302
+ /// defining contract, the values are retrieved from that view. Otherwise, the values are derived from other
303
+ /// common metadata views.
304
+ ///
305
+ /// @param forAssetType: The Type of the asset to retrieve onboarding values for
306
+ ///
307
+ /// @return The CadenceOnboardingValues struct containing the asset's name, symbol, identifier, contract address,
308
+ /// and contract URI
309
+ ///
310
+ access(all)
311
+ fun getCadenceOnboardingValues(forAssetType: Type): CadenceOnboardingValues {
312
+ pre {
313
+ self.isValidCadenceAsset(type: forAssetType): "This type is not a supported Flow asset type."
314
+ }
315
+ // If not an NFT, assumed to be fungible token.
316
+ let isNFT = forAssetType.isSubtype(of: Type<@{NonFungibleToken.NFT}>())
317
+
318
+ // Retrieve the Cadence type's defining contract name, address, & its identifier
319
+ var name = self.getContractName(fromType: forAssetType)
320
+ ?? panic("Could not contract name from type: ".concat(forAssetType.identifier))
321
+ let identifier = forAssetType.identifier
322
+ let cadenceAddress = self.getContractAddress(fromType: forAssetType)
323
+ ?? panic("Could not derive contract address for token type: ".concat(identifier))
324
+ // Initialize asset symbol which will be assigned later
325
+ // based on presence of asset-defined metadata
326
+ var symbol: String? = nil
327
+ // Borrow the ViewResolver to attempt to resolve the EVMBridgedMetadata view
328
+ let viewResolver = getAccount(cadenceAddress).contracts.borrow<&{ViewResolver}>(name: name)!
329
+ var contractURI = ""
330
+
331
+ // Try to resolve the EVMBridgedMetadata
332
+ let bridgedMetadata = viewResolver.resolveContractView(
333
+ resourceType: forAssetType,
334
+ viewType: Type<MetadataViews.EVMBridgedMetadata>()
335
+ ) as! MetadataViews.EVMBridgedMetadata?
336
+ // Default to project-defined URI if available
337
+ if bridgedMetadata != nil {
338
+ name = bridgedMetadata!.name
339
+ symbol = bridgedMetadata!.symbol
340
+ contractURI = bridgedMetadata!.uri.uri()
341
+ } else {
342
+ if isNFT {
343
+ // Otherwise, serialize collection-level NFTCollectionDisplay
344
+ if let collectionDisplay = viewResolver.resolveContractView(
345
+ resourceType: forAssetType,
346
+ viewType: Type<MetadataViews.NFTCollectionDisplay>()
347
+ ) as! MetadataViews.NFTCollectionDisplay? {
348
+ name = collectionDisplay.name
349
+ let serializedDisplay = SerializeMetadata.serializeFromDisplays(nftDisplay: nil, collectionDisplay: collectionDisplay)!
350
+ contractURI = "data:application/json;utf8,{".concat(serializedDisplay).concat("}")
351
+ }
352
+ if symbol == nil {
353
+ symbol = SerializeMetadata.deriveSymbol(fromString: name)
354
+ }
355
+ } else {
356
+ let ftDisplay = viewResolver.resolveContractView(
357
+ resourceType: forAssetType,
358
+ viewType: Type<FungibleTokenMetadataViews.FTDisplay>()
359
+ ) as! FungibleTokenMetadataViews.FTDisplay?
360
+ if ftDisplay != nil {
361
+ name = ftDisplay!.name
362
+ symbol = ftDisplay!.symbol
363
+ }
364
+ if contractURI.length == 0 && ftDisplay != nil {
365
+ let serializedDisplay = SerializeMetadata.serializeFTDisplay(ftDisplay!)
366
+ contractURI = "data:application/json;utf8,{".concat(serializedDisplay).concat("}")
367
+ }
368
+ }
369
+ }
370
+
371
+ return CadenceOnboardingValues(
372
+ contractAddress: cadenceAddress,
373
+ name: name,
374
+ symbol: symbol!,
375
+ identifier: identifier,
376
+ contractURI: contractURI
377
+ )
378
+ }
379
+
380
+ /// Retrieves identifying information about an EVM contract related to bridge onboarding.
381
+ ///
382
+ /// @param evmContractAddress: The EVM contract address to retrieve onboarding values for
383
+ ///
384
+ /// @return The EVMOnboardingValues struct containing the asset's name, symbol, decimals, contractURI, and
385
+ /// Cadence contract name as well as whether the asset is an ERC721
386
+ ///
387
+ access(all)
388
+ fun getEVMOnboardingValues(evmContractAddress: EVM.EVMAddress): EVMOnboardingValues {
389
+ // Retrieve the EVM contract's name, symbol, and contractURI
390
+ let name: String = self.getName(evmContractAddress: evmContractAddress)
391
+ let symbol: String = self.getSymbol(evmContractAddress: evmContractAddress)
392
+ let contractURI = self.getContractURI(evmContractAddress: evmContractAddress)
393
+ // Default to 18 decimals for ERC20s
394
+ var decimals: UInt8 = FlowEVMBridgeConfig.defaultDecimals
395
+
396
+ // Derive Cadence contract name
397
+ let isERC721: Bool = self.isERC721(evmContractAddress: evmContractAddress)
398
+ var cadenceContractName: String = ""
399
+ if isERC721 {
400
+ // Assert the contract is not mixed asset
401
+ let isERC20 = self.isERC20(evmContractAddress: evmContractAddress)
402
+ assert(!isERC20, message: "Contract is mixed asset and is not currently supported by the bridge")
403
+ // Derive the contract name from the ERC721 contract
404
+ cadenceContractName = self.deriveBridgedNFTContractName(from: evmContractAddress)
405
+ } else {
406
+ // Otherwise, treat as ERC20
407
+ let isERC20 = self.isERC20(evmContractAddress: evmContractAddress)
408
+ assert(
409
+ isERC20,
410
+ message: "Contract ".concat(evmContractAddress.toString()).concat("defines an asset that is not currently supported by the bridge")
411
+ )
412
+ cadenceContractName = self.deriveBridgedTokenContractName(from: evmContractAddress)
413
+ decimals = self.getTokenDecimals(evmContractAddress: evmContractAddress)
414
+ }
415
+
416
+ return EVMOnboardingValues(
417
+ evmContractAddress: evmContractAddress,
418
+ name: name,
419
+ symbol: symbol,
420
+ decimals: decimals,
421
+ contractURI: contractURI,
422
+ cadenceContractName: cadenceContractName,
423
+ isERC721: isERC721
424
+ )
425
+ }
426
+
427
+ /************************
428
+ EVM Call Wrappers
429
+ ************************/
430
+
431
+ /// Retrieves the NFT/FT name from the given EVM contract address - applies for both ERC20 & ERC721.
432
+ /// Reverts on EVM call failure.
433
+ ///
434
+ /// @param evmContractAddress: The EVM contract address to retrieve the name from
435
+ ///
436
+ /// @return the name of the asset
437
+ ///
438
+ access(all)
439
+ fun getName(evmContractAddress: EVM.EVMAddress): String {
440
+ let callResult = self.call(
441
+ signature: "name()",
442
+ targetEVMAddress: evmContractAddress,
443
+ args: [],
444
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
445
+ value: 0.0
446
+ )
447
+
448
+ assert(callResult.status == EVM.Status.successful, message: "Call for EVM asset name failed")
449
+ let decodedResult = EVM.decodeABI(types: [Type<String>()], data: callResult.data) as! [AnyStruct]
450
+ assert(decodedResult.length == 1, message: "Invalid response length")
451
+
452
+ return decodedResult[0] as! String
453
+ }
454
+
455
+ /// Retrieves the NFT/FT symbol from the given EVM contract address - applies for both ERC20 & ERC721
456
+ /// Reverts on EVM call failure.
457
+ ///
458
+ /// @param evmContractAddress: The EVM contract address to retrieve the symbol from
459
+ ///
460
+ /// @return the symbol of the asset
461
+ ///
462
+ access(all)
463
+ fun getSymbol(evmContractAddress: EVM.EVMAddress): String {
464
+ let callResult = self.call(
465
+ signature: "symbol()",
466
+ targetEVMAddress: evmContractAddress,
467
+ args: [],
468
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
469
+ value: 0.0
470
+ )
471
+ assert(callResult.status == EVM.Status.successful, message: "Call for EVM asset symbol failed")
472
+ let decodedResult = EVM.decodeABI(types: [Type<String>()], data: callResult.data) as! [AnyStruct]
473
+ assert(decodedResult.length == 1, message: "Invalid response length")
474
+ return decodedResult[0] as! String
475
+ }
476
+
477
+ /// Retrieves the tokenURI for the given NFT ID from the given EVM contract address. Reverts on EVM call failure.
478
+ /// Reverts on EVM call failure.
479
+ ///
480
+ /// @param evmContractAddress: The EVM contract address to retrieve the tokenURI from
481
+ /// @param id: The ID of the NFT for which to retrieve the tokenURI value
482
+ ///
483
+ /// @return the tokenURI of the ERC721
484
+ ///
485
+ access(all)
486
+ fun getTokenURI(evmContractAddress: EVM.EVMAddress, id: UInt256): String {
487
+ let callResult = self.call(
488
+ signature: "tokenURI(uint256)",
489
+ targetEVMAddress: evmContractAddress,
490
+ args: [id],
491
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
492
+ value: 0.0
493
+ )
494
+
495
+ assert(callResult.status == EVM.Status.successful, message: "Call to EVM for tokenURI failed")
496
+ let decodedResult = EVM.decodeABI(types: [Type<String>()], data: callResult.data) as! [AnyStruct]
497
+ assert(decodedResult.length == 1, message: "Invalid response length")
498
+
499
+ return decodedResult[0] as! String
500
+ }
501
+
502
+ /// Retrieves the contract URI from the given EVM contract address. Returns nil on EVM call failure.
503
+ ///
504
+ /// @param evmContractAddress: The EVM contract address to retrieve the contractURI from
505
+ ///
506
+ /// @return the contract's contractURI
507
+ ///
508
+ access(all)
509
+ fun getContractURI(evmContractAddress: EVM.EVMAddress): String? {
510
+ let callResult = self.call(
511
+ signature: "contractURI()",
512
+ targetEVMAddress: evmContractAddress,
513
+ args: [],
514
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
515
+ value: 0.0
516
+ )
517
+ if callResult.status != EVM.Status.successful {
518
+ return nil
519
+ }
520
+ let decodedResult = EVM.decodeABI(types: [Type<String>()], data: callResult.data) as! [AnyStruct]
521
+ return decodedResult.length == 1 ? decodedResult[0] as! String : nil
522
+ }
523
+
524
+ /// Retrieves the number of decimals for a given ERC20 contract address. Reverts on EVM call failure.
525
+ ///
526
+ /// @param evmContractAddress: The ERC20 contract address to retrieve the token decimals from
527
+ ///
528
+ /// @return the token decimals of the ERC20
529
+ ///
530
+ access(all)
531
+ fun getTokenDecimals(evmContractAddress: EVM.EVMAddress): UInt8 {
532
+ let callResult = self.call(
533
+ signature: "decimals()",
534
+ targetEVMAddress: evmContractAddress,
535
+ args: [],
536
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
537
+ value: 0.0
538
+ )
539
+
540
+ assert(callResult.status == EVM.Status.successful, message: "Call for EVM asset decimals failed")
541
+ let decodedResult = EVM.decodeABI(types: [Type<UInt8>()], data: callResult.data) as! [AnyStruct]
542
+ assert(decodedResult.length == 1, message: "Invalid response length")
543
+
544
+ return decodedResult[0] as! UInt8
545
+ }
546
+
547
+ /// Determines if the provided owner address is either the owner or approved for the NFT in the ERC721 contract
548
+ /// Reverts on EVM call failure.
549
+ ///
550
+ /// @param ofNFT: The ID of the NFT to query
551
+ /// @param owner: The owner address to query
552
+ /// @param evmContractAddress: The ERC721 contract address to query
553
+ ///
554
+ /// @return true if the owner is either the owner or approved for the NFT, false otherwise
555
+ ///
556
+ access(all)
557
+ fun isOwnerOrApproved(ofNFT: UInt256, owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): Bool {
558
+ return self.isOwner(ofNFT: ofNFT, owner: owner, evmContractAddress: evmContractAddress) ||
559
+ self.isApproved(ofNFT: ofNFT, owner: owner, evmContractAddress: evmContractAddress)
560
+ }
561
+
562
+ /// Returns whether the given owner is the owner of the given NFT. Reverts on EVM call failure.
563
+ ///
564
+ /// @param ofNFT: The ID of the NFT to query
565
+ /// @param owner: The owner address to query
566
+ /// @param evmContractAddress: The ERC721 contract address to query
567
+ ///
568
+ /// @return true if the owner is in fact the owner of the NFT, false otherwise
569
+ ///
570
+ access(all)
571
+ fun isOwner(ofNFT: UInt256, owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): Bool {
572
+ let callResult = self.call(
573
+ signature: "ownerOf(uint256)",
574
+ targetEVMAddress: evmContractAddress,
575
+ args: [ofNFT],
576
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
577
+ value: 0.0
578
+ )
579
+ assert(callResult.status == EVM.Status.successful, message: "Call to ERC721.ownerOf(uint256) failed")
580
+ let decodedCallResult = EVM.decodeABI(types: [Type<EVM.EVMAddress>()], data: callResult.data)
581
+ if decodedCallResult.length == 1 {
582
+ let actualOwner = decodedCallResult[0] as! EVM.EVMAddress
583
+ return actualOwner.equals(owner)
584
+ }
585
+ return false
586
+ }
587
+
588
+ /// Returns whether the given owner is approved for the given NFT. Reverts on EVM call failure.
589
+ ///
590
+ /// @param ofNFT: The ID of the NFT to query
591
+ /// @param owner: The owner address to query
592
+ /// @param evmContractAddress: The ERC721 contract address to query
593
+ ///
594
+ /// @return true if the owner is in fact approved for the NFT, false otherwise
595
+ ///
596
+ access(all)
597
+ fun isApproved(ofNFT: UInt256, owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): Bool {
598
+ let callResult = self.call(
599
+ signature: "getApproved(uint256)",
600
+ targetEVMAddress: evmContractAddress,
601
+ args: [ofNFT],
602
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
603
+ value: 0.0
604
+ )
605
+ assert(callResult.status == EVM.Status.successful, message: "Call to ERC721.getApproved(uint256) failed")
606
+ let decodedCallResult = EVM.decodeABI(types: [Type<EVM.EVMAddress>()], data: callResult.data)
607
+ if decodedCallResult.length == 1 {
608
+ let actualApproved = decodedCallResult[0] as! EVM.EVMAddress
609
+ return actualApproved.equals(owner)
610
+ }
611
+ return false
612
+ }
613
+
614
+ /// Returns whether the given ERC721 exists, assuming the ERC721 contract implements the `exists` method. While this
615
+ /// method is not part of the ERC721 standard, it is implemented in the bridge-deployed ERC721 implementation.
616
+ /// Reverts on EVM call failure.
617
+ ///
618
+ /// @param erc721Address: The EVM contract address of the ERC721 token
619
+ /// @param id: The ID of the ERC721 token to check
620
+ ///
621
+ /// @return true if the ERC721 token exists, false otherwise
622
+ ///
623
+ access(all)
624
+ fun erc721Exists(erc721Address: EVM.EVMAddress, id: UInt256): Bool {
625
+ let existsResponse = EVM.decodeABI(
626
+ types: [Type<Bool>()],
627
+ data: self.call(
628
+ signature: "exists(uint256)",
629
+ targetEVMAddress: erc721Address,
630
+ args: [id],
631
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
632
+ value: 0.0
633
+ ).data,
634
+ )
635
+ assert(existsResponse.length == 1, message: "Invalid response length")
636
+ return existsResponse[0] as! Bool
637
+ }
638
+
639
+ /// Returns the ERC20 balance of the owner at the given ERC20 contract address. Reverts on EVM call failure.
640
+ ///
641
+ /// @param owner: The owner address to query
642
+ /// @param evmContractAddress: The ERC20 contract address to query
643
+ ///
644
+ /// @return The UInt256 balance of the owner at the ERC20 contract address. Callers may wish to convert the return
645
+ /// value to a UFix64 via convertERC20AmountToCadenceAmount, though note there may be a loss of precision.
646
+ ///
647
+ access(all)
648
+ fun balanceOf(owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): UInt256 {
649
+ let callResult = self.call(
650
+ signature: "balanceOf(address)",
651
+ targetEVMAddress: evmContractAddress,
652
+ args: [owner],
653
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
654
+ value: 0.0
655
+ )
656
+ assert(callResult.status == EVM.Status.successful, message: "Call to ERC20.balanceOf(address) failed")
657
+ let decodedResult = EVM.decodeABI(types: [Type<UInt256>()], data: callResult.data) as! [AnyStruct]
658
+ assert(decodedResult.length == 1, message: "Invalid response length")
659
+ return decodedResult[0] as! UInt256
660
+ }
661
+
662
+ /// Determines if the owner has sufficient funds to bridge the given amount at the ERC20 contract address
663
+ /// Reverts on EVM call failure.
664
+ ///
665
+ /// @param amount: The amount to check if the owner has enough balance to cover
666
+ /// @param owner: The owner address to query
667
+ /// @param evmContractAddress: The ERC20 contract address to query
668
+ ///
669
+ /// @return true if the owner's balance >= amount, false otherwise
670
+ ///
671
+ access(all)
672
+ fun hasSufficientBalance(amount: UInt256, owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): Bool {
673
+ return self.balanceOf(owner: owner, evmContractAddress: evmContractAddress) >= amount
674
+ }
675
+
676
+ /// Retrieves the total supply of the ERC20 contract at the given EVM contract address. Reverts on EVM call failure.
677
+ ///
678
+ /// @param evmContractAddress: The EVM contract address to retrieve the total supply from
679
+ ///
680
+ /// @return the total supply of the ERC20
681
+ ///
682
+ access(all)
683
+ fun totalSupply(evmContractAddress: EVM.EVMAddress): UInt256 {
684
+ let callResult = self.call(
685
+ signature: "totalSupply()",
686
+ targetEVMAddress: evmContractAddress,
687
+ args: [],
688
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
689
+ value: 0.0
690
+ )
691
+ assert(callResult.status == EVM.Status.successful, message: "Call to ERC20.totalSupply() failed")
692
+ let decodedResult = EVM.decodeABI(types: [Type<UInt256>()], data: callResult.data) as! [AnyStruct]
693
+ assert(decodedResult.length == 1, message: "Invalid response length")
694
+ return decodedResult[0] as! UInt256
695
+ }
696
+
697
+ /// Converts the given amount of ERC20 tokens to the equivalent amount in FLOW tokens based on the ERC20s decimals
698
+ /// value. Note that may be some loss of decimal precision as UFix64 supports precision for 8 decimal places.
699
+ /// Reverts on EVM call failure.
700
+ ///
701
+ /// @param amount: The amount of ERC20 tokens to convert
702
+ /// @param erc20Address: The EVM contract address of the ERC20 token
703
+ ///
704
+ /// @return the equivalent amount in FLOW tokens as a UFix64
705
+ ///
706
+ access(all)
707
+ fun convertERC20AmountToCadenceAmount(_ amount: UInt256, erc20Address: EVM.EVMAddress): UFix64 {
708
+ return self.uint256ToUFix64(
709
+ value: amount,
710
+ decimals: self.getTokenDecimals(evmContractAddress: erc20Address)
711
+ )
712
+ }
713
+
714
+ /// Converts the given amount of Cadence fungible tokens to the equivalent amount in ERC20 tokens based on the
715
+ /// ERC20s decimals. Note that there may be some loss of decimal precision as UFix64 supports precision for 8
716
+ /// decimal places. Reverts on EVM call failure.
717
+ ///
718
+ /// @param amount: The amount of Cadence fungible tokens to convert
719
+ /// @param erc20Address: The EVM contract address of the ERC20 token
720
+ ///
721
+ /// @return the equivalent amount in ERC20 tokens as a UInt256
722
+ ///
723
+ access(all)
724
+ fun convertCadenceAmountToERC20Amount(_ amount: UFix64, erc20Address: EVM.EVMAddress): UInt256 {
725
+ return self.ufix64ToUInt256(value: amount, decimals: self.getTokenDecimals(evmContractAddress: erc20Address))
726
+ }
727
+
728
+ /************************
729
+ Derivation Utils
730
+ ************************/
731
+
732
+ /// Derives the StoragePath where the escrow locker is stored for a given Type of asset & returns. The given type
733
+ /// must be of an asset supported by the bridge.
734
+ ///
735
+ /// @param fromType: The type of the asset the escrow locker is being derived for
736
+ ///
737
+ /// @return The StoragePath associated with the type's escrow Locker, or nil if the type is not supported
738
+ ///
739
+ access(all)
740
+ view fun deriveEscrowStoragePath(fromType: Type): StoragePath? {
741
+ if !self.isValidCadenceAsset(type: fromType) {
742
+ return nil
743
+ }
744
+ var prefix = ""
745
+ if fromType.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) {
746
+ prefix = "flowEVMBridgeNFTEscrow"
747
+ } else if fromType.isSubtype(of: Type<@{FungibleToken.Vault}>()) {
748
+ prefix = "flowEVMBridgeTokenEscrow"
749
+ }
750
+ assert(prefix.length > 1, message: "Invalid prefix")
751
+ if let splitIdentifier = self.splitObjectIdentifier(identifier: fromType.identifier) {
752
+ let sourceContractAddress = Address.fromString("0x".concat(splitIdentifier[1]))!
753
+ let sourceContractName = splitIdentifier[2]
754
+ let resourceName = splitIdentifier[3]
755
+ return StoragePath(
756
+ identifier: prefix.concat(self.delimiter)
757
+ .concat(sourceContractAddress.toString()).concat(self.delimiter)
758
+ .concat(sourceContractName).concat(self.delimiter)
759
+ .concat(resourceName)
760
+ ) ?? nil
761
+ }
762
+ return nil
763
+ }
764
+
765
+ /// Derives the Cadence contract name for a given EVM NFT of the form
766
+ /// EVMVMBridgedNFT_<0xCONTRACT_ADDRESS>
767
+ ///
768
+ /// @param from evmContract: The EVM contract address to derive the Cadence NFT contract name for
769
+ ///
770
+ /// @return The derived Cadence FT contract name
771
+ ///
772
+ access(all)
773
+ view fun deriveBridgedNFTContractName(from evmContract: EVM.EVMAddress): String {
774
+ return self.contractNamePrefixes[Type<@{NonFungibleToken.NFT}>()]!["bridged"]!
775
+ .concat(self.delimiter)
776
+ .concat(evmContract.toString())
777
+ }
778
+
779
+ /// Derives the Cadence contract name for a given EVM fungible token of the form
780
+ /// EVMVMBridgedToken_<0xCONTRACT_ADDRESS>
781
+ ///
782
+ /// @param from evmContract: The EVM contract address to derive the Cadence FT contract name for
783
+ ///
784
+ /// @return The derived Cadence FT contract name
785
+ ///
786
+ access(all)
787
+ view fun deriveBridgedTokenContractName(from evmContract: EVM.EVMAddress): String {
788
+ return self.contractNamePrefixes[Type<@{FungibleToken.Vault}>()]!["bridged"]!
789
+ .concat(self.delimiter)
790
+ .concat(evmContract.toString())
791
+ }
792
+
793
+ /****************
794
+ Math Utils
795
+ ****************/
796
+
797
+ /// Raises the base to the power of the exponent
798
+ ///
799
+ access(all)
800
+ view fun pow(base: UInt256, exponent: UInt8): UInt256 {
801
+ if exponent == 0 {
802
+ return 1
803
+ }
804
+
805
+ var r = base
806
+ var exp: UInt8 = 1
807
+ while exp < exponent {
808
+ r = r * base
809
+ exp = exp + 1
810
+ }
811
+
812
+ return r
813
+ }
814
+
815
+ /// Raises the fixed point base to the power of the exponent
816
+ ///
817
+ access(all)
818
+ view fun ufixPow(base: UFix64, exponent: UInt8): UFix64 {
819
+ if exponent == 0 {
820
+ return 1.0
821
+ }
822
+
823
+ var r = base
824
+ var exp: UInt8 = 1
825
+ while exp < exponent {
826
+ r = r * base
827
+ exp = exp + 1
828
+ }
829
+
830
+ return r
831
+ }
832
+
833
+ /// Converts a UFix64 to a UInt256
834
+ //
835
+ access(all)
836
+ view fun ufix64ToUInt256(value: UFix64, decimals: UInt8): UInt256 {
837
+ // Default to 10e8 scale, catching instances where decimals are less than default and scale appropriately
838
+ let ufixScaleExp: UInt8 = decimals < 8 ? decimals : 8
839
+ var ufixScale = self.ufixPow(base: 10.0, exponent: ufixScaleExp)
840
+
841
+ // Separate the fractional and integer parts of the UFix64
842
+ let integer = UInt256(value)
843
+ var fractional = (value % 1.0) * ufixScale
844
+
845
+ // Calculate the multiplier for integer and fractional parts
846
+ var integerMultiplier: UInt256 = self.pow(base:10, exponent: decimals)
847
+ let fractionalMultiplierExp: UInt8 = decimals < 8 ? 0 : decimals - 8
848
+ var fractionalMultiplier: UInt256 = self.pow(base:10, exponent: fractionalMultiplierExp)
849
+
850
+ // Scale and sum the parts
851
+ return integer * integerMultiplier + UInt256(fractional) * fractionalMultiplier
852
+ }
853
+
854
+ /// Converts a UInt256 to a UFix64
855
+ ///
856
+ access(all)
857
+ view fun uint256ToUFix64(value: UInt256, decimals: UInt8): UFix64 {
858
+ // Calculate scale factors for the integer and fractional parts
859
+ let absoluteScaleFactor = self.pow(base: 10, exponent: decimals)
860
+
861
+ // Separate the integer and fractional parts of the value
862
+ let scaledValue = value / absoluteScaleFactor
863
+ var fractional = value % absoluteScaleFactor
864
+ // Scale the fractional part
865
+ let scaledFractional = self.uint256FractionalToScaledUFix64Decimals(value: fractional, decimals: decimals)
866
+
867
+ // Ensure the parts do not exceed the max UFix64 value before conversion
868
+ assert(
869
+ scaledValue <= UInt256(UFix64.max),
870
+ message: "Scaled integer value ".concat(value.toString()).concat(" exceeds max UFix64 value")
871
+ )
872
+ /// Check for the max value that can be converted to a UFix64 without overflowing
873
+ assert(
874
+ scaledValue == UInt256(UFix64.max) ? scaledFractional < 0.09551616 : true,
875
+ message: "Scaled integer value ".concat(value.toString()).concat(" exceeds max UFix64 value")
876
+ )
877
+
878
+ return UFix64(scaledValue) + scaledFractional
879
+ }
880
+
881
+ /// Converts a UInt256 fractional value with the given decimal places to a scaled UFix64. Note that UFix64 has
882
+ /// decimal precision of 8 places so converted values may lose precision and be rounded down.
883
+ ///
884
+ access(all)
885
+ view fun uint256FractionalToScaledUFix64Decimals(value: UInt256, decimals: UInt8): UFix64 {
886
+ pre {
887
+ self.getNumberOfDigits(value) <= decimals: "Fractional digits exceed the defined decimal places"
888
+ }
889
+ post {
890
+ result < 1.0: "Resulting scaled fractional exceeds 1.0"
891
+ }
892
+
893
+ var fractional = value
894
+ // Truncate fractional to the first 8 decimal places which is the max precision for UFix64
895
+ if decimals >= 8 {
896
+ fractional = fractional / self.pow(base: 10, exponent: decimals - 8)
897
+ }
898
+ // Return early if the truncated fractional part is now 0
899
+ if fractional == 0 {
900
+ return 0.0
901
+ }
902
+
903
+ // Scale the fractional part
904
+ let fractionalMultiplier = self.ufixPow(base: 0.1, exponent: decimals < 8 ? decimals : 8)
905
+ return UFix64(fractional) * fractionalMultiplier
906
+ }
907
+
908
+ /// Returns the value as a UInt64 if it fits, otherwise panics
909
+ ///
910
+ access(all)
911
+ view fun uint256ToUInt64(value: UInt256): UInt64 {
912
+ return value <= UInt256(UInt64.max) ? UInt64(value) : panic("Value too large to fit into UInt64")
913
+ }
914
+
915
+ /// Returns the number of digits in the given UInt256
916
+ ///
917
+ access(all)
918
+ view fun getNumberOfDigits(_ value: UInt256): UInt8 {
919
+ var tmp = value
920
+ var digits: UInt8 = 0
921
+ while tmp > 0 {
922
+ tmp = tmp / 10
923
+ digits = digits + 1
924
+ }
925
+ return digits
926
+ }
927
+
928
+ /***************************
929
+ Type Identifier Utils
930
+ ***************************/
931
+
932
+ /// Returns the contract address from the given Type's identifier
933
+ ///
934
+ /// @param fromType: The Type to extract the contract address from
935
+ ///
936
+ /// @return The defining contract's Address, or nil if the identifier does not have an associated Address
937
+ ///
938
+ access(all)
939
+ view fun getContractAddress(fromType: Type): Address? {
940
+ // Split identifier of format A.<CONTRACT_ADDRESS>.<CONTRACT_NAME>.<OBJECT_NAME>
941
+ if let identifierSplit = self.splitObjectIdentifier(identifier: fromType.identifier) {
942
+ return Address.fromString("0x".concat(identifierSplit[1]))
943
+ }
944
+ return nil
945
+ }
946
+
947
+ /// Returns the contract name from the given Type's identifier
948
+ ///
949
+ /// @param fromType: The Type to extract the contract name from
950
+ ///
951
+ /// @return The defining contract's name, or nil if the identifier does not have an associated contract name
952
+ ///
953
+ access(all)
954
+ view fun getContractName(fromType: Type): String? {
955
+ // Split identifier of format A.<CONTRACT_ADDRESS>.<CONTRACT_NAME>.<OBJECT_NAME>
956
+ if let identifierSplit = self.splitObjectIdentifier(identifier: fromType.identifier) {
957
+ return identifierSplit[2]
958
+ }
959
+ return nil
960
+ }
961
+
962
+ /// Returns the object's name from the given Type's identifier
963
+ ///
964
+ /// @param fromType: The Type to extract the object name from
965
+ ///
966
+ /// @return The object's name, or nil if the identifier does identify an object
967
+ ///
968
+ access(all)
969
+ view fun getObjectName(fromType: Type): String? {
970
+ // Split identifier of format A.<CONTRACT_ADDRESS>.<CONTRACT_NAME>.<OBJECT_NAME>
971
+ if let identifierSplit = self.splitObjectIdentifier(identifier: fromType.identifier) {
972
+ return identifierSplit[3]
973
+ }
974
+ return nil
975
+ }
976
+
977
+ /// Splits the given identifier into its constituent parts defined by a delimiter of '".'"
978
+ ///
979
+ /// @param identifier: The identifier to split
980
+ ///
981
+ /// @return An array of the identifier's constituent parts, or nil if the identifier does not have 4 parts
982
+ ///
983
+ access(all)
984
+ view fun splitObjectIdentifier(identifier: String): [String]? {
985
+ let identifierSplit = identifier.split(separator: ".")
986
+ return identifierSplit.length != 4 ? nil : identifierSplit
987
+ }
988
+
989
+ /// Builds a composite type from the given identifier parts
990
+ ///
991
+ /// @param address: The defining contract address
992
+ /// @param contractName: The defining contract name
993
+ /// @param resourceName: The resource name
994
+ ///
995
+ access(all)
996
+ view fun buildCompositeType(address: Address, contractName: String, resourceName: String): Type? {
997
+ let addressStr = address.toString()
998
+ let subtract0x = addressStr.slice(from: 2, upTo: addressStr.length)
999
+ let identifier = "A".concat(".").concat(subtract0x).concat(".").concat(contractName).concat(".").concat(resourceName)
1000
+ return CompositeType(identifier)
1001
+ }
1002
+
1003
+ /**************************
1004
+ FungibleToken Utils
1005
+ **************************/
1006
+
1007
+ /// Returns the `createEmptyVault()` function from a Vault Type's defining contract or nil if either the Type is not
1008
+ access(all) fun getCreateEmptyVaultFunction(forType: Type): (fun (Type): @{FungibleToken.Vault})? {
1009
+ // We can only reasonably assume that the requested function is accessible from a FungibleToken contract
1010
+ if !forType.isSubtype(of: Type<@{FungibleToken.Vault}>()) {
1011
+ return nil
1012
+ }
1013
+ // Vault Types should guarantee that the following forced optionals are safe
1014
+ let contractAddress = self.getContractAddress(fromType: forType)!
1015
+ let contractName = self.getContractName(fromType: forType)!
1016
+ let tokenContract: &{FungibleToken} = getAccount(contractAddress).contracts.borrow<&{FungibleToken}>(
1017
+ name: contractName
1018
+ )!
1019
+ return tokenContract.createEmptyVault
1020
+ }
1021
+
1022
+ /******************************
1023
+ Bridge-Access Only Utils
1024
+ ******************************/
1025
+
1026
+ /// Deposits fees to the bridge account's FlowToken Vault - helps fund asset storage
1027
+ ///
1028
+ access(account)
1029
+ fun depositFee(_ feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, feeAmount: UFix64) {
1030
+ let vault = self.account.storage.borrow<&FlowToken.Vault>(from: /storage/flowTokenVault)
1031
+ ?? panic("Could not borrow FlowToken.Vault reference")
1032
+
1033
+ let feeVault <-feeProvider.withdraw(amount: feeAmount) as! @FlowToken.Vault
1034
+ assert(feeVault.balance == feeAmount, message: "Fee provider did not return the requested fee")
1035
+
1036
+ vault.deposit(from: <-feeVault)
1037
+ }
1038
+
1039
+ /// Enables other bridge contracts to orchestrate bridge operations from contract-owned COA
1040
+ ///
1041
+ access(account)
1042
+ view fun borrowCOA(): auth(EVM.Call) &EVM.CadenceOwnedAccount {
1043
+ return self.account.storage.borrow<auth(EVM.Call) &EVM.CadenceOwnedAccount>(
1044
+ from: FlowEVMBridgeConfig.coaStoragePath
1045
+ ) ?? panic("Could not borrow COA reference")
1046
+ }
1047
+
1048
+ /// Shared helper simplifying calls using the bridge account's COA
1049
+ ///
1050
+ access(account)
1051
+ fun call(
1052
+ signature: String,
1053
+ targetEVMAddress: EVM.EVMAddress,
1054
+ args: [AnyStruct],
1055
+ gasLimit: UInt64,
1056
+ value: UFix64
1057
+ ): EVM.Result {
1058
+ let calldata = EVM.encodeABIWithSignature(signature, args)
1059
+ let valueBalance = EVM.Balance(attoflow: 0)
1060
+ valueBalance.setFLOW(flow: value)
1061
+ return self.borrowCOA().call(
1062
+ to: targetEVMAddress,
1063
+ data: calldata,
1064
+ gasLimit: gasLimit,
1065
+ value: valueBalance
1066
+ )
1067
+ }
1068
+
1069
+ /// Executes a safeTransferFrom call on the given ERC721 contract address, transferring the NFT from bridge escrow
1070
+ /// in EVM to the named recipient and asserting pre- and post-state changes.
1071
+ ///
1072
+ access(account)
1073
+ fun mustSafeTransferERC721(erc721Address: EVM.EVMAddress, to: EVM.EVMAddress, id: UInt256) {
1074
+ let bridgeCOAAddress = self.getBridgeCOAEVMAddress()
1075
+
1076
+ let bridgePreStatus = self.isOwner(ofNFT: id, owner: bridgeCOAAddress, evmContractAddress: erc721Address)
1077
+ let toPreStatus = self.isOwner(ofNFT: id, owner: to, evmContractAddress: erc721Address)
1078
+ assert(bridgePreStatus, message: "Bridge COA does not own ERC721 requesting to be transferred")
1079
+ assert(!toPreStatus, message: "Recipient already owns ERC721 attempting to be transferred")
1080
+
1081
+ let transferResult: EVM.Result = self.call(
1082
+ signature: "safeTransferFrom(address,address,uint256)",
1083
+ targetEVMAddress: erc721Address,
1084
+ args: [bridgeCOAAddress, to, id],
1085
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
1086
+ value: 0.0
1087
+ )
1088
+ assert(
1089
+ transferResult.status == EVM.Status.successful,
1090
+ message: "safeTransferFrom call to ERC721 transferring NFT from escrow to bridge recipient failed"
1091
+ )
1092
+
1093
+ let bridgePostStatus = self.isOwner(ofNFT: id, owner: bridgeCOAAddress, evmContractAddress: erc721Address)
1094
+ let toPostStatus = self.isOwner(ofNFT: id, owner: to, evmContractAddress: erc721Address)
1095
+ assert(!bridgePostStatus, message: "ERC721 is still in escrow after transfer")
1096
+ assert(toPostStatus, message: "ERC721 was not successfully transferred to recipient from escrow")
1097
+ }
1098
+
1099
+ /// Executes a safeMint call on the given ERC721 contract address, minting an ERC721 to the named recipient and
1100
+ /// asserting pre- and post-state changes. Assumes the bridge COA has the authority to mint the NFT.
1101
+ ///
1102
+ access(account)
1103
+ fun mustSafeMintERC721(erc721Address: EVM.EVMAddress, to: EVM.EVMAddress, id: UInt256, uri: String) {
1104
+ let bridgeCOAAddress = self.getBridgeCOAEVMAddress()
1105
+
1106
+ let mintResult: EVM.Result = self.call(
1107
+ signature: "safeMint(address,uint256,string)",
1108
+ targetEVMAddress: erc721Address,
1109
+ args: [to, id, uri],
1110
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
1111
+ value: 0.0
1112
+ )
1113
+ assert(mintResult.status == EVM.Status.successful, message: "Mint to bridge recipient failed")
1114
+
1115
+ let toPostStatus = self.isOwner(ofNFT: id, owner: to, evmContractAddress: erc721Address)
1116
+ assert(toPostStatus, message: "Recipient does not own the NFT after minting")
1117
+ }
1118
+
1119
+ /// Executes updateTokenURI call on the given ERC721 contract address, updating the tokenURI of the NFT. This is
1120
+ /// not a standard ERC721 function, but is implemented in the bridge-deployed ERC721 implementation to enable
1121
+ /// synchronization of token metadata with Cadence NFT state on bridging.
1122
+ ///
1123
+ access(account)
1124
+ fun mustUpdateTokenURI(erc721Address: EVM.EVMAddress, id: UInt256, uri: String) {
1125
+ let bridgeCOAAddress = self.getBridgeCOAEVMAddress()
1126
+
1127
+ let updateResult: EVM.Result = self.call(
1128
+ signature: "updateTokenURI(uint256,string)",
1129
+ targetEVMAddress: erc721Address,
1130
+ args: [id, uri],
1131
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
1132
+ value: 0.0
1133
+ )
1134
+ assert(updateResult.status == EVM.Status.successful, message: "URI update failed")
1135
+ }
1136
+
1137
+ /// Executes the provided method, assumed to be a protected transfer call, and confirms that the transfer was
1138
+ /// successful by validating the named owner is authorized to act on the NFT before the transfer, the transfer
1139
+ /// was successful, and the bridge COA owns the NFT after the protected transfer call.
1140
+ ///
1141
+ access(account)
1142
+ fun mustEscrowERC721(
1143
+ owner: EVM.EVMAddress,
1144
+ id: UInt256,
1145
+ erc721Address: EVM.EVMAddress,
1146
+ protectedTransferCall: fun (): EVM.Result
1147
+ ) {
1148
+ // Ensure the named owner is authorized to act on the NFT
1149
+ let isAuthorized = self.isOwnerOrApproved(ofNFT: id, owner: owner, evmContractAddress: erc721Address)
1150
+ assert(isAuthorized, message: "Named owner is not the owner of the ERC721")
1151
+
1152
+ // Call the protected transfer function which should execute a transfer call from the owner to escrow
1153
+ let transferResult = protectedTransferCall()
1154
+ assert(transferResult.status == EVM.Status.successful, message: "Transfer ERC721 to escrow via callback failed")
1155
+
1156
+ // Validate the NFT is now owned by the bridge COA, escrow the NFT
1157
+ let isEscrowed = self.isOwner(ofNFT: id, owner: self.getBridgeCOAEVMAddress(), evmContractAddress: erc721Address)
1158
+ assert(isEscrowed, message: "ERC721 was not successfully escrowed")
1159
+ }
1160
+
1161
+ /// Mints ERC20 tokens to the recipient and confirms that the recipient's balance was updated
1162
+ ///
1163
+ access(account)
1164
+ fun mustMintERC20(to: EVM.EVMAddress, amount: UInt256, erc20Address: EVM.EVMAddress) {
1165
+ let toPreBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address)
1166
+ // Mint tokens to the recipient
1167
+ let mintResult: EVM.Result = self.call(
1168
+ signature: "mint(address,uint256)",
1169
+ targetEVMAddress: erc20Address,
1170
+ args: [to, amount],
1171
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
1172
+ value: 0.0
1173
+ )
1174
+ assert(mintResult.status == EVM.Status.successful, message: "Mint to bridge ERC20 contract failed")
1175
+ // Ensure bridge to recipient was succcessful
1176
+ let toPostBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address)
1177
+ assert(
1178
+ toPostBalance == toPreBalance + amount,
1179
+ message: "Recipient didn't receive minted ERC20 tokens during bridging"
1180
+ )
1181
+ }
1182
+
1183
+ /// Transfers ERC20 tokens to the recipient and confirms that the recipient's balance was incremented and the escrow
1184
+ /// balance was decremented by the requested amount.
1185
+ ///
1186
+ access(account)
1187
+ fun mustTransferERC20(to: EVM.EVMAddress, amount: UInt256, erc20Address: EVM.EVMAddress) {
1188
+ let bridgeCOAAddress = self.getBridgeCOAEVMAddress()
1189
+
1190
+ let toPreBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address)
1191
+ let escrowPreBalance = self.balanceOf(
1192
+ owner: bridgeCOAAddress,
1193
+ evmContractAddress: erc20Address
1194
+ )
1195
+
1196
+ // Transfer tokens to the recipient
1197
+ let transferResult: EVM.Result = self.call(
1198
+ signature: "transfer(address,uint256)",
1199
+ targetEVMAddress: erc20Address,
1200
+ args: [to, amount],
1201
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
1202
+ value: 0.0
1203
+ )
1204
+ assert(transferResult.status == EVM.Status.successful, message: "transfer call to ERC20 contract failed")
1205
+
1206
+ // Ensure bridge to recipient was succcessful
1207
+ let toPostBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address)
1208
+ let escrowPostBalance = self.balanceOf(
1209
+ owner: bridgeCOAAddress,
1210
+ evmContractAddress: erc20Address
1211
+ )
1212
+ assert(
1213
+ toPostBalance == toPreBalance + amount,
1214
+ message: "Recipient's ERC20 balance did not increment by the requested amount after transfer from escrow"
1215
+ )
1216
+ assert(
1217
+ escrowPostBalance == escrowPreBalance - amount,
1218
+ message: "Escrow ERC20 balance did not decrement by the requested amount after transfer from escrow"
1219
+ )
1220
+ }
1221
+
1222
+ /// Executes the provided method, assumed to be a protected transfer call, and confirms that the transfer was
1223
+ /// successful by validating that the named owner's balance was decremented by the requested amount and the bridge
1224
+ /// escrow balance was incremented by the same amount.
1225
+ ///
1226
+ access(account)
1227
+ fun mustEscrowERC20(
1228
+ owner: EVM.EVMAddress,
1229
+ amount: UInt256,
1230
+ erc20Address: EVM.EVMAddress,
1231
+ protectedTransferCall: fun (): EVM.Result
1232
+ ) {
1233
+ // Ensure the caller is has sufficient balance to bridge the requested amount
1234
+ let hasSufficientBalance = self.hasSufficientBalance(
1235
+ amount: amount,
1236
+ owner: owner,
1237
+ evmContractAddress: erc20Address
1238
+ )
1239
+ assert(hasSufficientBalance, message: "Caller does not have sufficient balance to bridge requested tokens")
1240
+
1241
+ // Get the owner and escrow balances before transfer
1242
+ let ownerPreBalance = self.balanceOf(owner: owner, evmContractAddress: erc20Address)
1243
+ let bridgePreBalance = self.balanceOf(
1244
+ owner: self.getBridgeCOAEVMAddress(),
1245
+ evmContractAddress: erc20Address
1246
+ )
1247
+
1248
+ // Call the protected transfer function which should execute a transfer call from the owner to escrow
1249
+ let transferResult = protectedTransferCall()
1250
+ assert(transferResult.status == EVM.Status.successful, message: "Transfer via callback failed")
1251
+
1252
+ // Get the resulting balances after transfer
1253
+ let ownerPostBalance = self.balanceOf(owner: owner, evmContractAddress: erc20Address)
1254
+ let bridgePostBalance = self.balanceOf(
1255
+ owner: self.getBridgeCOAEVMAddress(),
1256
+ evmContractAddress: erc20Address
1257
+ )
1258
+
1259
+ // Confirm the transfer of the expected was successful in both sending owner and recipient escrow
1260
+ assert(ownerPostBalance == ownerPreBalance - amount, message: "Transfer to owner failed")
1261
+ assert(bridgePostBalance == bridgePreBalance + amount, message: "Transfer to bridge escrow failed")
1262
+ }
1263
+
1264
+ /// Calls to the bridge factory to deploy an ERC721/ERC20 contract and returns the deployed contract address
1265
+ ///
1266
+ access(account)
1267
+ fun mustDeployEVMContract(
1268
+ name: String,
1269
+ symbol: String,
1270
+ cadenceAddress: Address,
1271
+ flowIdentifier: String,
1272
+ contractURI: String,
1273
+ isERC721: Bool
1274
+ ): EVM.EVMAddress {
1275
+ let deployerTag = isERC721 ? "ERC721" : "ERC20"
1276
+ let deployResult: EVM.Result = self.call(
1277
+ signature: "deploy(string,string,string,string,string,string)",
1278
+ targetEVMAddress: self.bridgeFactoryEVMAddress,
1279
+ args: [deployerTag, name, symbol, cadenceAddress.toString(), flowIdentifier, contractURI],
1280
+ gasLimit: FlowEVMBridgeConfig.gasLimit,
1281
+ value: 0.0
1282
+ )
1283
+ assert(deployResult.status == EVM.Status.successful, message: "EVM Token contract deployment failed")
1284
+ let decodedResult: [AnyStruct] = EVM.decodeABI(types: [Type<EVM.EVMAddress>()], data: deployResult.data)
1285
+ assert(decodedResult.length == 1, message: "Invalid response length")
1286
+ return decodedResult[0] as! EVM.EVMAddress
1287
+ }
1288
+
1289
+ init() {
1290
+ // This is overridden so that we do not need to deal with contract init args. This should be part of the contract init instead
1291
+ let bridgeFactoryAddressHex = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
1292
+ self.delimiter = "_"
1293
+ self.contractNamePrefixes = {
1294
+ Type<@{NonFungibleToken.NFT}>(): {
1295
+ "bridged": "EVMVMBridgedNFT"
1296
+ },
1297
+ Type<@{FungibleToken.Vault}>(): {
1298
+ "bridged": "EVMVMBridgedToken"
1299
+ }
1300
+ }
1301
+ self.bridgeFactoryEVMAddress = EVM.addressFromString(bridgeFactoryAddressHex.toLower())
1302
+ }
1303
+ }