-
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 131: removal of custom destructors #131
Changes from 2 commits
a8a3cb4
503fd3e
a34a404
734a23c
9b48085
f9224cd
f22448f
fb8bf77
461c935
b7dacba
b66283a
6dfc819
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
--- | ||
status: proposed | ||
flip: NNN (set to PR number) | ||
authors: Daniel Sainati ([email protected]) | ||
sponsor: Daniel Sainati ([email protected]) | ||
updated: 2023-08-14 | ||
--- | ||
|
||
# FLIP NNN: Removal of Custom Destructors | ||
|
||
## Objective | ||
|
||
This FLIP proposes to remove support for destructors from Cadence. | ||
In particular, resources will neither be required nor able to declare a special `destroy` method. | ||
Instead, any nested resources will automatically be destroyed when the resource is destroyed. | ||
|
||
## Motivation | ||
|
||
The primary motivation for this is to resolve the "trolling attack" that can be used to | ||
exploit the attachments feature, whereby an attachment with a `panic`ing destructor is added to a resource | ||
without the owner's knowledge, and thus unremovably takes up space in an account. By removing the ability | ||
to declare custom destructors, malicious actors would not be able to prevent the destruction or removal | ||
of an attachment. | ||
|
||
## User Benefit | ||
|
||
The primary benefit of this change would be the ability to enable the attachments feature without any | ||
security concerns. | ||
|
||
However, a secondary benefit is that the language would change to reflect the practical realities of Cadence on-chain more closely. | ||
Although the language guarantees that the `destroy` method of a resource is invoked whenever that resource is destroyed with the `destroy` | ||
statement, the reality is more complicated. There are ways to take a resource (e.g. an NFT or a token) "out of circulation" without actually | ||
destroying it; an easy example would be sending that resource to a locked account where it is irretrievable. Such resources are destroyed in | ||
the sense that they cannot be interacted with further, and as such any logic that relies on destructors to guarantee an accurate count of how many | ||
of a resource exist (e.g. `totalSupply` in the Fungible Token contract), to prevent the destruction of resources that do not satisfy certain conditions, | ||
or to emit an event when a resource is destroyed, is in practice impossible. | ||
As such, removing support for custom destructors would prevent developers being mislead about the possible guarantees that can be made about their resources. | ||
|
||
## Design Proposal | ||
|
||
The design is simple; `destroy` will no longer be a declarable special function. Instead, | ||
when a resource is destroyed, any nested resources in that resource will be iteratively destroyed as well. Effectively, | ||
the behavior will be the same as if the author of the `destroy` method had simply done the minimal implementation of that method, | ||
destroying sub-resources and nothing else. | ||
|
||
So, e.g. a resource `R` defined like this: | ||
|
||
```cadence | ||
|
||
resource SubResource { | ||
//... | ||
} | ||
|
||
resource R { | ||
let subResource: @Sub | ||
let subArray: @[Sub] | ||
|
||
// ... | ||
} | ||
``` | ||
|
||
would automatically destroy the `subResource` and `subArray` fields when it itself is destroyed. Users would not be able to | ||
rely on any specific order for the execution of these destructors, but because nothing can happen in destructors except for destruction | ||
of sub-resource, it would not be possible for the order to matter. | ||
|
||
### Drawbacks | ||
|
||
This will break a large amount of existing code, and further increase the Stable Cadence migration burden. | ||
|
||
### Compatibility | ||
|
||
This will not be backwards compatible. | ||
|
||
### Alternatives Considered | ||
|
||
There has been some other discussion of potential alternatives to this in the past for solving the "trolling attack". | ||
The attack relies on a user defining an aborting destructor to make it impossible to destroy an attachment, and as such potential solutions | ||
have been proposed to specifically ban panicking or aborting operations in destructors, or to have some kind of try-catch mechanism to continue | ||
destruction after an abort. | ||
|
||
The issue with both of these solutions is their technical complexity; we estimate that either would take between 6 months to a year to design and | ||
implement, and as they are both breaking changes, we would need to delay the release of Stable Cadence until the feature was complete. The hope | ||
with this proposal is that the simpler change would both solve the problem and allow a faster release of Stable Cadence, with all its attendant benefits. | ||
|
||
### User Impact | ||
|
||
To evaluate the impact of this change, existing contracts on Mainnet were surveyed. | ||
A [custom linter](https://github.com/onflow/cadence-tools/pull/194) was defined to identify contracts containing complex destructors, | ||
where a complex destructor is defined as one that performs and logic beyond recursively destroying sub-resources. | ||
|
||
1711 total complex destructors exist currently on Mainnet. | ||
|
||
Of these, 6 distinct categories of operations were identified (note that some destructors may fall into more than 1 category): | ||
|
||
* Event emission: this destructor emits an event to signal that the resource was destroyed. 1328 of the complex destructors are of this kind | ||
* Total Supply Decrementing: this destructor decrements a `totalSupply` variable. 492 of the complex destructors are of this kind | ||
* Conditions/Assertions: this destructor asserts a condition, either with a pre/post condition or an assert statement. 29 of the complex destructors are of this kind | ||
* Logging: this destructor logs some data. 17 of the complex destructors are of this kind | ||
* Panic: this destructor panics under some circumstances. 10 of the complex destructors are of this kind | ||
* Other complex operations: this destructor performs some logic that is not included in these categories (e.g. function call, resource movement). 211 of the complex destructors are of this kind. | ||
|
||
Based on this data, we can see that the two largest use cases for complex destructors currently is emitting an event signalling destruction, and decrementing the total supply for a token of some kind. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problem with this kind of analysis is that it doesn't take into account that boilerplate contracts are likely to overly represent the state of things. For instance, what if something in the NFTStorefrontV2 contract was impacted by this? Everyone is supposed to use the same contract and so you would only see one count included in this analysis, despite that contract being a core piece of infrastructure. I'd like to see what contracts fall into these smaller counts, or at least publish all of these findings so the community can do it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This PR has the linter that was used to compute this info, which you can run this way: https://github.com/onflow/cadence-tools/tree/master/lint#analyzing-contracts-in-a-directory There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please run it for the community and post findings somewhere as part of this flip There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah stats here are a bit meaningless, 211 complex destructors are like >10% ( even without considering @austinkline 's point ) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added the output of the analysis to the PR There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with Austin that the blast radius for a change like this is bigger than what this tool might indicate because of certain contracts |
||
While developers cannot actually on this logic to actually execute (events will not be emitted nor supply decremented when a resource is sent to a burner account), these use cases would be most | ||
dsainati1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
negatively impacted by this change. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I follow this. Yes you can always send items to a non-accessible address like we do on ethereum, but those addresses can be marked or determined to be inaccessible which means indexers are able to still correctly calculate the total supply of a token or an NFT Collection Information like this is very important to have accurate, especially if defi is going to be focused on more on flow There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is only saying that you cannot rely on destructors executing to accurately compute this information, since there is no guarantee that the destructor is actually called if a resource is sent to an inaccessible account. Indexers would still be able to compute this information accurately by looking at all the existing, accessible supply of a resource. In fact, indexing this way would probably be the suggested approach for this kind of thing, instead of relying on a destructor, when accuracy is important. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
How would that actually work, though? A withdraw event for NFTs doesn't mean it's been destroyed. Nor does access or lackthereof now imply that it will always be inaccessible. Flowty escrows NFTs and FTs when loans are funded, and NFTs when a rental is filled. In both of those cases, there are resources you temporarily do not have access to, but the supply is not actually changed. If Flow is going to say that indexers can calculate this, then I'd like to see how it is actually possible which I'm pretty skeptical of right now |
||
|
||
The Condition/Assertion and Panic categories are uncommon, and also anti-patterns. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How is this an anti-pattern? My understanding of destruction and the use of conditions/panics it that it ensures that a resource is safe to be deleted in the first place. For example, Flowty uses this to guarantee that no one can destroy a loan which has an NFT in escrow There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, if the destructor is actually called, then the panic/assertion would ensure that the conditions are correct for it to be destroyed. However, if the resource is sent to an inaccessible account, then the destructor is never run. So, for example, someone could just send their loan away to a burner account, and it becomes inaccessible to everyone, effectively no longer existing, but the destructor is never called. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
These really aren't the same, though. One is meant to prevent deletion of a resource accidentally. The other is someone getting rid of something explicitly. Just because someone can get around destruction doesn't mean it has no use. Ways to circumvent accidentally taking the wrong action doesn't mean safeguards aren't valuable There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, this is a philosophical point that I think merits further discussion, maybe in a breakout session. But especially with the upcoming changes in Stable Cadence that will invalidate all references to a resource when the resource is moved, the extent to which a deleted and an inaccessible resource are functionally different is (imo) very minimal.
As for this, I think it cuts two ways. Yes, having safeguards can be valuable, but the existence of safeguards can also cause complacency in implementation, which is dangerous if those safeguards aren't 100% accurate. You see this all the time in type system design, where when something is 99% safe, users will assume that it works perfectly and won't take the proper precautions to handle the 1% of cases where it is not safe. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
What is unsafe, here, in this hypothetically? I get that in theory someone could get confused by the state of something being destroyed versus moved to an inaccessible address, but moving any resource anywhere is a core feature of cadence. If a developer isn't taking that into account, or doesn't understand that, then they are going to be running into all kinds of foot-guns in the future. Do you have any example of an application which will benefit from this, or which runs into this confusion? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possibly I am misunderstanding the Flowty use case that you identified earlier, but if you use panicking destructors to "guarantee that no one can destroy a loan which has an NFT in escrow", then is it not potentially a safety issue that someone can effectively destroy a loan (even with an escrowed NFT) by sending it to a burner account? Very possibly I am wrong about this fwiw, just want to check my understanding here. |
||
Destructors being able to abort execution is the source of the attachments-related "trolling" attack in the first place, | ||
and any solution we come up with for this would necessarily involve circumventing these operations. | ||
|
||
## Prior Art | ||
|
||
Move is an example of a resource-based smart contract language that does not allow the definition of custom destructors. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was under the impression that we compare Cadence with swift / go / java when considering language design changes, this feels a bit like step back to compare with Move. |
||
|
||
## Questions and Discussion Topics | ||
|
||
* Because this proposes to remove support for `destroy` entirely, it would be possible to re-add support back later once | ||
we can make the method safer with respect to the trolling problem (e.g. by adding support for try-catch). | ||
|
||
* It is theoretically plausible for us to special-case support for emitting events and decrementing total supply in destructors in order to maintain | ||
support for these use cases. However, there is a large amount of complexity involved, and support would be very restricted. | ||
In order to guarantee that the destructor cannot abort execution, we would have to prevent any operations that might possible abort at runtime. In particular, | ||
all function calls, array/dictionary accesses and numerical operations would not be permitted. As such, any events that take arguments would need to | ||
likely take those arguments as literals or fields on the resource; it would not be possible to do any computation as part of the event emission other than passing arguments. | ||
The total supply decrement support would be even more complex; in general subtracting two unsigned integers is not guaranteed not to abort because underflow is possible. | ||
We would need to somehow be able to identify programmatically what is and is not a "total supply" variable and allow it to be decremented in some way that cannot abort. | ||
Is it worth the additional effort to support either of these use cases? |
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.
Does this mean another alternative is to not release attachments with stable cadence?
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.
That would be up for discussion. If we don't solve this problem, then we would need to decide whether or not it is feasible to release attachments with the existing security concerns. It is possible that we might decide it's not possible, and if a solution to the attack isn't found that doesn't rely on breaking changes, we may need to scrap the feature.
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.
To be clear, it could be scrapped until something like try/catch is done. In which case it could be released alongside whatever solution is implemented
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.
The addition of try catch would not solve the problem unless the use of try catch is mandated in destructors, which would be a breaking change.