Smart Contract
FlowEVMBridgeHandlers
A.1e4aa0b87d10b141.FlowEVMBridgeHandlers
1import Burner from 0xf233dcee88fe0abe
2import FungibleToken from 0xf233dcee88fe0abe
3import NonFungibleToken from 0x1d7e57aa55817448
4import FlowToken from 0x1654653399040a61
5
6import EVM from 0xe467b9dd11fa00df
7
8import FlowEVMBridgeHandlerInterfaces from 0x1e4aa0b87d10b141
9import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141
10import FlowEVMBridgeUtils from 0x1e4aa0b87d10b141
11
12/// FlowEVMBridgeHandlers
13///
14/// This contract is responsible for defining and configuring bridge handlers for special cased assets.
15///
16access(all) contract FlowEVMBridgeHandlers {
17
18 /**********************
19 Contract Fields
20 ***********************/
21
22 /// The storage path for the HandlerConfigurator resource
23 access(all) let ConfiguratorStoragePath: StoragePath
24
25 /****************
26 Constructs
27 *****************/
28
29 /// Handler for bridging Cadence native fungible tokens to EVM. In the event a Cadence project migrates native
30 /// support to EVM, this Hander can be configured to facilitate bridging the Cadence tokens to EVM. This Handler
31 /// then effectively allows the bridge to treat such tokens as bridge-defined on the Cadence side and EVM-native on
32 /// the EVM side minting/burning in Cadence and escrowing in EVM.
33 /// In order for this to occur, neither the Cadence token nor the EVM contract can be onboarded to the bridge - in
34 /// essence, neither side of the asset can be onboarded to the bridge.
35 /// The Handler must be configured in the bridge via the HandlerConfigurator. Once added, the bridge will filter
36 /// requests to bridge the token Vault to EVM through this Handler which cannot be enabled until a target EVM
37 /// address is set. Once the corresponding EVM contract address is known, it can be set and the Handler. It's also
38 /// suggested that the Handler only be enabled once sufficient liquidity has been arranged in bridge escrow on the
39 /// EVM side.
40 ///
41 access(all) resource CadenceNativeTokenHandler : FlowEVMBridgeHandlerInterfaces.TokenHandler {
42 /// Flag determining if request handling is enabled
43 access(self) var enabled: Bool
44 /// The Cadence Type this handler fulfills requests for
45 access(self) var targetType: Type
46 /// The EVM contract address this handler fulfills requests for. This field is optional in the event the EVM
47 /// contract address is not yet known but the Cadence type must still be filtered via Handler to prevent the
48 /// type from being onboarded otherwise.
49 access(self) var targetEVMAddress: EVM.EVMAddress?
50 /// The expected minter type for minting tokens on fulfillment
51 access(self) let expectedMinterType: Type
52 /// The Minter enabling minting of Cadence tokens on fulfillment from EVM
53 access(self) var minter: @{FlowEVMBridgeHandlerInterfaces.TokenMinter}?
54
55 init(targetType: Type, targetEVMAddress: EVM.EVMAddress?, expectedMinterType: Type) {
56 pre {
57 expectedMinterType.isSubtype(of: Type<@{FlowEVMBridgeHandlerInterfaces.TokenMinter}>()):
58 "Invalid minter type"
59 }
60 self.enabled = false
61 self.targetType = targetType
62 self.targetEVMAddress = targetEVMAddress
63 self.expectedMinterType = expectedMinterType
64 self.minter <- nil
65 }
66
67 /* --- HandlerInfo --- */
68
69 /// Returns the enabled status of the handler
70 access(all) view fun isEnabled(): Bool {
71 return self.enabled
72 }
73
74 /// Returns the type of the asset the handler is configured to handle
75 access(all) view fun getTargetType(): Type? {
76 return self.targetType
77 }
78
79 /// Returns the EVM contract address the handler is configured to handle
80 access(all) view fun getTargetEVMAddress(): EVM.EVMAddress? {
81 return self.targetEVMAddress
82 }
83
84 /// Returns the expected minter type for the handler
85 access(all) view fun getExpectedMinterType(): Type? {
86 return self.expectedMinterType
87 }
88
89 /* --- TokenHandler --- */
90
91 /// Fulfill a request to bridge tokens from Cadence to EVM, burning the provided Vault and transferring from
92 /// EVM escrow to the named recipient. Assumes any fees are handled by the caller within the bridge contracts
93 ///
94 /// @param tokens: The Vault containing the tokens to bridge
95 /// @param to: The EVM address to transfer the tokens to
96 ///
97 access(account)
98 fun fulfillTokensToEVM(
99 tokens: @{FungibleToken.Vault},
100 to: EVM.EVMAddress
101 ) {
102 let evmAddress = self.getTargetEVMAddress()!
103
104 // Get values from vault and burn
105 let amount = tokens.balance
106 let uintAmount = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(amount, erc20Address: evmAddress)
107
108 assert(uintAmount > UInt256(0), message: "Amount to bridge must be greater than 0")
109
110 Burner.burn(<-tokens)
111
112 FlowEVMBridgeUtils.mustTransferERC20(to: to, amount: uintAmount, erc20Address: evmAddress)
113 }
114
115 /// Fulfill a request to bridge tokens from EVM to Cadence, minting the provided amount of tokens in Cadence
116 /// and transferring from the named owner to bridge escrow in EVM.
117 ///
118 /// @param owner: The EVM address of the owner of the tokens. Should also be the caller executing the protected
119 /// transfer call.
120 /// @param type: The type of the asset being bridged
121 /// @param amount: The amount of tokens to bridge
122 ///
123 /// @return The minted Vault containing the the requested amount of Cadence tokens
124 ///
125 access(account)
126 fun fulfillTokensFromEVM(
127 owner: EVM.EVMAddress,
128 type: Type,
129 amount: UInt256,
130 protectedTransferCall: fun (): EVM.Result
131 ): @{FungibleToken.Vault} {
132 let evmAddress = self.getTargetEVMAddress()!
133
134 // Convert the amount to a UFix64
135 let ufixAmount = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(
136 amount,
137 erc20Address: evmAddress
138 )
139 assert(ufixAmount > 0.0, message: "Amount to bridge must be greater than 0")
140
141 FlowEVMBridgeUtils.mustEscrowERC20(
142 owner: owner,
143 amount: amount,
144 erc20Address: evmAddress,
145 protectedTransferCall: protectedTransferCall
146 )
147
148 // After state confirmation, mint the tokens and return
149 let minter = self.borrowMinter()
150 ?? panic("Cannot bridge - Minter not set in ".concat(self.getType().identifier))
151 let minted <- minter.mint(amount: ufixAmount)
152 return <-minted
153 }
154
155 /* --- Admin --- */
156
157 /// Sets the target type for the handler
158 access(FlowEVMBridgeHandlerInterfaces.Admin)
159 fun setTargetType(_ type: Type) {
160 self.targetType = type
161 }
162
163 /// Sets the target EVM address for the handler
164 access(FlowEVMBridgeHandlerInterfaces.Admin)
165 fun setTargetEVMAddress(_ address: EVM.EVMAddress) {
166 self.targetEVMAddress = address
167 }
168
169 /// Sets the target type for the handler
170 access(FlowEVMBridgeHandlerInterfaces.Admin)
171 fun setMinter(_ minter: @{FlowEVMBridgeHandlerInterfaces.TokenMinter}) {
172 pre {
173 self.minter == nil: "Minter has already been set in ".concat(self.getType().identifier)
174 }
175 self.minter <-! minter
176 }
177
178 /// Enables the handler for request handling.
179 access(FlowEVMBridgeHandlerInterfaces.Admin)
180 fun enableBridging() {
181 pre {
182 self.minter != nil: "Cannot enable ".concat(self.getType().identifier).concat(" without a minter")
183 }
184 self.enabled = true
185 }
186
187 /// Disables the handler for request handling.
188 access(FlowEVMBridgeHandlerInterfaces.Admin)
189 fun disableBridging() {
190 self.enabled = false
191 }
192
193 /* --- Internal --- */
194
195 /// Returns an entitled reference to the encapsulated minter resource
196 access(self)
197 view fun borrowMinter(): auth(FlowEVMBridgeHandlerInterfaces.Mint) &{FlowEVMBridgeHandlerInterfaces.TokenMinter}? {
198 return &self.minter
199 }
200 }
201
202 /// Facilitates moving Flow between Cadence and EVM as WFLOW. Since WFLOW is an artifact of the EVM ecosystem,
203 /// wrapping the native token as an ERC20, it does not have a place in Cadence's fungible token ecosystem.
204 /// Given the native interface on EVM.CadenceOwnedAccount and EVM.EVMAddress to move FLOW between Cadence and EVM,
205 /// this handler treats requests to bridge FLOW as WFLOW as a special case.
206 ///
207 access(all) resource WFLOWTokenHandler : FlowEVMBridgeHandlerInterfaces.TokenHandler {
208 /// Flag determining if request handling is enabled
209 access(self) var enabled: Bool
210 /// The Cadence Type this handler fulfills requests for
211 access(self) var targetType: Type
212 /// The EVM contract address this handler fulfills requests for
213 access(self) var targetEVMAddress: EVM.EVMAddress
214
215 init(wflowEVMAddress: EVM.EVMAddress) {
216 self.enabled = false
217 self.targetType = Type<@FlowToken.Vault>()
218 self.targetEVMAddress = wflowEVMAddress
219 }
220
221 /// Returns whether the Handler is enabled
222 access(all) view fun isEnabled(): Bool {
223 return self.enabled
224 }
225 /// Returns the Cadence type handled by the Handler, nil if not set
226 access(all) view fun getTargetType(): Type? {
227 return self.targetType
228 }
229 /// Returns the EVM address handled by the Handler, nil if not set
230 access(all) view fun getTargetEVMAddress(): EVM.EVMAddress? {
231 return self.targetEVMAddress
232 }
233 /// Returns nil as this handler simply unwraps WFLOW to FLOW
234 access(all) view fun getExpectedMinterType(): Type? {
235 return nil
236 }
237
238 /* --- TokenHandler --- */
239
240 /// Fulfill a request to bridge tokens from Cadence to EVM, burning the provided Vault and transferring from
241 /// EVM escrow to the named recipient. Assumes any fees are handled by the caller within the bridge contracts
242 ///
243 /// @param tokens: The Vault containing the tokens to bridge
244 /// @param to: The EVM address to transfer the tokens to
245 ///
246 access(account)
247 fun fulfillTokensToEVM(
248 tokens: @{FungibleToken.Vault},
249 to: EVM.EVMAddress
250 ) {
251 let flowVault <- tokens as! @FlowToken.Vault
252 let wflowAddress = self.getTargetEVMAddress()!
253
254 // Get balance from vault
255 let balance = flowVault.balance
256 let uintAmount = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(balance, erc20Address: wflowAddress)
257
258 // Deposit to bridge COA
259 let coa = FlowEVMBridgeUtils.borrowCOA()
260 coa.deposit(from: <-flowVault)
261
262 let preBalance = FlowEVMBridgeUtils.balanceOf(owner: coa.address(), evmContractAddress: wflowAddress)
263
264 // Wrap the deposited FLOW as WFLOW, giving the bridge COA the necessary WFLOW to transfer
265 let wrapResult = FlowEVMBridgeUtils.call(
266 signature: "deposit()",
267 targetEVMAddress: wflowAddress,
268 args: [],
269 gasLimit: FlowEVMBridgeConfig.gasLimit,
270 value: balance
271 )
272 assert(wrapResult.status == EVM.Status.successful, message: "Failed to wrap FLOW as WFLOW")
273
274 let postBalance = FlowEVMBridgeUtils.balanceOf(owner: coa.address(), evmContractAddress: wflowAddress)
275
276 // Cover underflow
277 assert(
278 postBalance > preBalance,
279 message: "Escrowed WFLOW balance did not increment after wrapping FLOW - pre: "
280 .concat(preBalance.toString()).concat(" | post: ").concat(postBalance.toString())
281 )
282 // Confirm bridge COA's WFLOW balance has incremented by the expected amount
283 assert(
284 postBalance - preBalance == uintAmount,
285 message: "Escrowed WFLOW balance after wrapping does not match requested amount - expected: "
286 .concat((preBalance + uintAmount).toString())
287 .concat(" | actual: ")
288 .concat((postBalance - preBalance).toString())
289 )
290
291 // Transfer WFLOW to recipient
292 FlowEVMBridgeUtils.mustTransferERC20(to: to, amount: uintAmount, erc20Address: wflowAddress)
293 }
294
295 /// Fulfill a request to bridge tokens from EVM to Cadence, minting the provided amount of tokens in Cadence
296 /// and transferring from the named owner to bridge escrow in EVM.
297 ///
298 /// @param owner: The EVM address of the owner of the tokens. Should also be the caller executing the protected
299 /// transfer call.
300 /// @param type: The type of the asset being bridged
301 /// @param amount: The amount of tokens to bridge
302 ///
303 /// @return The minted Vault containing the the requested amount of Cadence tokens
304 ///
305 access(account)
306 fun fulfillTokensFromEVM(
307 owner: EVM.EVMAddress,
308 type: Type,
309 amount: UInt256,
310 protectedTransferCall: fun (): EVM.Result
311 ): @{FungibleToken.Vault} {
312 let wflowAddress = self.getTargetEVMAddress()!
313
314 // Convert the amount to a UFix64
315 let ufixAmount = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(
316 amount,
317 erc20Address: wflowAddress
318 )
319 assert(
320 ufixAmount > 0.0,
321 message: "Requested UInt256 amount ".concat(amount.toString()).concat(" converted to 0.0 ")
322 .concat(" - try bridging a larger amount to avoid UFix64 precision loss during conversion")
323 )
324
325 // Transfers WFLOW to bridge COA as escrow
326 FlowEVMBridgeUtils.mustEscrowERC20(
327 owner: owner,
328 amount: amount,
329 erc20Address: wflowAddress,
330 protectedTransferCall: protectedTransferCall
331 )
332
333 // Get the bridge COA's FLOW balance before unwrapping WFLOW
334 let coa = FlowEVMBridgeUtils.borrowCOA()
335 let preBalance = coa.balance().attoflow
336
337 // Unwrap the transferred WFLOW to FLOW, giving the bridge COA the necessary FLOW to withdraw from EVM
338 let unwrapResult = FlowEVMBridgeUtils.call(
339 signature: "withdraw(uint256)",
340 targetEVMAddress: wflowAddress,
341 args: [amount],
342 gasLimit: FlowEVMBridgeConfig.gasLimit,
343 value: 0.0
344 )
345 assert(unwrapResult.status == EVM.Status.successful, message: "Failed to unwrap WFLOW as FLOW")
346
347 let postBalance = coa.balance().attoflow
348
349 // Cover underflow
350 assert(
351 postBalance > preBalance,
352 message: "Escrowed FLOW Balance did not increment after unwrapping WFLOW - pre: ".concat(preBalance.toString())
353 .concat(" | post: ").concat(postBalance.toString())
354 )
355 // Confirm bridge COA's FLOW balance has incremented by the expected amount
356 assert(
357 UInt256(postBalance - preBalance) == amount,
358 message: "Escrowed WFLOW balance after unwrapping does not match requested amount - expected: "
359 .concat((UInt256(preBalance) + amount).toString())
360 .concat(" | actual: ")
361 .concat((postBalance - preBalance).toString())
362 )
363
364 // Withdraw escrowed FLOW from bridge COA
365 let withdrawBalance = EVM.Balance(attoflow: UInt(amount))
366 assert(
367 UInt256(withdrawBalance.attoflow) == amount,
368 message: "Requested balance failed to convert to attoflow - expected: "
369 .concat(amount.toString())
370 .concat(" | actual: ")
371 .concat(withdrawBalance.attoflow.toString())
372 )
373 let flowVault <- coa.withdraw(balance: withdrawBalance)
374 assert(
375 flowVault.balance == ufixAmount,
376 message: "Resulting FLOW Vault balance does not match requested amount - expected: "
377 .concat(ufixAmount.toString())
378 .concat(" | actual: ")
379 .concat(flowVault.balance.toString())
380 )
381 return <-flowVault
382 }
383
384 /* --- HandlerAdmin --- */
385 // Conforms to HandlerAdmin for enableBridging, but most of the methods are unnecessary given the strict
386 // association between FLOW and WFLOW
387
388 /// Sets the target type for the handler
389 access(FlowEVMBridgeHandlerInterfaces.Admin)
390 fun setTargetType(_ type: Type) {
391 panic("WFLOWTokenHandler has targetType set to "
392 .concat(self.targetType.identifier).concat(" at initialization"))
393 }
394
395 /// Sets the target EVM address for the handler
396 access(FlowEVMBridgeHandlerInterfaces.Admin)
397 fun setTargetEVMAddress(_ address: EVM.EVMAddress) {
398 panic("WFLOWTokenHandler has EVMAddress set to "
399 .concat(self.targetEVMAddress.toString()).concat(" at initialization"))
400 }
401
402 /// Sets the target type for the handler
403 access(FlowEVMBridgeHandlerInterfaces.Admin)
404 fun setMinter(_ minter: @{FlowEVMBridgeHandlerInterfaces.TokenMinter}) {
405 panic("WFLOWTokenHandler does not utilize a minter")
406 }
407
408 /// Enables the handler for request handling. The
409 access(FlowEVMBridgeHandlerInterfaces.Admin)
410 fun enableBridging() {
411 self.enabled = true
412 }
413
414 /// Disables the handler for request handling.
415 access(FlowEVMBridgeHandlerInterfaces.Admin)
416 fun disableBridging() {
417 self.enabled = false
418 }
419 }
420
421 /// This resource enables the configuration of Handlers. These Handlers are stored in FlowEVMBridgeConfig from which
422 /// further setting and getting can be executed.
423 ///
424 access(all) resource HandlerConfigurator {
425 /// Creates a new Handler and adds it to the bridge configuration
426 ///
427 /// @param handlerType: The type of handler to create as defined in this contract
428 /// @param targetType: The type of the asset the handler will handle
429 /// @param targetEVMAddress: The EVM contract address the handler will handle, can be nil if still unknown
430 /// @param expectedMinterType: The Type of the expected minter to be set for the created TokenHandler
431 ///
432 access(FlowEVMBridgeHandlerInterfaces.Admin)
433 fun createTokenHandler(
434 handlerType: Type,
435 targetType: Type,
436 targetEVMAddress: EVM.EVMAddress?,
437 expectedMinterType: Type?
438 ) {
439 switch handlerType {
440 case Type<@CadenceNativeTokenHandler>():
441 assert(
442 expectedMinterType != nil,
443 message: "CadenceNativeTokenHandler requires an expected minter type but received nil"
444 )
445 let handler <-create CadenceNativeTokenHandler(
446 targetType: targetType,
447 targetEVMAddress: targetEVMAddress,
448 expectedMinterType: expectedMinterType!
449 )
450 FlowEVMBridgeConfig.addTokenHandler(<-handler)
451 case Type<@WFLOWTokenHandler>():
452 assert(
453 targetEVMAddress != nil,
454 message: "WFLOWTokenHandler requires a target EVM address but received nil"
455 )
456 let handler <-create WFLOWTokenHandler(wflowEVMAddress: targetEVMAddress!)
457 FlowEVMBridgeConfig.addTokenHandler(<-handler)
458 default:
459 panic("Invalid Handler type requested")
460 }
461 }
462 }
463
464 init() {
465 self.ConfiguratorStoragePath = /storage/BridgeHandlerConfigurator
466 self.account.storage.save(<-create HandlerConfigurator(), to: self.ConfiguratorStoragePath)
467 }
468}
469