---
status: accepted
flip: [179](https://github.com/onflow/flips/pull/179)
authors: Giovanni Sanchez ([email protected])
sponsor: Giovanni Sanchez ([email protected])
updated: 2023-09-26
---
This proposal outlines a mechanism for automated and efficient contract updates to take effect at or beyond a specified block height with the goal of minimizing recovery time following breaking improvements. Included in this FLIP is a design enabling contract developers to pre-define a sequence of contract updates across an arbitrary number of contracts and accounts, and either execute these updates themselves or delegate deployment authority to some trusted party.
Immediately following planned Cadence improvements on Mainnet (marked by the Cadence 1.0 milestone), many contracts across the ecosystem will need to be updated. In preparation for this milestone, the Flow community is considering ways to make this migration as quick and seamless as possible, primarily motivated by the desire to minimize any user-perceived downtime. With this in mind, it's in the interest of everyone to provide helpful and reliable tools to empower and support developers in upgrading their contracts.
Surely, some developers will prefer to manually execute updates themselves. However, others may find it useful and even preferable to both codify their updated deployment using onchain constructs and additionally delegate update execution to an automated service run by Flow as a part of the ecosystem-wide post-spork migration.
This contract would benefit developers by making it easy for them to stage and even automate contract updates after breaking improvements and benefit users by minimizing perceived downtime.
Updates to a network of dependent contracts must occur according to their dependency ordering. Even when properly ordered, any dependent contracts must wait for their dependencies to complete state changes in the execution environment before they can successfully update. Said state change can only occur after the dependency updating transaction completes.
This is an important consideration in designing a wide-scale update mechanism because it means we must batch updates in stages, with each stage updating a set of contracts at the same level of the dependency graph.
If we were to take the following set of contracts with the pictured dependency graph and attempt to update them in a
single transaction, the attempt would fail. This is because B
depends on A
, and A
's updates won't yet take effect
until after the transaction has successfully executed.
Sequencing a set of contracts into discrete update transactions, order labeled on the right.
Instead, we update A
in one transaction, B
in the following, and lastly C
in a final transaction. Given a larger
number of contracts, we could update multiple contracts in a single transaction, but the updates would need to be
batched with contracts at a similar maximum depth in their dependency graph.
Take for instance the graph above. We would stage [A, D]
in the first transaction, [B, E]
in another, and finally
C
could be updated. Keep this mental model in mind as we put together this staged approach in the design below.
ℹ️ The proposed design is largely informed by a working prototype which can be found in this repo
Proposed is a single contract implementation defining a primary resource - referred to here as Updater
- encapsulating
a bundle of ordered contract updates, themselves bundled more granularly into stages. A developer provides AuthAccount
Capabilities on the contract hosting accounts, staged and ordered update configuration details (contract address, name,
and code) to this resource on initialization as well as a blockheight boundary, at or beyond which updates should be
deployed.
The owner of this resource can then call Updater.update()
to execute the updates bundled into the current deployment
stage. The current stage is incremented every time update()
is called, with updateComplete
set to true once all
deployment stages have been attempted.
In addition to the Updater
, the Delegatee
can capture delegated Updater
Capabilities. This allows a developer to
codify their deployment details in an Updater
, save the resource in their account, and offload the entirety of their
contract updates to a trusted party via Delegatee
.
With respect to Stable Cadence support, the idea is to provide a Flow-hosted Delegatee
that executes delegated updates
(after updating core contracts) following the spork upgrade. At migration time, Delegatee
can execute batches of
delegated updates, with each individual Updater
tracking the current stage and deploying stage-specific contract
updates as configured by the resource owner. Since Updater
s track their own stage, Delegatee
simply needs to track
which Updater
resources have remaining deployment stages and either execute those updates until all delegated updates
are complete or up to a maximum number of supported stages (preventing trolling attacks).
The overview above outlines the interface of ContractUpdater
and each of its defined constructs. Expand the toggles
below to view each component in more detail.
struct ContractUpdate
/// Representative of a single contract, its name, code and where it will be deployed
///
access(all) struct ContractUpdate {
access(all) let address: Address
access(all) let name: String
access(all) let code: String
access(all) fun toString(): String
access(all) fun codeAsCadence(): String
}
resource Updater
/// Public interface enabling queries about the Updater
///
access(all) resource interface UpdaterPublic {
access(all) fun getID(): UInt64
access(all) fun getBlockUpdateBoundary(): UInt64
access(all) fun getContractAccountAddresses(): [Address]
access(all) fun getDeployments(): [[ContractUpdate]]
access(all) fun getCurrentDeploymentStage(): Int
access(all) fun getFailedDeployments(): {Int: [String]}
access(all) fun hasBeenUpdated(): Bool
}
/// Resource that enables delayed contract updates to wrapped accounts at or beyond a specified block height
///
access(all) resource Updater : UpdaterPublic {
/// Update to occur at or beyond this block height
// TODO: Consider making this a contract-owned value as it's reflective of the spork height
access(self) let blockUpdateBoundary: UInt64
/// Update status for each contract
access(self) var updateComplete: Bool
/// Capabilities for contract hosting accounts
access(self) let accounts: {Address: Capability<auth(UpdateContract) &Account>}
/// Updates ordered by their deployment sequence and staged by their dependency depth
/// NOTE: Dev should be careful to validate their dependencies such that updates are performed from root
/// to leaf dependencies
access(self) let deployments: [[ContractUpdate]]
/// Current deployment stage
access(self) var currentDeploymentStage: Int
/// Contracts whose update failed keyed on their deployment stage
access(self) let failedDeployments: {Int: [String]}
/// Executes the next update stage for all contracts defined in deployment, returning true if all stages have
/// been attempted and false if stages remain
///
access(all) fun update(): Bool
/* --- Public getters --- */
//
access(all) fun getID(): UInt64
access(all) fun getBlockUpdateBoundary(): UInt64
access(all) fun getContractAccountAddresses(): [Address]
access(all) fun getDeployments(): [[ContractUpdate]]
access(all) fun getCurrentDeploymentStage(): Int
access(all) fun getFailedDeployments(): {Int: [String]}
access(all) fun hasBeenUpdated(): Bool
}
resource Delegatee
/// Public interface for Delegatee
///
access(all) resource interface DelegateePublic {
access(all) fun check(id: UInt64): Bool?
access(all) fun getUpdaterIDs(): [UInt64]
access(all) fun delegate(updaterCap: Capability<&Updater>)
access(all) fun removeAsUpdater(updaterCap: Capability<&Updater>)
}
/// Resource that executed delegated updates
///
access(all) resource Delegatee : DelegateePublic {
// TODO: Block Height - All DelegatedUpdaters must be updated at or beyond this block height
// access(self) let blockUpdateBoundary: UInt64
/// Track all delegated updaters
access(self) let delegatedUpdaters: {UInt64: Capability<&Updater>}
/// Checks if the specified DelegatedUpdater Capability is contained and valid
///
access(all) fun check(id: UInt64): Bool?
/// Returns the IDs of the delegated updaters
///
access(all) fun getUpdaterIDs(): [UInt64]
/// Allows for the delegation of updates to a contract
///
access(all) fun delegate(updaterCap: Capability<&Updater>)
/// Enables Updaters to remove their delegation
///
access(all) fun removeAsUpdater(updaterCap: Capability<&Updater>)
/// Executes update on the specified Updater, removing the Capability once update is completed
///
access(all) fun update(updaterIDs: [UInt64]): [UInt64]
/// Enables admin removal of a DelegatedUpdater Capability
///
access(all) fun removeDelegatedUpdater(id: UInt64)
}
Events
access(all) event UpdaterCreated(updaterUUID: UInt64, blockUpdateBoundary: UInt64)
access(all) event UpdaterUpdated(
updaterUUID: UInt64,
updaterAddress: Address?,
blockUpdateBoundary: UInt64,
updatedAddresses: [Address],
updatedContracts: [String],
failedAddresses: [Address],
failedContracts: [String],
updateComplete: Bool
)
access(all) event UpdaterDelegationChanged(updaterUUID: UInt64, updaterAddress: Address?, delegated: Bool)
Helper Methods
/// Returns the Address of the Delegatee associated with this contract
///
access(all) fun getContractDelegateeAddress(): Address
/// Helper method that returns the ordered array reflecting sequenced and staged deployments, with each contract
/// update represented by a ContractUpdate struct.
///
/// NOTE: deploymentConfig is ordered, and the order is used to determine both the order of the contracts in each
/// deployment and the order of the deployments themselves. Each entry in the inner array must be exactly one
/// key-value pair, where the key is the address of the associated contract name and code.
///
access(all) fun getDeploymentFromConfig(_ deploymentConfig: [[{Address: {String: String}}]]): [[ContractUpdate]]
/// Returns a new Updater resource
///
access(all) fun createNewUpdater(
blockUpdateBoundary: UInt64,
accounts: [Capability<auth(UpdateContract) &Account>],
deployments: [[ContractUpdate]]
): @Updater
/// Creates a new Delegatee resource enabling caller to self-host their Delegatee
///
access(all) fun createNewDelegatee(): @Delegatee
ℹ️ The update API discussed below has been implemented - see PR: onflow/cadence#2769
Currently, updating a contract
occurs via the API Contracts.update__experimental(name: String, code: [UInt8]): DeployedContract
which reverts when an
update fails. Reverting on failure is perfectly fine for individual updates; however, doing so in the context of
iterative updates would prevent successive updates from executing, leading to a fragile design with cumbersome
alternatives.
This design is heavily dependent on the existence of an alternative update API that avoids reverting in the event of a failed update. Without it, each update would need to be executed in discrete transactions, resulting in at least an order of magnitude more transaction executions to achieve the same result as this design, as well as more complex signing architecture to handle the requisite number of proposal keys for all those transactions.
The proposed tryUpdate()
API is its own issue (found here), but is
included below for context and consideration:
access(all) enum ErrorType: UInt8 {
access(all) case CONTRACT_NOT_FOUND
access(all) case MULTIPLE_CONTRACTS_DECLARED
access(all) case MISMATCHED_NAME
access(all) case UNDEFINED
}
access(all) struct DeploymentError {
access(all) let errorType: ErrorType
access(all) let errorMessage: String
}
access(all) struct DeploymentResult {
access(all) let success: Bool
access(all) let errorMessage: DeploymentError?
}
access(all) fun tryUpdate(name: String, code: [UInt8]): DeploymentResult
Ideally this method would return some error message in the event of a failed update; however, @turbolent mentioned security implications involved with exposing such error messages into the execution environment without reverting.
The proposed path forward then is to return the following DeploymentResult
, later adding errorMessage
once the
security implication has been mitigated:
access(all) struct DeploymentResult {
access(all) let success: Bool
}
With respect to the topic of this FLIP, the Updater
can check DeploymentResult.success
and emit any failed updates
within the executed stage as well as preserve the failure status within its fields. Direct follow up and intervention by
the Updater
owner would then be to correct the issue and manually execute the contract updates.
Update executions via Delegatee
can't account for the global dependency graph since ultimately contract account owners
have sovereignty over their update path. Therefore, Delegatee
depends on the Updater
's creator to sequence the
contained deployment appropriately given the contracts to be updated. This means that developers will still want to
validate their contracts are both individually Stable Cadence compatible and updatable, as well as sequenced correctly
within the Updater
resource.
Since we intend to iterate over a large number of updates per transaction, we cannot allow unsuccessful update attempts
to interrupt that iteration. Therefore, this mechanism provides no guarantees to the delegator that their updates will
be completed successfully or that any stages succeeding a failed contract update will be avoided. The only strong
guarantee to delegating parties is that the update deployment encapsulated in their Updater
will be attempted at or
beyond the collective block height boundary.
One significant callout deserving more investigation are the limitations presented by execution limits on both the
construction of the Updater
resource as well as the execution of delegated updates. The former is a concern because it
would influence the Updater
setup while the latter is a concern for update batch size executed by Delegatee
. The
hope is that all delegated updates can be executed within a dozen or so transactions post-spork (based on the current
number of mainnet contracts), but benchmarks are needed to determine a well-informed and robust migration protocol.
Another drawback is the complexity associated with configuring and managing the Updater
resource. This complexity is
three-fold:
- Configuring the
Updater
on initialization requires a fully sorted deployment complete with ordered stages, for which there is currently non-existent support. - Once configured, there is currently no way to test whether the
Updater
will execute all updates successfully. - Once updates are executed, an
Updater
's owner will want to know whether their update deployment completed successfully.
On 1/, it would be helpful to introduce a tool that would output a suggested Updater
deployment ordering given a set
of Mainnet contract addresses & names as well as a tool to configure a local Flow project into the proper Cadence
json arguments for easy CLI configuration.
On 2/, planned Stable Cadence emulator previews along with local Mainnet mirroring can fill the gaps here. The workflow
might look like: Mirror mainnet locally with the Stable Cadence preview running, configure your Updater
based on your
updated contracts, then simulate the update execution locally. Guides and best practices on this front should follow if
this FLIP is approved and implemented.
Regarding 3/, a helpful tool here would be an easy to use dashboard where the Updater
owner can authenticate and view
both the current status of their Updater
resource and any related events and transaction details where their updates
were executed. This would make it easy for developers to identify issues with their update deployments and
minimize follow up time.
These gaps are building opportunities beyond the scope of this design, and contributions on these fronts are very welcome.
Since we cannot centrally organize, coordinate, or initiate all contract updates, developers using this should be aware that any updates executed using this design should ensure that their contract dependencies are either core contracts or are entirely owned.
One considered alternative was to encapsulate contract updates on a single contract basis instead of bundling staged
deployments inside of Updater
. These atomized contract updates could then be delegated to Delegatee
and the
deployment ordered offchain based on the resolved dependency graph. This sorted update ordering could then be passed to
the Delegatee
in batches, ensuring that all contracts are updated according to their dependencies.
While this sounds much neater and is in essence the approach taken by centralized, top-down architectures, with more
investigation, this approach was revealed to be fragile. The thinking here is that since delegating updates cannot be
compulsory, the Delegatee
will inevitably lack full global context and control over contract updates. We cannot take a
top-down approach in a system that is fundamentally bottom-up.
The Delegatee
will inevitably lack the ability to update some members of the full dependency graph. And if some of the
contracts that we are tasked with updating depend on those we can't update, all the effort we invest into neatly
resolving updates will ultimately fail. The lift for this approach is significantly higher than the proposed
self-defined crowdsourcing design for essentially similar strength guarantees.
Another alternative is to temporarily allow contract updates to take place intra-transaction by hotswapping contract code. Hotswapping here means updating the execution state of the updated contract within the updating transaction instead of at the end. This would allow for a bundle of contract updates to occur in a single transaction without the need to batch updates into stages based on depth in their dependency graph.
Taking the example mentioned previously, instead of executing updates to contracts A
, B
, and C
in
discrete transactions, we would execute all three updates in a single transaction so long as the updates are
appropriately ordered.
However, hotswapping contracts comes with its own security concerns, and likely demands a sizeable implementation effort with unknown consequences presenting an undue burden for what is potentially a single-use feature.
As mentioned above, we'll want to examine performance benchmarks with a focus on Updater
construction - more generally
saving update deployments onchain - and Delegatee
update execution - more generally executing a large number of
contract updates in a single transaction.
As mentioned above, supporting resources, examples, and use guides should follow if this FLIP is adopted.
One thing to note though is that this design is not likely to cover use cases where the dependencies are not either core
contracts or fully owned by the developer since updates are not atomized and ordered at the level of Delegatee
.
For a working example, see this repo and README walkthrough demonstrating the end-to-end setup and update execution flow.
Note that the linked prototype utilizes the currently available update__experimental()
API, but emulates the proposed
design in the context of persistent update mechanisms via happy path (i.e. successful contract updates).
This design is fully compatible with existing and planned featuresets. The only dependency here is the addition of the
aforementioned Contracts.tryUpdate(): DeploymentResult
(issue found
here) which would enable batched updates without interruption.
- Issue: Staged Contract Update
- Tracker: Cadence 1.0
- Issue: Add
tryUpdate()
method - Issue: Flow CLI Contract to hex util
- What are the transaction limits - how do they affect
Updater
setup andDelegatee
update execution for large numbers of accounts? - What other persistent use cases would benefit from this mechanism?
- How many developers would delegate their contract updates if offered the service?