-
Notifications
You must be signed in to change notification settings - Fork 3
feat(contracts): add CrossVMConnectors #86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat(contracts): add CrossVMConnectors #86
Conversation
…ourcing - 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
loic1
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work. left a couple comments. the main ones are around caps vs refs and fee provider handling, which feel like important design choices to get right for a standard. also a couple feature suggestions. Let me know what you think!
| /// storage reservation. For other tokens, pass vault.balance. | ||
| access(all) let availableCadenceBalance: UFix64 | ||
| /// Capability to withdraw from the Cadence vault | ||
| access(self) let cadenceVault: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the reasoning behind using entitled capabilities instead of entitled references for the Cadence vault and COA?
My concern is that capabilities can be stored, which makes this a persistent access delegation primitive rather than a transaction-scoped helper. For our use case, we only need tx-scoped access. References provide that with security by default since they can't accidentally create persistent withdrawal rights. Using capabilities also adds a bit of ceremony in transactions (issuing/managing caps) that references would avoid
That said, I can see persistent delegation being useful for other ecosystem use cases down the road. Maybe that could be a separate source type if/when needed? (not a blocker for now, tho)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question! Using entitled capabilities aligns with the broader defi actions connector architecture throughout the rest of the codebase. I was following along with what was already in place.
- VaultSink and Source use capabilities, not references.
- The actual usage in our withdraw via unified source has the capabilities being issued in the
prepareblock but theUnifiedBalanceSourceitself is never stored (used in tx and then discarded). - References being safer by default is valid, but, capabilities provide flexibility for future use cases like persistent delegation.
Would it be worth adding documentation or preconditions that explicitly state these connectors should not be stored? Or are we okay with maintaining consistency with the existing defi actions patterns as is?
My vote is to keep capabilities for consistency unless there's a strong reason to diverge from the connector arch.
| /// Capability to the COA for native withdrawals and bridging | ||
| access(self) let coa: Capability<auth(EVM.Withdraw, EVM.Bridge) &EVM.CadenceOwnedAccount> | ||
| /// Capability to provide FLOW for bridge fees | ||
| access(self) let feeProvider: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Storing an entitled capability for fee provider means the struct holds unlimited withdrawal rights. If we accept a pre-scoped reference instead (auth(FungibleToken.Withdraw) &ScopedFTProviders.ScopedFTProvider), we'd bound the fee budget at the source and limit the surface if something goes wrong. it'd also align with the tx-scoped reference pattern above. thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a good change that improves security for sure. The tx can calculate a conservative fee estimate upfront. Would you want this applied to both cadenceVault and coa as well or just feeProvider?
| /// connectors can be used alone or in conjunction with other DeFi Actions connectors to create complex DeFi workflows | ||
| /// that span both Cadence vaults and EVM Cadence Owned Accounts (COA). | ||
| /// | ||
| access(all) contract CrossVMConnectors { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be great to emit a withdrawal summary event showing where funds were sourced from. something like:
access(all) event Withdrawn(
vaultType: String,
cadenceAmount: UFix64,
coaNativeAmount: UFix64,
coaBridgedAmount: UFix64,
bridgeFee: UFix64,
uniqueID: UInt64?
)
This would give visibility into the actual source breakdown (Cadence vs COA native vs bridged) and fees paid. useful for debugging, auditing, etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion! This could help with observability, but I want to point out a couple things:
- The
Sourceinterface already emits a DefiActions.Withdrawn event automatically via the postcondition. Your suggestion would add a supplementary detailed event specific toUnifiedBalanceSource. - The actual bridge fee paid is determined internally by the bridge operation and we would only provide an allowance to the scoped provider. To emit the actual fee paid we'd need to track the
feeProviderbefore/after the bridge operation or emit the allowance amount (which I think would be the upper bound, not the actual fee?) - For precise fee tracking we would have something like
// Before bridging
let feeProviderBefore = self.feeProvider.borrow()?.balance ?? 0.0
// ... bridge operation ...
// After bridging
let feeProviderAfter = self.feeProvider.borrow()?.balance ?? 0.0
let actualFeePaid = feeProviderBefore - feeProviderAfter
and then we could emit an event like you suggested with the breakdown. Is this worth adding tracking overhead for better observability?
| /// connectors can be used alone or in conjunction with other DeFi Actions connectors to create complex DeFi workflows | ||
| /// that span both Cadence vaults and EVM Cadence Owned Accounts (COA). | ||
| /// | ||
| access(all) contract CrossVMConnectors { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would also be useful to expose a utility function for querying unified balance without needing to create a Source (e.g., for scripts):
access(all) fun getUnifiedBalance(account: &Account, vaultType: Type): UFix64
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea this would be useful for scripts and offchain queries. A few considerations on the implementation:
- Remember above we were discussing capabilities. Scripts need public access. Our existing
UnifiedBalanceSourcerelies on the entitled capabilities which aren't available in script context. A utility function would need to work with public references. - The current pattern we have that we migrated from PM requires the caller to pre-compute availableCadenceBalance. A utility function would have to handle both cases.
- The function would need to locate the users COA. We seem to be storing it consistently at
storage/evmbut should the utility function accept a custom path or assume we can rely on the standard one?
Worth implementing or would a simpler signature returning UFix64 total be sufficient?
No description provided.