Smart Contract

DependencyAudit

A.e467b9dd11fa00df.DependencyAudit

Deployed

2w ago
Feb 14, 2026, 03:27:15 PM UTC

Dependents

0 imports
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