Closed JeffreyDoyle closed 6 days ago
This is a great idea, I was long time defending this approach.
I few comments:
Multisign, as an example, makes it a bit more complicated than it is. I think we should open up with simple single-signer transactions and then build on that slowly. I think the best entry-level example would be sending someone NFT.
pre/post-conditions are nice to have, but I am not sure if they are enough. I am still in favor of simulating the transaction and auto-fill the post conditions.
Some multi-step transaction examples also can shine here, something like exchanging FLOW to USDC, with this USDC, buying a kitty NFT, send this NFT to a third party. Instead of focusing on multiple actors, I think we should focus on multiple steps.
This is a bit bonus point, but I believe also we should utilize Type
a lot more in templated transactions. ( can be in pre/post conditions or directly in the fields )
I also feel this ( the fields in the transaction ) will bloat a lot when we have multi-steps
Very excited about this! I’ve been thinking a lot about the problem of transactions assuming the user's storage.
The problem is that no single party knows how to create the transactions, and the user, wallet, and app have to coordinate:
The coordination is further complicated by the fact that the application could be malicious.
Re: viable alternatives, I’m wondering if the app actually needs the information prior to the transaction, not in the transaction. This is because only the app has the domain-specific UI which can provide the user a rich environment. I’m thinking of this like a permission request mechanism between the app and the wallet. For example, if as a user, I’m trying to build my fighting robot from NFT parts, I actually want to use the app UI, not my wallet UI, to put the robot together. The wallet doesn’t know anything about robots and how they go together, let alone how to display it, so the application needs to make a request to the wallet to get a list of all the user’s robot parts. It’s only after the user has built their robot (and possibly selected new parts from the marketplace to buy) that the app would submit a transaction.
Essentially, once you factor in the need for the user to make choices through the application's UI, then the user's wallet is the wrong entity to be filling in a template. The user is making those choices outside the wallet.
But then there’s a remaining question of how to ensure that the end-result transaction actually matches the choices that the user/wallet made, and isn’t deceptively trying to steal assets. “Have the user read the transaction and approve or disapprove” is the fallback because it’s the easiest, but it’s also the worst for user protection. I thought Dete’s question about whether the post statement has AuthAccount access was interesting. Could it be something scoped to this application instead, for POLA reasons? Maybe the transaction gets flagged as probably malicious if the transaction uses anything beyond what was already requested by the application?
Implemented support for nested pragma declarations in Cadence in https://github.com/onflow/cadence/pull/2169
My only concern is; there is need for cadence parser everywhere. I really like the pragmas as native language feature, though I am also almost sure people will use regex instead. So maybe we need very light cadence parser ( native js possibly )
I am thinking a bit different again ( in the context of flix and this FLIP, synergy between them )
I think we can define interactions ( like in flix ) in smaller pieces where we can assign them to roles. For me it is a bit hard to describe in words but I will try my best.
What if we have some units ( I will call them actions ) that make state modification, and those can be assigned to roles.
For example: ( in the NFT vs FT trade example in the FLIP )
Buyer:
Seller:
Here if we had : 4 flix action ( giveNFT, giveFT, getNFT, getFT ) and all would have it's own pre/post/prepare/execute statements.
We could have nice UX:
Buyer:
Seller:
As those will be some kind of standard actions, technically we can make them struct. Implementing some generic interface. Possibly running their transactions pre/post/prepare/execute separately. Wallets would just need to create structs, possibly we would not even need separate prepare statements.
Possibly we can have a generic transaction runner transaction. ( considering we can assume passing auth account to flix would be safe, as they will be pinned by hash )
PS: I am just thinking this as a subset of this FLIP, something that would be nice to support alongside
This is great. Looking forward to a cleaner separation of concerns between dapp and wallet enabled by the improvements discussed here.
I was wondering if we could also enable the wallet to provide functionality (functions) rather than variables only. Let's see how well I can explain:
Problem
I'm assuming a [secondary] goal of this FLIP is for the wallets to produce secure and correct messages like "This transaction will do X with your Y asset" for any supported transaction. Secure interaction is enabled by exposing limited variables from the user's account using directives in the transaction. The problem is those variables are manipulated outside of the wallet's generated code, so the wallet needs to rely on post
statements to make sure the exposed variables are not abused and the intended action is executed correctly.
Let's introduce an example: An NFT game has provided you with an asset of type T
that you can upgrade by calling upgrade()
. With the current approach, the developer can declare a variable of type @T
that is initialized by the Player
role. The only way for the wallet to make sure only upgrade
is called in a given transaction is to verify somehow in a post
statement by checking all the other stuff that can go wrong. This will introduce mandatory "verifiability" property to all mutations.
Solution
A basic idea to solve this is to upgrade role
blocks into interfaces so the wallet can produce wrappers as well. Interfaces in Cadence can dictate what variables or functions are required without providing a concrete implementation. In this approach, the new transaction format will enable prepare
to accept interfaces that are defined in the transaction CDC file. Wallets can provide concrete implementations for those interfaces in separate code blocks. Developers can also mock the interfaces if needed during testing.
For the previous example the wallet can provide the implementation for upgrade(T)
that will make sure no other functions are called on the exposed capability.
Sorry if this is too half-baked, wanted to know what you think. I'm sure there are blind spots that I might have missed here.
Maybe I just need more time to think about it, but I don't see how this can be that beneficial besides to a very small subset of power users. I imagine that 99% of users will store their assets in standard paths and use standard interactions, so my preference is to try to make those kind of interactions as simple as possible for wallets and users and try to move as much logic as possible into contracts, which can be audited better. I just looked at this for the first time today though, so I'll need some more time to ponder it.
Would it be possible to write out the basic use case for this? I think I'm still trying to grasp exactly what kind of things the wallet (and not the application) might be able to fill in. If I understand correctly, this FLIP is limited to what the wallet specifically can fill in, right?
During the meeting, it sounded like there were two kinds of "slots":
Even for the basic use case, isn't the user still probably interacting through the application UI, so shouldn't that be filling in the slots instead of the wallet? For example, if I buy an NFT in a marketplace, I usually click on the NFT in the application UI, not through my wallet.
I'm guessing one or more of my assumptions here are wrong, but I'm still trying to pin down which. :)
From my limited understanding @katelynsills in that above example what the wallet can fill out is
Thanks @bjartek!
It sounds like the wallet is limited to handling the "checkout" process then. Just like normal retail requires you to choose which credit card you want to use and what address to send it to, in this case it's which vault to use to pay and which collection to put the NFT in. This would mean that the application handles everything else, including the storefront and selection of what to purchase and for how much.
If this sounds right, I think it would help to identify the "checkout" use case as the primary use case for this FLIP, so that it's clear what we actually expect the wallet to be able to fill in.
Hi all, we have some updates to share!
In this FLIPs working group we have decided on the following:
We have also began to think about & discuss:
role seller {
let nft: @NonFungibleToken.NFT
prepare() {
let x: StoragePath = WALLET.STORAGEPATH(@NonFungibleToken.NFT) | "/storage/foo" // Fallback to /storage/foo if wallet does not support this feature.
...
}
}
role buyer {
let receiver: Capability<&{NFT.Receiver}>
let vault: @FlowToken.Vault
prepare() {
let x: StoragePath = WALLET.STORAGEPATH({ type: @NonFungibleToken.NFT }) | “/storage/foo” // Fallback to /storage/foo if wallet does not support this feature.
...
vault = WALLET.VAULT({ type: @FlowToken.Vault, amount: "20.0" }) | …
cadence code to assign vault... // Fallback to the cadence code provided if wallet does not support this feature.
}
}
-
If you are not included in this FLIPs working group, and would like to be included, please message me on Discord (JeffD#6865) and we will add you to any future invites!
Wanted to clarify one thing while this is being implemented: Was the intention for each role
block to have one signer, and that each signer be assigned to one role
? Or is it okay for the prepare
functions in each role
to have the full list of signers in each case?
Or is it okay for the
prepare
functions in eachrole
to have the full list of signers in each case?
Roles should isolate signers, it would be no sense to have full list of signers in each case.
Hello everyone - we have some new updates to share!
As discussed in the previous breakout session, we're proposing adding a new concept to this FLIP known as the transaction resolve
phase.
The transaction resolve phase would be responsible for dealing with the 'outputs' of a transaction. It functions much in a similar way as the prepare phase, which is responsible for providing the 'inputs' of a transaction.
The resolve phase would be a dedicated phase where resources can be stored after the execution phase, similar to how the prepare phase is where resources are retrieved before the execution phase.
The various phases of a transaction's execution would be: prepare -> pre -> execute -> resolve -> post
In this FLIP, wallets could produce the content of prepare and resolve phases of a transaction for their assigned role block. This enables transaction cadence developers to not have to concern themselves about how to retrieve and store resources, or otherwise engage with any wallet controlled accounts involved in the transaction. A transaction cadence developer would only need to be concerned with the roles of a transaction, what variables are involved in a transaction, and how they want to operate on those variables (the execution phase), and any pre/post statements they require.
💡 Key Idea: This pattern isolates the concern of the inputs/outputs of the transaction to the wallet, and the transaction logic to the cadence transaction developer.
A transaction with this new resolve phase in this FLIP might look something like:
transaction(nftId: UFix64, amount: UFix64) {
var myExampleVar: Int
role Buyer {
input var payment: @FlowVault
output var newNft: @NFT
prepare(a: AuthAccount) {
payment <- a.borrow<FlowVault>(/private/FlowVault)!.withdraw(amount)
myExampleVar = 1
}
post {
newNft.id == nftId: "NFT ID must be correct! 😤"
}
resolve(b: AuthAccount) {
b.borrow<NFTCollection>(/private/NFTCollection)!.deposit(<-newNft)
}
}
role Seller {
input var newNft: @NFT
output var payment: @FlowVault
prepare(c: AuthAccount) {
newNft <- c.borrow<NFTCollection>(/private/NFTCollection)!.withdraw(id: nftId)
}
post {
payment.balance == amount: "Payment must be correct! 💰"
}
resolve(d: AuthAccount) {
d.borrow<FlowVault>(/private/FlowVault)!.deposit(<-payment)
}
}
execute {
...do stuff...
}
}
Role blocks would include:
If you are not included in this FLIPs working group, and would like to be included, please message me on Discord (JeffD#6865) and we will add you to any future invites!
I think 99% of the transactions are single signer ( single Role, ignoring duc etc ), thats why I think this level of detail seems not enough for me.
I think instead of Roles, we can use on-chain constructed Actions ( like we had standard in metadata views ) an Action can have; order, inputs / outputs, pre/post and prepare/resolve phases. I will try to make an example poc contract for this till our next meeting.
Hey @JeffreyDoyle - is there any update or movement on this?
We discussed this FLIP in yesterdays working group call, see the notes in https://github.com/onflow/Flow-Working-Groups/blob/main/cadence_language_and_execution_working_group/meetings/2024-10-29.md#flips.
Given that this FLIP has not seen any progress for a year, and is not in a state where it can be implemented, this FLIP is rejected for now.
Please feel free to open a more concrete proposal again. The Cadence team is happy to collaborate on it.
I'm very excited by this proposal! I think this shows a path towards a much better way of letting apps and wallets "negotiate" on what responsibility each of them has to provide loads of flexibility to app developers, without compromising on security.
Some thoughts:
prepare()
statements update specific variables might be "breaking the wall" between core language vs templating considerations. I agree that good coding practice and FLIX might require this, but asking the language to enforce it is asking the language to enforce syntax on comments. We should either promote Roles to a language feature, or let the language ignore them. My instinct is the latter (which makes it so the templating rules can change without language changes), in which case the only thing the language would enforce is that all transaction variables are initialized exactly once between all prepare() blocks.