Download audit report
This document presents the security review performed by Nethermind Security for Royco contracts. The audit review focused on Royco Boosts protocol which has one major component that contains the core logic and connects the system entirely namely; Incentive locker.
The audit comprises 919 lines of solidity code. The audit was performed using (a) manual analysis of the codebase, (b) automated analysis tools, and (c) creation of test cases.
| Audit assessment | |
| Audit Type | Security Review |
| Final Report | April 17, 2025 |
| Repositories | royco-boosts |
| Initial Commit | 1716b71 |
| Final Commit | 4d89987 |
| Documentation | Provided in the code's natspec |
| Documentation Assessment | Medium |
| Test Suite Assessment | High |
Findings by severity
| # | Contract | LoC | Comments | Ratio | Blank | Total |
| 1 | src/interfaces/IActionVerifier.sol | 41 | 32 | 78.0% | 5 | 78 |
| 2 | src/interfaces/IERC20.sol | 11 | 59 | 536.4% | 9 | 79 |
| 3 | src/periphery/market-hubs/MultiplierMarketHub.sol | 59 | 51 | 86.4% | 23 | 133 |
| 4 | src/core/IncentiveLocker.sol | 387 | 246 | 63.6% | 84 | 717 |
| 5 | src/core/base/PointsRegistry.sol | 82 | 92 | 112.2% | 27 | 201 |
| 6 | src/core/action-verifiers/uma/UmaMerkleChefAV.sol | 192 | 130 | 67.7% | 46 | 368 |
| 7 | src/core/action-verifiers/uma/base/UmaMerkleOracleBase.sol | 147 | 116 | 78.9% | 39 | 302 |
| Total | 919 | 726 | 79.0% | 233 | 1878 |
| # | Finding | Severity | Update |
| 1 | Duplicate entries in _modifyIncentiveStreams(...) result in loss of incentives | Low | Fixed |
| 2 | Malicious incentive token can restrict claiming of other incentives | Low | Acknowledged |
| 3 | UmaMerkleOracleBase::blacklistAsserters(...) does not revoke assertion privileges | Info | Fixed |
Royco Boosts introduces Incentivized Action Markets (IAMs), enabling anyone to set up a market that rewards specific actions, whether on-chain or off-chain. Within these markets, Incentive Providers (IPs) propose rewards—such as tokens or points—for Action Providers (APs) willing to complete certain tasks. IPs and APs negotiate terms by exchanging offers until they settle on a mutually acceptable reward. Once an agreement is reached, the AP is expected to carry out the agreed-upon actions as outlined in the on-chain contract to receive the promised incentive.
The protocol is composed of three main contracts with the IncentiveLocker being the main contract that contains the core logic.

The IncentiveLocker is the central contract to manage incentive campaigns. Incentive Providers (IPs) use it to deposit tokens or points into campaigns that incentivize specific actions. The main flows include the following:
ActionVerifier hooks).UmaMerkleChefAV is an action verifier used by the IncentiveLocker to validate and process incentive claims based on Merkle proofs. It enables streamed rewards using UMA’s optimistic oracle. The main flows include:
The MultiplierMarketHub enables negotiation and opt-in between IPs and APs for multiplier-based campaigns, and the contract is used off-chain for rewards calculations.
The risk rating methodology used by Nethermind Security follows the principles established by the OWASP Foundation. The severity of each finding is determined by two factors: Likelihood and Impact.
Likelihood measures how likely the finding is to be uncovered and exploited by an attacker. This factor will be one of the following values:
When defining the likelihood of a finding, other factors are also considered. These can include but are not limited to motive, opportunity, exploit accessibility, ease of discovery, and ease of exploit.
Impact is a measure of the damage that may be caused if an attacker exploits the finding. This factor will be one of the following values:
When defining the impact of a finding, other factors are also considered. These can include but are not limited to data and state integrity, loss of availability, financial loss, and reputation damage. After defining the likelihood and impact of an issue, the severity is determined according to the table below.
| Impact / Likelihood | Low | Medium | High |
| High | Medium | High | Critical |
| Medium | Low | Medium | High |
| Low | Info / Best Practices | Low | Medium |
| Undetermined | Undetermined | Undetermined | Undetermined |
To address issues that do not fit a High, Medium or Low severity, Nethermind Security also uses three more finding severities: Informational, Best Practices, and Undetermined.
_modifyIncentiveStreams(...) result in loss of incentives| File | Fix commit | Type | Status |
UmaMerkleChefAV.sol | PR #6 | Low | Fixed |
The IncentiveLocker::createIncentiveCampaign(...) function is used to create a campaign which invokes UmaMerkleChefAV::processIncentiveCampaignCreation(...). The processIncentiveCampaignCreation(...) invokes _modifyIncentiveStreams(...) to update the current rate for each incentive in incentiveCampaignIdToIncentiveToCurrentRate mapping.
function processIncentiveCampaignCreation(
bytes32 _incentiveCampaignId,
address[] memory _incentivesOffered,
uint256[] memory _incentiveAmountsOffered,
bytes memory _actionParams,
address /*_ip*/
)
external
override
onlyIncentiveLocker
{
ActionParams memory params = abi.decode(_actionParams, (ActionParams));
uint32 startTimestamp = params.startTimestamp;
uint32 endTimestamp = params.endTimestamp;
// ...
// Apply the modification to streams
_modifyIncentiveStreams(Modifications.INIT_STREAMS, _incentiveCampaignId, startTimestamp, endTimestamp, _incentivesOffered, _incentiveAmountsOffered);
}
function _modifyIncentiveStreams(
Modifications _modification,
bytes32 _incentiveCampaignId,
uint32 _startTimestamp,
uint32 _endTimestamp,
address[] memory _incentives,
uint256[] memory _incentiveAmounts
)
internal
{
// ...
// Make modifications to the appropriate incentive streams
uint256 numIncentives = _incentives.length;
uint256[] memory updatedRates = new uint256[](numIncentives);
for (uint256 i = 0; i < numIncentives; ++i) {
// If creating a new campaign
if (_modification == Modifications.INIT_STREAMS) {
// Initialize the rate on creation
updatedRates[i] = (_incentiveAmounts[i]).divWadDown(_endTimestamp - _startTimestamp);
// If adding or removing incentives from an already created campaign
} else {
// ...
}
// Update the rate for this incentive to the current rate
// @audit If _incentives includes duplicate entries, it will override the rate corresponding to previous entry.
incentiveCampaignIdToIncentiveToCurrentRate[_incentiveCampaignId][_incentives[i]] = updatedRates[i];
}
// ...
}However, if _incentives in _modifyIncentiveStreams(...) includes duplicate entries, it will override the rate corresponding to the previous entry. This would result in incorrect accounting.
Although, the merkle root submitted by IP can still correspond to correct accounting.
Recommendation
Consider handling the case where incentives array while creating a campaign includes duplicate entries.
| File | Fix commit | Type | Status |
IncentiveLocker.sol | — | Low | Acknowledged |
The claimIncentives(...) function is responsible for claiming incentives from a specific incentive campaign. It uses _remitIncentivesAndFees(...) to process individual incentive claims by iterating over each offered incentive. To transfer incentive amount and account for protocol fee, it calls _pushIncentivesAndAccountFees(...).
function _remitIncentivesAndFees(
ICS storage _ics,
address _ap,
address _protocolFeeClaimant,
address[] memory _incentives,
uint256[] memory _incentiveAmountsOwed
)
internal
returns (uint256[] memory incentiveAmountsPaid, uint256[] memory protocolFeesPaid)
{
// Cache for gas op
uint256 numIncentives = _incentives.length;
// ...
// Initialize array for event emission
incentiveAmountsPaid = new uint256[](numIncentives);
protocolFeesPaid = new uint256[](numIncentives);
for (uint256 i = 0; i < numIncentives; ++i) {
// Cache for gas op
address incentive = _incentives[i];
uint256 incentiveAmountOwed = _incentiveAmountsOwed[i];
// Account for spent incentives to prevent co-mingling of incentives
_ics.incentiveToAmountRemaining[incentive] -= incentiveAmountOwed;
// Calculate fee amounts based on the claim ratio.
protocolFeesPaid[i] = incentiveAmountOwed.mulWadDown(_ics.protocolFee);
// Calculate the net incentive amount to be paid after applying fees.
incentiveAmountsPaid[i] = incentiveAmountOwed - protocolFeesPaid[i];
// Push incentives to the action provider and account for fees.
// @audit If this reverts for one incentive, then the transfer for all other incentives will also revert.
_pushIncentivesAndAccountFees(incentive, _ap, _protocolFeeClaimant, incentiveAmountsPaid[i], protocolFeesPaid[i]);
}
}
function _pushIncentivesAndAccountFees(
address incentive,
address ap,
address protocolFeeClaimant,
uint256 incentiveAmount,
uint256 protocolFeeAmount
)
internal
{
// Take fees and push incentives to action provider
if (isPointsProgram(incentive)) {
// ...
} else {
// Make fees claimable by fee claimant
feeClaimantToTokenToAmount[protocolFeeClaimant][incentive] += protocolFeeAmount;
// Transfer incentives to the action provider
ERC20(incentive).safeTransfer(ap, incentiveAmount);
}
}However, If _pushIncentivesAndAccountFees(...) reverts for one incentive, then all other incentives can't be transferred too. This exploit allows IP or Co-IP to add a malicious token as incentive which behaves maliciously after the merkle root is verified, restricting claiming of incentives.
Although, a whitelisted asserter can then add a merkle root such that it excludes the malicious token.
Recommendation
Consider updating the implementation for transferring the incentives to handle the revert case.
Resolution
The asserters would just post another merkle root with the malicious tokens excluded from the leaves.
UmaMerkleOracleBase::blacklistAsserters(...) does not revoke assertion privileges| File | Fix commit | Type | Status |
UmaMerkleOracleBase.sol | 3f16f86 | Info | Fixed |
The blacklistAsserters(...) function in UmaMerkleOracleBase is intended to revoke assertion privileges by blacklisting asserters. However, the current implementation mistakenly sets the asserterToIsWhitelisted mapping to true for the provided addresses, which has the opposite effect—whitelisting the asserters instead of blacklisting them:
function blacklistAsserters(address[] memory _blacklistedAsserters) public virtual onlyOwner {
uint256 numAsserters = _blacklistedAsserters.length;
for (uint256 i = 0; i < numAsserters; ++i) {
==> asserterToIsWhitelisted[_blacklistedAsserters[i]] = true;
}
emit AssertersBlacklisted(_blacklistedAsserters);
}Though malicious asserters attempting to assert an incorrect Merkle root will lose the bond amount since the oracle will dispute their assertions.
Recommendation
Consider setting the asserterToIsWhitelisted to false instead of true.
Software documentation refers to the written or visual information that describes the functionality, architecture, design, and implementation of software. It provides a comprehensive overview of the software system and helps users, developers, and stakeholders understand how the software works, how to use it, and how to maintain it. Software documentation can take different forms, such as user manuals, system manuals, technical specifications, requirements documents, design documents, and code comments. Software documentation is critical in software development, enabling effective communication between developers, testers, users, and other stakeholders. It helps to ensure that everyone involved in the development process has a shared understanding of the software system and its functionality. Moreover, software documentation can improve software maintenance by providing a clear and complete understanding of the software system, making it easier for developers to maintain, modify, and update the software over time. Smart contracts can use various types of software documentation. Some of the most common types include:
These types of documentation are essential for smart contract development and maintenance. They help ensure that the contract is properly designed, implemented, and tested, and they provide a reference for developers who need to modify or maintain the contract in the future.
The Royco team has provided a comprehensive walkthrough of the project in the kick-off call, and the code natspec includes a detailed explanation of the intended functionalities. Moreover, the team addressed all questions and concerns raised by the Nethermind Security team, providing valuable insights and a comprehensive understanding of the project's technical aspects.
All the relevant issues raised by the AuditAgent have been incorporated into this report. The AuditAgent is an AI-powered smart contract auditing tool that analyses code, detects vulnerabilities, and provides actionable fixes. It accelerates the security analysis process, complementing human expertise with advanced AI models to deliver efficient and comprehensive smart contract audits. Available at https://app.auditagent.nethermind.io.
| 30Tests passed | 2Failed |
The complete test output is in the full report.
As auditors, we recommend that any changes or updates made to the audited codebase undergo a re-audit or security review to address potential vulnerabilities or risks introduced by the modifications. By conducting a re-audit or security review of the modified codebase, you can significantly enhance the overall security of your system and reduce the likelihood of exploitation. However, we do not possess the authority or right to impose obligations or restrictions on our clients regarding codebase updates, modifications, or subsequent audits. Accordingly, the decision to seek a re-audit or security review lies solely with you.
This report is based on the scope of materials and documentation provided by you to Nethermind in order that Nethermind could conduct the security review outlined in 1. Executive Summary and 2. Audited Files. The results set out in this report may not be complete nor inclusive of all vulnerabilities. Nethermind has provided the review and this report on an as-is, where-is, and as-available basis. You agree that your access and/or use, including but not limited to any associated services, products, protocols, platforms, content, and materials, will be at your sole risk. Blockchain technology remains under development and is subject to unknown risks and flaws. The review does not extend to the compiler layer, or any other areas beyond the programming language, or other programming aspects that could present security risks. This report does not indicate the endorsement of any particular project or team, nor guarantee its security. No third party should rely on this report in any way, including for the purpose of making any decisions to buy or sell a product, service or any other asset. To the fullest extent permitted by law, Nethermind disclaims any liability in connection with this report, its content, and any related services and products and your use thereof, including, without limitation, the implied warranties of merchantability, fitness for a particular purpose, and non-infringement. Nethermind does not warrant, endorse, guarantee, or assume responsibility for any product or service advertised or offered by a third party through the product, any open source or third-party software, code, libraries, materials, or information linked to, called by, referenced by or accessible through the report, its content, and the related services and products, any hyperlinked websites, any websites or mobile applications appearing on any advertising, and Nethermind will not be a party to or in any way be responsible for monitoring any transaction between you and any third-party providers of products or services. As with the purchase or use of a product or service through any medium or in any environment, you should use your best judgment and exercise caution where appropriate. FOR AVOIDANCE OF DOUBT, THE REPORT, ITS CONTENT, ACCESS, AND/OR USAGE THEREOF, INCLUDING ANY ASSOCIATED SERVICES OR MATERIALS, SHALL NOT BE CONSIDERED OR RELIED UPON AS ANY FORM OF FINANCIAL, INVESTMENT, TAX, LEGAL, REGULATORY, OR OTHER ADVICE.
The Leading Engineers of Blockchain Infrastructure