Summary
Timeline: From 2025-03-25 To 2025-03-27
Total Issues: 12 (0 resolved)
Critical Severity Issues: 0 (0 resolved)
High Severity Issues: 3 (0 resolved)
Medium Severity Issues: 5 (0 resolved)
Low Severity Issues: 2 (0 resolved)
Notes & Additional Information: 2 (0 resolved)
Scope
This report provides a review of the audit readiness of the in-scope contracts, identifying concerns with the current design and offering recommendations for improvement.
Please note that issues identified in the audit-readiness assessment are non-exhaustive. Only the most readily apparent and severe issues have been included in this report in order to reduce delays related to code changes.
The OpenZeppelin team was asked to perform an assessment of the audit readiness of the camconrad/compensator repository at commit eb4297f.
In scope were the following files:
contracts
├── Compensator.sol
├── CompensatorFactory.sol
├── IComp.sol
└── IGovernorBravo.sol
Overview
The Compensator
contract allows COMP token holders to delegate their voting power in exchange for rewards, encouraging increased participation in proposal votes. Delegates fund rewards for delegators by supplying COMP tokens into the Compensator
contract and setting a reward rate to distribute COMP rewards based on the proportional amount of tokens deposited by each delegator.
To prevent any single delegate from accumulating excessive voting power, a delegation cap of 5% of the total COMP supply is enforced. In addition, delegators can incentivize delegates to vote for or against specific proposals by staking their COMP tokens. After the voting period ends, delegators who staked for the winning option will pass their stake to the delegate, while those who staked for the losing option will have their stake returned.
High Severity
Stake Is Not Returned to Delegators
Delegators can incentivize delegates to vote for or against specific proposals by staking COMP. After a proposal is resolved, the stake is distributed based on the outcome:
- Delegators who staked for the winning option pass their stake to the delegate.
- Delegators who staked for the losing option get their stake back.
However, while stakes for the winning option are correctly transferred to the delegate, stakes for the losing option are not transferred back to the delegators. Instead, they are redundantly transferred to the Compensator
contract itself. As a result, stakes for the losing option will remain stuck in the Compensator
contract.
To address this issue, consider implementing a mechanism to allow delegators to withdraw their staked COMP if the proposal outcome is not the one they supported.
Compensator May Become Insolvent Due to Delegate Withdrawals
The delegateWithdraw
function allows delegates to withdraw COMP that has not been used for rewards. However, there are two issues with these withdrawals:
-
totalPendingRewards
is not updated before checking the withdrawal amount, which means that a delegate may withdraw rewards that should have been distributed among the delegators. -
Delegators continue earning rewards after the withdrawal. If a delegate withdraws all available rewards except for
totalPendingRewards
, there will not be enough rewards to cover all delegator claims in the future.
To ensure that delegators can always withdraw their earned rewards, consider updating totalPendingRewards
via _updateRewardsIndex
before checking the delegate’s withdrawal amount and preventing further reward accrual if availableRewards
is less than or equal to totalPendingRewards
.
Reward Loss When Increasing Delegator Deposit
Delegators can delegate tokens through delegatorDeposit
to receive rewards. When they do so, startRewardIndex
of the delegator is set to the updated rewardIndex
. However, updating startRewardIndex
without first claiming rewards effectively resets the delegator’s rewards, leading to the loss of all pending rewards.
To prevent loss of rewards for delegators, consider calculating and preserving the accrued rewards before updating startRewardIndex
.
Medium Severity
Manipulable Reward and Stake Distributions
Delegators are incentivized to deposit their COMP into a Compensator due to the rewards provided by the delegate. Additionally, they can stake COMP on a proposal, incentivizing the delegate to vote for or against the proposal. After the proposal is resolved either for or against, the delegate can distribute the stakes, passing them on for the winning vote to themselves and returning the stakes for the losing vote to the respective delegators. However, there are currently two sensitive actions that a malicious delegate could exploit that would result in economic loss for the delegators:
- Updating
rewardRate
: Delegates have total control overrewardRate
and can update it to any value, allowing them to slow down or stop reward distribution at will. This could betray delegators who expect consistent rewards. - Manipulation of Stake Distributions: Since there is no time limit on staking, it is possible for a malicious delegate to distribute the stakes before voting for a proposal has resolved. Moreover, even after the resolution of a proposal, the delegate is trusted to make a timely distribution of stakes back to the losing option. Furthermore, the delegate is trusted to correctly select the winning support of the option with the most votes. Delegators can also continue to stake on a proposal that has already resolved, which will have no effect, and they will have no guarantees of recovering those tokens.
Consider imposing more restrictions on the reward rate updates, such as enforcing that it remains constant for a certain period. The original StakingRewards contract by Synthetix calculates rewardRate
based on a rewardsDuration
value that cannot be changed during a given period of reward distribution, effectively making rewardRate
fixed over that period. In addition, instead of permissioning distributeStakes
to the delegate, consider allowing any user to distribute stakes after the proposal vote has concluded, relying on a permissionless query of the proposal state directly from the Compound Governor via the state
function. This result can then be cached in the Compensator, disabling further stakes for the proposal and enabling withdrawals for losing stakers.
Permissionless Compensator Deployment May Lead to Fragmentation
The createCompensator
function of the CompensatorFactory
contract creates a new Compensator for a delegate. However, this function contains several issues:
- It is permissionless, meaning that anyone can create a Compensator for any delegate and assign an arbitrary name to it.
- It does not enforce the condition that only one Compensator can be created per delegate. As a result, anyone can deploy multiple different Compensators for the same delegate. However, only the latest one will be accessible through the
delegateeToCompensator
andgetCompensator
functions. This could lead to delegators using different Compensators for a single delegate at the same time, complicating management for the delegate.
To avoid the aforementioned issues, consider limiting the number of Compensator instances to one per delegate and requiring the delegates themselves to deploy their associated instance with an appropriate name.
Rewards May Get Stuck In the Compensator
To initialize reward distribution, delegates must deposit rewards and set the reward rate on their Compensator contract. However, as soon as these steps are executed, rewards begin to be distributed. If no delegator deposits have been made until that point, any rewards distributed before the first deposit will be lost. This happens because, during the first delegatorDeposit
call, _updateRewardsIndex
is triggered, deducting rewards from availableRewards
and making it impossible for the delegate to recover those funds via delegateWithdraw
.
To prevent loss of rewards, consider updating availableRewards
only if there are existing delegator deposits. Alternatively, an initial deposit could be made during the reward setup process to ensure that rewards are captured until delegators start depositing.
getPendingRewards
Output May Be Unrealistically High
The getPendingRewards
function should return the total amount of rewards available for a delegator to claim. However, in the current reward index calculation within _getCurrentRewardsIndex
, availableRewards
is not considered as it is in _updateRewardsIndex
. As a result, getPendingRewards
may return an unrealistically high value, exceeding the actual available rewards.
To ensure accuracy, consider capping rewards
to availableRewards
in _getCurrentRewardsIndex
.
Insufficient Test Coverage
The Compensator
and CompensatorFactory
contracts currently have insufficient test coverage, potentially leaving critical parts of the code untested. This increases the risk of undetected vulnerabilities and reduces the clarity of expected behaviors, which could result in missed correctness issues. In addition, the test suite should be documented to allow reviewers to independently set up and run all tests, regardless of the development team.
To improve the system’s robustness and minimize the risk of bugs and vulnerabilities, consider adding both fuzzing and unit tests to raise the test coverage above 95%, following best practices for software security and reliability. Furthermore, consider integrating test coverage tracking into the repository’s Continuous Integration pipeline to provide ongoing quality assurance.
Low Severity
getCompensators
Should Implement Pagination
The getCompensators
function returns an array of all Compensator
contract addresses. However, as the number of Compensators grows indefinitely, retrieving the full list may eventually become infeasible.
To ensure scalability, consider implementing pagination in getCompensators
to allow fetching a subset of addresses at a time.
rewardsUntil
Output Is Incorrect When Reward Rate Is Zero
The rewardsUntil
function returns the timestamp until which rewards will be distributed. However, if rewardRate
is zero, it returns the current block.timestamp
instead of the lastRewarded
timestamp, which may lead to confusing results.
To ensure accurate behavior, consider updating rewardsUntil
to return lastRewarded
when rewardRate
is zero.
Notes & Additional Information
Code Clarity Recommendations
Throughout the codebase, multiple opportunities for improving code quality were identified:
- The
governorBravo
constant in theCompensator
contract is unused, and its name is outdated since Compound’s Governor implementation has been recently upgraded to use OpenZeppelin’s Governor. Consider removing the constant if it is not needed or updating its name and type if it is. - Instead of using the magic values 0 and 1 to indicate vote options, consider using an enum for better clarity.
- The
totalStakesFor
andtotalStakesAgainst
mappings could be unified into a singlemapping(uint256 proposalId => ProposalStake)
state variable. - Since
Compensator
instances are not used behind a proxy contract, the initialization logic should be included in their constructor instead of having a separateinitialize
function. - The
compensators
array in theCompensatorFactory
contract could be removed in favor of using anEnumerableMap
for thedelegateeToCompensator
mapping. - The
Clones
andSafeERC20
imports are unused and could be removed. - The
delegate
anddelegationCap
state variables should be madeimmutable
. - The
compToken
andgovernorBravo
constants should be written in uppercase to align with the Solidity style guide. - Key and value names can be utilized in the following mappings to improve readability:
delegateeToCompensator
,startRewardIndex
,proposalStakes
,totalStakesFor
, andtotalStakesAgainst
.
Consider incorporating the aforementioned changes to enhance code clarity and readability.
Redundant Code
Throughout the codebase, multiple instances of redundant code were identified:
- The check for
newRate
being non-negative could be omitted sinceuint
values cannot be negative. - The
totalDelegatedCOMP
state variable is redundant, as it tracks the same thing as thetotalSupply
.
Consider eliminating redundant code to enhance the readability and maintainability of the codebase.
Conclusion
Given the multiple technical and design issues outlined in the report, along with the lack of sufficient testing, the codebase, in its current state, is not ready for a comprehensive security audit.
Consider revising the present implementation of the Compensator
contract and more closely aligning with existing battle-tested frameworks like Synthetix’s StakingRewards
for the staking functionality. Additionally, consider leveraging Compound’s Governor for permissionless querying of proposal states.
Lastly, consider including a comprehensive testing suite to ensure high code coverage for all sensitive functions in the Compensator
and CompensatorFactory
contracts.