Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@joshuahannan
Copy link
Member

@joshuahannan joshuahannan commented Apr 23, 2025

Description

A recent update to Cadence made it so that pre-conditions in a default implementation do not execute if the implementation is overridden. This moves the default implementation for burnCallback to the Balance interface so that the pre conditions will always execute.

Need to be sure that this upgrade won't have any negative affects:

  • Those who were relying on the default implementation will still have it via the Balance interface
  • If there were any resources that implemented the Balance interface, which there probably weren't, then the addition of burnCallback and its default implementation will not be a problem because it is access(contract), so only the Burner and FungibleToken contracts can call it.

For contributor use:

  • Targeted PR against master branch
  • Code follows the standards mentioned here.
  • Re-reviewed Files changed in the Github PR explorer

dependabot bot and others added 4 commits April 14, 2025 19:02
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.31.0 to 0.35.0.
- [Commits](golang/crypto@v0.31.0...v0.35.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.35.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
@bluesign
Copy link

A recent update to Cadence made it so that pre-conditions in a default implementation do not execute if the implementation is overridden.

this sounds like a bug in Cadence imo

@joshuahannan
Copy link
Member Author

joshuahannan commented Apr 24, 2025

Yeah, I think that pre-conditions in interfaces should execute regardless of whether there is a default implementation, but I can see both sides of the argument. @turbolent Feel free to explain or link to any docs y'all have

@turbolent
Copy link
Member

The changes look good to me 👍

To answer the concerns regarding the behaviour:

An interface function with only conditions, and no statements, means the conditions are inherited in the implementation function.

If an interface functions has statements, then it is considered a default function.

If an implementation of the interface (type conforms to the interface) defines a function, then it is used instead of the default function, it overrides the default function.

This intended behaviour was unfortunately not correctly implemented in Cadence, and conditions of default functions were included when executing the function in the implementation of the interface.

This bug was recently fixed (onflow/cadence#3775, in particular onflow/cadence@6f24819).

Existing code might have relied on this wrong behaviour, for example the Fungible Token.

Unfortunately, it is not clearly documented what it means to define an interface function that has conditions and statements, neither in the documentation nor in FLIPs, which leads to users assuming certain behaviour – either that conditions are inherited; or they are not, and only apply to the default function.

Users might want to both define conditions that should be executed for every implementation, and at the same time also provide a default implementation.

One can argue for either behaviour (conditions being inherited or not), and neither is more "correct" than the other. For now we rectified the intended behaviour in the implementation. We will also look into how we can make this intent (providing a default function and also enforcing conditions) less error-prone / a footgun, e.g. by improving documentation, tooling (linting), and considering language improvements.

For now, this intent can be best implemented in a Cadence program by having two interfaces, i.e. splitting the function interface function with conditions and statements into two parts: One interface has the conditions that should be inherited in all implementations, and another interface that provides the default function.

@bluesign
Copy link

thanks @turbolent

yeah this is tricky; I think current implementation is the one that can make sense ( considering post conditions )

/// The interface that provides a standard field
/// for representing balance
///
access(all) resource interface Balance {
Copy link
Contributor

@sisyphusSmiling sisyphusSmiling Apr 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like this should inherit the Burner.Burnable interface. Not a big deal in the context of Vault implementations, but I'm wondering about other implementations that want a balance (e.g. a fractional NFT or Game resource).

For instance, let's say I have something like an in-game resource that has a balance.

access(all) resource NFT : FungibleToken.Balance, ... {
  access(all) var balance: UFix64
  ...
}

The NFT will inherit Balance and therefore the default burnerCallback method. But when the NFT is burned, Burner.burn won't actually execute the burnCallback due to this line in Burner

if let s <- r as? @{Burnable} 

So in the end, the NFT will just be destroyed and the Balance.burnCallback() won't be called. I think if we're going to include functionality that's explicitly related to the Burnable interface, it should probably be inherited here. But I'm not 100% convicted and I'm curious to hear other thoughts.

Edit: I just saw @turbolent's explanation above which makes sense.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the Burnable inheritance to Balance, so this won't be an issue

@joshuahannan joshuahannan merged commit e27c9b7 into master Apr 25, 2025
2 checks passed
@joshuahannan joshuahannan deleted the burnCallback branch April 25, 2025 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants