@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.
- package/README.md +1 -1
- package/contracts/FungibleTokenSwitchboard.cdc +360 -0
- package/contracts/MetadataViews.cdc +79 -6
- package/contracts/NonFungibleToken.cdc +17 -10
- package/contracts/capability-cache/CapabilityCache.cdc +97 -0
- package/contracts/dapper/TopShot.cdc +323 -259
- package/contracts/dapper/TopShotLocking.cdc +41 -15
- package/contracts/dapper/offers/DapperOffersV2.cdc +46 -43
- package/contracts/dapper/offers/OffersV2.cdc +40 -56
- package/contracts/dapper/offers/Resolver.cdc +20 -13
- package/contracts/emerald-city/FLOAT.cdc +259 -254
- package/contracts/evm/CrossVMNFT.cdc +50 -0
- package/contracts/evm/EVM.cdc +851 -0
- package/contracts/evm/FlowEVMBridgeConfig.cdc +454 -0
- package/contracts/evm/FlowEVMBridgeHandlerInterfaces.cdc +163 -0
- package/contracts/evm/FlowEVMBridgeHandlers.cdc +230 -0
- package/contracts/evm/FlowEVMBridgeUtils.cdc +1303 -0
- package/contracts/evm/IBridgePermissions.cdc +19 -0
- package/contracts/evm/ICrossVM.cdc +12 -0
- package/contracts/evm/ICrossVMAsset.cdc +19 -0
- package/contracts/evm/Serialize.cdc +141 -0
- package/contracts/evm/SerializeMetadata.cdc +221 -0
- package/contracts/example/ExampleNFT.cdc +2 -2
- package/contracts/find/FindViews.cdc +357 -353
- package/contracts/flow-utils/ScopedFTProviders.cdc +5 -2
- package/contracts/flow-utils/ScopedNFTProviders.cdc +6 -2
- package/contracts/flowty-drops/ContractManager.cdc +73 -0
- package/contracts/flowty-drops/DropFactory.cdc +75 -0
- package/contracts/flowty-drops/DropTypes.cdc +282 -0
- package/contracts/flowty-drops/FlowtyActiveCheckers.cdc +113 -0
- package/contracts/flowty-drops/FlowtyAddressVerifiers.cdc +64 -0
- package/contracts/flowty-drops/FlowtyDrops.cdc +461 -0
- package/contracts/flowty-drops/FlowtyPricers.cdc +48 -0
- package/contracts/flowty-drops/initializers/ContractBorrower.cdc +14 -0
- package/contracts/flowty-drops/initializers/ContractInitializer.cdc +7 -0
- package/contracts/flowty-drops/initializers/OpenEditionInitializer.cdc +57 -0
- package/contracts/flowty-drops/nft/BaseCollection.cdc +97 -0
- package/contracts/flowty-drops/nft/BaseNFT.cdc +107 -0
- package/contracts/flowty-drops/nft/ContractFactory.cdc +13 -0
- package/contracts/flowty-drops/nft/ContractFactoryTemplate.cdc +48 -0
- package/contracts/flowty-drops/nft/NFTMetadata.cdc +140 -0
- package/contracts/flowty-drops/nft/OpenEditionNFT.cdc +42 -0
- package/contracts/flowty-drops/nft/OpenEditionTemplate.cdc +54 -0
- package/contracts/flowty-drops/nft/UniversalCollection.cdc +29 -0
- package/contracts/fungible-token-router/FungibleTokenRouter.cdc +103 -0
- package/contracts/hybrid-custody/CapabilityDelegator.cdc +28 -26
- package/contracts/hybrid-custody/CapabilityFactory.cdc +20 -18
- package/contracts/hybrid-custody/CapabilityFilter.cdc +41 -24
- package/contracts/hybrid-custody/HybridCustody.cdc +303 -242
- package/contracts/hybrid-custody/factories/FTAllFactory.cdc +16 -4
- package/contracts/hybrid-custody/factories/FTBalanceFactory.cdc +16 -4
- package/contracts/hybrid-custody/factories/FTProviderFactory.cdc +17 -5
- package/contracts/hybrid-custody/factories/FTReceiverBalanceFactory.cdc +16 -4
- package/contracts/hybrid-custody/factories/FTReceiverFactory.cdc +16 -4
- package/contracts/hybrid-custody/factories/FTVaultFactory.cdc +46 -0
- package/contracts/hybrid-custody/factories/NFTCollectionFactory.cdc +45 -0
- package/contracts/hybrid-custody/factories/NFTCollectionPublicFactory.cdc +16 -4
- package/contracts/hybrid-custody/factories/NFTProviderAndCollectionFactory.cdc +22 -0
- package/contracts/hybrid-custody/factories/NFTProviderFactory.cdc +16 -4
- package/contracts/lost-and-found/LostAndFound.cdc +21 -17
- package/contracts/tokens/USDCFlow.cdc +232 -0
- package/flow.json +278 -7
- package/package.json +1 -1
- 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
|
+
}
|