Skip to content

Commit 1665d24

Browse files
author
chumeston
committed
feat(contracts): add CrossVMConnectors for unified cross-VM balance sourcing
- Add CrossVMConnectors.cdc implementing DeFiActions.Source interface - Enables withdrawals from combined Cadence vault + EVM COA balance - Withdrawal priority: Cadence vault → COA native FLOW → COA ERC-20 via bridge - Add tests covering all withdrawal scenarios - Add test transaction for unified balance withdrawals
1 parent 1522d35 commit 1665d24

4 files changed

Lines changed: 591 additions & 0 deletions

File tree

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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

Comments
 (0)