Download audit report
This document presents the results of the security review conducted by Nethermind Security for Suno Protocol's smart contracts.
The Suno Protocol's team has developed a modular staking and auto-compounding protocol centered around the uWatt token. The architecture utilizes an ERC4626-compliant vault (UWattsVault4626) that delegates asset management to a specialized strategy contract (StakingStrategy). This strategy optimizes yield by staking assets in the StakingUWatts contract and auto-compounding rewards. A notable feature of the system is its price-routing logic, which compares real-time quotes between a decentralized exchange (Uniswap V3) and a dedicated UWattVendor to ensure the most efficient acquisition of tokens during the harvest process.
Furthermore, the protocol implements a flexible reward distribution mechanism through the IRewarder interface, supporting both traditional index-based accrual via the Claim contract and real-time streaming via the SuperfluidAdapter. This adapter integrates with Superfluid’s General Distribution Agreement (GDA) to provide continuous reward flows to stakers. The entire suite is built with upgradeability in mind using the UUPS proxy pattern, allowing for future logic enhancements while maintaining a robust access control system across staking, distribution, and vendor components.
The audit comprises 641 lines of the Solidity code. The audit was performed using (a) manual analysis of the codebase, and (b) automated analysis tools.
| Audit assessment | |
| Audit Type | Security Review |
| Initial Report | February 24, 2026 |
| Final Report | March 24, 2026 |
| Initial Commit | 5afd58eafc |
| Final Commit | ee8947005f |
| Documentation Assessment | Low |
| Test Suite Assessment | Low |
Findings by severity
| # | Contract | LoC | Comments | Ratio | Blank | Total |
| 1 | contracts/staking_module/UWattsVault4626.sol | 161 | 117 | 72.7% | 37 | 315 |
| 2 | contracts/staking_module/StakingUWatts.sol | 216 | 148 | 68.5% | 36 | 400 |
| 3 | contracts/staking_module/StakingStrategy.sol | 264 | 183 | 69.3% | 50 | 497 |
| Total | 641 | 448 | 69.9% | 123 | 1212 |
The Suno Staking protocol is a yield-generation and auto-compounding system designed for uWatt token holders. The architecture is built around a core staking contract and extends into an ERC4626-compliant vault that automates reward reinvestment. To provide flexibility in yield distribution, the protocol employs a modular reward system, allowing administrators to switch between index-based claiming and continuous real-time token streaming via the Superfluid protocol.
The StakingUWatts contract serves as the foundational layer of the protocol, where users can directly stake and unstake their uWatt tokens.
stake(...) and unstake(...) functions to manage their principal. The contract actively tracks the totalStaked balance across the system and individual user balances within the stakedBalance mapping.WHITELIST_MANAGER_ROLE. Unless the whitelistBypassed flag is active, only approved addresses are permitted to deposit funds._notifyRewarder(...) internal function, which updates the user's state in the active reward distribution module.The protocol abstracts yield distribution logic into external modules that implement the IRewarder interface. This design allows the system to utilize different distribution strategies:
Claim Module: A traditional index-based rewarder that calculates proportional yield based on a user's staked balance and a globally tracked supplyIndex. It operates on a pull-based model where users actively execute the claim(...) function to transfer accumulated reward tokens to their wallets.SuperfluidAdapter Module: An advanced integration that leverages the Superfluid protocol to stream rewards continuously. Instead of requiring users to manually claim, this adapter converts the underlying reward tokens into SuperTokens. It then deploys individual Distributor contracts that connect to a General Distribution Agreement (GDA) pool, pushing real-time reward streams to stakers.New yields are introduced into the system through the reportRewards(...) function inside the StakingUWatts contract. This function is called by a designated REWARD_MANAGER_ROLE.
deltaIndex using the ratio between the incoming rewards and the current total supply of uWatt tokens. This index is then forwarded to the active reward contract alongside the transferred funds.Claim module is active, the reported rewards increase the global fundedRewards and update the supplyIndex. When a user claims, their pending rewards are calculated by multiplying their stake by the difference between the current global index and their personal userIndex.SuperfluidAdapter is active, the reported rewards are wrapped into SuperTokens and transferred to a new Distributor instance. The adapter calculates a specific flow rate to distribute the tokens over a 30-day period. Users can then withdraw their streaming SuperTokens by calling claim(...), which downgrades them back into the underlying ERC20 tokens and transfers them to the user.To optimize yield generation, the protocol offers the UWattsVault4626, an ERC4626-compliant vault designed to automate the compounding of rewards.
uWatt tokens into the vault and receive share tokens in return. The vault relies on a strategy contract, meaning it does not hold the assets itself but delegates management to a strategy contract.StakingStrategy Integration: The StakingStrategy acts as the active manager for the vault's assets. Upon receiving deposits from the vault, the strategy approves and directly deposits the uWatt tokens into the StakingUWatts contract to begin earning yield.The compounding lifecycle is executed via the permissionless harvest(...) function within the StakingStrategy contract. This flow ensures that generated rewards are converted back into principal.
StakingUWatts contract. A configured performanceFeeBps is deducted and sent to the protocol's fee recipient. To incentivize external callers to trigger the harvest, a harvester bounty is paid out to the address executing the transaction.quoter contract to simulate a swap on a Decentralized Exchange (DEX), such as Uniswap V3, to determine the expected uWatt output. It compares this output against a fixed quote from an external IUWattVendor contract.uWatt tokens. Finally, the newly acquired uWatt tokens are restaked into the core contract, effectively compounding the vault's underlying position and increasing the value of the users' shares.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.
increaseAllowance and transferFrom| File | Fix commit | Type | Status |
contracts/staking_module/UWattsVault4626.sol | 71fe9ba | Medium | Fixed |
The UWattsVault4626 contract implements a blocklist mechanism by overriding the approve, transfer, and transferFrom functions with the onlyNotBlocked modifier. This modifier checks that msg.sender is not in the isBlocked mapping.
However, the contract inherits from OZ's ERC20Upgradeable version 4.9.6 but does not override increaseAllowance or decreaseAllowance. As a result, a blocked user can still modify allowances by calling increaseAllowance (which calls the internal _approve function and bypasses the overridden approve check).
Furthermore, the transferFrom override only enforces the onlyNotBlocked check on msg.sender (the spender). It does not validate the status of the from or to addresses.
function transferFrom(
address from,
address to,
uint256 value
)
public
override(ERC20Upgradeable, IERC20Upgradeable)
// @audit-issue Checks msg.sender (spender), but allows transfer if 'from'
// is blocked.
onlyNotBlocked
returns (bool)
{
return super.transferFrom(from, to, value);
}This creates two bypass vectors:
increaseAllowance to authorize a fresh, unblocked wallet. The unblocked wallet then calls transferFrom to move the funds. Since the non-blocked spender is msg.sender in transferFrom, the check passes;transferFrom;Recommendation
Consider overriding the _beforeTokenTransfer hook to enforce blocklist checks. Alternatively, modify the internal _transfer and _approve functions to check that the from and to are not blocklisted.
Resolution
Replaced per-function onlyNotBlocked modifier overrides on approve, transfer, and transferFrom with a single _beforeTokenTransfer hook that validates from, to, and msg.sender against the blocklist. This closes the bypass vectors through increaseAllowance/ decreaseAllowance and ensures blocked addresses cannot operate as spenders via pre-existing allowances.
harvest(...) results in lower than expected harvester bounties| File | Fix commit | Type | Status |
contracts/staking_module/StakingStrategy.sol | 7cc4b87 | Low | Fixed |
The harvest(...) function in the StakingStrategy contract is responsible for claiming rewards, deducting fees, and auto-compounding the remaining tokens back into the staking protocol. To ensure the system remains sustainable and incentivized, the protocol defines a performanceFeeBps for the treasury and a harvesterBountyBps to reward the caller who triggers the harvest.
According to the setFees(...) function, the total fees are capped at 2000 basis points (20%) of the total rewards, calculated as the sum of both fee components:
function setFees(
uint256 _performanceFeeBps,
uint256 _harvesterBountyBps
) external onlyOwner {
// @audit Total allowed fees are validated as the sum of both BPS values
if (_performanceFeeBps + _harvesterBountyBps > 2000)
revert FeesTooHigh(); // max 20% total
// ...
}However, the harvest(...) function applies these fees sequentially rather than in parallel against the original reward balance. Specifically, the performance fee is subtracted first, and the harvester bounty is then calculated based on the _remaining_ balance.
// ...
// 2. Deduct performance fee
uint256 perfFee = (rewardBal * performanceFeeBps) / BPS;
if (perfFee > 0) {
rewardToken.safeTransfer(feeRecipient, perfFee);
}
// @audit The performance fee is subtracted from the total reward balance
uint256 remaining = rewardBal - perfFee;
// 3. Pay harvester bounty (incentivizes public harvest calls)
// @audit-issue The bounty is calculated on the 'remaining' amount instead of 'rewardBal'
uint256 bounty = (remaining * harvesterBountyBps) / BPS;
if (bounty > 0) {
rewardToken.safeTransfer(caller, bounty);
remaining -= bounty;
}
// ...This logic means the actual bounty paid to the harvester is (Reward * (1 - performanceFee) * harvesterBounty) instead of the expected (Reward * harvesterBounty). Consequently, the harvester receives less than the percentage defined in harvesterBountyBps, which may lead to insufficient incentives for external actors to maintain the vault's compounding frequency, especially when the performance fee is high.
Recommendation
Consider calculating both the perfFee and the bounty based on the initial rewardBal to ensure the distributed amounts match the basis points configured in the contract state.
Resolution
Both perfFee and bounty are now calculated in parallel using the original rewardBal as the reference value. This ensures that the distributed amounts exactly match the basis points configured through setFees().
| File | Fix commit | Type | Status |
contracts/staking_module/UWattsVault4626.sol | 6259b7f | Info | Fixed |
The UWattsVault4626 contract delegates asset management to the StakingStrategy contract. The owner can update this strategy reference using the setStrategy(...) function.
However, the setStrategy function only updates the strategy state variable pointer to the new address without withdrawing the underlying assets from the previous strategy or migrating them to the new one.
function setStrategy(address _strategy) external onlyOwner {
if (_strategy == address(0)) revert ZeroAddress();
IStrategy newStrategy = IStrategy(_strategy);
// Verify strategy asset matches vault asset
if (newStrategy.asset() != asset()) revert StrategyAssetMismatch();
address oldStrategy = address(strategy);
// @audit-issue The pointer is updated, but funds remain in 'oldStrategy'.
strategy = newStrategy;
emit StrategyUpdated(oldStrategy, _strategy);
}Once the strategy variable is overwritten, the UWattsVault4626 loses the ability to interact with the old strategy.
UWattsVault4626 can only call withdraw on the address stored in the strategy variable;StakingStrategy restricts the withdraw function to onlyVault;StakingStrategy also explicitly forbids the owner from using recoverToken to retrieve the underlying asset;This leads to a situation where funds held by the old strategy become locked. Furthermore, the UWattsVault4626's totalAssets() function immediately queries the _new_ strategy (which likely has zero balance), causing the share price to crash to zero.
function totalAssets() public view override returns (uint256) {
if (address(strategy) == address(0)) {
return 0;
}
// @audit Queries the NEW empty strategy, ignoring funds in the old one.
return strategy.totalUnderlying();
}Recommendation
Consider the above consequences when deciding to update the strategy contract. Before updating the strategy variable, the vault should withdraw the assets from the current strategy, approve the new strategy, and deposit the assets into it.
Resolution
Future logic upgrades can be performed through proxy upgrades to the strategy implementation itself. Therefore, a post initialization setStrategy() function is unnecessary.
The fix removes the public setStrategy() function entirely and replaces it with an internal _setStrategy() function that is executed exactly once during initialization. The strategy address is now immutable after deployment.
harvest(...) function logic may be inconsistent depending on the Quoter implementation| File | Fix commit | Type | Status |
contracts/staking_module/StakingStrategy.sol | 6de0936767cecc | Info | Fixed |
The harvest(...) function in the StakingStrategy contract is designed to compound rewards by choosing the most cost-effective source for uWatt tokens between a Decentralized Exchange (DEX) and a designated uWattVendor. To make this decision, it utilizes a quoter to estimate the output from the DEX.
function harvest(
address caller
) external override nonReentrant returns (uint256 uWattGained) {
// ...
if (remaining > 0) {
// @audit The quoter is used here to compare prices
uint256 uWattsFromDEX = quoter.quoteExactInput(
rewardToAssetPath,
remaining
);
uint256 uWattsFromVendor = uWattVendor.getUWattAmount(
remaining,
address(this)
);
if (uWattsFromDEX > uWattsFromVendor) {
// DEX is cheaper
uWattSource = "DEX";
// @audit This internal call calls the quoter again to set slippage
uint256 minUWattOut = _getMinimumSwapOutput(remaining);
// ...
IRouter.ExactInputParams memory params = IRouter
.ExactInputParams({
path: rewardToAssetPath,
recipient: address(this),
amountIn: remaining,
amountOutMinimum: minUWattOut
});
uWattGained = router.exactInput(params);
} else {
// ...
}
}
// ...
}The logic assumes a specific behavior from the IQuoter implementation. Depending on whether the quoter simulates a full swap or returns a spot price, two issues arise:
QuoterV2) simulates a full swap, it returns the exact amountOut expected (excluding slippage from state changes between the quote and execution). In this case, calculating minUWattOut by applying a maxSlippageBps reduction to the already accurate quote might be redundant or incorrectly calibrated, as uWattsFromDEX is already the "best estimate.";if (uWattsFromDEX > uWattsFromVendor) becomes inaccurate. The actual amount received from the DEX after price impact could be significantly lower than uWattsFromVendor, leading the strategy to pick the DEX when the Vendor was actually the cheaper option;Recommendation
Consider re-evaluating the integration with the quoter to ensure it aligns with the specific implementation used (e.g., Uniswap V3 QuoterV2).
Resolution
We agree that the contract previously did not clearly communicate which type of quoter implementation is required. The harvest() comparison between DEX and Vendor output is only valid when backed by a simulation-based quoter (e.g., Uniswap V3 `QuoterV2`) that accounts for full price impact. A spot-price quoter would overestimate DEX output and cause the strategy to prefer the DEX even when the Vendor yields more uWatts. To prevent future misconfiguration, explicit documentation has been added.
reportRewards function is incompatible with tokens that revert on zero approvals| File | Fix commit | Type | Status |
contracts/staking_module/StakingUWatts.sol | a7787bc03c89eb | Info | Fixed |
The reportRewards(...) function assumes that all ERC20 tokens will successfully execute an approve(...) call when the amount is set to 0. There is a known category of non-standard ERC20 tokens (weird tokens), such as the older BNB token on Ethereum ( 0xB8c77482e45F1F44dE1745F52C74426C631bDD52), that revert when their approve(...) function is called with a 0 value. This category of ERC20s can not be used as staking rewards
Recommendation
For this category of tokens, the allowance should be set directly to the new value.
Resolution
We've replaced it with forceApprove, which handles edge cases.
| File | Fix commit | Type | Status |
contracts/staking_module/SuperfluidAdapter.solcontracts/staking_module/StakingUWatts.sol | 09c1a8e | Info | Fixed |
In the SuperfluidAdapter contract, the following errors are unused:
error InvalidGDAForwarder();
error UnitsAlreadySynced();
error PoolNotConfigured();As well as the _downgradeTo function:
function _downgradeTo(address _to, uint256 _amount) internal {
if (_to == address(0)) revert ZeroAddressNotAllowed();
superfluidToken.downgradeTo(_to, _amount);
}Also, the treasury state variable inside the StakingUWatts seems to be unused. It is only set during initialization, but no function calls or uses the treasury.
Recommendation
Consider removing the dead code to improve readability.
Resolution
The code has been removed.
quoter contract in harvest(...) increase gas consumption| File | Fix commit | Type | Status |
contracts/staking_module/StakingStrategy.sol | 7399030 | Best Practices | Fixed |
The harvest(...) function in the StakingStrategy contract is designed to collect rewards, determine the most cost-effective source (DEX vs. Vendor) to acquire assetToken, and compound those rewards. When evaluating the DEX as a potential source, the contract utilizes a quoter contract (Uniswap V3 QuoterV2) to simulate the swap and determine the expected output.
Currently, if the DEX is identified as the better option, the contract calls _getMinimumSwapOutput(...) to calculate the slippage protection. This internal function performs a second, identical call to quoter.quoteExactInput(...) with the same parameters already used in the main harvest(...) logic.
As noted in the Uniswap documentation, Quoter contracts are intended primarily for off-chain estimation because they perform a full state-simulated swap, which is computationally expensive and gas-intensive when executed on-chain. Calling this function twice in a single transaction significantly increases the gas cost for the harvester.
function harvest(...) external override nonReentrant returns (uint256 uWattGained) {
// ...
if (remaining > 0) {
// @audit First call to the quoter to compare sources
uint256 uWattsFromDEX = quoter.quoteExactInput(
rewardToAssetPath,
remaining
);
uint256 uWattsFromVendor = uWattVendor.getUWattAmount(
remaining,
address(this)
);
if (uWattsFromDEX > uWattsFromVendor) {
uWattSource = "DEX";
// @audit Second call to the quoter inside this function
uint256 minUWattOut = _getMinimumSwapOutput(remaining);
// ...
}
}
// ...
}The redundant call occurs here:
function _getMinimumSwapOutput(
uint256 rewardAmount
) internal returns (uint256) {
// @audit-issue Redundant quoter call; the value was already fetched in harvest()
uint256 expectedUWattOut = quoter.quoteExactInput(
rewardToAssetPath,
rewardAmount
);
return (expectedUWattOut * (BPS - maxSlippageBps)) / BPS;
}Recommendation
Consider refactoring the _getMinimumSwapOutput(...) function to accept the expectedUWattOut as an input parameter instead of fetching it again. This allows the harvest(...) function to reuse the result from the first quoter call, significantly reducing the gas overhead of the compounding process.
Resolution
The redundant second call to quoter.quoteExactInput inside _getMinimumSwapOutput has been removed. The previously obtained uWattsFromDEX value is now reused directly to calculate the slippage floor, eliminating both the inconsistency risk between quotes and the unnecessary gas overhead.
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 Suno Protocol's team provided an overview of the system components during the kick-off call with a detailed explanation of the intended functionalities and use cases of the system. 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.
Remarks about the Suno Protocol's test suite
Initially, the Suno Protocol lacked a formal test suite. During the course of this engagement, the team developed and shared a comprehensive testing framework that addresses the core logic and fund flows of the Staking and Vault modules. We have reviewed this updated suite and note that it provides a foundational layer of security.
The new tests successfully exercise full user journeys, including deposits, withdrawals, and the auto-compounding harvest logic. While the addition of these tests significantly improves confidence and helps verify the mitigations for issues identified in this report, we recommend the team continues to maintain and expand this suite as the codebase evolves to ensure long-term stability and regression testing.
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.
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