September 22, 2022
This security assessment was prepared by OpenZeppelin, protecting the open economy.
Table of Contents
- Table of Contents
- Summary
- Scope
- Findings
- Medium Severity
- Low Severity
- Governor contract can create emergency proposals
- Incorrect public visibility
- Lack of address validation in setExecutor
- Missing documentation
- Missing error messages in require statements
- Missing modifier onlyIfNotMigrated enables new price requests on migrated contracts
- Missing test coverage
- Nonintuitive behavior of payable transactions in executeEmergencyProposal
- require statements with multiple conditions
- Unlocked Solidity version pragma
- Notes & Additional Information
- Lack of immutable identifier
- Misleading documentation
- Non-explicit imports are used
- Unnecessary modifier in setDelegate and setDelegator
- Redundant code
- Redundant event parameters
- Drift between test and production contracts
- Typographical errors
- Unnecessary non-empty return value on success
- Unnecessary public visibility in executeEmergencyProposal
- Unused imports
- Unused variables
- Conclusions
Summary
- Type
- DeFi – Oracles and Governance
- Timeline
- From 2022-08-22
- To 2022-09-02
- Languages
- Solidity
- Total Issues
- 23 (18 resolved, 1 partially resolved)
- Critical Severity Issues
- 0 (0 resolved)
- High Severity Issues
- 0 (0 resolved)
- Medium Severity Issues
- 1 (1 resolved)
- Low Severity Issues
- 10 (8 resolved, 1 partially resolved)
- Notes & Additional Information
- 12 (9 resolved)
Scope
We audited the following set of pull requests that introduce improvements and bug fixes to the UMAprotocol/protocol repository:
- PR #4123: This PR fixes an initialization issue that prevents the
OptimisticGovernor
contract from being used as a mastercopy for proxy contracts. - PR #4110: This PR adds new functionality to the existing
VotingV2
contract that allows delegates to claim rewards and restake tokens on behalf of the voter that owns the delegated account. - PR #4129: This PR simplifies the
DesignatedVotingV2
contract by allowing the voter to be their own delegate. - PR #4128: This PR adds an emergency proposal system that provides an alternate proposal execution path for restoring the functionality of the normal Data Verification Mechanism (DVM) voting system if it becomes non-functional.
- PR #4135: This PR refactors contract code in order to bring the
VotingV2
contract size back under the bytecode limit for deployment. - PR #4118: This PR adds the ability to forward price retrievals from
VotingV2
back to the previousVoting
contract in cases whereVotingV2
does not have the price information. - PR #4117: This PR adds support for the
VotingV2
contract to forward the retrieval of rewards to the previousVoting
contract, in order to allow users to claim their rewards post migration to V2. - PR #4121: This PR introduces an upgrade contract and associated scripts for migrating from the previous Voting contract to the
VotingV2
contract. TheVotingUpgraderV2
contract ensures the atomicity of the upgrade process.
The primary focus of these modifications is on the UMA Data Verification Mechanism (DVM) 2.0 system. PR #4135 is notable because it reverts some suggested changes from the prior UMA DVM 2.0 audit in order to reduce the VotingV2
contract size.
Review of changes to the following contracts was in-scope:
- PR #4123
OptimisticGovernor.sol
- PR #4110
Staker.sol
VotingV2.sol
- PR #4129
DesignatedVotingV2.sol
StakerInterface.sol
- PR #4128
EmergencyProposer.sol
GovernorV2.sol
EmergencyProposerTest.sol
- PR #4135
Staker.sol
VotingV2.sol
VotingV2Test.sol
MinimumVotingAncillaryInterface.sol
VotingV2Interface.sol
- PR #4118
VotingV2.sol
VotingV2Test.sol
- PR #4117
VotingV2.sol
- PR #4121
VotingUpgraderV2.sol
Findings
Here we present our findings.
Medium Severity
Lack of validation in EmergencyProposer setQuorum
and setMinimumWaitTime
The EmergencyProposer
contract is intended to function as an emergency recovery mechanism by allowing any user to construct a transaction that can bypass the VotingV2
contract. To submit a proposal, a user needs to post a large bond which is defined by the quorum
state variable. The user then needs to wait until the minimumWaitTime
has passed. However, there is a lack of validation when setting quorum
and minimumWaitTime
, which are crucial for correct functioning of the contract.
The setQuorum
function does not validate the size of the bond relative to the supply of the token used for the quorum
value. In the case that the quorum
is mistakenly set to be larger than the total token supply, no emergency proposal could be created.
The setMinimumWaitTime
function does not validate that the newMinimumWaitTime
is within a reasonable time frame. In the case that minimumWaitTime
is mistakenly set to be a large interval consisting of multiple months or even years, no emergency proposal would pass until the time frame could expire.
If either of these two variables are set incorrectly, the EmergencyProposer
contract would be rendered useless, and would need to be fixed via the voting process defined in the VotingV2
contract. This is counter to the goal of the EmergencyProposer
contract, which is supposed to be able to bypass the VotingV2
contract in case it was to break.
Consider adding validations on setMinimumWaitTime
and setQuorum
. The newMinimumWaitTime
time should be capped to a reasonable time frame, aligned with the desired time requirements for passing emergency proposals. For setQuorum
we suggest that at a minimum a check should be added to ensure the newQuorum
value is not larger than the token supply. Also consider further limiting the quorum amount to not exceed a certain fraction of the total token supply beyond which it would be unrealistic for a user or group of users to control.
Update: Fixed as of commit 9000d0aa644f3340deeb4eaa704d7432a7e55dc9
in PR #4153. newQuorum
is now checked to ensure it is smaller than the total supply of tokens, and newMinimumWaitTime
is checked to ensure it is not longer than 4 weeks.
Low Severity
Governor contract can create emergency proposals
The EmergencyProposal
contract is used to construct an emergency recovery transaction that bypasses the standard voting process. The intention is to provide an alternate execution path that can be utilized to fix an incorrectly configured VotingV2
contract.
Any user can propose an emergency transaction via the emergencyPropose
function, but the size of the required bond is expected to be large enough that it is unlikely that an individual or the UMA team would be able to sufficiently fund the bond amount on their own. Proposals are executed using the emergencyExecute
function in the GovernorV2
contract. The executor
address has the sole ability to call this function, giving it veto power over any proposal. The UMA team controls the executor
address.
There is currently no restriction that would prevent using the standard voting process to create a transaction that results in the GovernorV2
contract calling the emergencyPropose
function to create a proposal. While this is highly unlikely as this transaction would need to pass a standard voting process, this could potentially introduce undesired effects and deviate from the expected functioning of the contract.
An example of an unexpected interaction is that a GovernorV2
emergency proposal can not be slashed by using the slashProposal
function, as the slash mechanism transfers the slashed amount to the GovernorV2
address.
To avoid unexpected or undesirable behavior, consider preventing the GovernorV2
contract from being able to execute the emergencyPropose
function.
Update: Fixed as of commit 41a03995087d2e63d047075582ef49783074ba48
in PR #4151.
Incorrect public
visibility
In the VotingV2
contract, the function _getPriceFromPreviousVotingContract
allows the current voting contract to query and retrieve prices from a previous voting contract. However, the function is defined with public
visibility. As a result, this function can be directly called and could be used to bypass the _requireRegisteredContract
check used by the onlyRegisteredContract
modifier on hasPrice
and getPrice
. This would unintentionally allow users to access price details from a previous voting contract.
Consider changing the visibility of _getPriceFromPreviousVotingContract
from public
to private
.
Update: Fixed as of commit 4f1c1d741d67959ab69acdf8ff70f7eb4d65f429
in PR #4154.
Lack of address validation in setExecutor
In the EmergencyProposal
contract, the function setExecutor
allows the owner of the contract to set a new executor. However, the function does not validate that the newExecutor
address is not the zero address before updating the value.
Consider adding validation to prevent an unintended change that sets the executor to the zero address.
Update: Resolved, not an issue. Ability to set executor to zero is a feature of the UMA platform. UMA’s statement:
The audit fix proposes adding address validation to the setExecutor function such that this value can’t be set to the zero address. We choose to do nothing for this issue as being able to set the executor to the zero address is a feature: this acts to disable the emergencyProposer system, thereby disabling the emergency logic. This could be, for example, an upgrade path for the emergency proposal system or a path forward if governance deems the emergency proposal logic redundant.
Missing documentation
Several functions within the codebase have missing or incomplete documentation:
In PR #4110:
In PR #4118:
- The
_previousVotingContract
parameter has no corresponding @param line in theVotingV2
constructor docstring - The
_getPriceFromPreviousVotingContract
function inVotingV2
has no docstring
In PR #4128:
- The
emergencyPropose
function inEmergencyProposer
returns a value but its docstring has no corresponding @return line
In PR #4135:
- The
getCurrentTime
function inVotingV2Test
has no docstring - The
commitVote
function inVotingV2Test
lost its docstring when it was relocated fromVotingV2.sol
- The
revealVote
function inVotingV2Test
lost its docstring when it was relocated fromVotingV2.sol
- The
commitAndEmitEncryptedVote
function inVotingV2Test
lost its docstring when it was relocated fromVotingV2.sol
- The
getPendingPriceRequestsArray
function inVotingV2Test
has no docstring - The
getPriceRequestStatuses
function inVotingV2Test
lost its docstring when it was relocated fromVotingV2.sol
- The
retrieveRewardsOnMigratedVotingContract
function inVotingV2
returns a value but its docstring has no corresponding @return line
Consider thoroughly documenting all functions (and their parameters) that are part of the contracts’ public API. Functions that implement sensitive functionality, even if not public, should be clearly documented as well. When writing docstrings, consider following the Ethereum Natural Specification Format (NatSpec). Also consider providing explanatory comments for all events, structs, and storage variables that indicate their intended purpose.
Update: Partially fixed as of commit e729b6ba86529598326ec80083cdfbaf8541b94c
in PR #4155. UMA’s statement:
It has been decided not to add docstring in the test contracts as this is not done through the rest of the repo.
Missing error messages in require
statements
Within VotingV2.sol
there are require
statements that lack error messages. For instance:
- The
require
statement on line 257 ofVotingV2.sol
- The
require
statement on line 265 ofVotingV2.sol
- The
require
statement on line 486 ofVotingV2.sol
- The
require
statement on line 704 ofVotingV2.sol
- The
require
statement on line 928 ofVotingV2.sol
- The
require
statement on line 964 ofVotingV2.sol
- The
require
statement on line 971 ofVotingV2.sol
We are aware error messages were removed to reduce the contract size, however their absence makes interacting or debugging the contract more difficult.
Consider including specific, informative error messages in require
statements to improve overall code clarity and to facilitate troubleshooting whenever a requirement is not satisfied.
Update: Fixed as of commit a88a022a9a5b02c9cd0526c7fc2e72a094d02813
in PR #4173.
Missing modifier onlyIfNotMigrated
enables new price requests on migrated contracts
The modifier onlyIfNotMigrated
and its underlying private function _requireNotMigrated
are used to ensure that the voting contract being called is the most recent voting contract deployed. This prevents users from requesting prices and voting on price requests on an outdated voting contract. However, onlyIfNotMigrated
is missing from the functions requestGovernanceAction
and signalRequestsAsSpamForDeletion
. Both of these functions will call _requestPrice
, which initiates a new price request. Consequently, users will not be able to vote on this request due to the onlyIfNotMigrated
modifier being present on commitVote
, and could potentially be slashed as a result.
Consider adding the onlyIfNotMigrated
modifier to the functions requestGovernanceAction
and signalRequestsAsSpamForDeletion
.
Update: Fixed as of commit e62b25249441fc827f3fd6a336ed31d887c93dad
in PR #4157.
Missing test coverage
Pull request #4117 adds a new retrieveRewardsOnMigratedVotingContract
function to the VotingV2
contract, in order to enable voters to claim their rewards from the previous voting contract. However, this pull request does not include any test coverage for the newly added function.
Without an automated test for this function, manual testing is required to determine if the current implementation matches the functions’s expected behavior, which is error prone and deviates from the goal of continuous integration.
Consider adding test coverage for the new retrieveRewardsOnMigratedVotingContract
function.
Update: Resolved, not an issue. Tests were included but fell outside the scope of the audit. UMA’s statement:
Test coverage was actually already added for this function, but in a separate PR to when the logic was added. This was added in an adjacent PR where we proposed to decrease the contract bytecode. The test for this logic can be found here.
Nonintuitive behavior of payable transactions in executeEmergencyProposal
In the EmergencyProposal
contract, the executeEmergencyProposal
function iterates through the transactions submitted on the proposal and executes them. However, only the first element in the array is able to execute a payable transaction, as the first element of the proposed transactions will transfer the entirety of the contract ETH balance, meaning that subsequent transactions will have a value
of zero.
Consider documenting this behavior thoroughly as it may not be obvious or intuitive to the proposal submitter.
Update: Fixed as of commit 3b6239aa8b38610247674c952ba7379de898d4b6
in PR #4158. Further clarification provided by UMA regarding the behavior of the executeEmergencyProposal
function:
The emergency proposer forwards its entire balance (presumably the amount sent by the caller) to the governor in the first transaction. However, the governor could spend that balance over multiple transactions. The only requirement is that the execution transaction has the total of all ETH sent in all emergency execution transactions.
require
statements with multiple conditions
Throughout the codebase there are require
statements that contain multiple conditions. For instance:
- The
require
statement on line 202 ofStaker.sol
- The
require
statement on line 267 ofVotingV2.sol
- The
require
statement on line 762 ofVotingV2.sol
- The
require
statement on line 801 ofVotingV2.sol
- The
require
statement on line 986 ofVotingV2.sol
Bundling multiple conditions into a single require statement reduces the contract size, but also prevents being able to provide a separate error message for each failure case.
Consider isolating each condition in its own require
statement, to facilitate specific user-friendly error messages for every required condition.
Update: Acknowledged, and will not fix. UMA’s statement:
We won’t add this suggestion because of bytecode limitations.
Unlocked Solidity version pragma
There are two locations where the Solidity version used is ^0.8.0
:
This version string allows compilation with any version of Solidity from 0.8.0 up to the latest release. This may lead to unexpected behavior if the code is deployed with a different Solidity version than was used during testing. Further, allowing old versions of Solidity leaves the code potentially vulnerable to known security bugs which have already been patched. The official guidance is to always use the latest Solidity release when deploying contracts. When a bug is discovered that affects a range of Solidity versions, the general policy of the Solidity team is to only apply the fix to the latest release (i.e. no backporting of security updates).
Consider locking the version pragma to the same Solidity version used during development and testing. Also consider setting this version to be the latest release.
Update: Fixed as of commit abb30bb58fdf222a6d28363922b179043cea755c
in PR #4159 and commit 3be4710c3868ca553e8ab87a815febf939fe021f
in PR #4178._
Notes & Additional Information
Lack of immutable identifier
In the VotingUpgraderV2 contract, the mutable state variables existingGovernor
, newGovernor
, existingVoting
, newVoting
, and finder
are only set once within the constructor. Consider declaring these state variables as immutable to increase gas efficiency and enhance the clarity of your codebase.
Update: Fixed as of commit 1c77938f762f7bc55a387dbccab11b9a86a464bf
in PR #4160.
Misleading documentation
The codebase contains several occurrences of misleading or incorrect documentation:
In the contract VotingUpgraderV2: – The comment on line 41 “Existing governor is the only one who can initiate the upgrade” appears to be a copy & paste error from line 38 which is identical. Consider rewriting the comment. – The comment on line 52 “Addresses to upgrade” does not apply to the newVoting
variable on line 53. Consider rewriting the comment. – The comment on line 62 “Removes an address from the whitelist” does not apply to the VotingUpgraderV2
constructor. Consider rewriting the comment.
Update: Fixed as of commit 77c2370cd2d86920429e658c157941c019fbf697
in PR #4152 and commit 3be4710c3868ca553e8ab87a815febf939fe021f
in PR #4178.
Non-explicit imports are used
Non-explicit imports are used inside the codebase, which reduces code readability and could lead to conflicts between names defined locally and the ones imported. This is especially important if many contracts are defined within the same Solidity files or the inheritance chains are long.
Following the principle that clearer code is better code, consider using named import syntax (import {A, B, C} from "X"
) to explicitly declare which contracts are being imported.
For example, the DesignatedVotingV2 contract’s import block can be rewritten more explicitly as:
import { MultiCaller } from "../../common/implementation/MultiCaller.sol";
import { Stakeable, StakerInterface } from "../../common/implementation/Stakeable.sol";
import { FinderInterface } from "../interfaces/FinderInterface.sol";
import { OracleInterfaces } from "./Constants.sol";
The usage of explicit imports makes it clear that StakerInterface
is provided by Stakeable.sol
, and that OracleInterfaces
is provided by Constants.sol
.
Update: Acknowledged, and will not fix. UMA’s statement:
Although it is a good suggestion, we have decided not to incorporate it for consistency with the rest of the imports in the other contracts.
Unnecessary modifier in setDelegate
and setDelegator
In the Staker
contract, the functions setDelegate
and setDelegator use the nonReentrant
modifier. This is not required as these two functions are only updating the state and cannot be exploited through a reentrancy attack.
Consider removing the nonReentrant
modifier from these functions.
Update: Fixed as of commit 04dac164d4c8b026403aecb75730c7bc24f4400c
in PR #4156.
Redundant code
Consider making the following changes to eliminate unnecessary code:
- In PR #4110,
Staker.sol
:- line 246: The
override
keyword can be removed from thewithdrawAndRestake
function
- line 246: The
- In PR #4135,
VotingV2.sol
:- lines 978-980: The
uint256(i)
casts can be replaced with the variablei
becausei
is already declared as auint256
type on line 976 - line 1045:
(rewards)
can be changed torewards
- lines 978-980: The
Update: Fixed as of commit d5ab183d3a37a2114db7c3a6269ae66c932d1286
in PR #4162.
Redundant event parameters
In the EmergencyProposer
contract, the following events all contain a uint256 lockedTokens
parameter:
In each case, when these events are emitted, the value of lockedTokens
remains constant for a given proposal, which effectively provides no information because the value never changes.
Update: Acknowledged, and will not fix. UMA’s statement:
The value of lockedTokens is constant across all events of the same emergency proposal during its lifecycle. However, to facilitate individual reading of the events, we have opted to leave it as is.
Drift between test and production contracts
Several overloaded functions were deprecated and removed from the VotingV2
contract in order to reduce the total bytecode size, which included the commitVote
, revealVote
, and commitAndEmitEncryptedVote
functions. While these functions were removed from the version of the VotingV2
contract that will be deployed, they were added to the VotingV2Test
contract to ensure testing was not impacted by the removal. However, having two different versions of the voting contracts causes drift, which can make maintenance of the contracts more time consuming. Additionally, discrepancies between test code and production code can increase the likelihood of bugs not being caught during testing.
Consider removing the functions added to the VotingV2Test
contract that are no longer present in the VotingV2
contract, and update tests accordingly.
Update: Acknowledged, and will not fix. UMA’s statement:
We have decided not to incorporate these changes. Updates to the tests could take a long time compared to the outcome.
Typographical errors
Consider addressing the following typographical errors:
- In
EmergencyProposer.sol
(PR #4128):- line 82: “action..” should be “action.”
- In
Staker.sol
(PR #4110): - In
VotingV2.sol
(PR #4117): - In
VotingV2.sol
(PR #4118):- line 249: “Must be set to 0x0 for production environments that use live time.” refers to the deleted
_timerAddress
parameter and should be deleted
- line 249: “Must be set to 0x0 for production environments that use live time.” refers to the deleted
- In
VotingV2.sol
(PR #4139):- line 499: “participates” should be “participant”
- In
VotingV2.js
(PR #4139):- line 3123: “not posable” should be “not possible”
Update: Fixed as of commit c311e0a53c8b3a0ddb00fcea79341baca42d142a
in PR #4165.
Unnecessary non-empty return value on success
The internal function _getPriceOrError
in the VotingV2
contract returns three values: * bool
to indicate success or failure * uint256
to indicate price * string
which can be used to provide more context when _getPriceOrError
returns false
Per the comment on _getPriceOrError
, the string
return value should be non-empty only when the function returns false
. However, the additional logic to query prices from a previous voting contract via _getPriceFromPreviousVotingContract
may return a non-empty string even when the price was successfully retrieved. Because the string message is only used in the error case, users will never see the “Returned from previous contract” string.
Consider returning an empty string when _getPriceOrError
returns true
.
Update: Fixed as of commit 0aef75d2fe05488c31b462484c57277ee30e9cb0
in PR #4163.
Unnecessary public visibility in executeEmergencyProposal
In the EmergencyProposer
contract, the executeEmergencyProposal
function has public
visibility. This is not required as the function is not called within the contract.
Consider changing the visibility of the executeEmergencyProposal
function to external
.
Update: Fixed as of commit fd4813895c1f113f71b03ac9cf79c5ab22710514
in PR #4166.
Unused imports
In the EmergencyProposer
contract, the following files are imported but not used:
To improve readability and avoid confusion, consider removing the unused imports.
Update: Fixed as of commit a16bed5abe1de184c689ece8bb5e56e3c183f3e8
in PR #4164.
Unused variables
Within the EmergencyProposer
contract, several variables are defined but not used:
- The
finder
state variable is initialized in the constructor, but otherwise is not used - The
currentId
state variable is never used
To improve readability and avoid confusion, consider removing the unused variables.
Update: Fixed as of commit 5f7567e7c9686ae68565a09fe4db49a2cd29a2f4
in PR #4167.
Conclusions
A single medium severity issue was found. Some recommendations were also made to improve the quality of the codebase.
There were multiple changes regarding VotingV2
that were done to reduce the contract bytecode size as it was reaching the limit. However, some of these changes included removing errors from require
statements or grouping multiple conditions into a single statement. These changes make it more difficult for a user to debug failures when interacting with a contract and make the codebase less readable.