Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions flow.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,26 @@
"resourceID": "projects/flow-service-account-key/locations/global/keyRings/flow-service-account-signer-LL/cryptoKeys/flow-service-account-signer-LL/cryptoKeyVersions/1"
}
},
"fungible_token": {
"address": "f233dcee88fe0abe",
"key": {
"type": "google-kms",
"index": 0,
"signatureAlgorithm": "ECDSA_P256",
"hashAlgorithm": "SHA2_256",
"resourceID": "projects/dl-flow/locations/global/keyRings/mainnet/cryptoKeys/service-account/cryptoKeyVersions/1"
}
},
"staking_proxy": {
"address": "62430cf28c26d095",
"key": {
"type": "google-kms",
"index": 0,
"signatureAlgorithm": "ECDSA_P256",
"hashAlgorithm": "SHA2_256",
"resourceID": "projects/dl-flow/locations/global/keyRings/mainnet/cryptoKeys/token-admin/cryptoKeyVersions/1"
}
},
"emulator-account": {
"address": "f8d6e0586b0a20c7",
"key": "d5547bbeea9acdf1a95b0df2dbe2b3022acbcd850e83671594b6e240f01bacc9"
Expand Down
297 changes: 297 additions & 0 deletions transactions/update-contract/2026/apr-20-part-2/FungibleToken.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
/**

# The Flow Fungible Token standard

## `FungibleToken` contract

If a users wants to deploy a new token contract, their contract
needs to implement the FungibleToken interface and their tokens
need to implement the interfaces defined in this contract.

/// Contributors (please add to this list if you contribute!):
/// - Joshua Hannan - https://github.com/joshuahannan
/// - Bastian Müller - https://twitter.com/turbolent
/// - Dete Shirley - https://twitter.com/dete73
/// - Bjarte Karlsen - https://twitter.com/0xBjartek
/// - Austin Kline - https://twitter.com/austin_flowty
/// - Giovanni Sanchez - https://twitter.com/gio_incognito
/// - Deniz Edincik - https://twitter.com/bluesign
/// - Jonny - https://github.com/dryruner
///
/// Repo reference: https://github.com/onflow/flow-ft

## `Vault` resource interface

Each fungible token resource type needs to implement the `Vault` resource interface.

## `Provider`, `Receiver`, and `Balance` resource interfaces

These interfaces declare pre-conditions and post-conditions that restrict
the execution of the functions in the Vault.

It gives users the ability to make custom resources that implement
these interfaces to do various things with the tokens.
For example, a faucet can be implemented by conforming
to the Provider interface.

*/

import ViewResolver from 0x1d7e57aa55817448
import Burner from 0xf233dcee88fe0abe

/// FungibleToken
///
/// Fungible Token implementations should implement the fungible token
/// interface.
access(all) contract interface FungibleToken: ViewResolver {

// An entitlement for allowing the withdrawal of tokens from a Vault
access(all) entitlement Withdraw

/// The event that is emitted when tokens are withdrawn
/// from any Vault that implements the `Vault` interface
access(all) event Withdrawn(type: String,
amount: UFix64,
from: Address?,
fromUUID: UInt64,
withdrawnUUID: UInt64,
balanceAfter: UFix64)

/// The event that is emitted when tokens are deposited to
/// any Vault that implements the `Vault` interface
access(all) event Deposited(type: String,
amount: UFix64,
to: Address?,
toUUID: UInt64,
depositedUUID: UInt64,
balanceAfter: UFix64)

/// Event that is emitted when the global `Burner.burn()` method
/// is called with a non-zero balance
access(all) event Burned(type: String, amount: UFix64, fromUUID: UInt64)

/// Balance
///
/// The interface that provides a standard field
/// for representing balance
///
access(all) resource interface Balance: Burner.Burnable {
access(all) var balance: UFix64

// This default implementation needs to be in a separate interface
// from the one in `Vault` so that the conditions get enforced
// in the correct one
access(contract) fun burnCallback() {
self.balance = 0.0
}
}

/// Provider
///
/// The interface that enforces the requirements for withdrawing
/// tokens from the implementing type.
///
/// It does not enforce requirements on `balance` here,
/// because it leaves open the possibility of creating custom providers
/// that do not necessarily need their own balance.
///
access(all) resource interface Provider {

/// Function to ask a provider if a specific amount of tokens
/// is available to be withdrawn
/// This could be useful to avoid panicking when calling withdraw
/// when the balance is unknown
/// Additionally, if the provider is pulling from multiple vaults
/// it only needs to check some of the vaults until the desired amount
/// is reached, potentially helping with performance.
///
/// @param amount the amount of tokens requested to potentially withdraw
/// @return Bool Whether or not this amount is available to withdraw
///
access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool

/// withdraw subtracts tokens from the implementing resource
/// and returns a Vault with the removed tokens.
///
/// The function's access level is `access(Withdraw)`
/// So in order to access it, one would either need the object itself
/// or an entitled reference with `Withdraw`.
///
/// @param amount the amount of tokens to withdraw from the resource
/// @return The Vault with the withdrawn tokens
///
access(Withdraw) fun withdraw(amount: UFix64): @{Vault} {
post {
// `result` refers to the return value
result.balance == amount:
"FungibleToken.Provider.withdraw: Cannot withdraw tokens! The balance of the withdrawn tokens (\(result.balance)) is not equal to the amount requested to be withdrawn (\(amount))"
}
}
}

/// Receiver
///
/// The interface that enforces the requirements for depositing
/// tokens into the implementing type.
///
/// We do not include a condition that checks the balance because
/// we want to give users the ability to make custom receivers that
/// can do custom things with the tokens, like split them up and
/// send them to different places.
///
access(all) resource interface Receiver {

/// deposit takes a Vault and deposits it into the implementing resource type
///
/// @param from the Vault that contains the tokens to deposit
///
access(all) fun deposit(from: @{Vault})

/// getSupportedVaultTypes returns a dictionary of Vault types
/// and whether the type is currently supported by this Receiver
///
/// @return {Type: Bool} A dictionary that indicates the supported types
/// If a type is not supported, it should be `nil`, not false
///
access(all) view fun getSupportedVaultTypes(): {Type: Bool}

/// Returns whether or not the given type is accepted by the Receiver
/// A vault that can accept any type should just return true by default
///
/// @param type The type to query about
/// @return Bool Whether or not the vault type is supported
///
access(all) view fun isSupportedVaultType(type: Type): Bool
}

/// Vault
/// Conforms to all other interfaces so that implementations
/// only have to conform to `Vault`
///
access(all) resource interface Vault: Receiver, Provider, Balance, ViewResolver.Resolver, Burner.Burnable {

/// Field that tracks the balance of a vault
access(all) var balance: UFix64

/// Called when a fungible token is burned via the `Burner.burn()` method
/// Implementations can do any bookkeeping or emit any events
/// that should be emitted when a vault is destroyed.
/// Many implementations will want to update the token's total supply
/// to reflect that the tokens have been burned and removed from the supply.
/// Implementations also need to set the balance to zero before the end of the function
/// This is to prevent vault owners from spamming fake Burned events.
access(contract) fun burnCallback() {
pre {
emit Burned(
type: self.getType().identifier,
amount: self.balance,
fromUUID: self.uuid
)
}
post {
self.balance == 0.0:
"FungibleToken.Vault.burnCallback: Cannot burn this `Vault` with `Burner.burn()`. The balance must be set to zero during the `burnCallback` method so that it cannot be spammed."
}
}

/// getSupportedVaultTypes
/// The default implementation is included here because vaults are expected
/// to only accepted their own type, so they have no need to provide an implementation
/// for this function
///
access(all) view fun getSupportedVaultTypes(): {Type: Bool} {
// Below check is implemented to make sure that run-time type would
// only get returned when the parent resource conforms with `FungibleToken.Vault`.
if self.getType().isSubtype(of: Type<@{FungibleToken.Vault}>()) {
return {self.getType(): true}
} else {
// Return an empty dictionary as the default value for resource who don't
// implement `FungibleToken.Vault`, such as `FungibleTokenSwitchboard`, `TokenForwarder` etc.
return {}
}
}

/// Checks if the given type is supported by this Vault
access(all) view fun isSupportedVaultType(type: Type): Bool {
return self.getSupportedVaultTypes()[type] ?? false
}

/// withdraw subtracts `amount` from the Vault's balance
/// and returns a new Vault with the subtracted balance
///
access(Withdraw) fun withdraw(amount: UFix64): @{Vault} {
pre {
self.balance >= amount:
"FungibleToken.Vault.withdraw: Cannot withdraw tokens! The amount requested to be withdrawn (\(amount)) is greater than the balance of the `Vault` (\(self.balance))."
}
post {
result.getType() == self.getType():
"FungibleToken.Vault.withdraw: Cannot withdraw tokens! The withdraw method tried to return an incompatible `Vault` type <\(result.getType().identifier)>. It must return a `Vault` with the same type as self <\(self.getType().identifier)>."

// use the special function `before` to get the value of the `balance` field
// at the beginning of the function execution
//
self.balance == before(self.balance) - amount:
"FungibleToken.Vault.withdraw: Cannot withdraw tokens! The sender's balance after the withdrawal (\(self.balance)) must be the difference of the previous balance (\(before(self.balance))) and the amount withdrawn (\(amount))"

emit Withdrawn(
type: result.getType().identifier,
amount: amount,
from: self.owner?.address,
fromUUID: self.uuid,
withdrawnUUID: result.uuid,
balanceAfter: self.balance
)
}
}

/// deposit takes a Vault and adds its balance to the balance of this Vault
///
access(all) fun deposit(from: @{FungibleToken.Vault}) {
// Assert that the concrete type of the deposited vault is the same
// as the vault that is accepting the deposit
pre {
from.isInstance(self.getType()):
"FungibleToken.Vault.deposit: Cannot deposit tokens! The type of the deposited tokens <\(from.getType().identifier)> has to be the same type as the `Vault` being deposited into <\(self.getType().identifier)>. Check that you are withdrawing and depositing to the correct paths in the sender and receiver accounts and that those paths hold the same `Vault` types."
}
post {
emit Deposited(
type: before(from.getType().identifier),
amount: before(from.balance),
to: self.owner?.address,
toUUID: self.uuid,
depositedUUID: before(from.uuid),
balanceAfter: self.balance
)
self.balance == before(self.balance) + before(from.balance):
"FungibleToken.Vault.deposit: Cannot deposit tokens! The receiver's balance after the deposit (\(self.balance)) must be the sum of the previous balance (\(before(self.balance))) and the amount deposited (\(before(from.balance)))"
}
}

/// createEmptyVault allows any user to create a new Vault that has a zero balance
///
/// @return A Vault of the same type that has a balance of zero
access(all) fun createEmptyVault(): @{Vault} {
post {
result.balance == 0.0:
"FungibleToken.Vault.createEmptyVault: Empty `Vault` creation failed! The newly created `Vault` must have zero balance but it has a balance of \(result.balance)"

result.getType() == self.getType():
"FungibleToken.Vault.createEmptyVault: Empty `Vault` creation failed! The type of the new `Vault` <\(result.getType().identifier)> has to be the same type as the `Vault` that created it <\(self.getType().identifier)>."
}
}
}

/// createEmptyVault allows any user to create a new Vault that has a zero balance
///
/// @return A Vault of the requested type that has a balance of zero
access(all) fun createEmptyVault(vaultType: Type): @{FungibleToken.Vault} {
post {
result.balance == 0.0:
"FungibleToken.createEmptyVault: Empty `Vault` creation failed! The newly created `Vault` must have zero balance but it has a balance of \(result.balance)"

result.getType() == vaultType:
"FungibleToken.createEmptyVault: Empty `Vault` creation failed! The type of the new `Vault` <\(result.getType().identifier)> has to be the same as the type that was requested <\(vaultType.identifier)>."
}
}
}
61 changes: 61 additions & 0 deletions transactions/update-contract/2026/apr-20-part-2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# FungibleToken and StakingProxy update

Transactions to upgrade FungibleToken and StakingProxy contracts

Signer:
1. Fungible Token: `0xf233dcee88fe0abe`
2. StakingProxy: `0x62430cf28c26d095`

## Transactions

[update contract](../../../../transactions/update-contract)

### FungibleToken

Used this to generate the contract code arguments:

` wget https://raw.githubusercontent.com/onflow/flow-ft/refs/heads/master/contracts/FungibleToken.cdc`

- Update imports

`cat "./FungibleToken.cdc" | xxd -p | tr -d '\n'`

Verified using:
```
$ cat arguments-fungibletoken-contract.json | jq '.[1] | .value' | xxd -r -p > /tmp/temp.txt
$ diff /tmp/temp.txt FungibleToken.cdc
(Should produce no difference)
```

### StakingProxy

Used this to generate the contract code arguments:

` wget https://raw.githubusercontent.com/onflow/flow-core-contracts/refs/heads/master/contracts/StakingProxy.cdc`

- Update imports

`cat "./StakingProxy.cdc" | xxd -p | tr -d '\n'`

Verified using:
```
$ cat arguments-stakingproxy-contract.json | jq '.[1] | .value' | xxd -r -p > /tmp/temp.txt
$ diff /tmp/temp.txt StakingProxy.cdc
(Should produce no difference)
```


## Steps to upgrade

1. `flow transactions send ./templates/update_contract.cdc --signer fungible_token --args-json "$(cat "./transactions/update-contract/2026/apr-20-part-2/arguments-fungibletoken-contract.json")" -n mainnet --compute-limit 9999`
2. `flow transactions send ./templates/update_contract.cdc --signer staking_proxy --args-json "$(cat "./transactions/update-contract/2026/apr-20-part-2/arguments-stakingproxy-contract.json")" -n mainnet --compute-limit 9999`
___

### Results

1. FungibleToken:
i. Success: https://www.flowscan.io/tx/26aec575a7aae0cc2676e2615ebe496642b25c4bb1e1548ad83a3da31ead24e2
2. StakingProxy:
i. Failure: https://www.flowscan.io/tx/fcdb2001f42293f475be3465b8c5fa1ecc8d9890430ca2524b5678e1382350f4?tab=script - invalid signature
ii. Success: https://www.flowscan.io/tx/da7471dbe45ade9e737d65e55a057aaa821c0624533bac3d987f44e1b103cd1e

Loading