diff --git a/contracts/FlowExecutionParameters.cdc b/contracts/FlowExecutionParameters.cdc index 3375a47e..7cdb1dc5 100644 --- a/contracts/FlowExecutionParameters.cdc +++ b/contracts/FlowExecutionParameters.cdc @@ -1,18 +1,21 @@ +/// FlowExecutionParameters stores the parameters for metering +/// transaction fees for Flow transactions + access(all) contract FlowExecutionParameters { - // Gets Execution Effort Weights from the service account's storage + // Gets Execution Effort Weights from the parameters account's storage access(all) view fun getExecutionEffortWeights(): {UInt64: UInt64} { return self.account.storage.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 parameters account's storage access(all) view fun getExecutionMemoryWeights(): {UInt64: UInt64} { return self.account.storage.copy<{UInt64: UInt64}>(from: /storage/executionMemoryWeights) ?? panic("execution memory weights not set yet") } - // Gets Execution Memory Limit from the service account's storage + // Gets Execution Memory Limit from the parameters account's storage access(all) view fun getExecutionMemoryLimit(): UInt64 { return self.account.storage.copy(from: /storage/executionMemoryLimit) ?? panic("execution memory limit not set yet") diff --git a/contracts/FlowFees.cdc b/contracts/FlowFees.cdc index 7d90414c..515d401f 100644 --- a/contracts/FlowFees.cdc +++ b/contracts/FlowFees.cdc @@ -1,6 +1,6 @@ -import FungibleToken from 0xf233dcee88fe0abe -import FlowToken from 0x1654653399040a61 -import FlowStorageFees from 0xe467b9dd11fa00df +import "FungibleToken" +import "FlowToken" +import "FlowStorageFees" access(all) contract FlowFees { @@ -28,7 +28,23 @@ access(all) contract FlowFees { /// Get the balance of the Fees Vault access(all) fun getFeeBalance(): UFix64 { - return self.vault.balance + let childFeeAccounts = self.account.storage.borrow<&[Capability]>(from: /storage/ChildFeeAccounts) + + // fallback in case no child accounts were created yet + if childFeeAccounts == nil || childFeeAccounts!.length == 0 { + return self.vault.balance + } + + var totalFees = 0.0 + totalFees = totalFees + self.vault.balance + + for feeAccountRef in childFeeAccounts! { + if let feeAccount = feeAccountRef.borrow() { + totalFees = totalFees + feeAccount.availableBalance + } + } + + return totalFees } access(all) resource Administrator { @@ -36,9 +52,61 @@ access(all) contract FlowFees { // // Allows the administrator to withdraw tokens from the fee vault access(all) fun withdrawTokensFromFeeVault(amount: UFix64): @{FungibleToken.Vault} { - let vault <- FlowFees.vault.withdraw(amount: amount) + var remainingAmount = amount + var withdrawAmount = 0.0 + if FlowFees.vault.balance < remainingAmount { + withdrawAmount = FlowFees.vault.balance + } else { + withdrawAmount = remainingAmount + } + remainingAmount = remainingAmount - withdrawAmount + var vault <- FlowFees.vault.withdraw(amount: withdrawAmount) + + let childFeeAccounts = FlowFees.account.storage.borrow<&[Capability]>(from: /storage/ChildFeeAccounts) + + // fallback in case no child accounts were created yet + if childFeeAccounts == nil || childFeeAccounts!.length == 0 { + if remainingAmount > 0.0 { + panic("Cannot withdraw the requested amount of fee tokens. The amount of FLOW of \(amount) requested to withdraw is greater than the total fees in the fee vaults.") + } + if vault.balance != amount { + // unreachable + panic("Unexpected return vault balance!") + } + + emit TokensWithdrawn(amount: amount) + return <- vault + } + + var accountIndex = 0; + while accountIndex < childFeeAccounts!.length && remainingAmount > 0.0 { + if let feeAccount = childFeeAccounts![accountIndex].borrow() { + if let childVaultRef = feeAccount.storage.borrow(from: /storage/flowTokenVault) { + let availableBalance = feeAccount.availableBalance + + var withdrawAmount = 0.0 + if availableBalance < remainingAmount { + withdrawAmount = availableBalance + } else { + withdrawAmount = remainingAmount + } + remainingAmount = remainingAmount - withdrawAmount + vault.deposit(from: <- childVaultRef.withdraw(amount: withdrawAmount)) + } + } + accountIndex = accountIndex + 1 + } + + if remainingAmount > 0.0 { + panic("Cannot withdraw the requested amount of fee tokens. The amount of FLOW of \(amount) requested to withdraw is greater than the total fees in the fee vaults.") + } + if vault.balance != amount { + // unreachable + panic("Unexpected return vault balance!") + } + emit TokensWithdrawn(amount: amount) - return <-vault + return <- vault } /// Allows the administrator to change all the fee parameters at once @@ -143,7 +211,7 @@ access(all) contract FlowFees { } let tokenVault = acct.storage.borrow(from: /storage/flowTokenVault) - ?? panic("Unable to borrow reference to the default token vault") + ?? panic("FlowFees.deductTransactionFee: Unable to borrow reference to the default token vault") if feeAmount > tokenVault.balance { @@ -156,14 +224,39 @@ access(all) contract FlowFees { } let feeVault <- tokenVault.withdraw(amount: feeAmount) - self.vault.deposit(from: <-feeVault) + + self.collectFeesOnChildAccounts(<- feeVault) // The fee calculation can be reconstructed using the data from this event and the FeeParameters at the block when the event happened emit FeesDeducted(amount: feeAmount, inclusionEffort: inclusionEffort, executionEffort: executionEffort) } + /// The supplied vault will go to one child fee account according to the current number of child fee accounts and the current transaction index. + /// if there are no child accounts, the fees will be collected in the self.vault + access(self) fun collectFeesOnChildAccounts(_ vault: @{FungibleToken.Vault}) { + let childFeeAccounts = self.account.storage.borrow<&[Capability]>(from: /storage/ChildFeeAccounts) + + // fallback in case no child accounts were created yet + if childFeeAccounts == nil || childFeeAccounts!.length == 0 { + self.vault.deposit(from: <-vault) + return + } + + let txIndex = getTransactionIndex() + let accountIndex = Int(txIndex % UInt32(childFeeAccounts!.length)) + + if let feeAccount = childFeeAccounts![accountIndex].borrow() { + if let receiver = feeAccount.capabilities.borrow<&{FungibleToken.Receiver}>(/public/flowTokenReceiver) { + receiver.deposit(from: <-vault) + return + } + } + // fallback in case there is a problem borrowing a child account + self.vault.deposit(from: <-vault) + } + access(all) view fun getFeeParameters(): FeeParameters { - return self.account.storage.copy(from: /storage/FlowTxFeeParameters) ?? panic("Error getting tx fee parameters. They need to be initialized first!") + return self.account.storage.copy(from: /storage/FlowTxFeeParameters) ?? panic("FlowFees.getFeeParameters: Error getting tx fee parameters. They need to be initialized first!") } access(self) fun setFeeParameters(_ feeParameters: FeeParameters) { @@ -182,11 +275,11 @@ access(all) contract FlowFees { return totalFees } - init(adminAccount: auth(SaveValue) &Account) { + init() { // Create a new FlowToken Vault and save it in storage self.vault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) as! @FlowToken.Vault let admin <- create Administrator() - adminAccount.storage.save(<-admin, to: /storage/flowFeesAdmin) + self.account.storage.save(<-admin, to: /storage/flowFeesAdmin) } -} \ No newline at end of file +} diff --git a/contracts/FlowIDTableStaking.cdc b/contracts/FlowIDTableStaking.cdc index e46ac749..f22e9b49 100644 --- a/contracts/FlowIDTableStaking.cdc +++ b/contracts/FlowIDTableStaking.cdc @@ -27,10 +27,10 @@ */ -import FungibleToken from 0xf233dcee88fe0abe -import FlowToken from 0x1654653399040a61 -import FlowFees from 0xf919ee77447b7497 -import Burner from 0xf233dcee88fe0abe +import "FungibleToken" +import "FlowToken" +import "Burner" +import "FlowFees" import Crypto access(all) contract FlowIDTableStaking { @@ -143,7 +143,10 @@ access(all) contract FlowIDTableStaking { /// List of delegators for this node operator access(all) let delegators: @{UInt32: DelegatorRecord} - /// The incrementing ID used to register new delegators + /// The incrementing ID used to register new delegators. + /// NOTE: delegatorIDs are scoped per node — the unique identifier for a delegator is + /// the pair (nodeID, delegatorID), not delegatorID alone. Two delegators at different + /// nodes may share the same delegatorID without any collision or conflict. access(all) var delegatorIDCounter: UInt32 /// The amount of tokens that this node has requested to unstake for the next epoch @@ -162,16 +165,16 @@ access(all) contract FlowIDTableStaking { tokensCommitted: @{FungibleToken.Vault} ) { pre { - id.length == 64: "Node ID length must be 32 bytes (64 hex characters)" - FlowIDTableStaking.isValidNodeID(id): "The node ID must have only numbers and lowercase hex characters" - FlowIDTableStaking.nodes[id] == nil: "The ID cannot already exist in the record" - role >= UInt8(1) && role <= UInt8(5): "The role must be 1, 2, 3, 4, or 5" - FlowIDTableStaking.isValidNetworkingAddress(address: networkingAddress): "The networkingAddress must be a valid domain name with a port (e.g., node.flow.com:3569), must not exceed 510 characters, and cannot be an IP address" - networkingKey.length == 128: "The networkingKey length must be exactly 64 bytes (128 hex characters)" - stakingKey.length == 192: "The stakingKey length must be exactly 96 bytes (192 hex characters)" - !FlowIDTableStaking.getNetworkingAddressClaimed(address: networkingAddress): "The networkingAddress cannot have already been claimed" - !FlowIDTableStaking.getNetworkingKeyClaimed(key: networkingKey): "The networkingKey cannot have already been claimed" - !FlowIDTableStaking.getStakingKeyClaimed(key: stakingKey): "The stakingKey cannot have already been claimed" + id.length == 64: "FlowIDTableStaking.NodeRecord.init: Node ID length must be 32 bytes (64 hex characters) but got \(id.length)" + FlowIDTableStaking.isValidNodeID(id): "FlowIDTableStaking.NodeRecord.init: The node ID must have only numbers and lowercase hex characters but got \(id)" + FlowIDTableStaking.nodes[id] == nil: "FlowIDTableStaking.NodeRecord.init: The node ID \(id) already exists in the identity table and cannot be used again" + role >= UInt8(1) && role <= UInt8(5): "FlowIDTableStaking.NodeRecord.init: The role must be 1, 2, 3, 4, or 5 but got \(role)" + FlowIDTableStaking.isValidNetworkingAddress(address: networkingAddress): "FlowIDTableStaking.NodeRecord.init: The networkingAddress must be a valid domain name with a port (e.g., node.flow.com:3569), must not exceed 510 characters, and cannot be an IP address, but got \(networkingAddress)" + networkingKey.length == 128: "FlowIDTableStaking.NodeRecord.init: The networkingKey length must be exactly 64 bytes (128 hex characters) but got \(networkingKey.length)" + stakingKey.length == 192: "FlowIDTableStaking.NodeRecord.init: The stakingKey length must be exactly 96 bytes (192 hex characters) but got \(stakingKey.length)" + !FlowIDTableStaking.getNetworkingAddressClaimed(address: networkingAddress): "FlowIDTableStaking.NodeRecord.init: The networkingAddress \(networkingAddress) has already been claimed by another node and cannot be used again" + !FlowIDTableStaking.getNetworkingKeyClaimed(key: networkingKey): "FlowIDTableStaking.NodeRecord.init: The networkingKey \(networkingKey) has already been claimed by another node and cannot be used again" + !FlowIDTableStaking.getStakingKeyClaimed(key: stakingKey): "FlowIDTableStaking.NodeRecord.init: The stakingKey \(stakingKey) has already been claimed by another node and cannot be used again" } let stakeKey = PublicKey( @@ -182,10 +185,8 @@ access(all) contract FlowIDTableStaking { // Verify the proof of possesion of the private staking key assert( stakeKey.verifyPoP(stakingKeyPoP.decodeHex()), - message: - "FlowIDTableStaking.NodeRecord.init: Cannot create node with ID " - .concat(id).concat(". The Proof of Possession (").concat(stakingKeyPoP) - .concat(") for the node's staking key (").concat(") is invalid") + message: + "FlowIDTableStaking.NodeRecord.init: Cannot create node with ID \(id). The Proof of Possession (\(stakingKeyPoP)) for the node's staking key (\(stakingKey)) is invalid" ) let netKey = PublicKey( @@ -229,7 +230,7 @@ access(all) contract FlowIDTableStaking { access(account) view fun borrowDelegatorRecord(_ delegatorID: UInt32): auth(FungibleToken.Withdraw) &DelegatorRecord { pre { self.delegators[delegatorID] != nil: - "Specified delegator ID does not exist in the record" + "FlowIDTableStaking.NodeRecord.borrowDelegatorRecord: Specified delegator ID \(delegatorID) does not exist in the record" } return (&self.delegators[delegatorID] as auth(FungibleToken.Withdraw) &DelegatorRecord?)! } @@ -436,9 +437,9 @@ access(all) contract FlowIDTableStaking { /// Change the node's networking address to a new one access(NodeOperator) fun updateNetworkingAddress(_ newAddress: String) { pre { - FlowIDTableStaking.stakingEnabled(): "Cannot update networking address if the staking auction isn't in progress" - FlowIDTableStaking.isValidNetworkingAddress(address: newAddress): "The networkingAddress must be a valid domain name with a port (e.g., node.flow.com:3569), must not exceed 510 characters, and cannot be an IP address" - !FlowIDTableStaking.getNetworkingAddressClaimed(address: newAddress): "The networkingAddress cannot have already been claimed" + FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeStaker.updateNetworkingAddress: Cannot update networking address if the staking auction isn't in progress" + FlowIDTableStaking.isValidNetworkingAddress(address: newAddress): "FlowIDTableStaking.NodeStaker.updateNetworkingAddress: The networkingAddress must be a valid domain name with a port (e.g., node.flow.com:3569), must not exceed 510 characters, and cannot be an IP address" + !FlowIDTableStaking.getNetworkingAddressClaimed(address: newAddress): "FlowIDTableStaking.NodeStaker.updateNetworkingAddress: The networkingAddress cannot have already been claimed" } // Borrow the node's record from the staking contract @@ -456,7 +457,7 @@ access(all) contract FlowIDTableStaking { /// Add new tokens to the system to stake during the next epoch access(NodeOperator) fun stakeNewTokens(_ tokens: @{FungibleToken.Vault}) { pre { - FlowIDTableStaking.stakingEnabled(): "Cannot stake if the staking auction isn't in progress" + FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeStaker.stakeNewTokens: Cannot stake if the staking auction isn't in progress" } // Borrow the node's record from the staking contract @@ -479,7 +480,7 @@ access(all) contract FlowIDTableStaking { /// Stake tokens that are in the tokensUnstaked bucket access(NodeOperator) fun stakeUnstakedTokens(amount: UFix64) { pre { - FlowIDTableStaking.stakingEnabled(): "Cannot stake if the staking auction isn't in progress" + FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeStaker.stakeUnstakedTokens: Cannot stake if the staking auction isn't in progress" } let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id) @@ -513,7 +514,7 @@ access(all) contract FlowIDTableStaking { /// Stake tokens that are in the tokensRewarded bucket access(NodeOperator) fun stakeRewardedTokens(amount: UFix64) { pre { - FlowIDTableStaking.stakingEnabled(): "Cannot stake if the staking auction isn't in progress" + FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeStaker.stakeRewardedTokens: Cannot stake if the staking auction isn't in progress" } let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id) @@ -534,7 +535,7 @@ access(all) contract FlowIDTableStaking { /// Request amount tokens to be removed from staking at the end of the next epoch access(NodeOperator) fun requestUnstaking(amount: UFix64) { pre { - FlowIDTableStaking.stakingEnabled(): "Cannot unstake if the staking auction isn't in progress" + FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeStaker.requestUnstaking: Cannot unstake if the staking auction isn't in progress" } let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id) @@ -545,7 +546,7 @@ access(all) contract FlowIDTableStaking { nodeRecord.tokensStaked.balance + nodeRecord.tokensCommitted.balance >= amount + nodeRecord.tokensRequestedToUnstake, - message: "Not enough tokens to unstake!" + message: "FlowIDTableStaking.NodeStaker.requestUnstaking: Not enough tokens to unstake! Only have \(nodeRecord.tokensStaked.balance + nodeRecord.tokensCommitted.balance) tokens but requested to unstake \(amount + nodeRecord.tokensRequestedToUnstake) tokens" ) // Node operators who have delegators have to have enough of their own tokens staked @@ -553,7 +554,7 @@ access(all) contract FlowIDTableStaking { assert ( nodeRecord.delegators.length == 0 || FlowIDTableStaking.isGreaterThanMinimumForRole(numTokens: FlowIDTableStaking.NodeInfo(nodeID: nodeRecord.id).totalCommittedWithoutDelegators() - amount, role: nodeRecord.role), - message: "Cannot unstake below the minimum if there are delegators" + message: "FlowIDTableStaking.NodeStaker.requestUnstaking: Cannot unstake below the minimum stake requirement if there are delegators." ) let amountCommitted = nodeRecord.tokensCommitted.balance @@ -584,14 +585,14 @@ access(all) contract FlowIDTableStaking { // Remove the node as a candidate node if they were one before but aren't now if !self.isEligibleForCandidateNodeStatus(nodeRecord) { FlowIDTableStaking.removeFromCandidateNodeList(nodeID: self.id, role: nodeRecord.role) - } + } } /// Requests to unstake all of the node operators staked and committed tokens /// as well as all the staked and committed tokens of all of their delegators access(NodeOperator) fun unstakeAll() { pre { - FlowIDTableStaking.stakingEnabled(): "Cannot unstake if the staking auction isn't in progress" + FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeStaker.unstakeAll: Cannot unstake if the staking auction isn't in progress" } let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id) @@ -638,7 +639,7 @@ access(all) contract FlowIDTableStaking { } /// Public interface to query information about a delegator - /// from the account it is stored in + /// from the account it is stored in access(all) resource interface NodeDelegatorPublic { access(all) let id: UInt32 access(all) let nodeID: String @@ -660,7 +661,7 @@ access(all) contract FlowIDTableStaking { /// Delegate new tokens to the node operator access(DelegatorOwner) fun delegateNewTokens(from: @{FungibleToken.Vault}) { pre { - FlowIDTableStaking.stakingEnabled(): "Cannot delegate if the staking auction isn't in progress" + FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeDelegator.delegateNewTokens: Cannot delegate if the staking auction isn't in progress" } // borrow the node record of the node in order to get the delegator record @@ -678,7 +679,7 @@ access(all) contract FlowIDTableStaking { /// Delegate tokens from the unstaked bucket to the node operator access(DelegatorOwner) fun delegateUnstakedTokens(amount: UFix64) { pre { - FlowIDTableStaking.stakingEnabled(): "Cannot delegate if the staking auction isn't in progress" + FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeDelegator.delegateUnstakedTokens: Cannot delegate if the staking auction isn't in progress" } let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.nodeID) @@ -707,7 +708,7 @@ access(all) contract FlowIDTableStaking { /// Delegate tokens from the rewards bucket to the node operator access(DelegatorOwner) fun delegateRewardedTokens(amount: UFix64) { pre { - FlowIDTableStaking.stakingEnabled(): "Cannot delegate if the staking auction isn't in progress" + FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeDelegator.delegateRewardedTokens: Cannot delegate if the staking auction isn't in progress" } let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.nodeID) @@ -723,7 +724,7 @@ access(all) contract FlowIDTableStaking { /// Request to unstake delegated tokens during the next epoch access(DelegatorOwner) fun requestUnstaking(amount: UFix64) { pre { - FlowIDTableStaking.stakingEnabled(): "Cannot request unstaking if the staking auction isn't in progress" + FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeDelegator.requestUnstaking: Cannot request unstaking if the staking auction isn't in progress" } let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.nodeID) @@ -734,7 +735,7 @@ access(all) contract FlowIDTableStaking { delRecord.tokensStaked.balance + delRecord.tokensCommitted.balance >= amount + delRecord.tokensRequestedToUnstake, - message: "Not enough tokens to unstake!" + message: "FlowIDTableStaking.NodeDelegator.requestUnstaking: Not enough tokens to unstake! Only have \(delRecord.tokensStaked.balance + delRecord.tokensCommitted.balance) tokens but requested to unstake \(amount + delRecord.tokensRequestedToUnstake) tokens" ) // if the request can come from committed, withdraw from committed to unstaked @@ -817,7 +818,7 @@ access(all) contract FlowIDTableStaking { self.delegatorRewards[delegatorID] = reward * scalingFactor } } - + access(all) fun scaleOperatorRewards(scalingFactor: UFix64) { self.nodeRewards = self.nodeRewards * scalingFactor } @@ -857,7 +858,7 @@ access(all) contract FlowIDTableStaking { access(all) fun setMinimumStakeRequirements(_ newRequirements: {UInt8: UFix64}) { pre { newRequirements.keys.length == 5: - "There must be six entries for node minimum stake requirements" + "FlowIDTableStaking.Admin.setMinimumStakeRequirements: There must be five entries for node minimum stake requirements but got \(newRequirements.keys.length)" } FlowIDTableStaking.minimumStakeRequired = newRequirements emit NewStakingMinimums(newMinimums: newRequirements) @@ -883,7 +884,7 @@ access(all) contract FlowIDTableStaking { access(all) fun setCutPercentage(_ newCutPercentage: UFix64) { pre { newCutPercentage > 0.0 && newCutPercentage < 1.0: - "Cut percentage must be between 0 and 1!" + "FlowIDTableStaking.Admin.setCutPercentage: Cut percentage must be between 0 and 1 but got \(newCutPercentage)" } if newCutPercentage != FlowIDTableStaking.nodeDelegatingRewardCut { emit NewDelegatorCutPercentage(newCutPercentage: newCutPercentage) @@ -891,10 +892,10 @@ access(all) contract FlowIDTableStaking { FlowIDTableStaking.nodeDelegatingRewardCut = newCutPercentage } - /// Sets new limits to the number of candidate nodes for an epoch + /// Sets new limits to the number of candidate nodes for an epoch for a specific role access(all) fun setCandidateNodeLimit(role: UInt8, newLimit: UInt64) { pre { - role >= UInt8(1) && role <= UInt8(5): "The role must be 1, 2, 3, 4, or 5" + role >= UInt8(1) && role <= UInt8(5): "FlowIDTableStaking.Admin.setCandidateNodeLimit: The role must be 1, 2, 3, 4, or 5 but got \(role)" } let candidateNodeLimits = FlowIDTableStaking.account.storage.load<{UInt8: UInt64}>(from: /storage/idTableCandidateNodeLimits)! @@ -908,12 +909,12 @@ access(all) contract FlowIDTableStaking { /// even if the number of participant nodes exceeds the slot limit. access(all) fun setSlotLimits(slotLimits: {UInt8: UInt16}) { pre { - slotLimits.keys.length == 5: "Slot Limits Dictionary can only have 5 entries" - slotLimits[1] != nil: "Need to have a limit set for collector nodes" - slotLimits[2] != nil: "Need to have a limit set for consensus nodes" - slotLimits[3] != nil: "Need to have a limit set for execution nodes" - slotLimits[4] != nil: "Need to have a limit set for verification nodes" - slotLimits[5] != nil: "Need to have a limit set for access nodes" + slotLimits.keys.length == 5: "FlowIDTableStaking.Admin.setSlotLimits: Slot Limits Dictionary can only have 5 entries but got \(slotLimits.keys.length)" + slotLimits[1] != nil: "FlowIDTableStaking.Admin.setSlotLimits: Need to have a limit set for collector nodes" + slotLimits[2] != nil: "FlowIDTableStaking.Admin.setSlotLimits: Need to have a limit set for consensus nodes" + slotLimits[3] != nil: "FlowIDTableStaking.Admin.setSlotLimits: Need to have a limit set for execution nodes" + slotLimits[4] != nil: "FlowIDTableStaking.Admin.setSlotLimits: Need to have a limit set for verification nodes" + slotLimits[5] != nil: "FlowIDTableStaking.Admin.setSlotLimits: Need to have a limit set for access nodes" } FlowIDTableStaking.account.storage.load<{UInt8: UInt16}>(from: /storage/flowStakingSlotLimits) @@ -925,7 +926,7 @@ access(all) contract FlowIDTableStaking { /// but other node types will be added in the future access(all) fun setOpenNodeSlots(openSlots: {UInt8: UInt16}) { pre { - openSlots[5] != nil: "Need to have a value set for access nodes" + openSlots[5] != nil: "FlowIDTableStaking.Admin.setOpenNodeSlots: Need to have a value set for access nodes" } FlowIDTableStaking.account.storage.load<{UInt8: UInt16}>(from: /storage/flowStakingOpenNodeSlots) @@ -933,7 +934,7 @@ access(all) contract FlowIDTableStaking { } /// Sets a list of node IDs who will not receive rewards for the current epoch - /// This is used during epochs to punish nodes who have poor uptime + /// This is used during epochs to punish nodes who have poor uptime /// or who do not update to latest node software quickly enough /// The parameter is a dictionary mapping node IDs /// to a percentage, which is the percentage of their expected rewards that @@ -942,7 +943,7 @@ access(all) contract FlowIDTableStaking { for percentage in nodeIDs.values { assert( percentage >= 0.0 && percentage < 1.0, - message: "Percentage value to decrease rewards payout should be between 0 and 1" + message: "FlowIDTableStaking.Admin.setNonOperationalNodesList: Percentage value to decrease rewards payout should be between 0 and 1 but got \(percentage)" ) } FlowIDTableStaking.account.storage.load<{String: UFix64}>(from: /storage/idTableNonOperationalNodesList) @@ -953,7 +954,7 @@ access(all) contract FlowIDTableStaking { /// if their staked amount changes or if they are removed access(all) fun setNodeWeight(nodeID: String, weight: UInt64) { if weight > 100 { - panic("Specified node weight out of range.") + panic("FlowIDTableStaking.Admin.setNodeWeight: Specified node weight out of range. Must be between 0 and 100 but got \(weight)") } let nodeRecord = FlowIDTableStaking.borrowNodeRecord(nodeID) @@ -966,18 +967,18 @@ access(all) contract FlowIDTableStaking { /// and not considered to be a proposed/staked node access(all) fun setApprovedList(_ newApproveList: {String: Bool}) { let currentApproveList = FlowIDTableStaking.getApprovedList() - ?? panic("Could not load approve list from storage") + ?? panic("FlowIDTableStaking.Admin.setApprovedList: Could not load approve list from storage") - for id in newApproveList.keys { + for id in newApproveList { if FlowIDTableStaking.nodes[id] == nil { - panic("Approved node ".concat(id).concat(" does not already exist in the identity table")) + panic("FlowIDTableStaking.Admin.setApprovedList: Approved node \(id) does not already exist in the identity table") } } // If one of the nodes has been removed from the approve list // it need to be set as movesPending so it will be caught in the `removeInvalidNodes` method // If this happens not during the staking auction, the node should be removed and marked to unstake immediately - for id in currentApproveList.keys { + for id in currentApproveList { if newApproveList[id] == nil { if FlowIDTableStaking.stakingEnabled() { FlowIDTableStaking.modifyNewMovesPending(nodeID: id, delegatorID: nil, existingList: nil) @@ -992,7 +993,7 @@ access(all) contract FlowIDTableStaking { /// Sets the approved list without validating it (requires caller to validate) access(self) fun unsafeSetApprovedList(_ newApproveList: {String: Bool}) { let currentApproveList = FlowIDTableStaking.account.storage.load<{String: Bool}>(from: /storage/idTableApproveList) - ?? panic("Could not load the current approve list from storage") + ?? panic("FlowIDTableStaking.Admin.unsafeSetApprovedList: Could not load the current approve list from storage") FlowIDTableStaking.account.storage.save<{String: Bool}>(newApproveList, to: /storage/idTableApproveList) } @@ -1025,7 +1026,7 @@ access(all) contract FlowIDTableStaking { } var movesPendingList = FlowIDTableStaking.account.storage.borrow(from: /storage/idTableMovesPendingList) - ?? panic("No moves pending list in account storage") + ?? panic("FlowIDTableStaking.Admin.unsafeRemoveAndRefundNodeRecord: No moves pending list in account storage") // Iterate through all delegators and unstake their tokens // since their node has unstaked @@ -1064,7 +1065,7 @@ access(all) contract FlowIDTableStaking { access(all) fun removeAndRefundNodeRecord(_ nodeID: String) { // remove the refunded node from the approve list let approveList = FlowIDTableStaking.getApprovedList() - ?? panic("Could not load approve list from storage") + ?? panic("FlowIDTableStaking.Admin.removeAndRefundNodeRecord: Could not load approve list from storage") approveList.remove(key: nodeID) self.unsafeSetApprovedList(approveList) self.unsafeRemoveAndRefundNodeRecord(nodeID) @@ -1097,13 +1098,13 @@ access(all) contract FlowIDTableStaking { /// it moves their committed tokens to their unstaked bucket access(all) fun removeInvalidNodes(): {String: Bool} { let approvedNodeIDs = FlowIDTableStaking.getApprovedList() - ?? panic("Could not read the approve list from storage") + ?? panic("FlowIDTableStaking.Admin.removeInvalidNodes: Could not read the approve list from storage") let movesPendingList = FlowIDTableStaking.getMovesPendingList() - ?? panic("Could not copy moves pending list from storage") + ?? panic("FlowIDTableStaking.Admin.removeInvalidNodes: Could not copy moves pending list from storage") let participantList = FlowIDTableStaking.getParticipantNodeList() - ?? panic("Could not copy participant list from storage") + ?? panic("FlowIDTableStaking.Admin.removeInvalidNodes: Could not copy participant list from storage") // We only iterate through movesPendingList here because any node // that has insufficient stake committed will be because it has submitted @@ -1112,7 +1113,7 @@ access(all) contract FlowIDTableStaking { // to get their initialWeight set to 100 // Nodes removed from the approve list are already refunded at the time // of removal in the setApprovedList method - for nodeID in movesPendingList.keys { + for nodeID in movesPendingList { let nodeRecord = FlowIDTableStaking.borrowNodeRecord(nodeID) let totalTokensCommitted = nodeRecord.nodeFullCommittedBalance() @@ -1131,7 +1132,7 @@ access(all) contract FlowIDTableStaking { // permissionless node roles (access) // NOTE: Access nodes which registered prior to the 100-FLOW stake requirement - // (which must be approved) are not removed during a temporary grace period during + // (which must be approved) are not removed during a temporary grace period during // which these grandfathered node operators may submit the necessary stake requirement. // Therefore Access nodes must either be approved OR have sufficient stake: // - Old ANs must be approved, but are allowed to have zero stake @@ -1152,9 +1153,9 @@ access(all) contract FlowIDTableStaking { /// so if there are more candidate nodes for that role than there are slots /// nodes are randomly selected from the list to be included. /// Nodes which are not selected for inclusion are removed and refunded in this function. - /// All candidate nodes left staked after this function exits are implicitly selected to fill the + /// All candidate nodes left staked after this function exits are implicitly selected to fill the /// available slots, and will become participants at the next epoch transition. - /// + /// access(all) fun fillNodeRoleSlots(): [String] { var currentNodeCount: {UInt8: UInt16} = FlowIDTableStaking.getCurrentRoleNodeCounts() @@ -1162,7 +1163,7 @@ access(all) contract FlowIDTableStaking { let slotLimits: {UInt8: UInt16} = FlowIDTableStaking.getRoleSlotLimits() let openSlots = FlowIDTableStaking.getOpenNodeSlots() - + let nodesToAdd: [String] = [] // Load and reset the candidate node list @@ -1177,20 +1178,20 @@ access(all) contract FlowIDTableStaking { if currentNodeCount[role]! >= slotLimits[role]! { // if all slots are full, remove and refund all pending nodes - for nodeID in candidateNodesForRole.keys { + for nodeID in candidateNodesForRole { self.removeAndRefundNodeRecord(nodeID) } } else if currentNodeCount[role]! + UInt16(candidateNodesForRole.keys.length) > slotLimits[role]! { - + // Not all slots are full, but addition of all the candidate nodes exceeds the slot limit // Calculate how many nodes to remove from the candidate list for this role var numNodesToRemove: UInt16 = currentNodeCount[role]! + UInt16(candidateNodesForRole.keys.length) - slotLimits[role]! - + let numNodesToAdd = UInt16(candidateNodesForRole.keys.length) - numNodesToRemove // Indicates which indicies in the candidate nodes array will be removed var deletionList: {UInt16: Bool} = {} - + // Randomly select which indicies will be removed while numNodesToRemove > 0 { let selection = revertibleRandom(modulo: UInt16(candidateNodesForRole.keys.length)) @@ -1203,7 +1204,7 @@ access(all) contract FlowIDTableStaking { } // Remove and Refund the selected nodes - for nodeIndex in deletionList.keys { + for nodeIndex in deletionList { let nodeID = candidateNodesForRole.keys[nodeIndex] self.removeAndRefundNodeRecord(nodeID) nodesToRemoveFromCandidateNodes.append(nodeID) @@ -1248,7 +1249,7 @@ access(all) contract FlowIDTableStaking { let rewardsBreakdownArray = rewardsSummary.breakdown let totalRewards = rewardsSummary.totalRewards - + // If there are no node operators to pay rewards to, do not mint new tokens if rewardsBreakdownArray.length == 0 { emit EpochTotalRewardsPaid(total: totalRewards, fromFees: 0.0, minted: 0.0, feesBurned: 0.0, epochCounterForRewards: forEpochCounter) @@ -1258,7 +1259,7 @@ access(all) contract FlowIDTableStaking { self.setNonOperationalNodesList(emptyNodeList) return - } + } let feeBalance = FlowFees.getFeeBalance() var mintedRewards: UFix64 = 0.0 @@ -1273,20 +1274,20 @@ access(all) contract FlowIDTableStaking { // Mint the remaining FLOW for rewards if mintedRewards > 0.0 { let flowTokenMinter = FlowIDTableStaking.account.storage.borrow<&FlowToken.Minter>(from: /storage/flowTokenMinter) - ?? panic("Could not borrow minter reference") + ?? panic("FlowIDTableStaking.NodeStaker.payRewards: Could not borrow minter reference") rewardsVault.deposit(from: <-flowTokenMinter.mintTokens(amount: mintedRewards)) } for rewardBreakdown in rewardsBreakdownArray { let nodeRecord = FlowIDTableStaking.borrowNodeRecord(rewardBreakdown.nodeID) let nodeReward = rewardBreakdown.nodeRewards - + nodeRecord.tokensRewarded.deposit(from: <-rewardsVault.withdraw(amount: nodeReward)) for delegator in rewardBreakdown.delegatorRewards.keys { let delRecord = nodeRecord.borrowDelegatorRecord(delegator) let delegatorReward = rewardBreakdown.delegatorRewards[delegator]! - + delRecord.tokensRewarded.deposit(from: <-rewardsVault.withdraw(amount: delegatorReward)) emit DelegatorRewardsPaid(nodeID: rewardBreakdown.nodeID, delegatorID: delegator, amount: delegatorReward, epochCounter: forEpochCounter) } @@ -1331,7 +1332,7 @@ access(all) contract FlowIDTableStaking { // Iterate through all the non-operational nodes and calculate // their rewards that will be withheld let nonOperationalNodes = FlowIDTableStaking.getNonOperationalNodesList() - for nodeID in nonOperationalNodes.keys { + for nodeID in nonOperationalNodes { let nodeRecord = FlowIDTableStaking.borrowNodeRecord(nodeID) // Each node's rewards can be decreased to a different percentage @@ -1387,11 +1388,17 @@ access(all) contract FlowIDTableStaking { rewardsBreakdownArray.append(rewardsBreakdown) } - var withheldRewardsScale = sumRewardsWithheld / (totalStaked - sumStakeFromNonOperationalStakers) + // If all staked nodes are non-operational, the denominator is zero and there are + // no operational nodes to redistribute withheld rewards to, so the scale is zero. + var withheldRewardsScale = 0.0 + let operationalStake = totalStaked - sumStakeFromNonOperationalStakers + if operationalStake > 0.0 { + withheldRewardsScale = sumRewardsWithheld / operationalStake + } let totalRewardsPlusWithheld = totalRewardScale + withheldRewardsScale /// iterate through all the nodes to pay - for nodeID in stakedNodeIDs.keys { + for nodeID in stakedNodeIDs { if nonOperationalNodes[nodeID] != nil { continue } let nodeRecord = FlowIDTableStaking.borrowNodeRecord(nodeID) @@ -1428,7 +1435,7 @@ access(all) contract FlowIDTableStaking { } rewardsBreakdown.setDelegatorReward(delegatorID: delegator, rewards: delegatorRewardAmount) } - + rewardsBreakdown.setNodeRewards(nodeRewardAmount) rewardsBreakdownArray.append(rewardsBreakdown) } @@ -1445,24 +1452,24 @@ access(all) contract FlowIDTableStaking { /// Unstaking requests are filled by moving those tokens from staked to unstaking access(all) fun moveTokens(newEpochCounter: UInt64) { pre { - !FlowIDTableStaking.stakingEnabled(): "Cannot move tokens if the staking auction is still in progress" + !FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeStaker.moveTokens: Cannot move tokens if the staking auction is still in progress" } let approvedNodeIDs = FlowIDTableStaking.getApprovedList() - ?? panic("Could not read the approve list from storage") + ?? panic("FlowIDTableStaking.NodeStaker.moveTokens: Could not read the approve list from storage") let movesPendingNodeIDs = FlowIDTableStaking.account.storage.load<{String: {UInt32: Bool}}>(from: /storage/idTableMovesPendingList) - ?? panic("No moves pending list in account storage") + ?? panic("FlowIDTableStaking.NodeStaker.moveTokens: No moves pending list in account storage") // Reset the movesPendingList var emptyMovesPendingList: {String: {UInt32: Bool}} = {} FlowIDTableStaking.account.storage.save(emptyMovesPendingList, to: /storage/idTableMovesPendingList) let newMovesPendingList = FlowIDTableStaking.account.storage.borrow(from: /storage/idTableMovesPendingList) - ?? panic("No moves pending list in account storage") + ?? panic("FlowIDTableStaking.NodeStaker.moveTokens: No moves pending list in account storage") let stakedNodeIDs: {String: Bool} = FlowIDTableStaking.getParticipantNodeList()! - for nodeID in movesPendingNodeIDs.keys { + for nodeID in movesPendingNodeIDs { let nodeRecord = FlowIDTableStaking.borrowNodeRecord(nodeID) let approved = approvedNodeIDs[nodeID] ?? false @@ -1497,7 +1504,7 @@ access(all) contract FlowIDTableStaking { let pendingDelegatorsList = movesPendingNodeIDs[nodeID]! // move all the delegators' tokens between buckets - for delegator in pendingDelegatorsList.keys { + for delegator in pendingDelegatorsList { let delRecord = nodeRecord.borrowDelegatorRecord(delegator) // If the delegator's committed tokens for the next epoch @@ -1571,9 +1578,13 @@ access(all) contract FlowIDTableStaking { { assert ( FlowIDTableStaking.stakingEnabled(), - message: "Cannot register a node operator if the staking auction isn't in progress" + message: "FlowIDTableStaking.NodeStaker.addNodeRecord: Cannot register a node operator if the staking auction isn't in progress" ) + // NOTE: An empty vault is intentionally passed to NodeRecord here. The caller-provided + // tokensCommitted vault is validated against the minimum stake requirement at line below, + // then deposited via stakeNewTokens() immediately after the node is stored. The NodeRecord + // is created first so it exists in storage before tokens are committed to it. let newNode <- create NodeRecord(id: id, role: role, networkingAddress: networkingAddress, @@ -1586,7 +1597,7 @@ access(all) contract FlowIDTableStaking { assert( self.isGreaterThanMinimumForRole(numTokens: tokensCommitted.balance, role: role), - message: "Tokens committed for registration is not above the minimum (".concat(minimum.toString()).concat(") for the chosen node role (".concat(role.toString()).concat(")")) + message: "FlowIDTableStaking.NodeStaker.addNodeRecord: The amount of tokens committed for registration of \(tokensCommitted.balance) is not above the minimum (\(minimum)) for the chosen node role (\(role))" ) FlowIDTableStaking.nodes[id] <-! newNode @@ -1604,25 +1615,25 @@ access(all) contract FlowIDTableStaking { access(all) fun registerNewDelegator(nodeID: String, tokensCommitted: @{FungibleToken.Vault}): @NodeDelegator { assert ( FlowIDTableStaking.stakingEnabled(), - message: "Cannot register a delegator if the staking auction isn't in progress" + message: "FlowIDTableStaking.NodeStaker.registerNewDelegator: Cannot register a delegator if the staking auction isn't in progress" ) let nodeRecord = FlowIDTableStaking.borrowNodeRecord(nodeID) assert ( nodeRecord.role != UInt8(5), - message: "Cannot register a delegator for an access node" + message: "FlowIDTableStaking.NodeStaker.registerNewDelegator: Cannot register a delegator for an access node" ) let minimum = self.getDelegatorMinimumStakeRequirement() assert( tokensCommitted.balance >= minimum, - message: "Tokens committed for delegator registration is not above the minimum (".concat(minimum.toString()).concat(")") + message: "FlowIDTableStaking.NodeStaker.registerNewDelegator: The amount of tokens committed for registration of \(tokensCommitted.balance) is not above the minimum (\(minimum)) for delegators" ) assert ( FlowIDTableStaking.isGreaterThanMinimumForRole(numTokens: nodeRecord.nodeFullCommittedBalance(), role: nodeRecord.role), - message: "Cannot register a delegator if the node operator is below the minimum stake" + message: "FlowIDTableStaking.NodeStaker.registerNewDelegator: Cannot register a delegator because the node operator for ID \(nodeID) is below the minimum stake requirement for their node role (\(nodeRecord.role))" ) // increment the delegator ID counter for this node @@ -1645,7 +1656,7 @@ access(all) contract FlowIDTableStaking { access(account) view fun borrowNodeRecord(_ nodeID: String): auth(FungibleToken.Withdraw) &NodeRecord { pre { FlowIDTableStaking.nodes[nodeID] != nil: - "Specified node ID does not exist in the record" + "FlowIDTableStaking.NodeStaker.borrowNodeRecord: Specified node ID \(nodeID) does not exist in the identity table" } return (&FlowIDTableStaking.nodes[nodeID] as auth(FungibleToken.Withdraw) &NodeRecord?)! } @@ -1653,7 +1664,7 @@ access(all) contract FlowIDTableStaking { /// borrow a reference to the `FlowFees` admin resource for paying rewards access(account) view fun borrowFeesAdmin(): &FlowFees.Administrator { let feesAdmin = self.account.storage.borrow<&FlowFees.Administrator>(from: /storage/flowFeesAdmin) - ?? panic("Could not borrow a reference to the FlowFees Admin object") + ?? panic("FlowIDTableStaking.NodeStaker.borrowFeesAdmin: Could not borrow a reference to the FlowFees Admin object") return feesAdmin } @@ -1662,7 +1673,7 @@ access(all) contract FlowIDTableStaking { /// a piece of node metadata has been claimed by a node access(account) fun updateClaimed(path: StoragePath, _ key: String, claimed: Bool) { let claimedDictionary = self.account.storage.borrow(from: path) - ?? panic("Invalid path for dictionary") + ?? panic("FlowIDTableStaking.NodeStaker.updateClaimed: Invalid path for dictionary") if claimed { claimedDictionary[key] = true @@ -1699,7 +1710,7 @@ access(all) contract FlowIDTableStaking { existingList: auth(Mutate) &{String: {UInt32: Bool}}?) { let movesPendingList = existingList ?? (self.account.storage.borrow(from: /storage/idTableMovesPendingList) - ?? panic("No moves pending list in account storage")) + ?? panic("FlowIDTableStaking.NodeStaker.modifyNewMovesPending: No moves pending list in account storage")) // If there is already a list for the given node ID, overwrite the created one if let existingDelegatorList = movesPendingList.remove(key: nodeID) { @@ -1742,15 +1753,15 @@ access(all) contract FlowIDTableStaking { /// Adds the provided node ID to the candidate node list access(contract) fun addToCandidateNodeList(nodeID: String, roleToAdd: UInt8) { pre { - roleToAdd >= UInt8(1) && roleToAdd <= UInt8(5): "The role must be 1, 2, 3, 4, or 5" + roleToAdd >= UInt8(1) && roleToAdd <= UInt8(5): "FlowIDTableStaking.NodeStaker.addToCandidateNodeList: The role must be 1, 2, 3, 4, or 5 but got \(roleToAdd)" } var candidateNodes = FlowIDTableStaking.account.storage.borrow(from: /storage/idTableCandidateNodes)! var candidateNodesForRole = candidateNodes.remove(key: roleToAdd) - ?? panic("Could not get candidate nodes for role: ".concat(roleToAdd.toString())) + ?? panic("FlowIDTableStaking.NodeStaker.addToCandidateNodeList: Could not get candidate nodes for role: \(roleToAdd)") if UInt64(candidateNodesForRole.keys.length) >= self.getCandidateNodeLimits()![roleToAdd]! { - panic("Candidate node limit exceeded for node role ".concat(roleToAdd.toString())) + panic("FlowIDTableStaking.NodeStaker.addToCandidateNodeList: Candidate node limit of \(self.getCandidateNodeLimits()![roleToAdd]!) exceeded for node role \(roleToAdd)") } candidateNodesForRole[nodeID] = true @@ -1760,14 +1771,14 @@ access(all) contract FlowIDTableStaking { /// Removes the provided node ID from the candidate node list access(contract) fun removeFromCandidateNodeList(nodeID: String, role: UInt8) { pre { - role >= UInt8(1) && role <= UInt8(5): "The role must be 1, 2, 3, 4, or 5" + role >= UInt8(1) && role <= UInt8(5): "FlowIDTableStaking.NodeStaker.removeFromCandidateNodeList: The role must be 1, 2, 3, 4, or 5 but got \(role)" } var candidateNodes = FlowIDTableStaking.account.storage.borrow(from: /storage/idTableCandidateNodes) - ?? panic("Could not load candidate node list from storage") + ?? panic("FlowIDTableStaking.NodeStaker.removeFromCandidateNodeList: Could not load candidate node list from storage") var candidateNodesForRole = candidateNodes.remove(key: role) - ?? panic("Could not get candidate nodes for role: ".concat(role.toString())) - + ?? panic("FlowIDTableStaking.NodeStaker.removeFromCandidateNodeList: Could not get candidate nodes for role: \(role)") + candidateNodesForRole.remove(key: nodeID) candidateNodes[role] = candidateNodesForRole } @@ -1784,7 +1795,7 @@ access(all) contract FlowIDTableStaking { ?? {1: 0, 2: 0, 3: 0, 4: 0, 5: 0} } - /// Gets the number of auto-opened slots for each node role. + /// Gets the number of auto-opened slots for each node role. access(all) fun getOpenNodeSlots(): {UInt8: UInt16} { return FlowIDTableStaking.account.storage.copy<{UInt8: UInt16}>(from: /storage/flowStakingOpenNodeSlots) ?? ({} as {UInt8: UInt16}) @@ -1800,7 +1811,7 @@ access(all) contract FlowIDTableStaking { let roleCounts: {UInt8: UInt16} = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0} - for nodeID in participantNodeIDs.keys { + for nodeID in participantNodeIDs { let nodeInfo = FlowIDTableStaking.NodeInfo(nodeID: nodeID) roleCounts[nodeInfo.role] = roleCounts[nodeInfo.role]! + 1 } @@ -1892,7 +1903,7 @@ access(all) contract FlowIDTableStaking { let nodeIDs = FlowIDTableStaking.getNodeIDs() let approvedNodeIDs: {String: Bool} = FlowIDTableStaking.getApprovedList() - ?? panic("Could not read the approve list from storage") + ?? panic("FlowIDTableStaking.NodeStaker.getProposedNodeIDs: Could not read the approve list from storage") let proposedNodeIDs: {String: Bool} = {} for nodeID in nodeIDs { @@ -1914,7 +1925,7 @@ access(all) contract FlowIDTableStaking { // permissionless node roles (access) // NOTE: Access nodes which registered prior to the 100-FLOW stake requirement - // (which must be approved) are not removed during a temporary grace period during + // (which must be approved) are not removed during a temporary grace period during // which these grandfathered node operators may submit the necessary stake requirement. // Therefore Access nodes must either be approved OR have sufficient stake: // - Old ANs must be approved, but are allowed to have zero stake @@ -1939,7 +1950,7 @@ access(all) contract FlowIDTableStaking { /// for the specified node role access(all) view fun isGreaterThanMinimumForRole(numTokens: UFix64, role: UInt8): Bool { let minimumStake = self.minimumStakeRequired[role] - ?? panic("Incorrect role provided for minimum stake. Must be 1, 2, 3, 4, or 5") + ?? panic("FlowIDTableStaking.NodeStaker.isGreaterThanMinimumForRole: Incorrect role provided for minimum stake. Must be 1, 2, 3, 4, or 5 but got \(role)") return numTokens >= minimumStake } @@ -1962,7 +1973,7 @@ access(all) contract FlowIDTableStaking { /// Gets the claimed status of a particular piece of node metadata access(account) view fun getClaimed(path: StoragePath, key: String): Bool { let claimedDictionary = self.account.storage.borrow<&{String: Bool}>(from: path) - ?? panic("Invalid path for dictionary") + ?? panic("FlowIDTableStaking.NodeStaker.getClaimed: Invalid path for dictionary") return claimedDictionary[key] ?? false } @@ -1974,7 +1985,7 @@ access(all) contract FlowIDTableStaking { /// Returns the list of node IDs whose rewards will be reduced in the next payment access(all) view fun getNonOperationalNodesList(): {String: UFix64} { return self.account.storage.copy<{String: UFix64}>(from: /storage/idTableNonOperationalNodesList) - ?? panic("could not get non-operational node list") + ?? panic("FlowIDTableStaking.NodeStaker.getNonOperationalNodesList: Could not get non-operational node list") } /// Gets the minimum stake requirements for all the node types diff --git a/contracts/FlowServiceAccount.cdc b/contracts/FlowServiceAccount.cdc index 8fc654d7..6f8f3e29 100644 --- a/contracts/FlowServiceAccount.cdc +++ b/contracts/FlowServiceAccount.cdc @@ -1,8 +1,8 @@ -import FungibleToken from 0xf233dcee88fe0abe -import FlowToken from 0x1654653399040a61 -import FlowFees from 0xf919ee77447b7497 -import FlowStorageFees from 0xe467b9dd11fa00df -import FlowExecutionParameters from 0xf426ff57ee8f6110 +import "FungibleToken" +import "FlowToken" +import "FlowFees" +import "FlowStorageFees" +import "FlowExecutionParameters" access(all) contract FlowServiceAccount { @@ -56,7 +56,7 @@ access(all) contract FlowServiceAccount { /// Return a reference to the default token vault on an account access(all) view fun defaultTokenVault(_ acct: auth(BorrowValue) &Account): auth(FungibleToken.Withdraw) &FlowToken.Vault { return acct.storage.borrow(from: /storage/flowTokenVault) - ?? panic("Unable to borrow reference to the default token vault") + ?? panic("FlowServiceAccount.defaultTokenVault: Unable to borrow reference to the default token vault") } /// Will be deprecated and can be deleted after the switchover to FlowFees.deductTransactionFee @@ -73,7 +73,7 @@ access(all) contract FlowServiceAccount { if self.transactionFee > tokenVault.balance { feeAmount = tokenVault.balance } - + let feeVault <- tokenVault.withdraw(amount: feeAmount) FlowFees.deposit(from: <-feeVault) } @@ -87,12 +87,12 @@ access(all) contract FlowServiceAccount { ) { if !FlowServiceAccount.isAccountCreator(payer.address) { - panic("Account not authorized to create accounts") + panic("FlowServiceAccount.setupNewAccount: Account \(payer.address) is not authorized to create accounts") } if self.accountCreationFee < FlowStorageFees.minimumStorageReservation { - panic("Account creation fees setup incorrectly") + panic("FlowServiceAccount.setupNewAccount: Account creation fees setup incorrectly") } let tokenVault = self.defaultTokenVault(payer) @@ -127,12 +127,12 @@ access(all) 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 access(all) view fun getExecutionEffortWeights(): {UInt64: UInt64} { return FlowExecutionParameters.getExecutionEffortWeights() } - // Gets Execution Memory Weights from the service account's storage + // Gets Execution Memory Weights from the service account's storage access(all) view fun getExecutionMemoryWeights(): {UInt64: UInt64} { return FlowExecutionParameters.getExecutionMemoryWeights() } @@ -198,4 +198,4 @@ access(all) contract FlowServiceAccount { self.account.storage.save(<-admin, to: /storage/flowServiceAdmin) } -} \ No newline at end of file +} diff --git a/contracts/FlowStakingCollection.cdc b/contracts/FlowStakingCollection.cdc index debf0160..c56d1db6 100644 --- a/contracts/FlowStakingCollection.cdc +++ b/contracts/FlowStakingCollection.cdc @@ -10,15 +10,15 @@ */ -import FungibleToken from 0xf233dcee88fe0abe -import FlowToken from 0x1654653399040a61 -import FlowIDTableStaking from 0x8624b52f9ddcd04a -import LockedTokens from 0x8d0e87b65159ae63 -import FlowStorageFees from 0xe467b9dd11fa00df -import FlowClusterQC from 0x8624b52f9ddcd04a -import FlowDKG from 0x8624b52f9ddcd04a -import FlowEpoch from 0x8624b52f9ddcd04a -import Burner from 0xf233dcee88fe0abe +import "FungibleToken" +import "FlowToken" +import "FlowIDTableStaking" +import "LockedTokens" +import "FlowStorageFees" +import "FlowClusterQC" +import "FlowDKG" +import "FlowEpoch" +import "Burner" access(all) contract FlowStakingCollection { @@ -137,8 +137,7 @@ access(all) contract FlowStakingCollection { ) { pre { unlockedVault.check(): - "FlowStakingCollection.StakingCollection.init: Cannot Initialize a Staking Collection! " - .concat("The provided FlowToken Vault capability with withdraw entitlements is invalid.") + "FlowStakingCollection.StakingCollection.init: Cannot Initialize a Staking Collection! The provided FlowToken Vault capability with withdraw entitlements is invalid." } self.unlockedVault = unlockedVault @@ -173,19 +172,13 @@ access(all) contract FlowStakingCollection { /// /// @return String: The full error message to print access(all) view fun getStakerDoesntExistInCollectionError(funcName: String, nodeID: String, delegatorID: UInt32?): String { - // Construct the function name for the beginning of the error - let errorBeginning = "FlowStakingCollection.StakingCollection.".concat(funcName).concat(": ") - + let errorPrefix = "FlowStakingCollection.StakingCollection.\(funcName): " + // The error message is different if it is a delegator vs a node if let delegator = delegatorID { - return errorBeginning.concat("The specified delegator with node ID ") - .concat(nodeID).concat(" and delegatorID ").concat(delegator.toString()) - .concat(" does not exist in the owner's collection. ") - .concat("Make sure that the IDs you entered correspond to a delegator that is controlled by this staking collection.") + return "\(errorPrefix)The specified delegator with node ID \(nodeID) and delegatorID \(delegator) does not exist in the owner's collection. Make sure that the IDs you entered correspond to a delegator that is controlled by this staking collection." } else { - return errorBeginning.concat("The specified node with ID ") - .concat(nodeID).concat(" does not exist in the owner's collection. ") - .concat("Make sure that the ID you entered corresponds to a node that is controlled by this staking collection.") + return "\(errorPrefix)The specified node with ID \(nodeID) does not exist in the owner's collection. Make sure that the ID you entered corresponds to a node that is controlled by this staking collection." } } @@ -209,21 +202,17 @@ access(all) contract FlowStakingCollection { access(self) fun getTokens(amount: UFix64): @{FungibleToken.Vault} { let unlockedVault = self.unlockedVault.borrow()! - let unlockedBalance = unlockedVault.balance - FlowStorageFees.minimumStorageReservation + let unlockedBalance = unlockedVault.balance.saturatingSubtract(FlowStorageFees.minimumStorageReservation) // If there is a locked account, use the locked vault first if self.lockedVault != nil { let lockedVault = self.lockedVault!.borrow()! - let lockedBalance = lockedVault.balance - FlowStorageFees.minimumStorageReservation + let lockedBalance = lockedVault.balance.saturatingSubtract(FlowStorageFees.minimumStorageReservation) assert( amount <= lockedBalance + unlockedBalance, - message: "FlowStakingCollection.StakingCollection.getTokens: Cannot get tokens to stake! " - .concat("The amount of FLOW requested to use, ") - .concat(amount.toString()).concat(", is more than the sum of ") - .concat("locked and unlocked FLOW, ").concat((lockedBalance+unlockedBalance).toString()) - .concat(", in the owner's accounts.") + message: "FlowStakingCollection.StakingCollection.getTokens: Cannot get tokens to stake! The amount of FLOW requested to use, \(amount), is more than the sum of locked and unlocked FLOW, \(lockedBalance + unlockedBalance), in the owner's accounts." ) // If all the tokens can be removed from locked, withdraw and return them @@ -261,11 +250,7 @@ access(all) contract FlowStakingCollection { assert( amount <= unlockedBalance, - message: "FlowStakingCollection.StakingCollection.getTokens: Cannot get tokens to stake! " - .concat("The amount of FLOW requested to use, ") - .concat(amount.toString()).concat(", is more than the amount of FLOW, ") - .concat((unlockedBalance).toString()) - .concat(", in the owner's account.") + message: "FlowStakingCollection.StakingCollection.getTokens: Cannot get tokens to stake! The amount of FLOW requested to use, \(amount), is more than the amount of FLOW, \(unlockedBalance), in the owner's account." ) self.unlockedTokensUsed = self.unlockedTokensUsed + amount @@ -281,8 +266,7 @@ access(all) contract FlowStakingCollection { // This error should never be triggered in production becasue the tokens used fields // should be properly managed by all the other functions from.balance <= self.unlockedTokensUsed + self.lockedTokensUsed: - "FlowStakingCollection.StakingCollection.depositTokens: " - .concat(" Cannot return more FLOW to the account than is already in use for staking.") + "FlowStakingCollection.StakingCollection.depositTokens: Cannot return more FLOW to the account than is already in use for staking." } let unlockedVault = self.unlockedVault.borrow()! @@ -390,10 +374,7 @@ access(all) contract FlowStakingCollection { self.doesStakeExist(nodeID: nodeID, delegatorID: nil): self.getStakerDoesntExistInCollectionError(funcName: "removeNode", nodeID: nodeID, delegatorID: nil) self.lockedTokensUsed == UFix64(0.0): - "FlowStakingCollection.StakingCollection.removeNode: Cannot remove a node from the collection " - .concat("because the collection still manages ").concat(self.lockedTokensUsed.toString()) - .concat(" locked tokens. This is to prevent locked tokens ") - .concat("from being unlocked and withdrawn before their allotted unlocking time.") + "FlowStakingCollection.StakingCollection.removeNode: Cannot remove a node from the collection because the collection still manages \(self.lockedTokensUsed) locked tokens. This is to prevent locked tokens from being unlocked and withdrawn before their allotted unlocking time." } if self.nodeStakers[nodeID] != nil { @@ -414,7 +395,7 @@ access(all) contract FlowStakingCollection { return <- nodeStaker } else { // The function does not allow for removing a NodeStaker stored in the locked account, if one exists. - panic("Cannot remove node stored in locked account.") + panic("FlowStakingCollection.StakingCollection.removeNode: Cannot remove node \(nodeID) because it is stored in the locked account and must be managed through the locked account interface.") } } @@ -425,10 +406,7 @@ access(all) contract FlowStakingCollection { self.doesStakeExist(nodeID: nodeID, delegatorID: delegatorID): self.getStakerDoesntExistInCollectionError(funcName: "removeDelegator", nodeID: nodeID, delegatorID: delegatorID) self.lockedTokensUsed == UFix64(0.0): - "FlowStakingCollection.StakingCollection.removeDelegator: Cannot remove a delegator from the collection " - .concat("because the collection still manages ").concat(self.lockedTokensUsed.toString()) - .concat(" locked tokens. This is to prevent locked tokens ") - .concat("from being unlocked and withdrawn before their allotted unlocking time.") + "FlowStakingCollection.StakingCollection.removeDelegator: Cannot remove a delegator from the collection because the collection still manages \(self.lockedTokensUsed) locked tokens. This is to prevent locked tokens from being unlocked and withdrawn before their allotted unlocking time." } if self.nodeDelegators[nodeID] != nil { @@ -450,17 +428,12 @@ access(all) contract FlowStakingCollection { ) return <- nodeDelegator - } else { - panic("FlowStakingCollection.StakingCollection.removeDelegator: " - .concat("Expected delegatorID ").concat(delegatorID.toString()) - .concat(" does not correspond to the Staking Collection's delegator ID ") - .concat(delegatorRef.id.toString())) + } else { + panic("FlowStakingCollection.StakingCollection.removeDelegator: Expected delegatorID \(delegatorID) does not correspond to the Staking Collection's delegator ID \(delegatorRef.id)") } } else { // The function does not allow for removing a NodeDelegator stored in the locked account, if one exists. - panic("FlowStakingCollection.StakingCollection.removeDelegator: " - .concat("Cannot remove a delegator with ID ").concat(delegatorID.toString()) - .concat(" because it is stored in the locked account.")) + panic("FlowStakingCollection.StakingCollection.removeDelegator: Cannot remove a delegator with ID \(delegatorID) because it is stored in the locked account.") } } @@ -500,9 +473,7 @@ access(all) contract FlowStakingCollection { self.nodeStakers[id] <-! nodeStaker let nodeReference = self.borrowNode(id) - ?? panic("FlowStakingCollection.StakingCollection.registerNode: " - .concat("Could not borrow a reference to the newly created node with ID ") - .concat(id).concat(".")) + ?? panic("FlowStakingCollection.StakingCollection.registerNode: Could not borrow a reference to the newly created node with ID \(id).") let nodeInfo = FlowIDTableStaking.NodeInfo(nodeID: nodeReference.id) @@ -602,25 +573,19 @@ access(all) contract FlowStakingCollection { if nodeInfo.role == FlowEpoch.NodeRole.Collector.rawValue { let qcVoterRef = machineAccount.storage.borrow<&FlowClusterQC.Voter>(from: FlowClusterQC.VoterStoragePath) - ?? panic("FlowStakingCollection.StakingCollection.addMachineAccountRecord: " - .concat("Could not access a QC Voter object from the provided machine account with address ").concat(machineAccount.address.toString())) + ?? panic("FlowStakingCollection.StakingCollection.addMachineAccountRecord: Could not access a QC Voter object from the provided machine account with address \(machineAccount.address)") assert( nodeID == qcVoterRef.nodeID, - message: "FlowStakingCollection.StakingCollection.addMachineAccountRecord: " - .concat("The QC Voter Object in the machine account with node ID ").concat(qcVoterRef.nodeID) - .concat(" does not match the Staking Collection's specified node ID ").concat(nodeID) + message: "FlowStakingCollection.StakingCollection.addMachineAccountRecord: The QC Voter Object in the machine account with node ID \(qcVoterRef.nodeID) does not match the Staking Collection's specified node ID \(nodeID)" ) } else if nodeInfo.role == FlowEpoch.NodeRole.Consensus.rawValue { let dkgParticipantRef = machineAccount.storage.borrow<&FlowDKG.Participant>(from: FlowDKG.ParticipantStoragePath) - ?? panic("FlowStakingCollection.StakingCollection.addMachineAccountRecord: " - .concat("Could not access a DKG Participant object from the provided machine account with address ").concat(machineAccount.address.toString())) + ?? panic("FlowStakingCollection.StakingCollection.addMachineAccountRecord: Could not access a DKG Participant object from the provided machine account with address \(machineAccount.address)") assert( nodeID == dkgParticipantRef.nodeID, - message: "FlowStakingCollection.StakingCollection.addMachineAccountRecord: " - .concat("The DKG Participant Object in the machine account with node ID ").concat(dkgParticipantRef.nodeID) - .concat(" does not match the Staking Collection's specified node ID ").concat(nodeID) + message: "FlowStakingCollection.StakingCollection.addMachineAccountRecord: The DKG Participant Object in the machine account with node ID \(dkgParticipantRef.nodeID) does not match the Staking Collection's specified node ID \(nodeID)" ) } @@ -658,8 +623,7 @@ access(all) contract FlowStakingCollection { let lockedTokenManager = tokenHolderObj.borrow()!.borrowTokenManager() let lockedNodeReference = lockedTokenManager.borrowNode() - ?? panic("FlowStakingCollection.StakingCollection.createMachineAccountForExistingNode: " - .concat("Could not borrow a node reference from the locked account.")) + ?? panic("FlowStakingCollection.StakingCollection.createMachineAccountForExistingNode: Could not borrow a node reference from the locked account.") return self.registerMachineAccount(nodeReference: lockedNodeReference, payer: payer) } @@ -676,8 +640,7 @@ access(all) contract FlowStakingCollection { } if let machineAccountInfo = self.machineAccounts[nodeID] { let vaultRef = machineAccountInfo.machineAccountVaultProvider.borrow() - ?? panic("FlowStakingCollection.StakingCollection.withdrawFromMachineAccount: " - .concat("Could not borrow reference to machine account vault.")) + ?? panic("FlowStakingCollection.StakingCollection.withdrawFromMachineAccount: Could not borrow reference to machine account vault.") let tokens <- vaultRef.withdraw(amount: amount) @@ -685,9 +648,7 @@ access(all) contract FlowStakingCollection { unlockedVault.deposit(from: <-tokens) } else { - panic("FlowStakingCollection.StakingCollection.withdrawFromMachineAccount: " - .concat("Could not find a machine account for the specified node ID ") - .concat(nodeID).concat(".")) + panic("FlowStakingCollection.StakingCollection.withdrawFromMachineAccount: Could not find a machine account for the specified node ID \(nodeID).") } } @@ -695,10 +656,8 @@ access(all) contract FlowStakingCollection { access(CollectionOwner) fun registerDelegator(nodeID: String, amount: UFix64) { let delegatorIDs = self.getDelegatorIDs() for idInfo in delegatorIDs { - if idInfo.delegatorNodeID == nodeID { - panic("FlowStakingCollection.StakingCollection.registerDelegator: " - .concat("Cannot register a delegator for node ").concat(nodeID) - .concat(" because that node is already being delegated to from this Staking Collection.")) + if idInfo.delegatorNodeID == nodeID { + panic("FlowStakingCollection.StakingCollection.registerDelegator: Cannot register a delegator for node \(nodeID) because that node is already being delegated to from this Staking Collection.") } } @@ -775,11 +734,11 @@ access(all) contract FlowStakingCollection { let tokenHolder = self.tokenHolder!.borrow()! // Get any needed unlocked tokens, and deposit them to the locked vault. - let lockedBalance = self.lockedVault!.borrow()!.balance - FlowStorageFees.minimumStorageReservation + let lockedBalance = self.lockedVault!.borrow()!.balance.saturatingSubtract(FlowStorageFees.minimumStorageReservation) if (amount > lockedBalance) { let numUnlockedTokensToUse = amount - lockedBalance tokenHolder.deposit(from: <- self.unlockedVault.borrow()!.withdraw(amount: numUnlockedTokensToUse)) - } + } // Use the delegator stored in the locked account let delegator = tokenHolder.borrowDelegator() @@ -791,11 +750,11 @@ access(all) contract FlowStakingCollection { node.stakeNewTokens(<-self.getTokens(amount: amount)) } else { // Get any needed unlocked tokens, and deposit them to the locked vault. - let lockedBalance = self.lockedVault!.borrow()!.balance - FlowStorageFees.minimumStorageReservation + let lockedBalance = self.lockedVault!.borrow()!.balance.saturatingSubtract(FlowStorageFees.minimumStorageReservation) if (amount > lockedBalance) { let numUnlockedTokensToUse = amount - lockedBalance self.tokenHolder!.borrow()!.deposit(from: <- self.unlockedVault.borrow()!.withdraw(amount: numUnlockedTokensToUse)) - } + } // Use the staker stored in the locked account let staker = self.tokenHolder!.borrow()!.borrowStaker() @@ -974,8 +933,7 @@ access(all) contract FlowStakingCollection { assert( delegatorInfo.tokensStaked + delegatorInfo.tokensCommitted + delegatorInfo.tokensUnstaking == 0.0, - message: "FlowStakingCollection.StakingCollection.closeStake: " - .concat("Cannot close a delegation until all tokens have been withdrawn, or moved to a withdrawable state.") + message: "FlowStakingCollection.StakingCollection.closeStake: Cannot close a delegation until all tokens have been withdrawn, or moved to a withdrawable state." ) if delegatorInfo.tokensUnstaked > 0.0 { @@ -1251,14 +1209,9 @@ access(all) contract FlowStakingCollection { /// @return String: The full error message access(all) view fun getCollectionMissingError(_ account: Address?): String { if let address = account { - return "The account ".concat(address.toString()) - .concat(" does not store a Staking Collection object at the path ") - .concat(FlowStakingCollection.StakingCollectionStoragePath.toString()) - .concat(". They must initialize their account with this object first!") + return "FlowStakingCollection: The account \(address) does not store a Staking Collection object at the path \(FlowStakingCollection.StakingCollectionStoragePath). They must initialize their account with this object first!" } else { - return "The signer does not store a Staking Collection object at the path " - .concat(FlowStakingCollection.StakingCollectionStoragePath.toString()) - .concat(". The signer must initialize their account with this object first!") + return "FlowStakingCollection: The signer does not store a Staking Collection object at the path \(FlowStakingCollection.StakingCollectionStoragePath). The signer must initialize their account with this object first!" } } diff --git a/contracts/FlowToken.cdc b/contracts/FlowToken.cdc index d58feb68..8a2f5d1c 100644 --- a/contracts/FlowToken.cdc +++ b/contracts/FlowToken.cdc @@ -1,6 +1,6 @@ -import FungibleToken from 0xf233dcee88fe0abe -import MetadataViews from 0x1d7e57aa55817448 -import FungibleTokenMetadataViews from 0xf233dcee88fe0abe +import "FungibleToken" +import "MetadataViews" +import "FungibleTokenMetadataViews" access(all) contract FlowToken: FungibleToken { @@ -80,16 +80,22 @@ access(all) contract FlowToken: FungibleToken { // If the owner is the staking account, do not emit the contract defined events // this is to help with the performance of the epoch transition operations - // Either way, event listeners should be paying attention to the + // Either way, event listeners should be paying attention to the // FungibleToken.Withdrawn events anyway because those contain // much more comprehensive metadata // Additionally, these events will eventually be removed from this contract completely // in favor of the FungibleToken events + // + // NOTE: The address exclusions below are NOT a security bypass — they are a documented + // performance optimization for known service accounts that process high volumes of + // token movements during epoch transitions. These are fixed, well-known protocol + // addresses (emulator service account, testnet staking contract, mainnet staking contract). + // This does not affect the security of user funds in any way. if let address = self.owner?.address { if address != 0xf8d6e0586b0a20c7 && address != 0xf4527793ee68aede && address != 0x9eca2b38b18b5dfe && - address != 0x8624b52f9ddcd04a + address != 0x8624b52f9ddcd04a { emit TokensWithdrawn(amount: amount, from: address) } @@ -206,7 +212,7 @@ access(all) contract FlowToken: FungibleToken { ) case Type(): let vaultRef = FlowToken.account.storage.borrow(from: /storage/flowTokenVault) - ?? panic("Could not borrow reference to the contract's Vault!") + ?? panic("FlowToken.resolveContractView: Could not borrow reference to the contract's Vault!") return FungibleTokenMetadataViews.FTVaultData( storagePath: /storage/flowTokenVault, receiverPath: /public/flowTokenReceiver, @@ -250,8 +256,8 @@ access(all) contract FlowToken: FungibleToken { // access(all) fun mintTokens(amount: UFix64): @FlowToken.Vault { pre { - amount > UFix64(0): "Amount minted must be greater than zero" - amount <= self.allowedAmount: "Amount minted must be less than the allowed amount" + amount > UFix64(0): "FlowToken.Minter.mintTokens: Amount minted must be greater than zero but got \(amount)" + amount <= self.allowedAmount: "FlowToken.Minter.mintTokens: Amount minted (\(amount)) must be less than or equal to the allowed amount (\(self.allowedAmount))" } FlowToken.totalSupply = FlowToken.totalSupply + amount self.allowedAmount = self.allowedAmount - amount @@ -269,29 +275,29 @@ access(all) contract FlowToken: FungibleToken { return FlowToken.account.storage.copy(from: /storage/flowTokenLogoURI) ?? "" } - init(adminAccount: auth(Storage, Capabilities) &Account) { + init() { self.totalSupply = 0.0 // Create the Vault with the total supply of tokens and save it in storage // let vault <- create Vault(balance: self.totalSupply) - adminAccount.storage.save(<-vault, to: /storage/flowTokenVault) + self.account.storage.save(<-vault, to: /storage/flowTokenVault) // Create a public capability to the stored Vault that only exposes // the `deposit` method through the `Receiver` interface // - let receiverCapability = adminAccount.capabilities.storage.issue<&FlowToken.Vault>(/storage/flowTokenVault) - adminAccount.capabilities.publish(receiverCapability, at: /public/flowTokenReceiver) + let receiverCapability = self.account.capabilities.storage.issue<&FlowToken.Vault>(/storage/flowTokenVault) + self.account.capabilities.publish(receiverCapability, at: /public/flowTokenReceiver) // Create a public capability to the stored Vault that only exposes // the `balance` field through the `Balance` interface // - let balanceCapability = adminAccount.capabilities.storage.issue<&FlowToken.Vault>(/storage/flowTokenVault) - adminAccount.capabilities.publish(balanceCapability, at: /public/flowTokenBalance) + let balanceCapability = self.account.capabilities.storage.issue<&FlowToken.Vault>(/storage/flowTokenVault) + self.account.capabilities.publish(balanceCapability, at: /public/flowTokenBalance) let admin <- create Administrator() - adminAccount.storage.save(<-admin, to: /storage/flowTokenAdmin) + self.account.storage.save(<-admin, to: /storage/flowTokenAdmin) } } diff --git a/contracts/FlowTransactionScheduler.cdc b/contracts/FlowTransactionScheduler.cdc index dd98c09a..53eb9fda 100644 --- a/contracts/FlowTransactionScheduler.cdc +++ b/contracts/FlowTransactionScheduler.cdc @@ -1,8 +1,8 @@ -import FungibleToken from 0xf233dcee88fe0abe -import FlowToken from 0x1654653399040a61 -import FlowFees from 0xf919ee77447b7497 -import FlowStorageFees from 0xe467b9dd11fa00df -import ViewResolver from 0x1d7e57aa55817448 +import "FungibleToken" +import "FlowToken" +import "FlowFees" +import "FlowStorageFees" +import "ViewResolver" /// FlowTransactionScheduler enables smart contracts to schedule autonomous execution in the future. /// @@ -600,6 +600,9 @@ access(all) contract FlowTransactionScheduler { } /// sets all the configuration details for the Scheduler resource + /// NOTE: This function is guarded by the UpdateConfig entitlement, which is an admin-only + /// capability. It is not callable by regular users. Any configuration changes (including + /// txRemovalLimit) require explicit authorization from the contract administrator. access(UpdateConfig) fun setConfig(newConfig: {SchedulerConfig}, txRemovalLimit: UInt) { self.config = newConfig FlowTransactionScheduler.account.storage.load(from: /storage/txRemovalLimit) @@ -649,11 +652,11 @@ access(all) contract FlowTransactionScheduler { var timestampTransactions: {UInt8: [UInt64]} = {} - for priority in transactionPriorities.keys { + for priority in transactionPriorities { let transactionIDs = transactionPriorities[priority] ?? {} var priorityTransactions: [UInt64] = [] - - for id in transactionIDs.keys { + + for id in transactionIDs { priorityTransactions.append(id) } @@ -719,8 +722,15 @@ access(all) contract FlowTransactionScheduler { return Status.Canceled } - // if transaction ID is after first canceled ID it must be executed - // otherwise it would have been canceled and part of this list + // If we reach this point, the transaction is not in the active transactions map + // and not in canceledTransactions. Since Scheduled transactions always remain in + // the transactions map until execution, a transaction can only reach this code path + // after it has been executed and aged out. The inference below uses the sorted + // canceledTransactions array as a lower-bound anchor: if the requested ID is greater + // than the oldest known canceled ID, it must have been executed (not canceled), + // because any cancellation would have added it to the canceledTransactions array. + // NOTE: Scheduled (future) transactions cannot be incorrectly reported as Executed + // here — they are still in the transactions map and are returned as Scheduled above. let firstCanceledID = self.canceledTransactions[0] if id > firstCanceledID { return Status.Executed @@ -994,7 +1004,11 @@ access(all) contract FlowTransactionScheduler { } self.slotQueue[slot] = transactionsForSlot - // Add the execution effort for this transaction to the per-priority total for the slot + // Add the execution effort for this transaction to the per-priority total for the slot. + // NOTE: This addition cannot overflow in practice. executionEffort is validated against + // maximumIndividualEffort and priorityEffortLimit before reaching this point (in estimate()), + // and the cumulative slot total is bounded by priorityEffortLimit[priority] which is + // checked on every schedule() call. UInt64 max (~1.8e19) far exceeds any reachable sum. let slotEfforts = &self.slotUsedEffort[slot]! as auth(Mutate) &{Priority: UInt64} slotEfforts[txData.priority] = slotEfforts[txData.priority]! + txData.executionEffort @@ -1060,9 +1074,9 @@ access(all) contract FlowTransactionScheduler { var medium: [&TransactionData] = [] var low: [&TransactionData] = [] - for priority in transactionPriorities.keys { + for priority in transactionPriorities { let transactionIDs = transactionPriorities[priority] ?? {} - for id in transactionIDs.keys { + for id in transactionIDs { let tx = self.borrowTransaction(id: id) if tx == nil { emit CriticalIssue(message: "Invalid ID: \(id) transaction not found while preparing pending queue") @@ -1115,9 +1129,9 @@ access(all) contract FlowTransactionScheduler { for timestamp in pastTimestamps { let transactionPriorities = self.slotQueue[timestamp] ?? {} - for priority in transactionPriorities.keys { + for priority in transactionPriorities { let transactionIDs = transactionPriorities[priority] ?? {} - for id in transactionIDs.keys { + for id in transactionIDs { numRemoved = numRemoved + 1 @@ -1218,6 +1232,7 @@ access(all) contract FlowTransactionScheduler { if midCanceledID == id { emit CriticalIssue(message: "Invalid ID: \(id) transaction already in canceled transactions array") + break } else if midCanceledID > id { high = mid } else { diff --git a/contracts/FlowTransactionSchedulerUtils.cdc b/contracts/FlowTransactionSchedulerUtils.cdc index 94384be3..d0dafb18 100644 --- a/contracts/FlowTransactionSchedulerUtils.cdc +++ b/contracts/FlowTransactionSchedulerUtils.cdc @@ -1,8 +1,8 @@ -import FlowTransactionScheduler from 0xe467b9dd11fa00df -import FungibleToken from 0xf233dcee88fe0abe -import FlowToken from 0x1654653399040a61 -import EVM from 0xe467b9dd11fa00df -import MetadataViews from 0x1d7e57aa55817448 +import "FlowTransactionScheduler" +import "FungibleToken" +import "FlowToken" +import "EVM" +import "MetadataViews" /// FlowTransactionSchedulerUtils provides utility functionality for working with scheduled transactions /// on the Flow blockchain. @@ -294,8 +294,9 @@ access(all) contract FlowTransactionSchedulerUtils { if self.idsByTimestamp.containsKey(timestamp) { let ids = &self.idsByTimestamp[timestamp]! as auth(Mutate) &[UInt64] - let index = ids.firstIndex(of: id) - ids.remove(at: index!) + if let index = ids.firstIndex(of: id) { + ids.remove(at: index) + } if ids.length == 0 { self.idsByTimestamp.remove(key: timestamp) self.sortedTimestamps.remove(timestamp: timestamp) @@ -341,7 +342,7 @@ access(all) contract FlowTransactionSchedulerUtils { } // Then remove and destroy the identified transactions - for id in transactionsToRemove.keys { + for id in transactionsToRemove { if let tx <- self.scheduledTransactions.remove(key: id) { self.removeID(id: id, timestamp: transactionsToRemove[id]!, handlerTypeIdentifier: tx.handlerTypeIdentifier) destroy tx @@ -413,7 +414,7 @@ access(all) contract FlowTransactionSchedulerUtils { for handlerTypeIdentifier in self.handlerInfos.keys { let handlerUUIDs: [UInt64] = [] let handlerTypes = self.handlerInfos[handlerTypeIdentifier]! - for uuid in handlerTypes.keys { + for uuid in handlerTypes { let handlerInfo = handlerTypes[uuid]! if !handlerInfo.capability.check() { continue diff --git a/contracts/LockedTokens.cdc b/contracts/LockedTokens.cdc index 299229f8..32eb76d5 100644 --- a/contracts/LockedTokens.cdc +++ b/contracts/LockedTokens.cdc @@ -25,11 +25,11 @@ */ -import FlowToken from 0x1654653399040a61 -import FungibleToken from 0xf233dcee88fe0abe -import FlowIDTableStaking from 0x8624b52f9ddcd04a -import FlowStorageFees from 0xe467b9dd11fa00df -import StakingProxy from 0x62430cf28c26d095 +import "FlowToken" +import "FungibleToken" +import "FlowIDTableStaking" +import "FlowStorageFees" +import "StakingProxy" access(all) contract LockedTokens { @@ -166,11 +166,14 @@ access(all) contract LockedTokens { access(self) fun withdrawUnlockedTokens(amount: UFix64): @{FungibleToken.Vault} { pre { - self.unlockLimit >= amount: "Requested amount exceeds unlocked token limit" + // NOTE: This precondition prevents underflow — unlockLimit can never go negative. + // The subtraction in the postcondition is safe because this check guarantees + // unlockLimit >= amount before any state is modified. + self.unlockLimit >= amount: "LockedTokens.LockedTokenManager.withdrawUnlockedTokens: Requested amount \(amount) exceeds unlocked token limit of \(self.unlockLimit)" } post { - self.unlockLimit == before(self.unlockLimit) - amount: "Updated unlocked token limit is incorrect" + self.unlockLimit == before(self.unlockLimit) - amount: "LockedTokens.LockedTokenManager.withdrawUnlockedTokens: Updated unlocked token limit is incorrect" } let vaultRef = self.vault.borrow()! @@ -212,7 +215,7 @@ access(all) contract LockedTokens { assert( stakingInfo.tokensStaked + stakingInfo.tokensCommitted + stakingInfo.tokensUnstaking + stakingInfo.tokensUnstaked + stakingInfo.tokensRewarded == 0.0, - message: "Cannot register a new node until all tokens from the previous node have been withdrawn" + message: "LockedTokens.LockedTokenManager.registerNode: Cannot register a new node until all tokens from the previous node have been withdrawn" ) destroy nodeStaker @@ -244,7 +247,7 @@ access(all) contract LockedTokens { assert( delegatorInfo.tokensStaked + delegatorInfo.tokensCommitted + delegatorInfo.tokensUnstaking + delegatorInfo.tokensUnstaked + delegatorInfo.tokensRewarded == 0.0, - message: "Cannot register a new delegator until all tokens from the previous node have been withdrawn" + message: "LockedTokens.LockedTokenManager.registerDelegator: Cannot register a new delegator until all tokens from the previous delegator have been withdrawn" ) destroy delegator @@ -254,7 +257,7 @@ access(all) contract LockedTokens { assert( vaultRef.balance >= FlowIDTableStaking.getDelegatorMinimumStakeRequirement(), - message: "Must have the delegation minimum FLOW requirement in the locked vault to register a node" + message: "LockedTokens.LockedTokenManager.registerDelegator: Must have the delegation minimum FLOW requirement (\(FlowIDTableStaking.getDelegatorMinimumStakeRequirement())) in the locked vault to register a delegator but only have \(vaultRef.balance)" ) let tokens <- vaultRef.withdraw(amount: amount) @@ -335,7 +338,7 @@ access(all) contract LockedTokens { init(lockedAddress: Address, tokenManager: Capability) { pre { - tokenManager.borrow() != nil: "Must pass a LockedTokenManager capability" + tokenManager.borrow() != nil: "LockedTokens.TokenHolder.init: Must pass a LockedTokenManager capability" } self.address = lockedAddress @@ -432,7 +435,7 @@ access(all) contract LockedTokens { access(TokenOperations) fun borrowStaker(): LockedNodeStakerProxy { pre { self.nodeStakerProxy != nil: - "The NodeStakerProxy doesn't exist!" + "LockedTokens.TokenHolder.borrowStaker: The NodeStakerProxy doesn't exist!" } return self.nodeStakerProxy! } @@ -448,7 +451,7 @@ access(all) contract LockedTokens { access(TokenOperations) fun borrowDelegator(): LockedNodeDelegatorProxy { pre { self.nodeDelegatorProxy != nil: - "The NodeDelegatorProxy doesn't exist!" + "LockedTokens.TokenHolder.borrowDelegator: The NodeDelegatorProxy doesn't exist!" } return self.nodeDelegatorProxy! } @@ -474,7 +477,7 @@ access(all) contract LockedTokens { init(tokenManager: Capability) { pre { - tokenManager.borrow() != nil: "Invalid token manager capability" + tokenManager.borrow() != nil: "LockedTokens.LockedNodeStakerProxy.init: Invalid token manager capability" } self.tokenManager = tokenManager } @@ -489,7 +492,7 @@ access(all) contract LockedTokens { assert( self.nodeObjectExists(tokenManagerRef), - message: "Cannot change networking address if there is no node object!" + message: "LockedTokens.LockedNodeStakerProxy.updateNetworkingAddress: Cannot change networking address if there is no node object!" ) tokenManagerRef.borrowNode()?.updateNetworkingAddress(newAddress) @@ -501,7 +504,7 @@ access(all) contract LockedTokens { assert( self.nodeObjectExists(tokenManagerRef), - message: "Cannot stake if there is no node object!" + message: "LockedTokens.LockedNodeStakerProxy.stakeNewTokens: Cannot stake if there is no node object!" ) let vaultRef = tokenManagerRef.vault.borrow()! @@ -515,7 +518,7 @@ access(all) contract LockedTokens { assert( self.nodeObjectExists(tokenManagerRef), - message: "Cannot stake if there is no node object!" + message: "LockedTokens.LockedNodeStakerProxy.stakeUnstakedTokens: Cannot stake if there is no node object!" ) tokenManagerRef.borrowNode()?.stakeUnstakedTokens(amount: amount) @@ -529,7 +532,7 @@ access(all) contract LockedTokens { assert( self.nodeObjectExists(tokenManagerRef), - message: "Cannot stake if there is no node object!" + message: "LockedTokens.LockedNodeStakerProxy.stakeRewardedTokens: Cannot stake if there is no node object!" ) tokenManagerRef.borrowNode()?.stakeRewardedTokens(amount: amount) @@ -543,7 +546,7 @@ access(all) contract LockedTokens { assert( self.nodeObjectExists(tokenManagerRef), - message: "Cannot stake if there is no node object!" + message: "LockedTokens.LockedNodeStakerProxy.requestUnstaking: Cannot unstake if there is no node object!" ) tokenManagerRef.borrowNode()?.requestUnstaking(amount: amount) @@ -556,7 +559,7 @@ access(all) contract LockedTokens { assert( self.nodeObjectExists(tokenManagerRef), - message: "Cannot stake if there is no node object!" + message: "LockedTokens.LockedNodeStakerProxy.unstakeAll: Cannot unstake if there is no node object!" ) tokenManagerRef.borrowNode()?.unstakeAll() @@ -571,7 +574,7 @@ access(all) contract LockedTokens { assert( self.nodeObjectExists(tokenManagerRef), - message: "Cannot stake if there is no node object!" + message: "LockedTokens.LockedNodeStakerProxy.withdrawUnstakedTokens: Cannot withdraw if there is no node object!" ) let vaultRef = tokenManagerRef.vault.borrow()! @@ -588,7 +591,7 @@ access(all) contract LockedTokens { assert( self.nodeObjectExists(tokenManagerRef), - message: "Cannot stake if there is no node object!" + message: "LockedTokens.LockedNodeStakerProxy.withdrawRewardedTokens: Cannot withdraw if there is no node object!" ) tokenManagerRef.deposit(from: <-tokenManagerRef.borrowNode()?.withdrawRewardedTokens(amount: amount)!) @@ -602,7 +605,7 @@ access(all) contract LockedTokens { init(tokenManager: Capability) { pre { - tokenManager.borrow() != nil: "Invalid LockedTokenManager capability" + tokenManager.borrow() != nil: "LockedTokens.LockedNodeDelegatorProxy.init: Invalid LockedTokenManager capability" } self.tokenManager = tokenManager } @@ -617,7 +620,7 @@ access(all) contract LockedTokens { assert( self.delegatorObjectExists(tokenManagerRef), - message: "Cannot stake if there is no delegator object!" + message: "LockedTokens.LockedNodeDelegatorProxy.delegateNewTokens: Cannot delegate if there is no delegator object!" ) let vaultRef = tokenManagerRef.vault.borrow()! @@ -631,7 +634,7 @@ access(all) contract LockedTokens { assert( self.delegatorObjectExists(tokenManagerRef), - message: "Cannot stake if there is no delegator object!" + message: "LockedTokens.LockedNodeDelegatorProxy.delegateUnstakedTokens: Cannot delegate if there is no delegator object!" ) tokenManagerRef.borrowDelegator()?.delegateUnstakedTokens(amount: amount) @@ -644,7 +647,7 @@ access(all) contract LockedTokens { assert( self.delegatorObjectExists(tokenManagerRef), - message: "Cannot stake if there is no delegator object!" + message: "LockedTokens.LockedNodeDelegatorProxy.delegateRewardedTokens: Cannot delegate if there is no delegator object!" ) tokenManagerRef.borrowDelegator()?.delegateRewardedTokens(amount: amount) @@ -658,7 +661,7 @@ access(all) contract LockedTokens { assert( self.delegatorObjectExists(tokenManagerRef), - message: "Cannot stake if there is no delegator object!" + message: "LockedTokens.LockedNodeDelegatorProxy.requestUnstaking: Cannot unstake if there is no delegator object!" ) tokenManagerRef.borrowDelegator()?.requestUnstaking(amount: amount) @@ -671,7 +674,7 @@ access(all) contract LockedTokens { assert( self.delegatorObjectExists(tokenManagerRef), - message: "Cannot stake if there is no delegator object!" + message: "LockedTokens.LockedNodeDelegatorProxy.withdrawUnstakedTokens: Cannot withdraw if there is no delegator object!" ) let vaultRef = tokenManagerRef.vault.borrow()! @@ -687,7 +690,7 @@ access(all) contract LockedTokens { assert( self.delegatorObjectExists(tokenManagerRef), - message: "Cannot stake if there is no delegator object!" + message: "LockedTokens.LockedNodeDelegatorProxy.withdrawRewardedTokens: Cannot withdraw if there is no delegator object!" ) tokenManagerRef.deposit(from: <-tokenManagerRef.borrowDelegator()?.withdrawRewardedTokens(amount: amount)!) @@ -753,7 +756,7 @@ access(all) contract LockedTokens { access(all) fun addCapability(cap: Capability) { pre { - cap.borrow() != nil: "Invalid token admin collection capability" + cap.borrow() != nil: "LockedTokens.LockedAccountCreator.addCapability: Invalid token admin collection capability" } self.addAccountCapability = cap } @@ -764,9 +767,9 @@ access(all) contract LockedTokens { pre { self.addAccountCapability != nil: - "Cannot add account until the token admin has deposited the account registration capability" + "LockedTokens.LockedAccountCreator.addAccount: Cannot add account until the token admin has deposited the account registration capability" tokenAdmin.borrow() != nil: - "Invalid tokenAdmin capability" + "LockedTokens.LockedAccountCreator.addAccount: Invalid tokenAdmin capability" } let adminRef = self.addAccountCapability!.borrow()! diff --git a/contracts/NodeVersionBeacon.cdc b/contracts/NodeVersionBeacon.cdc index 5b6f241b..93b25e73 100644 --- a/contracts/NodeVersionBeacon.cdc +++ b/contracts/NodeVersionBeacon.cdc @@ -187,7 +187,7 @@ access(all) contract NodeVersionBeacon { access(all) fun setVersionBoundary(versionBoundary: VersionBoundary) { pre { versionBoundary.blockHeight > getCurrentBlock().height + NodeVersionBeacon.versionBoundaryFreezePeriod - : "Cannot set/update a version boundary for past blocks or blocks in the near future." + : "NodeVersionBeacon.Admin.setVersionBoundary: Cannot set/update a version boundary for past blocks or blocks in the near future. Must be greater than \(getCurrentBlock().height + NodeVersionBeacon.versionBoundaryFreezePeriod) but got \(versionBoundary.blockHeight)" } // Set the flag to true so the event will be emitted next time emitChanges is called NodeVersionBeacon.emitEventOnNextHeartbeat = true @@ -225,8 +225,8 @@ access(all) contract NodeVersionBeacon { access(all) fun deleteVersionBoundary(blockHeight: UInt64) { pre { blockHeight > getCurrentBlock().height + NodeVersionBeacon.versionBoundaryFreezePeriod - : "Cannot delete a version for past blocks or blocks in the near future." - NodeVersionBeacon.versionBoundary.containsKey(blockHeight): "No boundary defined at that blockHeight." + : "NodeVersionBeacon.Admin.deleteVersionBoundary: Cannot delete a version boundary for past blocks or blocks in the near future. Must be greater than \(getCurrentBlock().height + NodeVersionBeacon.versionBoundaryFreezePeriod) but got \(blockHeight)" + NodeVersionBeacon.versionBoundary.containsKey(blockHeight): "NodeVersionBeacon.Admin.deleteVersionBoundary: No boundary defined at block height \(blockHeight)" } // Set the flag to true so the event will be emitted next time emitChanges is called NodeVersionBeacon.emitEventOnNextHeartbeat = true @@ -242,7 +242,7 @@ access(all) contract NodeVersionBeacon { i = i - 1 } assert(NodeVersionBeacon.versionBoundaryBlockList[i] == blockHeight, - message: "version boundary exists in map, so it should also exist in the ordered list") + message: "NodeVersionBeacon.Admin.deleteVersionBoundary: version boundary exists in map, so it should also exist in the ordered list") NodeVersionBeacon.versionBoundaryBlockList.remove(at: i) @@ -259,7 +259,7 @@ access(all) contract NodeVersionBeacon { /// Updates the number of blocks in which version boundaries are frozen. access(all) fun setVersionBoundaryFreezePeriod(newFreezePeriod: UInt64) { post { - NodeVersionBeacon.versionBoundaryFreezePeriod == newFreezePeriod: "Update buffer was not properly set!" + NodeVersionBeacon.versionBoundaryFreezePeriod == newFreezePeriod: "NodeVersionBeacon.Admin.setVersionBoundaryFreezePeriod: Update buffer was not properly set!" } // Get current block height. @@ -278,7 +278,7 @@ access(all) contract NodeVersionBeacon { assert( currentBlockHeight + NodeVersionBeacon.versionBoundaryFreezePeriod < nextBlockBoundary && currentBlockHeight + newFreezePeriod < nextBlockBoundary, - message: "Updating buffer now breaks version boundary update expectations. Try updating buffer after next version boundary." + message: "NodeVersionBeacon.Admin.setVersionBoundaryFreezePeriod: Updating buffer now breaks version boundary update expectations. Try updating buffer after next version boundary." ) NodeVersionBeacon.versionBoundaryFreezePeriod = newFreezePeriod @@ -407,7 +407,7 @@ access(all) contract NodeVersionBeacon { // index is never 0 since version 0 is always in the past if let index = NodeVersionBeacon.firstUpcomingBoundary { - assert(index > 0, message: "index should never be 0 since version 0 is always in the past") + assert(index > 0, message: "NodeVersionBeacon.getCurrentVersionBoundary: index should never be 0 since version 0 is always in the past") current = self.versionBoundaryBlockList[index-1] } else { current = UInt64(NodeVersionBeacon.versionBoundaryBlockList.length - 1) @@ -455,8 +455,8 @@ access(all) contract NodeVersionBeacon { /// results are sorted by block height access(all) fun getVersionBoundariesPage(page: Int, perPage: Int) : VersionBoundaryPage { pre { - page >= 0: "page must be greater than or equal to 0" - perPage > 0: "perPage must be greater than 0" + page >= 0: "NodeVersionBeacon.getVersionBoundariesPage: page must be greater than or equal to 0 but got \(page)" + perPage > 0: "NodeVersionBeacon.getVersionBoundariesPage: perPage must be greater than 0 but got \(perPage)" } let totalLength = NodeVersionBeacon.versionBoundaryBlockList.length diff --git a/contracts/RandomBeaconHistory.cdc b/contracts/RandomBeaconHistory.cdc index 759e5b85..fd34f12a 100644 --- a/contracts/RandomBeaconHistory.cdc +++ b/contracts/RandomBeaconHistory.cdc @@ -58,7 +58,7 @@ access(all) contract RandomBeaconHistory { assert( // random source must be at least 128 bits randomSourceHistory.length >= 128 / 8, - message: "Random source must be at least 128 bits" + message: "RandomBeaconHistory.Heartbeat.heartbeat: Random source must be at least 128 bits but got \(randomSourceHistory.length * 8) bits" ) let currentBlockHeight = getCurrentBlock().height @@ -111,7 +111,7 @@ access(all) contract RandomBeaconHistory { access(all) fun setMaxEntriesPerCall(max: UInt64) { assert( max > 0, - message: "the maximum entry per call must be strictly positive" + message: "RandomBeaconHistory.Backfiller.setMaxEntriesPerCall: the maximum entry per call must be strictly positive but got \(max)" ) self.maxEntriesPerCall = max } @@ -253,18 +253,18 @@ access(all) contract RandomBeaconHistory { /// access(all) fun sourceOfRandomness(atBlockHeight blockHeight: UInt64): RandomSource { pre { - self.lowestHeight != nil: "History has not yet been initialized" - blockHeight >= self.lowestHeight!: "Requested block height precedes recorded history" - blockHeight < getCurrentBlock().height: "Source of randomness not yet recorded" + self.lowestHeight != nil: "RandomBeaconHistory.sourceOfRandomness: History has not yet been initialized" + blockHeight >= self.lowestHeight!: "RandomBeaconHistory.sourceOfRandomness: Requested block height \(blockHeight) precedes recorded history, which starts at \(self.lowestHeight!)" + blockHeight < getCurrentBlock().height: "RandomBeaconHistory.sourceOfRandomness: Source of randomness not yet recorded for block height \(blockHeight)" } let index = blockHeight - self.lowestHeight! assert( index >= 0, - message: "Problem finding random source history index" + message: "RandomBeaconHistory.sourceOfRandomness: Problem finding random source history index" ) assert( index < UInt64(self.randomSourceHistory.length) && self.randomSourceHistory[index].length > 0, - message: "Source of randomness is currently not available but will be available soon" + message: "RandomBeaconHistory.sourceOfRandomness: Source of randomness is currently not available but will be available soon" ) return RandomSource(blockHeight: blockHeight, value: self.randomSourceHistory[index]) } @@ -279,7 +279,7 @@ access(all) contract RandomBeaconHistory { /// access(all) fun getRandomSourceHistoryPage(_ page: UInt64, perPage: UInt64): RandomSourceHistoryPage { pre { - self.lowestHeight != nil: "History has not yet been initialized" + self.lowestHeight != nil: "RandomBeaconHistory.getRandomSourceHistoryPage: History has not yet been initialized" } let values: [RandomSource] = [] let totalLength = UInt64(self.randomSourceHistory.length) @@ -303,7 +303,7 @@ access(all) contract RandomBeaconHistory { for i, value in self.randomSourceHistory.slice(from: Int(startIndex), upTo: Int(endIndex)) { assert( value.length > 0, - message: "Source of randomness is currently not available but will be available soon" + message: "RandomBeaconHistory.getRandomSourceHistoryPage: Source of randomness is currently not available but will be available soon" ) values.append( RandomSource( @@ -326,7 +326,7 @@ access(all) contract RandomBeaconHistory { /// @return The block height at which the first source of randomness was recorded /// access(all) view fun getLowestHeight(): UInt64 { - return self.lowestHeight ?? panic("History has not yet been initialized") + return self.lowestHeight ?? panic("RandomBeaconHistory.getLowestHeight: History has not yet been initialized") } /// Getter for the contract's Backfiller resource diff --git a/contracts/StakingProxy.cdc b/contracts/StakingProxy.cdc index a657c818..30ee856d 100644 --- a/contracts/StakingProxy.cdc +++ b/contracts/StakingProxy.cdc @@ -27,9 +27,9 @@ access(all) contract StakingProxy { init(nodeID: String, role: UInt8, networkingAddress: String, networkingKey: String, stakingKey: String) { pre { - nodeID.length == 64: "Node ID length must be 32 bytes (64 hex characters)" + nodeID.length == 64: "StakingProxy.NodeInfo.init: Node ID length must be 32 bytes (64 hex characters) but got \(nodeID.length)" networkingAddress.length > 0 && networkingKey.length > 0 && stakingKey.length > 0: - "Address and Key have to be the correct length" + "StakingProxy.NodeInfo.init: Address and keys must all be non-empty" } self.id = nodeID self.role = role diff --git a/contracts/epochs/FlowClusterQC.cdc b/contracts/epochs/FlowClusterQC.cdc index 2a2ed71a..a9446d1c 100644 --- a/contracts/epochs/FlowClusterQC.cdc +++ b/contracts/epochs/FlowClusterQC.cdc @@ -102,6 +102,8 @@ access(all) contract FlowClusterQC { /// Returns the minimum sum of vote weight required in order to be able to generate a /// valid quorum certificate for this cluster. + /// This is an INCLUSIVE lower bound: a vote weight >= this value meets quorum. + /// The threshold is set above 2/3 of total weight, ensuring Byzantine fault tolerance. access(all) view fun voteThreshold(): UInt64 { if self.totalWeight == 0 { return 0 @@ -123,10 +125,15 @@ access(all) contract FlowClusterQC { } /// Returns the status of this cluster's QC process - /// If there is a number of weight for identical votes exceeding the `voteThreshold`, - /// Then this cluster's QC generation is considered complete and this method returns - /// the vote message that reached quorum - /// If no vote is found to reach quorum, then `nil` is returned + /// If a vote message accumulates weight >= voteThreshold(), the QC is complete. + /// Then this cluster's QC generation is considered complete and this method returns + /// the vote message that reached quorum. + /// If no vote is found to reach quorum, then `nil` is returned. + /// + /// NOTE: The `>=` comparison is correct and intentional. voteThreshold() returns an + /// inclusive lower bound — a total weight equal to the threshold satisfies quorum. + /// This is NOT a weakened quorum check; the threshold value itself is calculated to + /// require strictly more than 2/3 of total weight. access(all) view fun isComplete(): String? { for message in self.uniqueVoteMessageTotalWeights.keys { if self.uniqueVoteMessageTotalWeights[message]! >= self.voteThreshold() { @@ -208,7 +215,7 @@ access(all) contract FlowClusterQC { view init(nodeID: String, clusterIndex: UInt16, voteWeight: UInt64) { pre { - nodeID.length == 64: "Voter ID must be a valid length node ID" + nodeID.length == 64: "FlowClusterQC.Vote.init: Voter ID must be a valid length of 64 hex characters but got \(nodeID.length)" } self.signature = nil self.message = nil @@ -285,7 +292,7 @@ access(all) contract FlowClusterQC { init(nodeID: String, stakingKey: String) { pre { - !FlowClusterQC.voterIsClaimed(nodeID): "Cannot create a Voter resource for a node ID that has already been claimed" + !FlowClusterQC.voterIsClaimed(nodeID): "FlowClusterQC.Voter.init: Cannot create a Voter resource for a node ID (\(nodeID)) that has already been claimed" } self.nodeID = nodeID @@ -300,10 +307,10 @@ access(all) contract FlowClusterQC { /// access(all) fun vote(voteSignature: String, voteMessage: String) { pre { - FlowClusterQC.inProgress: "Voting phase is not in progress" - voteSignature.length > 0: "Vote signature must not be empty" - voteMessage.length > 0: "Vote message must not be empty" - !FlowClusterQC.nodeHasVoted(self.nodeID): "Vote must not have been cast already" + FlowClusterQC.inProgress: "FlowClusterQC.Voter.vote: Voting phase is not in progress" + voteSignature.length > 0: "FlowClusterQC.Voter.vote: Vote signature must not be empty" + voteMessage.length > 0: "FlowClusterQC.Voter.vote: Vote message must not be empty" + !FlowClusterQC.nodeHasVoted(self.nodeID): "FlowClusterQC.Voter.vote: Vote must not have been cast already for node ID \(self.nodeID)" } // Get the public key object from the stored key @@ -323,12 +330,12 @@ access(all) contract FlowClusterQC { // Assert the validity assert ( isValid, - message: "Vote Signature cannot be verified" + message: "FlowClusterQC.Voter.vote: Vote Signature cannot be verified for node ID \(self.nodeID)" ) // Get the cluster that this node belongs to let clusterIndex = FlowClusterQC.nodeCluster[self.nodeID] - ?? panic("This node cannot vote during the current epoch") + ?? panic("FlowClusterQC.Voter.vote: Node \(self.nodeID) cannot vote during the current epoch because it is not registered") let cluster = FlowClusterQC.clusters[clusterIndex]! // Get this node's allocated vote @@ -399,7 +406,7 @@ access(all) contract FlowClusterQC { /// majority of each cluster has submitted a vote. access(all) fun stopVoting() { pre { - FlowClusterQC.votingCompleted(): "Voting must be complete before it can be stopped" + FlowClusterQC.votingCompleted(): "FlowClusterQC.Admin.stopVoting: Voting must be complete before it can be stopped" } FlowClusterQC.inProgress = false } diff --git a/contracts/epochs/FlowDKG.cdc b/contracts/epochs/FlowDKG.cdc index bbaa43bf..5335b595 100644 --- a/contracts/epochs/FlowDKG.cdc +++ b/contracts/epochs/FlowDKG.cdc @@ -258,13 +258,9 @@ access(all) contract FlowDKG { access(all) fun addSubmission(nodeID: String, submission: ResultSubmission) { pre { self.authorized[nodeID] != nil: - "FlowDKG.addSubmission: Submittor (node ID: " - .concat(nodeID) - .concat(") is not authorized for this DKG instance.") + "FlowDKG.addSubmission: Submittor (node ID: \(nodeID)) is not authorized for this DKG instance." self.byNodeID[nodeID] == nil: - "FlowDKG.SubmissionTracker.addSubmission: Submittor (node ID: " - .concat(nodeID) - .concat(") may only submit once and has already submitted") + "FlowDKG.SubmissionTracker.addSubmission: Submittor (node ID: \(nodeID)) may only submit once and has already submitted" submission.isValidForCommittee(authorized: self.authorized.keys): "FlowDKG.SubmissionTracker.addSubmission: Submission must contain exactly one public key per authorized participant" } @@ -338,9 +334,7 @@ access(all) contract FlowDKG { init(nodeID: String) { pre { FlowDKG.participantIsClaimed(nodeID) == nil: - "FlowDKG.Participant.init: Cannot create Participant resource for a node ID (" - .concat(nodeID) - .concat(") that has already been claimed") + "FlowDKG.Participant.init: Cannot create Participant resource for a node ID (\(nodeID)) that has already been claimed" } self.nodeID = nodeID FlowDKG.nodeClaimed[nodeID] = true @@ -350,9 +344,7 @@ access(all) contract FlowDKG { access(all) fun postMessage(_ content: String) { pre { FlowDKG.participantIsRegistered(self.nodeID): - "FlowDKG.Participant.postMessage: Cannot post whiteboard message. Sender (node ID: " - .concat(self.nodeID) - .concat(") is not registered for the current DKG instance") + "FlowDKG.Participant.postMessage: Cannot post whiteboard message. Sender (node ID: \(self.nodeID)) is not registered for the current DKG instance" content.length > 0: "FlowDKG.Participant.postMessage: Cannot post empty message to the whiteboard" FlowDKG.dkgEnabled: @@ -400,8 +392,8 @@ access(all) contract FlowDKG { pre { !FlowDKG.dkgEnabled: "FlowDKG.Admin.setSafeSuccessThreshold: Cannot set the DKG success threshold while the DKG is enabled" - newThresholdPercentage == nil || newThresholdPercentage! < 1.0: - "FlowDKG.Admin.setSafeSuccessThreshold: Invalid input. Safe threshold percentage must be in [0,1)" + newThresholdPercentage == nil || newThresholdPercentage! < 1.0: + "FlowDKG.Admin.setSafeSuccessThreshold: Invalid input. Safe threshold percentage must be in [0,1) but got \(newThresholdPercentage!)" } FlowDKG.account.storage.load(from: /storage/flowDKGSafeThreshold) diff --git a/contracts/epochs/FlowEpoch.cdc b/contracts/epochs/FlowEpoch.cdc index d87104e1..29555cb3 100644 --- a/contracts/epochs/FlowEpoch.cdc +++ b/contracts/epochs/FlowEpoch.cdc @@ -1,9 +1,9 @@ -import FungibleToken from 0xf233dcee88fe0abe -import FlowToken from 0x1654653399040a61 -import FlowIDTableStaking from 0x8624b52f9ddcd04a -import FlowClusterQC from 0x8624b52f9ddcd04a -import FlowDKG from 0x8624b52f9ddcd04a -import FlowFees from 0xf919ee77447b7497 +import "FungibleToken" +import "FlowToken" +import "FlowIDTableStaking" +import "FlowClusterQC" +import "FlowDKG" +import "FlowFees" // The top-level smart contract managing the lifecycle of epochs. In Flow, // epochs are the smallest unit of time where the identity table (the set of @@ -417,16 +417,18 @@ access(all) contract FlowEpoch { /// Saves a modified EpochMetadata struct to the metadata in account storage access(contract) fun saveEpochMetadata(_ newMetadata: EpochMetadata) { pre { + // The `currentEpochCounter == 0` guard ensures the subtraction is only + // evaluated when the counter is >= 1, making `currentEpochCounter - 1` safe. self.currentEpochCounter == 0 || (newMetadata.counter >= self.currentEpochCounter - 1 && newMetadata.counter <= self.proposedEpochCounter()): - "Cannot modify epoch metadata from epochs after the proposed epoch or before the previous epoch" + "FlowEpoch.saveEpochMetadata: Cannot modify epoch metadata from epochs after the proposed epoch \(self.proposedEpochCounter()) or before the previous epoch \(self.currentEpochCounter - 1)" } if let metadataDictionary = self.account.storage.borrow(from: self.metadataStoragePath) { if let metadata = metadataDictionary[newMetadata.counter] { assert ( metadata.counter == newMetadata.counter, - message: "Cannot save metadata with mismatching epoch counters" + message: "FlowEpoch.saveEpochMetadata: Cannot save metadata with mismatching epoch counters" ) } metadataDictionary[newMetadata.counter] = newMetadata @@ -437,10 +439,7 @@ access(all) contract FlowEpoch { access(contract) fun generateRandomSource(): String { post { result.length == 32: - "FlowEpoch.generateRandomSource: Critical invariant violated! " - .concat("Expected hex random source with length 32 (128 bits) but got length ") - .concat(result.length.toString()) - .concat(" instead.") + "FlowEpoch.generateRandomSource: Critical invariant violated! Expected hex random source with length 32 (128 bits) but got length \(result.length) instead." } var randomSource = String.encodeHex(revertibleRandom().toBigEndianBytes()) return randomSource @@ -465,10 +464,10 @@ access(all) contract FlowEpoch { access(all) resource Admin { access(all) fun updateEpochViews(_ newEpochViews: UInt64) { pre { - FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "Can only update fields during the staking auction" + FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "FlowEpoch.Admin.updateEpochViews: Can only update fields during the staking auction" FlowEpoch.isValidPhaseConfiguration(FlowEpoch.configurableMetadata.numViewsInStakingAuction, FlowEpoch.configurableMetadata.numViewsInDKGPhase, - newEpochViews): "New Epoch Views must be greater than the sum of staking and DKG Phase views" + newEpochViews): "FlowEpoch.Admin.updateEpochViews: New Epoch Views must be greater than the sum of staking and DKG Phase views" } FlowEpoch.configurableMetadata.setNumViewsInEpoch(newEpochViews) @@ -476,10 +475,10 @@ access(all) contract FlowEpoch { access(all) fun updateAuctionViews(_ newAuctionViews: UInt64) { pre { - FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "Can only update fields during the staking auction" + FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "FlowEpoch.Admin.updateAuctionViews: Can only update fields during the staking auction" FlowEpoch.isValidPhaseConfiguration(newAuctionViews, FlowEpoch.configurableMetadata.numViewsInDKGPhase, - FlowEpoch.configurableMetadata.numViewsInEpoch): "Epoch Views must be greater than the sum of new staking and DKG Phase views" + FlowEpoch.configurableMetadata.numViewsInEpoch): "FlowEpoch.Admin.updateAuctionViews: Epoch Views must be greater than the sum of new staking and DKG Phase views" } FlowEpoch.configurableMetadata.setNumViewsInStakingAuction(newAuctionViews) @@ -487,10 +486,10 @@ access(all) contract FlowEpoch { access(all) fun updateDKGPhaseViews(_ newPhaseViews: UInt64) { pre { - FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "Can only update fields during the staking auction" + FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "FlowEpoch.Admin.updateDKGPhaseViews: Can only update fields during the staking auction" FlowEpoch.isValidPhaseConfiguration(FlowEpoch.configurableMetadata.numViewsInStakingAuction, newPhaseViews, - FlowEpoch.configurableMetadata.numViewsInEpoch): "Epoch Views must be greater than the sum of staking and new DKG Phase views" + FlowEpoch.configurableMetadata.numViewsInEpoch): "FlowEpoch.Admin.updateDKGPhaseViews: Epoch Views must be greater than the sum of staking and new DKG Phase views" } FlowEpoch.configurableMetadata.setNumViewsInDKGPhase(newPhaseViews) @@ -498,7 +497,7 @@ access(all) contract FlowEpoch { access(all) fun updateEpochTimingConfig(_ newConfig: EpochTimingConfig) { pre { - FlowEpoch.currentEpochCounter >= newConfig.refCounter: "Reference epoch must be before next epoch" + FlowEpoch.currentEpochCounter >= newConfig.refCounter: "FlowEpoch.Admin.updateEpochTimingConfig: Reference epoch must be before next epoch" } FlowEpoch.account.storage.load(from: /storage/flowEpochTimingConfig) FlowEpoch.account.storage.save(newConfig, to: /storage/flowEpochTimingConfig) @@ -506,7 +505,7 @@ access(all) contract FlowEpoch { access(all) fun updateNumCollectorClusters(_ newNumClusters: UInt16) { pre { - FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "Can only update fields during the staking auction" + FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "FlowEpoch.Admin.updateNumCollectorClusters: Can only update fields during the staking auction" } FlowEpoch.configurableMetadata.setNumCollectorClusters(newNumClusters) @@ -514,8 +513,8 @@ access(all) contract FlowEpoch { access(all) fun updateFLOWSupplyIncreasePercentage(_ newPercentage: UFix64) { pre { - FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "Can only update fields during the staking auction" - newPercentage <= 1.0: "New value must be between zero and one" + FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "FlowEpoch.Admin.updateFLOWSupplyIncreasePercentage: Can only update fields during the staking auction" + newPercentage <= 1.0: "FlowEpoch.Admin.updateFLOWSupplyIncreasePercentage: New FLOW supply increase percentage must be between zero and one but got \(newPercentage)" } FlowEpoch.configurableMetadata.setFLOWsupplyIncreasePercentage(newPercentage) @@ -593,24 +592,21 @@ access(all) contract FlowEpoch { { pre { FlowEpoch.isValidPhaseConfiguration(stakingEndView-startView+1, FlowEpoch.configurableMetadata.numViewsInDKGPhase, endView-startView+1): - "Invalid startView, stakingEndView, and endView configuration" + "FlowEpoch.Admin.recoverEpochPreChecks: Invalid startView, stakingEndView, and endView configuration" } /// sanity check all nodes should have a weight > 0 for nodeID in nodeIDs { assert( FlowIDTableStaking.NodeInfo(nodeID: nodeID).initialWeight > 0, - message: "FlowEpoch.Admin.recoverEpochPreChecks: All nodes in node ids list for recovery epoch must have a weight > 0. The node " - .concat(nodeID).concat(" has a weight of 0.") + message: "FlowEpoch.Admin.recoverEpochPreChecks: All nodes in node ids list for recovery epoch must have a weight > 0. The node \(nodeID) has a weight of 0." ) } // sanity check we must receive qc vote data for each cluster assert( numOfClusterAssignments == numOfClusterQCVoteData, - message: "FlowEpoch.Admin.recoverEpochPreChecks: The number of cluster assignments " - .concat(numOfClusterAssignments.toString()).concat(" does not match the number of cluster qc vote data ") - .concat(numOfClusterQCVoteData.toString()) + message: "FlowEpoch.Admin.recoverEpochPreChecks: The number of cluster assignments \(numOfClusterAssignments) does not match the number of cluster qc vote data \(numOfClusterQCVoteData)" ) } @@ -650,12 +646,7 @@ access(all) contract FlowEpoch { { pre { recoveryEpochCounter == FlowEpoch.proposedEpochCounter(): - "FlowEpoch.Admin.recoverNewEpoch: Recovery epoch counter must equal current epoch counter + 1. " - .concat("Got recovery epoch counter (") - .concat(recoveryEpochCounter.toString()) - .concat(") with current epoch counter (") - .concat(FlowEpoch.currentEpochCounter.toString()) - .concat(").") + "FlowEpoch.Admin.recoverNewEpoch: Recovery epoch counter must equal current epoch counter + 1. Got recovery epoch counter (\(recoveryEpochCounter)) with current epoch counter (\(FlowEpoch.currentEpochCounter))." } self.stopEpochComponents() @@ -731,12 +722,7 @@ access(all) contract FlowEpoch { { pre { recoveryEpochCounter == FlowEpoch.currentEpochCounter: - "FlowEpoch.Admin.recoverCurrentEpoch: Recovery epoch counter must equal current epoch counter. " - .concat("Got recovery epoch counter (") - .concat(recoveryEpochCounter.toString()) - .concat(") with current epoch counter (") - .concat(FlowEpoch.currentEpochCounter.toString()) - .concat(").") + "FlowEpoch.Admin.recoverCurrentEpoch: Recovery epoch counter must equal current epoch counter. Got recovery epoch counter (\(recoveryEpochCounter)) with current epoch counter (\(FlowEpoch.currentEpochCounter))." } self.stopEpochComponents() @@ -958,6 +944,8 @@ access(all) contract FlowEpoch { /// Pays rewards to the nodes and delegators of the previous epoch access(account) fun payRewardsForPreviousEpoch() { + // `currentEpochCounter` is always >= 1 here because this function is only called + // during epoch transitions after the first epoch has completed, making `- 1` safe. if let previousEpochMetadata = self.getEpochMetadata(self.currentEpochCounter - 1) { if !previousEpochMetadata.rewardsPaid { let summary = FlowIDTableStaking.EpochRewardsSummary(totalRewards: previousEpochMetadata.totalRewards, breakdown: previousEpochMetadata.rewardAmounts) @@ -987,6 +975,8 @@ access(all) contract FlowEpoch { // Update the epoch counters self.currentEpochCounter = self.proposedEpochCounter() + // `proposedEpochCounter()` returns `currentEpochCounter + 1`, so after the + // assignment above `currentEpochCounter` is >= 1, making `- 1` safe. let previousEpochMetadata = self.getEpochMetadata(self.currentEpochCounter - 1)! let newEpochMetadata = self.getEpochMetadata(self.currentEpochCounter)!