Smart Contract
EVMTokenConnectors
A.1a771b21fcceadc2.EVMTokenConnectors
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}