Smart Contract

MigrationContractStaging

A.56100d46aa9b0212.MigrationContractStaging

Valid From

83,252,794

Deployed

1h ago
Mar 01, 2026, 03:43:20 AM UTC

Dependents

0 imports
1/// This contract is intended for use in the Cadence 1.0 contract migration across the Flow network.
2///
3/// In preparation for this milestone, your contract will NEED to be updated! Once you've updated your code for
4/// Cadence 1.0, you MUST stage your contract code in this contract so that the update can be executed as a part of the
5/// network-wide state migration.
6///
7/// To stage your contract update:
8/// 1. create a Host & save in your contract-hosting account
9/// 2. call stageContract() passing a reference to you Host, the contract name, and the updated Cadence code
10///
11/// This can be done in a single transaction! For more code context, see https://github.com/onflow/contract-updater
12///
13access(all) contract MigrationContractStaging {
14
15    // Path constants
16    //
17    access(self) let delimiter: String
18    access(self) let capsulePathPrefix: String
19    access(all) let HostStoragePath: StoragePath
20    access(all) let AdminStoragePath: StoragePath
21    /// Maps contract addresses to an array of staged contract names
22    access(self) let stagedContracts: {Address: [String]}
23    /// The block height at which updates can no no longer be staged. If nil, updates can be staged indefinitely until
24    /// the cutoff value is set.
25    access(self) var stagingCutoff: UInt64?
26    /// Results of the last emulated contract migration, committed by the admin after offchain contract migration
27    access(all) var lastEmulatedMigrationResult: EmulatedMigrationResult?
28
29    /// Event emitted when a contract's code is staged, replaced or unstaged
30    /// `action` ∈ {"stage", "replace", "unstage"} each denoting the action being taken on the staged contract
31    /// NOTE: Does not guarantee that the contract code is valid Cadence
32    access(all) event StagingStatusUpdated(
33        capsuleUUID: UInt64,
34        address: Address,
35        codeHash: [UInt8],
36        contractIdentifier: String,
37        action: String
38    )
39    /// Emitted when emulated contract migrations have been completed, where failedContracts are named by their
40    /// contract identifier - A.ADDRESS.NAME where ADDRESS is the host address without 0x
41    access(all) event EmulatedMigrationResultCommitted(
42        snapshotTimestamp: UFix64,
43        committedTimestamp: UFix64,
44        failedContracts: [String]
45    )
46    /// Emitted when the stagingCutoff value is updated
47    access(all) event StagingCutoffUpdated(old: UInt64?, new: UInt64?)
48
49
50    /********************
51        Public Methods
52     ********************/
53
54    /* --- Staging Methods --- */
55
56    /// 1 - Create a host and save it in your contract-hosting account at MigrationContractStaging.HostStoragePath
57    ///
58    /// Creates a Host serving as identification for the contract account. Reference to this resource identifies the
59    /// calling address so it must be saved in account storage before being used to stage a contract update.
60    ///
61    access(all) fun createHost(): @Host {
62        return <- create Host()
63    }
64
65    /// 2 - Call stageContract() with the host reference and contract name and contract code you wish to stage
66    ///
67    /// Stages the contract code for the given contract name at the host address. If the contract is already staged,
68    /// the code will be replaced.
69    /// NOTE: making updates to staged code resets validation status for that contract.
70    ///
71    access(all) fun stageContract(host: &Host, name: String, code: String) {
72        pre {
73            self.isStagingPeriodActive(): "Staging period has ended"
74        }
75        let capsulePath = self.deriveCapsuleStoragePath(contractAddress: host.address(), contractName: name)
76        if self.stagedContracts[host.address()] == nil {
77            // First time we're seeing contracts from this address - insert the address and contract name
78            self.stagedContracts.insert(key: host.address(), [name])
79            // Create a new Capsule to store the staged code
80            let capsule <- self.createCapsule(host: host, name: name, code: code)
81            self.account.storage.save(<-capsule, to: capsulePath)
82            return
83        }
84        // We've seen contracts from this host address before - check if the contract is already staged
85        if let contractIndex = self.stagedContracts[host.address()]!.firstIndex(of: name) {
86            // The contract is already staged - replace the code
87            let capsule = self.account.storage.borrow<&Capsule>(from: capsulePath)
88                ?? panic("Could not borrow existing Capsule from storage for staged contract")
89            capsule.replaceCode(code: code)
90            return
91        }
92        // First time staging this contract - add the contract name to the list of contracts staged for host
93        self.stagedContracts[host.address()]!.append(name)
94        self.account.storage.save(<-self.createCapsule(host: host, name: name, code: code), to: capsulePath)
95    }
96
97    /// Removes the staged contract code from the staging environment.
98    ///
99    access(all) fun unstageContract(host: &Host, name: String) {
100        pre {
101            self.isStagingPeriodActive(): "Staging period has ended"
102        }
103        post {
104            !self.isStaged(address: host.address(), name: name): "Contract is still staged"
105        }
106        let address = host.address()
107        if self.stagedContracts[address] == nil {
108            return
109        }
110        let capsuleUUID = self.removeStagedContract(address: address, name: name)
111            ?? panic("Problem destroying update Capsule")
112        emit StagingStatusUpdated(
113            capsuleUUID: capsuleUUID,
114            address: address,
115            codeHash: [],
116            contractIdentifier: name,
117            action: "unstage"
118        )
119    }
120
121    /* --- Public Getters --- */
122
123    /// Returns the last block height at which updates can be staged
124    ///
125    access(all) view fun getStagingCutoff(): UInt64? {
126        return self.stagingCutoff
127    }
128
129    /// Returns whether the staging period is currently active
130    ///
131    access(all) view fun isStagingPeriodActive(): Bool {
132        return self.stagingCutoff == nil || getCurrentBlock().height <= self.stagingCutoff!
133    }
134
135    /// Returns true if the contract is currently staged.
136    ///
137    access(all) view fun isStaged(address: Address, name: String): Bool {
138        return self.stagedContracts[address]?.contains(name) ?? false
139    }
140
141    /// Returns true if the contract is currently validated and nil if it's not staged.
142    ///
143    access(all) view fun isValidated(address: Address, name: String): Bool? {
144        return self.getStagedContractUpdate(address: address, name: name)?.isValidated() ?? nil
145    }
146
147    /// Returns the names of all staged contracts for the given address.
148    ///
149    access(all) view fun getStagedContractNames(forAddress: Address): [String] {
150        return self.stagedContracts[forAddress] ?? []
151    }
152
153    /// Returns the staged contract Cadence code for the given address and name.
154    ///
155    access(all) view fun getStagedContractCode(address: Address, name: String): String? {
156        return self.getStagedContractUpdate(address: address, name: name)?.code
157    }
158
159    /// Returns the staged contract code hash for the given address and name or nil if it's not staged
160    ///
161    access(all) view fun getStagedContractCodeHash(address: Address, name: String): [UInt8]? {
162        if let update = self.getStagedContractUpdate(address: address, name: name) {
163            return self.getCodeHash(update.code)
164        }
165        return nil
166    }
167
168    /// Returns the ContractUpdate struct for the given contract if it's been staged.
169    ///
170    access(all) view fun getStagedContractUpdate(address: Address, name: String): ContractUpdate? {
171        let capsulePath = self.deriveCapsuleStoragePath(contractAddress: address, contractName: name)
172        if let capsule = self.account.storage.borrow<&Capsule>(from: capsulePath) {
173            return capsule.getContractUpdate()
174        } else {
175            return nil
176        }
177    }
178
179    /// Returns an array of all staged contract host addresses.
180    ///
181    access(all) view fun getAllStagedContractHosts(): [Address] {
182        return self.stagedContracts.keys
183    }
184
185    /// Returns a dictionary of all staged contract code for the given address.
186    ///
187    access(all) view fun getAllStagedContractCode(forAddress: Address): {String: String} {
188        let contractNames = self.stagedContracts[forAddress]
189        if contractNames == nil {
190            return {}
191        }
192        let stagedCode: {String: String} = {}
193        for name in contractNames! {
194            if let update = self.getStagedContractUpdate(address: forAddress, name: name) {
195                stagedCode[update.name] = update.code
196            }
197        }
198        return stagedCode
199    }
200
201    /// Returns all staged contracts as a mapping of address to an array of contract names
202    ///
203    access(all) view fun getAllStagedContracts(): {Address: [String]} {
204        return self.stagedContracts
205    }
206
207    /// Returns a StoragePath to store the Capsule of the form:
208    ///     /storage/self.capsulePathPrefix_ADDRESS_NAME
209    access(all) view fun deriveCapsuleStoragePath(contractAddress: Address, contractName: String): StoragePath {
210        let identifier = self.capsulePathPrefix
211            .concat(self.delimiter)
212            .concat(contractAddress.toString())
213            .concat(self.delimiter)
214            .concat(contractName)
215        return StoragePath(identifier: identifier)
216            ?? panic("Could not derive Capsule StoragePath for given address")
217    }
218
219    /* --- Util --- */
220
221    /// Returns the hash of the given code, hashing with SHA3-256
222    ///
223    access(all) view fun getCodeHash(_ code: String): [UInt8] {
224        return HashAlgorithm.SHA3_256.hash(code.utf8)
225    }
226
227    /* ------------------------------------------------------------------------------------------------------------ */
228    /* ------------------------------------------------ Constructs ------------------------------------------------ */
229    /* ------------------------------------------------------------------------------------------------------------ */
230
231
232    /*******************************
233        EmulatedMigrationResult
234     *******************************/
235
236    /// Represents the results of an emulated contract migration, containing the start & committed time and any failed
237    /// contract names. If a contract was staged by the start time and is not listed in failedContracts, its migration
238    /// can be considered successful
239    ///
240    access(all) struct EmulatedMigrationResult {
241        /// Timestamp that the migration snapshot was taken
242        access(all) let snapshot: UFix64
243        /// Timestamp that the migration results were committed
244        access(all) let committed: UFix64
245        /// Identifiers of the contracts that failed validation during the emulated migration
246        access(all) let failedContracts: [String]
247
248        init(snapshot: UFix64, failedContracts: [String]) {
249            post {
250                self.snapshot < self.committed: "Snapshot must be in the past"
251            }
252            self.snapshot = snapshot
253            self.committed = getCurrentBlock().timestamp
254            self.failedContracts = failedContracts
255        }
256    }
257
258    /********************
259        ContractUpdate
260     ********************/
261
262    /// Represents contract and its corresponding code.
263    ///
264    access(all) struct ContractUpdate {
265        /// Address of the contract host
266        access(all) let address: Address
267        /// Name of the contract
268        access(all) let name: String
269        /// The updated Cadence 1.0 code
270        access(all) var code: String
271        /// Timestamp the code was last updated
272        access(all) var lastUpdated: UFix64
273
274        init(address: Address, name: String, code: String) {
275            self.address = address
276            self.name = name
277            self.code = code
278            self.lastUpdated = getCurrentBlock().timestamp
279        }
280
281        /// Validates that the named contract exists at the target address.
282        ///
283        access(all) view fun exists(): Bool {
284            return getAccount(self.address).contracts.names.contains(self.name)
285        }
286
287        /// Serializes the address and name into a string of the form 0xADDRESS.NAME
288        ///
289        access(all) view fun toString(): String {
290            return self.address.toString().concat(".").concat(self.name)
291        }
292
293        /// Serializes contact into its string identifier of the form A.ADDRESS.NAME where ADDRESS is lacks 0x
294        ///
295        access(all) view fun identifier(): String {
296            let sans0x = self.address.toString().slice(from: 2, upTo: self.address.toString().length)
297            let prefix = "A".concat(".").concat(sans0x).concat(".")
298            return prefix.concat(self.name)
299        }
300
301        /// Returns whether this contract update passed the last emulated migration, validating the contained code.
302        /// NOTE: false could mean validation hasn't begun, the code wasn't included in emulation, or validation failed
303        ///
304        access(all) view fun isValidated(): Bool {
305            // This code was contained in the last emulated migration and didn't fail
306            if let lastEmulatedMigrationResult = MigrationContractStaging.lastEmulatedMigrationResult {
307                return self.lastUpdated < lastEmulatedMigrationResult.snapshot 
308                    && !lastEmulatedMigrationResult.failedContracts.contains(self.identifier())
309            }
310            return false
311        }
312
313        /// Replaces the ContractUpdate code with that provided.
314        ///
315        access(contract) fun replaceCode(_ code: String) {
316            self.code = code
317            self.lastUpdated = getCurrentBlock().timestamp
318        }
319    }
320
321    /********************
322            Host
323     ********************/
324
325    /// Serves as identification for a caller's address.
326    /// NOTE: Should be saved in storage and access safeguarded as reference grants access to contract staging. If a
327    /// contract host wishes to delegate staging to another account (e.g. multisig account setup enabling a developer
328    /// to stage on its behalf), it should create a PRIVATE Host capability and publish it to the receiving account.
329    ///
330    access(all) resource Host {
331        /// Returns the resource owner's address
332        ///
333        access(all) view fun address(): Address {
334            return self.owner?.address ?? panic("Host is unowned!")
335        }
336    }
337
338    /********************
339            Capsule
340     ********************/
341
342    /// Resource that stores pending contract updates in a ContractUpdate struct. On staging a contract update for the
343    /// first time, a Capsule will be created and stored in this contract account. Any time a stageContract() call is
344    /// made again for the same contract, the code in the Capsule will be replaced. As you see, the Capsule is merely
345    /// intended to store the code, as contract updates will be executed by state migration across the network at the
346    /// Cadence 1.0 milestone.
347    ///
348    access(all) resource Capsule {
349        /// The address, name and code of the contract that will be updated.
350        access(self) let update: ContractUpdate
351
352        init(update: ContractUpdate) {
353            pre {
354                update.exists(): "Target contract does not exist"
355            }
356            self.update = update
357        }
358
359        /// Returns the staged contract update in the form of a ContractUpdate struct.
360        ///
361        access(all) view fun getContractUpdate(): ContractUpdate {
362            return self.update
363        }
364
365        /// Replaces the staged contract code with the given updated Cadence code.
366        ///
367        access(contract) fun replaceCode(code: String) {
368            self.update.replaceCode(code)
369            emit StagingStatusUpdated(
370                capsuleUUID: self.uuid,
371                address: self.update.address,
372                codeHash: MigrationContractStaging.getCodeHash(code),
373                contractIdentifier: self.update.name,
374                action: "replace"
375            )
376        }
377    }
378
379    /********************
380            Admin
381     ********************/
382
383    /// Admin resource for updating the stagingCutoff value
384    ///
385    access(all) resource Admin {
386
387        /// Sets the block height at which updates can no longer be staged
388        ///
389        access(all) fun setStagingCutoff(at height: UInt64?) {
390            pre {
391                height == nil || height! > getCurrentBlock().height:
392                    "Height must be nil or greater than current block height"
393            }
394            emit StagingCutoffUpdated(old: MigrationContractStaging.stagingCutoff, new: height)
395            MigrationContractStaging.stagingCutoff = height
396        }
397
398        /// Commits the results of an emulated contract migration
399        ///
400        access(all) fun commitMigrationResults(snapshot: UFix64, failed: [String]) {
401            MigrationContractStaging.lastEmulatedMigrationResult = EmulatedMigrationResult(
402                snapshot: snapshot,
403                failedContracts: failed
404            )
405            emit EmulatedMigrationResultCommitted(
406                snapshotTimestamp: snapshot,
407                committedTimestamp: MigrationContractStaging.lastEmulatedMigrationResult!.committed,
408                failedContracts: failed
409            )
410        }
411    }
412
413    /*********************
414        Internal Methods
415     *********************/
416
417    /// Creates a Capsule resource with the given Host and ContractUpdate. Will be stored at the derived path in this
418    /// contract's account storage.
419    ///
420    access(self) fun createCapsule(host: &Host, name: String, code: String): @Capsule {
421        let update = ContractUpdate(address: host.address(), name: name, code: code)
422        let capsule <- create Capsule(update: update)
423        emit StagingStatusUpdated(
424            capsuleUUID: capsule.uuid,
425            address: host.address(),
426            codeHash: MigrationContractStaging.getCodeHash(code),
427            contractIdentifier: name,
428            action: "stage"
429        )
430        return <- capsule
431    }
432
433    /// Removes the staged update's Capsule from storage and returns the UUID of the removed Capsule or nil if it
434    /// wasn't found. Also removes the contract name from the stagedContracts mapping.
435    ///
436    access(self) fun removeStagedContract(address: Address, name: String): UInt64? {
437        let contractIndex = self.stagedContracts[address]!.firstIndex(of: name)!
438        self.stagedContracts[address]!.remove(at: contractIndex)
439        // Remove the Address from the stagedContracts mapping if it has no staged contracts remain for the host address
440        if self.stagedContracts[address]!.length == 0 {
441            self.stagedContracts.remove(key: address)
442        }
443        return self.destroyCapsule(address: address, name: name)
444    }
445
446    /// Destroys the Capsule resource at the derived path in this contract's account storage and returns the UUID of
447    /// the destroyed Capsule if it existed.
448    ///
449    access(self) fun destroyCapsule(address: Address, name: String): UInt64? {
450        let capsulePath = self.deriveCapsuleStoragePath(contractAddress: address, contractName: name)
451        if let capsule <- self.account.storage.load<@Capsule>(from: capsulePath) {
452            let capsuleUUID = capsule.uuid
453            destroy capsule
454            return capsuleUUID
455        }
456        return nil
457    }
458
459    init() {
460        self.delimiter = "_"
461        self.HostStoragePath = StoragePath(
462                identifier: "MigrationContractStagingHost".concat(self.delimiter).concat(self.account.address.toString())
463            ) ?? panic("Could not derive Host StoragePath")
464        self.AdminStoragePath = /storage/MigrationContractStagingAdmin
465        self.capsulePathPrefix = "MigrationContractStagingCapsule"
466            .concat(self.delimiter)
467            .concat(self.account.address.toString())
468        self.stagedContracts = {}
469        self.stagingCutoff = nil
470        self.lastEmulatedMigrationResult = nil
471
472        self.account.storage.save(<-create Admin(), to: self.AdminStoragePath)
473    }
474}
475