Smart Contract

EVMTokenConnectors

A.cc15a0c9c656b648.EVMTokenConnectors

Valid From

123,613,920

Deployed

1w ago
Feb 18, 2026, 10:43:17 PM UTC

Dependents

0 imports
1import EVM from 0xe467b9dd11fa00df
2import Burner from 0xf233dcee88fe0abe
3import FlowToken from 0x1654653399040a61
4import FungibleToken from 0xf233dcee88fe0abe
5import FlowEVMBridgeUtils from 0x1e4aa0b87d10b141
6import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141
7import FlowEVMBridge from 0x1e4aa0b87d10b141
8import DeFiActions from 0x92195d814edf9cb0
9import DeFiActionsUtils from 0x92195d814edf9cb0
10
11/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12/// THIS CONTRACT IS IN BETA AND IS NOT FINALIZED - INTERFACES MAY CHANGE AND/OR PENDING CHANGES MAY REQUIRE REDEPLOYMENT
13/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
14///
15/// EVMTokenConnectors
16///
17/// A collection of DeFiActions connectors that deposit/withdraw tokens to/from EVM addresses.
18/// NOTE: These connectors move FLOW to/from the COA's WFLOW balance, not it's native FLOW balance. See
19///       EVMNativeFlowConnectors for connectors that move FLOW to/from the COA's native FLOW balance.
20///
21access(all) contract EVMTokenConnectors {
22
23    /// Sink
24    ///
25    /// A DeFiActions connector that deposits tokens to an EVM address's balance of ERC20 tokens
26    /// NOTE: If FLOW is deposited, it affects the COA's WFLOW balance not it's native FLOW balance.
27    ///
28    access(all) struct Sink : DeFiActions.Sink {
29        /// The maximum balance of the COA, checked before executing a deposit
30        access(self) let maximumBalance: UFix64
31        /// The type of the Vault to deposit
32        access(self) let depositVaultType: Type
33        /// The EVM address of the linked COA
34        access(self) let address: EVM.EVMAddress
35        /// The source of the VM bridge fees, providing FLOW
36        access(self) let feeSource: {DeFiActions.Sink, DeFiActions.Source}
37        /// The unique identifier of the sink
38        access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
39
40        init(
41            max: UFix64?,
42            depositVaultType: Type,
43            address: EVM.EVMAddress,
44            feeSource: {DeFiActions.Sink, DeFiActions.Source},
45            uniqueID: DeFiActions.UniqueIdentifier?
46        ) {
47            pre {
48                FlowEVMBridgeConfig.getEVMAddressAssociated(with: depositVaultType) != nil:
49                "Provided type \(depositVaultType.identifier) has not been onboarded to the VM bridge - "
50                    .concat("Ensure the type & ERC20 contracts are associated via the VM bridge")
51                feeSource.getSinkType() == Type<@FlowToken.Vault>() && feeSource.getSourceType() == Type<@FlowToken.Vault>():
52                "Provided feeSource must provide FlowToken.Vault but provides \(feeSource.getSourceType().identifier)"
53            }
54            self.maximumBalance = max ?? UFix64.max // assume no maximum if none provided
55            self.depositVaultType = depositVaultType
56            self.address = address
57            self.feeSource = feeSource
58            self.uniqueID = uniqueID
59        }
60
61        /// Returns a ComponentInfo struct containing information about this Sink and its inner DFA components
62        ///
63        /// @return a ComponentInfo struct containing information about this component and a list of ComponentInfo for
64        ///     each inner component in the stack.
65        ///
66        access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
67            return DeFiActions.ComponentInfo(
68                type: self.getType(),
69                id: self.id(),
70                innerComponents: [
71                    self.feeSource.getComponentInfo()
72                ]
73            )
74        }
75        /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to
76        /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information.
77        ///
78        /// @param id: the UniqueIdentifier to set for this component
79        ///
80        access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
81            self.uniqueID = id
82        }
83        /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
84        /// a DeFiActions stack. See DeFiActions.align() for more information.
85        ///
86        /// @return a copy of the struct's UniqueIdentifier
87        ///
88        access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
89            return self.uniqueID
90        }
91        /// Returns the type of the Vault this Sink accepts
92        ///
93        /// @return the type of the Vault this Sink accepts
94        ///
95        access(all) view fun getSinkType(): Type {
96            return self.depositVaultType
97        }
98        /// Returns the minimum capacity of this Sink
99        ///
100        /// @return the minimum capacity of this Sink
101        ///
102        access(all) fun minimumCapacity(): UFix64 {
103            let erc20Address = FlowEVMBridgeConfig.getEVMAddressAssociated(with: self.depositVaultType)!
104            let balance = FlowEVMBridgeUtils.balanceOf(owner: self.address, evmContractAddress: erc20Address)
105            let balanceInCadence = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(
106                balance,
107                erc20Address: erc20Address
108            )
109            return balanceInCadence < self.maximumBalance ? self.maximumBalance - balanceInCadence : 0.0
110        }
111        /// Deposits the given Vault into the EVM address's balance
112        ///
113        /// @param from: an authorized reference to the Vault from which to deposit funds
114        ///
115        access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
116            if from.getType() != self.depositVaultType {
117                return // unrelated vault type
118            }
119
120            // assess amount to deposit
121            let capacity = self.minimumCapacity()
122            let amount = from.balance > capacity ? capacity : from.balance
123            if amount == 0.0 {
124                return // can't deposit without sufficient capacity
125            }
126
127            // collect VM bridge fees
128            let feeAmount = FlowEVMBridgeConfig.baseFee * 2.0
129            if self.feeSource.minimumAvailable() < feeAmount {
130                return // early return here instead of reverting in bridge scope on insufficient fees
131            }
132            let fees <- self.feeSource.withdrawAvailable(maxAmount: feeAmount)
133
134            // deposit tokens and handle remaining fees
135            FlowEVMBridge.bridgeTokensToEVM(
136                vault: <-from.withdraw(amount: amount),
137                to: self.address,
138                feeProvider: &fees as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}
139            )
140            self._handleRemainingFees(<-fees)
141        }
142        /// Handles the remaining fees after a withdrawal
143        ///
144        /// @param feeVault: the Vault containing the remaining fees
145        ///
146        access(self) fun _handleRemainingFees(_ feeVault: @{FungibleToken.Vault}) {
147            if feeVault.balance > 0.0 {
148                self.feeSource.depositCapacity(from: &feeVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
149            }
150            Burner.burn(<-feeVault)
151        }
152    }
153
154    /// Source
155    ///
156    /// A DeFiActions connector that withdraws tokens from a CadenceOwnedAccount's balance of ERC20 tokens
157    /// NOTE: If FLOW is withdrawn, it affects the COA's WFLOW balance not it's native FLOW balance.
158    ///
159    access(all) struct Source : DeFiActions.Source {
160        /// The minimum balance of the COA, checked before executing a withdrawal
161        access(self) let minimumBalance: UFix64
162        /// The type of the Vault to withdraw
163        access(self) let withdrawVaultType: Type
164        /// The COA to withdraw tokens from
165        access(self) let coa: Capability<auth(EVM.Bridge) &EVM.CadenceOwnedAccount>
166        /// The EVM address of the linked COA
167        access(self) let address: EVM.EVMAddress
168        /// The source of the VM bridge fees, providing FLOW
169        access(self) let feeSource: {DeFiActions.Sink, DeFiActions.Source}
170        /// The unique identifier of the source
171        access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
172
173        init(
174            min: UFix64?,
175            withdrawVaultType: Type,
176            coa: Capability<auth(EVM.Bridge) &EVM.CadenceOwnedAccount>,
177            feeSource: {DeFiActions.Sink, DeFiActions.Source},
178            uniqueID: DeFiActions.UniqueIdentifier?
179        ) {
180            pre {
181                FlowEVMBridgeConfig.getEVMAddressAssociated(with: withdrawVaultType) != nil:
182                "Provided type \(withdrawVaultType.identifier) has not been onboarded to the VM bridge - "
183                    .concat("Ensure the type & ERC20 contracts are associated via the VM bridge")
184                DeFiActionsUtils.definingContractIsFungibleToken(withdrawVaultType):
185                "The contract defining Vault \(withdrawVaultType.identifier) does not conform to FungibleToken contract interface"
186                coa.check():
187                "Provided COA Capability is invalid - provided an invalid Capability<auth(EVM.Bridge) &EVM.CadenceOwnedAccount>"
188                feeSource.getSinkType() == Type<@FlowToken.Vault>() && feeSource.getSourceType() == Type<@FlowToken.Vault>():
189                "Provided feeSource must provide FlowToken.Vault but provides \(feeSource.getSourceType().identifier)"
190            }
191            self.minimumBalance = min ?? 0.0
192            self.withdrawVaultType = withdrawVaultType
193            self.coa = coa
194            self.feeSource = feeSource
195            self.address = coa.borrow()!.address()
196            self.uniqueID = uniqueID
197        }
198
199        /// Returns a ComponentInfo struct containing information about this Source and its inner DFA components
200        ///
201        /// @return a ComponentInfo struct containing information about this component and a list of ComponentInfo for
202        ///     each inner component in the stack.
203        ///
204        access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
205            return DeFiActions.ComponentInfo(
206                type: self.getType(),
207                id: self.id(),
208                innerComponents: []
209            )
210        }
211        /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to
212        /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information.
213        ///
214        /// @param id: the UniqueIdentifier to set for this component
215        ///
216        access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
217            self.uniqueID = id
218        }
219        /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
220        /// a DeFiActions stack. See DeFiActions.align() for more information.
221        ///
222        /// @return a copy of the struct's UniqueIdentifier
223        ///
224        access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
225            return self.uniqueID
226        }
227        /// Returns the type of the Vault this Source accepts
228        ///
229        /// @return the type of the Vault this Source accepts
230        ///
231        access(all) view fun getSourceType(): Type {
232            return self.withdrawVaultType
233        }
234        /// Returns the minimum available balance of this Source
235        ///
236        /// @return the minimum available balance of this Source
237        ///
238        access(all) fun minimumAvailable(): UFix64 {
239            if let coa = self.coa.borrow() {
240                let erc20Address = FlowEVMBridgeConfig.getEVMAddressAssociated(with: self.withdrawVaultType)!
241                let balance = FlowEVMBridgeUtils.balanceOf(owner: coa.address(), evmContractAddress: erc20Address)
242                let balanceInCadence = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(
243                    balance,
244                    erc20Address: erc20Address
245                )
246                return self.minimumBalance < balanceInCadence ? balanceInCadence - self.minimumBalance : 0.0
247            }
248            return 0.0
249        }
250        /// Withdraws the given amount of tokens from the CadenceOwnedAccount's balance of ERC20 tokens
251        ///
252        /// @param maxAmount: the maximum amount of tokens to withdraw
253        ///
254        /// @return a Vault containing the withdrawn tokens
255        ///
256        access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} {
257            let available = self.minimumAvailable()
258            let coa = self.coa.borrow()
259
260            // collect VM bridge fees
261            let feeAmount = FlowEVMBridgeConfig.baseFee
262            if available > 0.0 && coa != nil && self.feeSource.minimumAvailable() >= feeAmount {
263                // convert final cadence amount to erc20 amount
264                let ufixAmount = available > maxAmount ? maxAmount : available
265                let erc20Address = FlowEVMBridgeConfig.getEVMAddressAssociated(with: self.withdrawVaultType)!
266                let uintAmount = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(ufixAmount, erc20Address: erc20Address)
267
268                // withdraw tokens & handle fees
269                let fees <- self.feeSource.withdrawAvailable(maxAmount: feeAmount)
270                let tokens <- coa!.withdrawTokens(
271                    type: self.getSourceType(),
272                    amount: uintAmount,
273                    feeProvider: &fees as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}
274                )
275                self._handleRemainingFees(<-fees)
276
277                return <- tokens
278            }
279
280            return <- DeFiActionsUtils.getEmptyVault(self.getSourceType())
281        }
282        /// Handles the remaining fees after a withdrawal
283        ///
284        /// @param feeVault: the Vault containing the remaining fees
285        ///
286        access(self) fun _handleRemainingFees(_ feeVault: @{FungibleToken.Vault}) {
287            if feeVault.balance > 0.0 {
288                self.feeSource.depositCapacity(from: &feeVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
289            }
290            Burner.burn(<-feeVault)
291        }
292    }
293}
294