-
Notifications
You must be signed in to change notification settings - Fork 23
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
FLIP 262: Require matching access modifiers for interface implementation members #263
FLIP 262: Require matching access modifiers for interface implementation members #263
Conversation
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.
+1. I'm very much in favour of this proposal
My main concern is what this means for some things like HybridCustody which use access modifiers to allow hooks between different interfaces. For instance, when a parent account adds a child account capability, an If we cannot use this form of access control, we have no way to only permit this type of callback within the internal mechanisms of a contract while also allowing said resources/interfaces to be composable, we'd have to instead use concrete types which means we're stuck on a specific implementation going forward some example definitions:
and a sample use: Additionally, this would mean that the Burner contract defined to allow pre-burn methods to be defined (when sent to the Burner for destruction) would not be safe because anyone would be able to call the burn method and then store their resource again |
Yeah, after thinking about it some more, I don't know if I am in favor of this any more. I think if this would have come up a while ago, I would be in favor because we would have had plenty of time to figure out how to work around the restrictions, but like Austin said, this does limit the functionality that is possible in some contracts and in the |
this change would break this FLIP i believe because it relies on Since this FLIP would already be a breaking change then an acceptable replacement could be making Unless there is a direct replacement for this functionality, I am against this change. |
@cody-evaluate The only reason we made |
couldnt
isnt there any security risks with that? i know |
I am against this change. Yes it might be confusing but that can be documented. On the orher hand it removes some patterns that are useful. |
I am against too, I think this limits the usefulness of interface default implementations. Also patterns like Burner or Callbacks are really hard to replicate. |
It isn't a problem. I mistakenly thought it was originally which is why I was recommending |
assuming
why would this not be possible?
isnt that the reason the migration guide has this disclaimer? with this change it would force developers to allow this to happen because the interface would dictate you would have to drop developers would still need to make sure |
Thanks for pointing that out. I'm going to update that right now. That isn't true. anyone who wants to modify the |
oh then yeah that makes sense, wasnt even aware of the if |
I am against it as well, It imposes a lot of limitations on the Interface. |
Chiming in here to say I'm in agreement with others. While I agree the implications of inheriting |
Thank you everyone for the great feedback, it's much appreciated!
I don't know much about the Hybrid Custody contract, so I'm trying to understand what is written in the contract, and the original intent. What do you mean with the above? Is it OK that code in another contract that contains an implementation of this interface can call the function? This would be the case if the implementation uses Is it OK that potentially any code can call this function? This would be the case if the implementation of the interface uses
Do you mean if the proposal would be accepted, which means Note that this already possible, in the current version of Cadence, even if this proposal does not get accepted: If that is a problem, then the proposal does not make the situation not worse. It is true that it does not make the use-case work, but at least it removes the potential footgun caused by misunderstanding what a non-public/entitled access modifier means in an interface. I'm fairly confident that we can still figure out a proper solution for contracts like |
To help clarify the current behaviour, here is an example that demonstrates the usage of non-public/entitled access modifiers ( Let's assume contract access(all)
contract C1 {
access(all)
struct interface SI {
access(contract)
fun contractTest()
access(account)
fun accountTest()
}
access(all)
fun test(_ si: {SI}) {
si.contractTest()
si.accountTest()
}
} Note how Let's also assume contract As required by the interface, both implementations define the two functions, While the first implementation import C1 from 0x1
access(all)
contract C2 {
access(all)
struct S1: C1.SI {
access(contract)
fun contractTest() {}
access(account)
fun accountTest() {}
}
access(all)
struct S2: C1.SI {
access(all)
fun contractTest() {}
access(all)
fun accountTest() {}
}
access(all)
fun test() {
S1().contractTest()
S1().accountTest()
S2().contractTest()
S2().accountTest()
}
} Note how To be clear, this is the current behaviour, not what is being proposed in this proposal, i.e. this code is valid in the current version of Cadence (v0.42). There isn't necessarily anything wrong here, and maybe the outcome of this proposal and discussion is just that this behaviour needs to be documented better. However, answering questions around this, it seemed to me that some developers assumed that the above code would be invalid, or for example that only If developers can make wrong assumptions about access control behaviour, that is potentially dangerous. Maybe I also just assumed misunderstanding, and this behaviour is actually understood well and totally acceptable. Please let me know if you have any questions or concerns! I'll likely add this to the FLIP itself. |
Technically yes, but then you can combat that by understanding what type/implementation of a resource is doing what, and decide to ignore them (or only look for those that you know). If you were to do entitlements, I could just mark owners as I see fit without going to the hassle of implementing things. Risk vector is the same, but much easier to do with entitlements The real reason we have these callbacks is for the happy path of child account/parent account interactions to automatically keep state in sync for you. Perhaps there is a way around that, but the main thing I am trying to demonstrate here is the value in allowing interfaces to communicate while not needing a concrete implementation. I really just don't see a way to do that with entitlements without opening up the access of a method significantly
At a high level, the Burner's job is to permit logic to be run before destruction of something. It might be true that a contract can call it themselves if it wants to, but at that point you are just harming yourself by calling it. It is generally my view that all code within a contract trusts itself. Maybe that gets a little stranger with things like Hybrid Custody which talk to interfaces versus concrete implementations, but that isn't very relevant for the Burner. If a contract wants to go around its own safeguards and do something it is of course welcome to do so, but that is the same as any other bug you might have in your smart contract. I would argue this proposal does make the situation worse because anyone would be able to call that burn method now, not just the contract itself. Let's say there is a token that uses the burn callback method to change its total supply. I could simply take my vault out of storage and call that callback method as a holder of the token over and over to get whatever effect I want. That's quite different than the contract itself acting outside the boundaries of the typical burn lifecycle At the end of the day, the argument I have here is not so much tied to individual implementations but to get across what they are making use of to achieve their objectives. Restricting the ability to call a method to only be from the interface definition and the actual implementation is very powerful. Entitlements just do not have a way to express that unfortunately |
Has it been considered that an implementing contract cannot change the modifier if functions in an interface? Not sure what consequences that would bring though. |
Burner is not the issue here, it can be solved ( as it is passing resource ) It can delegate burning part to the contract. There are many options, can pass resource, can pass resource in envelope resource etc. Callbacks are a bit tricky, only workaround is passing envelope ( a resource ) and to be honest it is a bit ugly. Create resource, send it to callback as parameter, and callback checks resource , if it is valid then destroys voucher resource and continues as valid callback. for sure this looks like anti-pattern, but also this change can be helpful in the future for breaking interface-contract 100% trust relationship, so I am a bit in the middle. |
Do you mean the second part of the proposal, https://github.com/onflow/flips/pull/263/files#diff-7886c5900aee294558f3b08638f4322958c013fbe9a8cb429f8d8827f29752d6R51 ? Yeah, we could definitely also consider a subset of the proposal, i.e. remove the first part of the proposal, forbidding the use of for non-public/entitled access modifiers, and only keep the second part of the proposal, requiring the access modifier in an implementation to be the same as in the interface. 👍 |
cadence/20240415-remove-non-public-entitled-interface-members.md
Outdated
Show resolved
Hide resolved
Here's an attempt for me to frame the problem in a way that makes it easier for me to think about. Perhaps it'll be useful for others, and make discussion easier. There are two axes for access control in Cadence 1.0. The obvious one is Entitlements, and – by design – Entitlements are restrictions on access control that are managed by the owner of the object. The owner effectively has all Entitlements, and when they provide a reference or Capability to the object to someone else, they can provide a subset of those Entitlements to limit access. (Note that "owner" can mean "a user that has the object in their storage", but also "code that has the object in an However, there is another axis of access control, which is less obvious, and that is These "implementor restrictions" exist to make implementation easier. They allow implementors to structure their protected code into a convenient number of functions, objects, or even contracts within a single account, to match the design they find most effective, while ensuring that even the owner of those objects (who have access to all Entitlements) don't mess with the fundamental behaviour of the object as designed. The problem that Bastian identified in this FLIP is that it's really not clear what functions, objects, and contracts we mean when we use one of these implementor restrictions on an interface. Is it the context where the interface was defined? Where the interface was implemented? Or both? And what if the implementor restrictions are expanded? Here's an example I found useful to think through the problem: Imagine a game with a "Player" resource type, and an "Equipment" interface. There will be lots of Equipment implementations, and I even want third-parties to be able to implement new Equipment types. Additionally, I want the Equipment to be able to update the Player object in limited ways (mediated by the When the user first equips their Player object with new Equipment, it's useful for the Equipment to react, possibly updating the Player object. So I want a function like this in the Equipment interface: access(???) fun attached(toPlayer: &{Player{EquipmentUpdates}}) An "Armor" Equipment, for example, could use that callback to increase the Player's defence stat. I want Entitlements don't save me; a user that owns a Player and one or more Equipments can call the So, let's imagine we only allow In the game example from above, I see two workarounds, neither of which is very satisfying:
I don't love these workarounds, but they seem sound to me (in the sense that they work, not that they reflect good design principles!). Leaving things as they stand also seems problematic. So I think we need something in the middle. My current thinking is as follows:
Obviously the question of what we should do depends a lot on implementation effort (both by the Cadence team, and any contract devs who have to react to those Cadence changes). |
Thanks for your feedback @dete!
This is already the current behaviour.
That is basically the second part of the proposal.
We could adjust the proposal to only forbid |
We had a short chat about this proposal in the Cadence 1.0 office hours today, and mainly discussed revising the proposal to just the second part, requiring access modifiers in implementations to match those of the interface. Sentiment was positive, so I'm going to make the appropriate changes to the title and contents of the FLIP and we can further discuss and maybe vote on the proposal in the working group call tomorrow. |
Please take another look @SupunS @austinkline @joshuahannan @bjartek @bluesign @cody-evaluate @btspoony @dete @sisyphusSmiling |
Now it is more reasonable and will not cause too much impact! |
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.
This sounds good to me
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.
Great conversation thread! I'm in agreement with the updated FLIP.
LGTM |
With the FLIP scope now changed to only be about enforcing access modifiers matching across interface and implementations, I am in favor of this as well |
In today's working group call we had another round of discussion about this FLIP. We discussed the two questions raised by @dete:
Unless there are any further concerns, the FLIP is planned to be accepted by Tuesday next week. |
it is at least as common as access(contract) in interfaces |
In the Cadence repo we have a tool to parse Cadence code. It supports parsing all contracts in a CSV file, and allows querying the AST with JQ. I did a quick analysis of a recent dump of all Mainnet contracts (find all interface declarations, find all members in them, and show the identifiers of the ones where the access is parse -readCSV -jqAST '.. | objects | select(.Type == "InterfaceDeclaration") | .Members | .Declarations | select (. != null) | add | select(.Access == "AccessContract") | .Identifier.Identifier' The usage of |
FLIP #262