|
| 1 | +import "FungibleToken" |
| 2 | +import "FlowToken" |
| 3 | +import "EVM" |
| 4 | +import "FlowEVMBridge" |
| 5 | +import "FlowEVMBridgeConfig" |
| 6 | +import "FlowEVMBridgeUtils" |
| 7 | +import "ScopedFTProviders" |
| 8 | + |
| 9 | +import "DeFiActions" |
| 10 | +import "DeFiActionsUtils" |
| 11 | + |
| 12 | +/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! |
| 13 | +/// THIS CONTRACT IS IN BETA AND IS NOT FINALIZED - INTERFACES MAY CHANGE AND/OR PENDING CHANGES MAY REQUIRE REDEPLOYMENT |
| 14 | +/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! |
| 15 | +/// |
| 16 | +/// CrossVMConnectors |
| 17 | +/// |
| 18 | +/// This contract defines DeFi Actions Source connector implementations for unified cross-VM balance operations. These |
| 19 | +/// connectors can be used alone or in conjunction with other DeFi Actions connectors to create complex DeFi workflows |
| 20 | +/// that span both Cadence vaults and EVM Cadence Owned Accounts (COA). |
| 21 | +/// |
| 22 | +access(all) contract CrossVMConnectors { |
| 23 | + |
| 24 | + /// UnifiedBalanceSource |
| 25 | + /// |
| 26 | + /// A DeFiActions.Source connector that provides unified balance sourcing across Cadence and EVM. |
| 27 | + /// |
| 28 | + /// Withdrawal Priority: |
| 29 | + /// 1. Cadence vault balance (no fees) |
| 30 | + /// 2. COA native FLOW balance - for FlowToken only (no bridge fees) |
| 31 | + /// 3. COA ERC-20 balance via bridge (incurs bridge fees) |
| 32 | + /// |
| 33 | + /// This ordering minimizes bridge fees by preferring Cadence and native FLOW withdrawals. |
| 34 | + /// |
| 35 | + /// Usage: |
| 36 | + /// ```cadence |
| 37 | + /// let source = CrossVMConnectors.UnifiedBalanceSource( |
| 38 | + /// vaultType: Type<@FlowToken.Vault>(), |
| 39 | + /// cadenceVault: vaultCap, |
| 40 | + /// coa: coaCap, |
| 41 | + /// feeProvider: feeProviderCap, |
| 42 | + /// availableCadenceBalance: signer.availableBalance, |
| 43 | + /// uniqueID: DeFiActions.createUniqueIdentifier() |
| 44 | + /// ) |
| 45 | + /// let vault <- source.withdrawAvailable(maxAmount: 100.0) |
| 46 | + /// ``` |
| 47 | + /// |
| 48 | + access(all) struct UnifiedBalanceSource: DeFiActions.Source { |
| 49 | + /// The FungibleToken vault type this source provides (e.g., Type<@FlowToken.Vault>()) |
| 50 | + access(all) let vaultType: Type |
| 51 | + /// Whether this source handles FlowToken (enables native FLOW optimization) |
| 52 | + access(all) let isFlowToken: Bool |
| 53 | + /// The EVM contract address of the bridged token |
| 54 | + access(all) let evmAddress: EVM.EVMAddress |
| 55 | + /// Available Cadence balance at initialization. For FlowToken, pass signer.availableBalance to account for |
| 56 | + /// storage reservation. For other tokens, pass vault.balance. |
| 57 | + access(all) let availableCadenceBalance: UFix64 |
| 58 | + /// Capability to withdraw from the Cadence vault |
| 59 | + access(self) let cadenceVault: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}> |
| 60 | + /// Capability to the COA for native withdrawals and bridging |
| 61 | + access(self) let coa: Capability<auth(EVM.Withdraw, EVM.Bridge) &EVM.CadenceOwnedAccount> |
| 62 | + /// Capability to provide FLOW for bridge fees |
| 63 | + access(self) let feeProvider: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}> |
| 64 | + /// Optional identifier for DeFiActions tracing |
| 65 | + access(contract) var uniqueID: DeFiActions.UniqueIdentifier? |
| 66 | + |
| 67 | + /// Creates a new UnifiedBalanceSource |
| 68 | + /// |
| 69 | + /// @param vaultType: The FungibleToken vault type to withdraw |
| 70 | + /// @param cadenceVault: Capability to the user's Cadence vault (must be valid) |
| 71 | + /// @param coa: Capability to the user's COA (must be valid) |
| 72 | + /// @param feeProvider: Capability for bridge fee payment (must be valid) |
| 73 | + /// @param availableCadenceBalance: Pre-computed available balance from Cadence. For FlowToken, use |
| 74 | + /// signer.availableBalance to account for storage reservation. For other tokens, use vault.balance. |
| 75 | + /// @param uniqueID: Optional identifier for Flow Actions tracing |
| 76 | + /// |
| 77 | + init( |
| 78 | + vaultType: Type, |
| 79 | + cadenceVault: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>, |
| 80 | + coa: Capability<auth(EVM.Withdraw, EVM.Bridge) &EVM.CadenceOwnedAccount>, |
| 81 | + feeProvider: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>, |
| 82 | + availableCadenceBalance: UFix64, |
| 83 | + uniqueID: DeFiActions.UniqueIdentifier? |
| 84 | + ) { |
| 85 | + pre { |
| 86 | + cadenceVault.check(): |
| 87 | + "Provided invalid Cadence vault Capability" |
| 88 | + coa.check(): |
| 89 | + "Provided invalid COA Capability" |
| 90 | + feeProvider.check(): |
| 91 | + "Provided invalid fee provider Capability" |
| 92 | + DeFiActionsUtils.definingContractIsFungibleToken(vaultType): |
| 93 | + "The contract defining Vault \(vaultType.identifier) does not conform to FungibleToken contract interface" |
| 94 | + } |
| 95 | + let evmAddr = FlowEVMBridge.getAssociatedEVMAddress(with: vaultType) |
| 96 | + ?? panic("Token type \(vaultType.identifier) is not bridgeable - ensure the token is onboarded to the VM bridge") |
| 97 | + |
| 98 | + self.vaultType = vaultType |
| 99 | + self.isFlowToken = vaultType == Type<@FlowToken.Vault>() |
| 100 | + self.evmAddress = evmAddr |
| 101 | + self.availableCadenceBalance = availableCadenceBalance |
| 102 | + self.cadenceVault = cadenceVault |
| 103 | + self.coa = coa |
| 104 | + self.feeProvider = feeProvider |
| 105 | + self.uniqueID = uniqueID |
| 106 | + } |
| 107 | + |
| 108 | + /// Returns a ComponentInfo struct containing information about this UnifiedBalanceSource and its inner DFA |
| 109 | + /// components |
| 110 | + /// |
| 111 | + /// @return a ComponentInfo struct containing information about this component and a list of ComponentInfo for |
| 112 | + /// each inner component in the stack. |
| 113 | + /// |
| 114 | + access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { |
| 115 | + return DeFiActions.ComponentInfo( |
| 116 | + type: self.getType(), |
| 117 | + id: self.id(), |
| 118 | + innerComponents: [] |
| 119 | + ) |
| 120 | + } |
| 121 | + |
| 122 | + /// Returns the Vault type provided by this Source |
| 123 | + /// |
| 124 | + /// @return the type of the Vault this Source provides |
| 125 | + /// |
| 126 | + access(all) view fun getSourceType(): Type { |
| 127 | + return self.vaultType |
| 128 | + } |
| 129 | + |
| 130 | + /// Returns a copy of the struct's UniqueIdentifier, used in extending a stack to identify another connector in |
| 131 | + /// a DeFiActions stack. See DeFiActions.align() for more information. |
| 132 | + /// |
| 133 | + /// @return a copy of the struct's UniqueIdentifier |
| 134 | + /// |
| 135 | + access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? { |
| 136 | + return self.uniqueID |
| 137 | + } |
| 138 | + |
| 139 | + /// Sets the UniqueIdentifier of this component to the provided UniqueIdentifier, used in extending a stack to |
| 140 | + /// identify another connector in a DeFiActions stack. See DeFiActions.align() for more information. |
| 141 | + /// |
| 142 | + /// @param id: the UniqueIdentifier to set for this component |
| 143 | + /// |
| 144 | + access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { |
| 145 | + self.uniqueID = id |
| 146 | + } |
| 147 | + |
| 148 | + /// Returns an estimate of how much of the associated Vault can be provided by this Source |
| 149 | + /// |
| 150 | + /// @return the total available balance across Cadence vault and COA |
| 151 | + /// |
| 152 | + access(all) fun minimumAvailable(): UFix64 { |
| 153 | + return self._getCadenceBalance() + self._getCOABalance() |
| 154 | + } |
| 155 | + |
| 156 | + /// Withdraws the lesser of maxAmount or minimumAvailable(). If none is available, an empty Vault is returned. |
| 157 | + /// Withdrawal priority: Cadence vault → COA native FLOW (for FlowToken) → COA ERC-20 via bridge. |
| 158 | + /// |
| 159 | + /// @param maxAmount: the maximum amount to withdraw |
| 160 | + /// |
| 161 | + /// @return a Vault containing the withdrawn funds (may be less than maxAmount if insufficient balance) |
| 162 | + /// |
| 163 | + access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { |
| 164 | + let available = self.minimumAvailable() |
| 165 | + if available == 0.0 || maxAmount == 0.0 { |
| 166 | + return <-DeFiActionsUtils.getEmptyVault(self.vaultType) |
| 167 | + } |
| 168 | + |
| 169 | + let withdrawAmount = available <= maxAmount ? available : maxAmount |
| 170 | + let cadenceBalance = self._getCadenceBalance() |
| 171 | + |
| 172 | + // Calculate amounts from each source |
| 173 | + let amountFromCadence = cadenceBalance < withdrawAmount ? cadenceBalance : withdrawAmount |
| 174 | + var amountFromCOA = withdrawAmount - amountFromCadence |
| 175 | + |
| 176 | + // Withdraw from Cadence vault |
| 177 | + let vault = self.cadenceVault.borrow()! |
| 178 | + let result <- vault.withdraw(amount: amountFromCadence) |
| 179 | + |
| 180 | + // Bridge from COA if Cadence balance was insufficient |
| 181 | + if amountFromCOA > 0.0 { |
| 182 | + let coaRef = self.coa.borrow()! |
| 183 | + var remaining = amountFromCOA |
| 184 | + |
| 185 | + // For FlowToken: withdraw native FLOW first (no bridge fees) |
| 186 | + if self.isFlowToken { |
| 187 | + let nativeFlowBalance = coaRef.balance().inFLOW() |
| 188 | + if nativeFlowBalance > 0.0 { |
| 189 | + let nativeWithdraw = nativeFlowBalance < remaining ? nativeFlowBalance : remaining |
| 190 | + let withdrawBal = EVM.Balance(attoflow: 0) |
| 191 | + withdrawBal.setFLOW(flow: nativeWithdraw) |
| 192 | + result.deposit(from: <-coaRef.withdraw(balance: withdrawBal)) |
| 193 | + remaining = remaining - nativeWithdraw |
| 194 | + } |
| 195 | + } |
| 196 | + |
| 197 | + // Bridge ERC-20 if still needed |
| 198 | + if remaining > 0.0 { |
| 199 | + let scopedProvider <- ScopedFTProviders.createScopedFTProvider( |
| 200 | + provider: self.feeProvider, |
| 201 | + filters: [ScopedFTProviders.AllowanceFilter(FlowEVMBridgeUtils.calculateBridgeFee(bytes: 400_000))], |
| 202 | + expiration: getCurrentBlock().timestamp + 1.0 |
| 203 | + ) |
| 204 | + |
| 205 | + let bridged <- coaRef.withdrawTokens( |
| 206 | + type: self.vaultType, |
| 207 | + amount: FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(remaining, erc20Address: self.evmAddress), |
| 208 | + feeProvider: &scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} |
| 209 | + ) |
| 210 | + result.deposit(from: <-bridged) |
| 211 | + destroy scopedProvider |
| 212 | + } |
| 213 | + } |
| 214 | + |
| 215 | + return <-result |
| 216 | + } |
| 217 | + |
| 218 | + /// Returns the available Cadence balance (passed at initialization) |
| 219 | + access(self) fun _getCadenceBalance(): UFix64 { |
| 220 | + return self.availableCadenceBalance |
| 221 | + } |
| 222 | + |
| 223 | + /// Returns the total COA balance (native FLOW for FlowToken + ERC-20) |
| 224 | + access(self) fun _getCOABalance(): UFix64 { |
| 225 | + if let coaRef = self.coa.borrow() { |
| 226 | + var balance: UFix64 = 0.0 |
| 227 | + |
| 228 | + // Add native FLOW balance for FlowToken |
| 229 | + if self.isFlowToken { |
| 230 | + balance = balance + coaRef.balance().inFLOW() |
| 231 | + } |
| 232 | + |
| 233 | + // Add ERC-20 balance |
| 234 | + let erc20Balance = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount( |
| 235 | + FlowEVMBridgeUtils.balanceOf(owner: coaRef.address(), evmContractAddress: self.evmAddress), |
| 236 | + erc20Address: self.evmAddress |
| 237 | + ) |
| 238 | + balance = balance + erc20Balance |
| 239 | + |
| 240 | + return balance |
| 241 | + } |
| 242 | + return 0.0 |
| 243 | + } |
| 244 | + } |
| 245 | +} |
0 commit comments