Compensator Audit-Readiness Assessment

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 over rewardRate 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 and getCompensator 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:

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 since uint values cannot be negative.
  • The totalDelegatedCOMP state variable is redundant, as it tracks the same thing as the totalSupply.

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.