Smart Contract

FlowEVMBridgeHandlers

A.1e4aa0b87d10b141.FlowEVMBridgeHandlers

Valid From

85,982,158

Deployed

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

Dependents

2 imports
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