From 0932685a270fe299621dd38cea6e0f29485dd656 Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Wed, 15 May 2024 18:01:42 +0200 Subject: [PATCH 1/2] Add dependency audit contract --- contracts/DependencyAudit.cdc | 110 +++++++++++++++++++++++++++++++ contracts/FlowServiceAccount.cdc | 23 +++++-- 2 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 contracts/DependencyAudit.cdc diff --git a/contracts/DependencyAudit.cdc b/contracts/DependencyAudit.cdc new file mode 100644 index 000000000..8c8451e41 --- /dev/null +++ b/contracts/DependencyAudit.cdc @@ -0,0 +1,110 @@ +import "MigrationContractStaging" + +// This contract is intended to be deployed on the service account. +// It is used by the FVM calling the `checkDependencies` function from a function of the same name and singature in the FlowServiceAccount contract, +// at the end of every transaction. +// The `dependenciesAddresses` and `dependenciesNames` will be all the dependencies needded to run that transaction. +// +// The `checkDependencies` function will check if any of the dependencies are not staged in the MigrationContractStaging contract. +// 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. +access(all) contract DependencyAudit { + + access(all) let AdministratorStoragePath: StoragePath + + // The system addresses have contracts that will not be stages via the migration contract so we exclude them from the dependency chekcs + access(self) var excludedAddresses: {Address: Bool} + + access(all) var panicOnUnstaged: Bool + + access(all) event UnstagedDependencies(dependenciesAddresses: [Address], dependenciesNames: [String]) + + access(all) event PanicOnUnstagedDependenciesChanged(shouldPanic: Bool) + + // checkDependencies is called from the FlowServiceAccount contract + access(self) fun checkDependencies(_ dependenciesAddresses: [Address], _ dependenciesNames: [String], _ authorizers: [Address]) { + var numDependencies = dependenciesAddresses.length + var i = 0 + + var unstagedDependenciesAddresses: [Address] = [] + var unstagedDependenciesNames: [String] = [] + + while i < numDependencies { + let isExcluded = DependencyAudit.excludedAddresses[dependenciesAddresses[i]] ?? false + if isExcluded { + i = i + 1 + continue + } + + let staged = MigrationContractStaging.isStaged(address: dependenciesAddresses[i], name: dependenciesNames[i]) + if !staged { + unstagedDependenciesAddresses.append(dependenciesAddresses[i]) + unstagedDependenciesNames.append(dependenciesNames[i]) + } + + i = i + 1 + } + + if unstagedDependenciesAddresses.length > 0 { + if DependencyAudit.panicOnUnstaged { + // If `panicOnUnstaged` is set to true, the transaction will panic if there are any unstaged dependencies + // the panic message will include the unstaged dependencies + var unstagedDependenciesString = "" + var numUnstagedDependencies = unstagedDependenciesAddresses.length + var j = 0 + while j < numUnstagedDependencies { + if j > 0 { + unstagedDependenciesString = unstagedDependenciesString.concat(", ") + } + unstagedDependenciesString = unstagedDependenciesString.concat("A.").concat(unstagedDependenciesAddresses[j].toString()).concat(".").concat(unstagedDependenciesNames[j]) + + j = j + 1 + } + + // the transactions will fail with a message that looks like this: `error: panic: Found unstaged dependencies: A.0x2ceae959ed1a7e7a.MigrationContractStaging, A.0x2ceae959ed1a7e7a.DependencyAudit` + panic("Found unstaged dependencies: ".concat(unstagedDependenciesString)) + } else { + emit UnstagedDependencies(dependenciesAddresses: unstagedDependenciesAddresses, dependenciesNames: unstagedDependenciesNames) + } + } + } + + // The Administrator resorce can be used to add or remove addresses from the excludedAddresses dictionary + // + access(all) resource Administrator { + // addExcludedAddresses add the addresses to the excludedAddresses dictionary + access(all) fun addExcludedAddresses(addresses: [Address]) { + for address in addresses { + DependencyAudit.excludedAddresses[address] = true + } + } + + // removeExcludedAddresses remove the addresses from the excludedAddresses dictionary + access(all) fun removeExcludedAddresses(addresses: [Address]) { + for address in addresses { + DependencyAudit.excludedAddresses.remove(key: address) + } + } + + // setPanicOnUnstagedDependencies sets the `panicOnUnstaged` variable to the value of `shouldPanic` + access(all) fun setPanicOnUnstagedDependencies(shouldPanic: Bool) { + DependencyAudit.panicOnUnstaged = shouldPanic + emit PanicOnUnstagedDependenciesChanged(shouldPanic: shouldPanic) + } + } + + // The admin resource is saved to the storage so that the admin can be accessed by the service account + // The `excludedAddresses` will be the addresses with the system contracracts. + init(excludedAddresses: [Address]) { + self.excludedAddresses = {} + self.panicOnUnstaged = false + + self.AdministratorStoragePath = /storage/flowDependencyAuditAdmin + + for address in excludedAddresses { + self.excludedAddresses[address] = true + } + + let admin <- create Administrator() + self.account.save(<-admin, to: self.AdministratorStoragePath) + } +} diff --git a/contracts/FlowServiceAccount.cdc b/contracts/FlowServiceAccount.cdc index 5b93ccbfc..642716146 100644 --- a/contracts/FlowServiceAccount.cdc +++ b/contracts/FlowServiceAccount.cdc @@ -2,6 +2,7 @@ import FungibleToken from "FungibleToken" import FlowToken from 0xFLOWTOKENADDRESS import FlowFees from 0xFLOWFEESADDRESS import FlowStorageFees from 0xFLOWSTORAGEFEESADDRESS +import DependencyAudit from "DependencyAudit" pub contract FlowServiceAccount { @@ -78,7 +79,7 @@ pub contract FlowServiceAccount { if self.transactionFee > tokenVault.balance { feeAmount = tokenVault.balance } - + let feeVault <- tokenVault.withdraw(amount: feeAmount) FlowFees.deposit(from: <-feeVault) } @@ -128,21 +129,21 @@ pub contract FlowServiceAccount { return self.accountCreators.keys } - // Gets Execution Effort Weights from the service account's storage + // Gets Execution Effort Weights from the service account's storage pub fun getExecutionEffortWeights(): {UInt64: UInt64} { - return self.account.copy<{UInt64: UInt64}>(from: /storage/executionEffortWeights) + return self.account.copy<{UInt64: UInt64}>(from: /storage/executionEffortWeights) ?? panic("execution effort weights not set yet") } - // Gets Execution Memory Weights from the service account's storage + // Gets Execution Memory Weights from the service account's storage pub fun getExecutionMemoryWeights(): {UInt64: UInt64} { - return self.account.copy<{UInt64: UInt64}>(from: /storage/executionMemoryWeights) + return self.account.copy<{UInt64: UInt64}>(from: /storage/executionMemoryWeights) ?? panic("execution memory weights not set yet") } // Gets Execution Memory Limit from the service account's storage pub fun getExecutionMemoryLimit(): UInt64 { - return self.account.copy(from: /storage/executionMemoryLimit) + return self.account.copy(from: /storage/executionMemoryLimit) ?? panic("execution memory limit not set yet") } @@ -191,6 +192,16 @@ pub contract FlowServiceAccount { } } + /// checkDependencies is called by the FVM at the end of the transaction execution + /// The `dependenciesAddresses` and `dependenciesNames` are the addresses and names of all the contracts that the transaction depends on. + /// The `authorizers` are the addresses of the accounts that authorized the transaction and the payer. + /// The FVM can call this even though it is private. + /// checkDependencies is not intended to be called by the user. + access(self) fun checkDependencies(_ dependenciesAddresses: [Address], _ dependenciesNames: [String], _ authorizers: [Address]) { + // forwad the call to the DependencyAudit, where the actual check is done. + DependencyAudit.checkDependencies(dependenciesAddresses, dependenciesNames, authorizers) + } + init() { self.transactionFee = 0.0 self.accountCreationFee = 0.0 From 18e49e255fbe988ef31c7d90b27e25bfa424ce8f Mon Sep 17 00:00:00 2001 From: Janez Podhostnik Date: Thu, 16 May 2024 15:09:27 +0200 Subject: [PATCH 2/2] move DependencyAudit to separate repo --- contracts/DependencyAudit.cdc | 110 ---------------------------------- 1 file changed, 110 deletions(-) delete mode 100644 contracts/DependencyAudit.cdc diff --git a/contracts/DependencyAudit.cdc b/contracts/DependencyAudit.cdc deleted file mode 100644 index 8c8451e41..000000000 --- a/contracts/DependencyAudit.cdc +++ /dev/null @@ -1,110 +0,0 @@ -import "MigrationContractStaging" - -// This contract is intended to be deployed on the service account. -// It is used by the FVM calling the `checkDependencies` function from a function of the same name and singature in the FlowServiceAccount contract, -// at the end of every transaction. -// The `dependenciesAddresses` and `dependenciesNames` will be all the dependencies needded to run that transaction. -// -// The `checkDependencies` function will check if any of the dependencies are not staged in the MigrationContractStaging contract. -// 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. -access(all) contract DependencyAudit { - - access(all) let AdministratorStoragePath: StoragePath - - // The system addresses have contracts that will not be stages via the migration contract so we exclude them from the dependency chekcs - access(self) var excludedAddresses: {Address: Bool} - - access(all) var panicOnUnstaged: Bool - - access(all) event UnstagedDependencies(dependenciesAddresses: [Address], dependenciesNames: [String]) - - access(all) event PanicOnUnstagedDependenciesChanged(shouldPanic: Bool) - - // checkDependencies is called from the FlowServiceAccount contract - access(self) fun checkDependencies(_ dependenciesAddresses: [Address], _ dependenciesNames: [String], _ authorizers: [Address]) { - var numDependencies = dependenciesAddresses.length - var i = 0 - - var unstagedDependenciesAddresses: [Address] = [] - var unstagedDependenciesNames: [String] = [] - - while i < numDependencies { - let isExcluded = DependencyAudit.excludedAddresses[dependenciesAddresses[i]] ?? false - if isExcluded { - i = i + 1 - continue - } - - let staged = MigrationContractStaging.isStaged(address: dependenciesAddresses[i], name: dependenciesNames[i]) - if !staged { - unstagedDependenciesAddresses.append(dependenciesAddresses[i]) - unstagedDependenciesNames.append(dependenciesNames[i]) - } - - i = i + 1 - } - - if unstagedDependenciesAddresses.length > 0 { - if DependencyAudit.panicOnUnstaged { - // If `panicOnUnstaged` is set to true, the transaction will panic if there are any unstaged dependencies - // the panic message will include the unstaged dependencies - var unstagedDependenciesString = "" - var numUnstagedDependencies = unstagedDependenciesAddresses.length - var j = 0 - while j < numUnstagedDependencies { - if j > 0 { - unstagedDependenciesString = unstagedDependenciesString.concat(", ") - } - unstagedDependenciesString = unstagedDependenciesString.concat("A.").concat(unstagedDependenciesAddresses[j].toString()).concat(".").concat(unstagedDependenciesNames[j]) - - j = j + 1 - } - - // the transactions will fail with a message that looks like this: `error: panic: Found unstaged dependencies: A.0x2ceae959ed1a7e7a.MigrationContractStaging, A.0x2ceae959ed1a7e7a.DependencyAudit` - panic("Found unstaged dependencies: ".concat(unstagedDependenciesString)) - } else { - emit UnstagedDependencies(dependenciesAddresses: unstagedDependenciesAddresses, dependenciesNames: unstagedDependenciesNames) - } - } - } - - // The Administrator resorce can be used to add or remove addresses from the excludedAddresses dictionary - // - access(all) resource Administrator { - // addExcludedAddresses add the addresses to the excludedAddresses dictionary - access(all) fun addExcludedAddresses(addresses: [Address]) { - for address in addresses { - DependencyAudit.excludedAddresses[address] = true - } - } - - // removeExcludedAddresses remove the addresses from the excludedAddresses dictionary - access(all) fun removeExcludedAddresses(addresses: [Address]) { - for address in addresses { - DependencyAudit.excludedAddresses.remove(key: address) - } - } - - // setPanicOnUnstagedDependencies sets the `panicOnUnstaged` variable to the value of `shouldPanic` - access(all) fun setPanicOnUnstagedDependencies(shouldPanic: Bool) { - DependencyAudit.panicOnUnstaged = shouldPanic - emit PanicOnUnstagedDependenciesChanged(shouldPanic: shouldPanic) - } - } - - // The admin resource is saved to the storage so that the admin can be accessed by the service account - // The `excludedAddresses` will be the addresses with the system contracracts. - init(excludedAddresses: [Address]) { - self.excludedAddresses = {} - self.panicOnUnstaged = false - - self.AdministratorStoragePath = /storage/flowDependencyAuditAdmin - - for address in excludedAddresses { - self.excludedAddresses[address] = true - } - - let admin <- create Administrator() - self.account.save(<-admin, to: self.AdministratorStoragePath) - } -}