Smart Contract

EVMTokenConnectors

A.1a771b21fcceadc2.EVMTokenConnectors

Valid From

142,104,193

Deployed

2w ago
Feb 13, 2026, 05:25:40 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 0x6d888f175c158410
9import DeFiActionsUtils from 0x6d888f175c158410
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 feeAmount = FlowEVMBridgeConfig.baseFee
104            if self.feeSource.minimumAvailable() < feeAmount {
105                return 0.0
106            }
107            let erc20Address = FlowEVMBridgeConfig.getEVMAddressAssociated(with: self.depositVaultType)!
108            let balance = FlowEVMBridgeUtils.balanceOf(owner: self.address, evmContractAddress: erc20Address)
109            let balanceInCadence = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(
110                balance,
111                erc20Address: erc20Address
112            )
113            return balanceInCadence < self.maximumBalance ? self.maximumBalance - balanceInCadence : 0.0
114        }
115        /// Deposits the given Vault into the EVM address's balance
116        ///
117        /// @param from: an authorized reference to the Vault from which to deposit funds
118        ///
119        access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
120            if from.getType() != self.depositVaultType {
121                return // unrelated vault type
122            }
123
124            // assess amount to deposit
125            let capacity = self.minimumCapacity()
126            let amount = from.balance > capacity ? capacity : from.balance
127            if amount == 0.0 {
128                return // can't deposit without sufficient capacity
129            }
130
131            // collect VM bridge fees
132            let feeAmount = FlowEVMBridgeConfig.baseFee
133            if self.feeSource.minimumAvailable() < feeAmount {
134                return // early return here instead of reverting in bridge scope on insufficient fees
135            }
136            let fees <- self.feeSource.withdrawAvailable(maxAmount: feeAmount)
137
138            // deposit tokens and handle remaining fees
139            FlowEVMBridge.bridgeTokensToEVM(
140                vault: <-from.withdraw(amount: amount),
141                to: self.address,
142                feeProvider: &fees as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}
143            )
144            self._handleRemainingFees(<-fees)
145        }
146        /// Handles the remaining fees after a withdrawal
147        ///
148        /// @param feeVault: the Vault containing the remaining fees
149        ///
150        access(self) fun _handleRemainingFees(_ feeVault: @{FungibleToken.Vault}) {
151            if feeVault.balance > 0.0 {
152                self.feeSource.depositCapacity(from: &feeVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
153            }
154            Burner.burn(<-feeVault)
155        }
156    }
157
158    /// Source
159    ///
160    /// A DeFiActions connector that withdraws tokens from a CadenceOwnedAccount's balance of ERC20 tokens
161    /// NOTE: If FLOW is withdrawn, it affects the COA's WFLOW balance not it's native FLOW balance.
162    ///
163    access(all) struct Source : DeFiActions.Source {
164        /// The minimum balance of the COA, checked before executing a withdrawal
165        access(self) let minimumBalance: UFix64
166        /// The type of the Vault to withdraw
167        access(self) let withdrawVaultType: Type
168        /// The COA to withdraw tokens from
169        access(self) let coa: Capability<auth(EVM.Bridge) &EVM.CadenceOwnedAccount>
170        /// The EVM address of the linked COA
171        access(self) let address: EVM.EVMAddress
172        /// The source of the VM bridge fees, providing FLOW
173        access(self) let feeSource: {DeFiActions.Sink, DeFiActions.Source}
174        /// The unique identifier of the source
175        access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
176
177        init(
178            min: UFix64?,
179            withdrawVaultType: Type,
180            coa: Capability<auth(EVM.Bridge) &EVM.CadenceOwnedAccount>,
181            feeSource: {DeFiActions.Sink, DeFiActions.Source},
182            uniqueID: DeFiActions.UniqueIdentifier?
183        ) {
184            pre {
185                FlowEVMBridgeConfig.getEVMAddressAssociated(with: withdrawVaultType) != nil:
186                "Provided type \(withdrawVaultType.identifier) has not been onboarded to the VM bridge - "
187                    .concat("Ensure the type & ERC20 contracts are associated via the VM bridge")
188                DeFiActionsUtils.definingContractIsFungibleToken(withdrawVaultType):
189                "The contract defining Vault \(withdrawVaultType.identifier) does not conform to FungibleToken contract interface"
190                coa.check():
191                "Provided COA Capability is invalid - provided an invalid Capability<auth(EVM.Bridge) &EVM.CadenceOwnedAccount>"
192                feeSource.getSinkType() == Type<@FlowToken.Vault>() && feeSource.getSourceType() == Type<@FlowToken.Vault>():
193                "Provided feeSource must provide FlowToken.Vault but provides \(feeSource.getSourceType().identifier)"
194            }
195            self.minimumBalance = min ?? 0.0
196            self.withdrawVaultType = withdrawVaultType
197            self.coa = coa
198            self.feeSource = feeSource
199            self.address = coa.borrow()!.address()
200            self.uniqueID = uniqueID
201        }
202
203        /// Returns a ComponentInfo struct containing information about this Source and its inner DFA components
204        ///
205        /// @return a ComponentInfo struct containing information about this component and a list of ComponentInfo for
206        ///     each inner component in the stack.
207        ///
208        access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
209            return DeFiActions.ComponentInfo(
210                type: self.getType(),
211                id: self.id(),
212                innerComponents: [
213                    self.feeSource.getComponentInfo()
214                ]
215            )
216        }
217        /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to
218        /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information.
219        ///
220        /// @param id: the UniqueIdentifier to set for this component
221        ///
222        access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
223            self.uniqueID = id
224        }
225        /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in
226        /// a DeFiActions stack. See DeFiActions.align() for more information.
227        ///
228        /// @return a copy of the struct's UniqueIdentifier
229        ///
230        access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
231            return self.uniqueID
232        }
233        /// Returns the type of the Vault this Source accepts
234        ///
235        /// @return the type of the Vault this Source accepts
236        ///
237        access(all) view fun getSourceType(): Type {
238            return self.withdrawVaultType
239        }
240        /// Returns the minimum available balance of this Source
241        ///
242        /// @return the minimum available balance of this Source
243        ///
244        access(all) fun minimumAvailable(): UFix64 {
245            if let coa = self.coa.borrow() {
246                let erc20Address = FlowEVMBridgeConfig.getEVMAddressAssociated(with: self.withdrawVaultType)!
247                let balance = FlowEVMBridgeUtils.balanceOf(owner: coa.address(), evmContractAddress: erc20Address)
248                let balanceInCadence = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(
249                    balance,
250                    erc20Address: erc20Address
251                )
252                return self.minimumBalance < balanceInCadence ? balanceInCadence - self.minimumBalance : 0.0
253            }
254            return 0.0
255        }
256        /// Withdraws the given amount of tokens from the CadenceOwnedAccount's balance of ERC20 tokens
257        ///
258        /// @param maxAmount: the maximum amount of tokens to withdraw
259        ///
260        /// @return a Vault containing the withdrawn tokens
261        ///
262        access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} {
263            let available = self.minimumAvailable()
264            let coa = self.coa.borrow()
265
266            // collect VM bridge fees
267            let feeAmount = FlowEVMBridgeConfig.baseFee
268            if available > 0.0 && coa != nil && self.feeSource.minimumAvailable() >= feeAmount {
269                // convert final cadence amount to erc20 amount
270                let ufixAmount = available > maxAmount ? maxAmount : available
271                let erc20Address = FlowEVMBridgeConfig.getEVMAddressAssociated(with: self.withdrawVaultType)!
272                let uintAmount = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(ufixAmount, erc20Address: erc20Address)
273
274                // withdraw tokens & handle fees
275                let fees <- self.feeSource.withdrawAvailable(maxAmount: feeAmount)
276                let tokens <- coa!.withdrawTokens(
277                    type: self.getSourceType(),
278                    amount: uintAmount,
279                    feeProvider: &fees as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}
280                )
281                self._handleRemainingFees(<-fees)
282
283                return <- tokens
284            }
285
286            return <- DeFiActionsUtils.getEmptyVault(self.getSourceType())
287        }
288        /// Handles the remaining fees after a withdrawal
289        ///
290        /// @param feeVault: the Vault containing the remaining fees
291        ///
292        access(self) fun _handleRemainingFees(_ feeVault: @{FungibleToken.Vault}) {
293            if feeVault.balance > 0.0 {
294                self.feeSource.depositCapacity(from: &feeVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
295            }
296            // feeSource should not enforce a deposit capacity limit, as it is only a vault backing a sink.
297            // Assert here to catch unexpected partial deposits.
298            assert(
299                feeVault.balance == 0.0,
300                message: "Fee sink failed to accept full balance; feeVault still contains funds"
301            )
302            Burner.burn(<-feeVault)
303        }
304    }
305}