Smart Contract
DependencyAudit
A.e467b9dd11fa00df.DependencyAudit
1import MigrationContractStaging from 0x56100d46aa9b0212
2
3// This contract is is used by the FVM calling the `checkDependencies` function from a function of the same name and singnature in the FlowServiceAccount contract,
4// at the end of every transaction.
5// The `dependenciesAddresses` and `dependenciesNames` will be all the dependencies needded to run that transaction.
6//
7// The `checkDependencies` function will check if any of the dependencies are not staged in the MigrationContractStaging contract.
8// If any of the dependencies are not staged, the function will emit an event with the unstaged dependencies, or panic if `panicOnUnstaged` is set to true.
9access(all) contract DependencyAudit {
10
11 access(all) let AdministratorStoragePath: StoragePath
12
13 // The system addresses have contracts that will not be stages via the migration contract so we exclude them from the dependency chekcs
14 access(self) var excludedAddresses: {Address: Bool}
15
16 access(all) var panicOnUnstaged: Bool
17
18 access(all) event UnstagedDependencies(dependencies: [Dependency])
19
20 access(all) event PanicOnUnstagedDependenciesChanged(shouldPanic: Bool)
21
22 access(all) event BlockBoundariesChanged(start: UInt64?, end: UInt64?)
23
24 // checkDependencies is called from the FlowServiceAccount contract
25 access(account) fun checkDependencies(_ dependenciesAddresses: [Address], _ dependenciesNames: [String], _ authorizers: [Address]) {
26 var unstagedDependencies: [Dependency] = []
27
28 var numDependencies = dependenciesAddresses.length
29 var i = 0
30 while i < numDependencies {
31 let isExcluded = DependencyAudit.excludedAddresses[dependenciesAddresses[i]] ?? false
32 if isExcluded {
33 i = i + 1
34 continue
35 }
36
37 let staged = MigrationContractStaging.isStaged(address: dependenciesAddresses[i], name: dependenciesNames[i])
38 if !staged {
39 unstagedDependencies.append(Dependency(address: dependenciesAddresses[i], name: dependenciesNames[i]))
40 }
41
42 i = i + 1
43 }
44
45 if unstagedDependencies.length > 0 {
46 self.maybePanicOnUnstagedDependencies(unstagedDependencies)
47
48 emit UnstagedDependencies(dependencies: unstagedDependencies)
49 }
50 }
51
52 access(self) fun maybePanicOnUnstagedDependencies(_ unstagedDependencies: [Dependency]) {
53 // If `panicOnUnstaged` is set to false, the function will return without panicking
54 // Then check if we should panic randomly
55 if !DependencyAudit.panicOnUnstaged || !self.shouldPanicRandomly() {
56 return
57 }
58
59 var unstagedDependenciesString = ""
60 var numUnstagedDependencies = unstagedDependencies.length
61 var j = 0
62 while j < numUnstagedDependencies {
63 if j > 0 {
64 unstagedDependenciesString = unstagedDependenciesString.concat(", ")
65 }
66 unstagedDependenciesString = unstagedDependenciesString.concat(unstagedDependencies[j].toString())
67
68 j = j + 1
69 }
70
71 // the transactions will fail with a message that looks like this: `error: panic: Found unstaged dependencies: A.2ceae959ed1a7e7a.MigrationContractStaging, A.2ceae959ed1a7e7a.DependencyAudit`
72 panic("This transaction is using dependencies not staged for Crescendo upgrade coming soon! Learn more: https://bit.ly/FLOWCRESCENDO. Dependencies not staged: ".concat(unstagedDependenciesString))
73 }
74
75 // shouldPanicRandomly is used to randomly panic on unstaged dependencies
76 // The probability of panicking is based on the current block height and the start and end block heights
77 // If the start block height is greater than or equal to the end block height, the function will return true
78 // The function will always return true if the current block is more than the end block height
79 // The function will always return false if the current block is less than the start block height
80 // The function will return true if a random number between the start and end block heights is less than the current block height
81 // This means the probability of panicking increases linearly as the current block height approaches the end block height
82 access(self) fun shouldPanicRandomly(): Bool {
83 // get start block height or true
84 // get end block height or true
85 // get current block height
86 // get random number between start and end
87 // if random number is less than current block return true
88 // else return false
89
90 let maybeBoundaries = self.getBoundaries()
91 if maybeBoundaries == nil {
92 // if settings are invalid use default behaviour: panic true
93 return true
94 }
95 let boundaries = maybeBoundaries!
96
97 let startBlock: UInt64 = boundaries.start
98 let endBlock: UInt64 = boundaries.end
99 let currentBlock: UInt64 = getCurrentBlock().height
100
101 if startBlock >= endBlock {
102 // this should never happen becuse we validate the boundaries when setting them
103 // if settings are invalid use default behaviour: panic true
104 return true
105 }
106
107 let dif = endBlock - startBlock
108 var rnd = revertibleRandom<UInt64>() % dif
109 rnd = rnd + startBlock
110
111 // fail if the random number is less than the current block
112 return rnd < currentBlock
113 }
114
115 access(all) struct Boundaries {
116 access(all) let start: UInt64
117 access(all) let end: UInt64
118
119 init(start: UInt64, end: UInt64) {
120 self.start = start
121 self.end = end
122 }
123 }
124
125 access(all) fun getBoundaries(): Boundaries? {
126 return self.account.storage.copy<Boundaries>(from: /storage/flowDependencyAuditBoundaries)
127 }
128
129 access(all) fun getCurrentFailureProbability(): UFix64 {
130 if !DependencyAudit.panicOnUnstaged {
131 return 0.0 as UFix64
132 }
133
134 let maybeBoundaries = self.getBoundaries()
135 if maybeBoundaries == nil {
136 return 1.0 as UFix64
137 }
138
139 let boundaries = maybeBoundaries!
140
141 let startBlock: UInt64 = boundaries.start
142 let endBlock: UInt64 = boundaries.end
143 let currentBlock: UInt64 = getCurrentBlock().height
144
145 if startBlock >= endBlock {
146 return 1.0 as UFix64
147 }
148 if currentBlock >= endBlock {
149 return 1.0 as UFix64
150 }
151 if currentBlock < startBlock {
152 return 0.0 as UFix64
153 }
154
155 let dif = endBlock - startBlock
156 let currentDif = currentBlock - startBlock
157
158 return UFix64(currentDif) / UFix64(dif)
159 }
160
161 access(self) fun setBoundaries(boundaries: Boundaries) {
162 self.account.storage.load<Boundaries>(from: /storage/flowDependencyAuditBoundaries)
163 self.account.storage.save(boundaries, to: /storage/flowDependencyAuditBoundaries)
164 }
165
166 access(self) fun unsetBoundaries() {
167 self.account.storage.load<Boundaries>(from: /storage/flowDependencyAuditBoundaries)
168 }
169
170 // The Administrator resorce can be used to add or remove addresses from the excludedAddresses dictionary
171 //
172 access(all) resource Administrator {
173 // addExcludedAddresses add the addresses to the excludedAddresses dictionary
174 access(all) fun addExcludedAddresses(addresses: [Address]) {
175 for address in addresses {
176 DependencyAudit.excludedAddresses[address] = true
177 }
178 }
179
180 // removeExcludedAddresses remove the addresses from the excludedAddresses dictionary
181 access(all) fun removeExcludedAddresses(addresses: [Address]) {
182 for address in addresses {
183 DependencyAudit.excludedAddresses.remove(key: address)
184 }
185 }
186
187 // setPanicOnUnstagedDependencies sets the `panicOnUnstaged` variable to the value of `shouldPanic`
188 access(all) fun setPanicOnUnstagedDependencies(shouldPanic: Bool) {
189 DependencyAudit.panicOnUnstaged = shouldPanic
190 emit PanicOnUnstagedDependenciesChanged(shouldPanic: shouldPanic)
191 }
192
193 // setStartEndBlock sets the start and end block heights for the `shouldPanicRandomly` function
194 access(all) fun setStartEndBlock(start: UInt64, end: UInt64) {
195 pre {
196 start < end: "Start block height must be less than end block height"
197 }
198
199 let boundaries = Boundaries(start: start, end: end)
200 DependencyAudit.setBoundaries(boundaries: boundaries)
201 emit BlockBoundariesChanged(start: start, end: end)
202 }
203
204 // unsetStartEndBlock unsets the start and end block heights for the `shouldPanicRandomly` function
205 access(all) fun unsetStartEndBlock() {
206 DependencyAudit.unsetBoundaries()
207 emit BlockBoundariesChanged(start: nil, end: nil)
208 }
209
210 // testCheckDependencies is used for testing purposes
211 // It will call the `checkDependencies` function with the provided dependencies
212 // `checkDependencies` is otherwise not callable from the outside
213 access(all) fun testCheckDependencies(_ dependenciesAddresses: [Address], _ dependenciesNames: [String], _ authorizers: [Address]) {
214 return DependencyAudit.checkDependencies(dependenciesAddresses, dependenciesNames, authorizers)
215 }
216 }
217
218 access(all) struct Dependency {
219 access(all) let address: Address
220 access(all) let name: String
221
222 init(address: Address, name: String) {
223 self.address = address
224 self.name = name
225 }
226
227 access(all) fun toString(): String {
228 var addressString = self.address.toString()
229 // remove 0x prefix
230 addressString = addressString.slice(from: 2, upTo: addressString.length)
231 return "A.".concat(addressString).concat(".").concat(self.name)
232 }
233 }
234
235 // The admin resource is saved to the storage so that the admin can be accessed by the service account
236 // The `excludedAddresses` will be the addresses with the system contracts.
237 init(excludedAddresses: [Address]) {
238 self.excludedAddresses = {}
239 self.panicOnUnstaged = false
240
241 self.AdministratorStoragePath = /storage/flowDependencyAuditAdmin
242
243 for address in excludedAddresses {
244 self.excludedAddresses[address] = true
245 }
246
247 let admin <- create Administrator()
248 self.account.storage.save(<-admin, to: self.AdministratorStoragePath)
249 }
250}
251