Smart Contract
MigrationContractStaging
A.56100d46aa9b0212.MigrationContractStaging
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