Manifest engaged Guardian to review the security of their Manifest Finance Protocol. From the 9th of September to the 17th of September, a team of 5 auditors reviewed the source code in scope.
- Published
- Review window
- September 9 to 17, 2025
- Rounds
- Main Review, Remediation Review
- Language
- Solidity
- Chains
- Ethereum
- Sector
- Real-world assets
- 0 Critical
- 1 High
- 6 Medium
- 26 Low
- 28 Informational
Scope
-
gitlab.com/manifest-finance/ush
280c9bdd2981234f25bd
Overview
Manifest engaged Guardian to review the security of their Manifest Finance Protocol. From the 9th of September to the 17th of September, a team of 5 auditors reviewed the source code in scope.
Findings 61
Main Review
41 findings-
H-01 High AuthHook's Authentication Can Be Bypassed Trust Assumptions Resolved
Description
Proof of concept: PoC
The
_checkSenderonAuthHook.solis correctly trying to retrieve the user bymsgSender(). However this practice must be done only if the periphery interacting withPoolManageris trusted.Otherwise, the
msgSender()can be manipulated to return any address.Recommendation
Use a trusted pool of periphery contracts that will be calling
PoolManageras stated in Uniswap v4 docs. Otherwise, do not allow the execution.Resolution
Manifest Team: Resolved.
-
M-01 Medium Bypass KYC Check By Transferring Validation Acknowledged
Description
Proof of concept: PoC
There is no KYC check in the
_updatefunction ofStakedUSHBase.sol. If a user’s KYC is revoked, they cannot callcooldownAssetssince it will revert.However, they can still call
transferto move their tokens to another KYC’d account, which can then callcooldownAssets.Recommendation
Remove in
_checkRestrictionsthecheckSanctionedandcheckBanned, and usecheckUserinstead. Otherwise, acknowledge this behavior as an acceptable risk and ensure it is documented.Resolution
Manifest Team: Acknowledged.
-
M-02 Medium sUSH Cannot Be Redistributed Validation Resolved
Description
Proof of concept: PoC
sUSH holders may be banned or sanctioned. When either status applies, users are blocked from depositing/withdrawing/transferring sUSH.
Separately, the sUSH admin can redistribute a restricted user’s balance (either burn to rewards or mint to another user) via
redistributeLockedAmount, which is documented as:The address to burn the entire balance which is restricted (banned or sanctioned)However,
redistributeLockedAmountonly checksisBannedforfromandto, and does not check the sanctioned status.As a result, sanctioned users are treated as unrestricted in this path, causing the function to revert (since
isFromRestrictedis false) and effectively preventing admins from redistributing sanctioned users’ sUSH, and leaving them stuck in the users' wallets.Recommendation
Include
isSanctionedcheck for both from and to users inredistributeLockedAmount.Resolution
Manifest Team: Resolved.
-
M-03 Medium Price Decimal Mismatch In USHPriceOracle Unexpected Behavior Resolved
Description
The
USHPriceOraclecontract will be used to updateUSHTokenprices, which are expected to have 8 decimals based on comments in the contract.However, the
MIN_PRICE(1e4) andMAX_PRICE(1e12) constants are defined based on 6-decimal price values.If the minimum and maximum price constants are correct and the updater submits prices using 6-decimal precision, a 100x discrepancy will occur since external integrators expect 8-decimal precision.
Conversely, if the updater submits prices using 8-decimal precision, external integrators will receive the correct values, but the effective minimum and maximum bounds will be significantly lower than intended:
the minimum price will be $0.0001 instead of $0.01, and the maximum price will be $10,000 instead of $1,000,000.
Recommendation
Resolve the discrepancy according to the intended decimal precision. If 8 decimals are expected as indicated by the comments, update the
MIN_PRICEandMAX_PRICEconstants to 1e6 and 1e14 respectively.Resolution
Manifest Team: Resolved.
-
M-04 Medium Unauthorized Pool Initialization And Donations Access Control Resolved
Description
The
AuthHookdoes not havebeforeInitializepermission.If the hook deployment and the initialization of the Manifest-controlled permissioned UniV4 pool are not performed in the same transaction, any user can initialize the pool and set an arbitrary initial
sqrtPriceX96, since the parameters required to calculate thepoolKeycan be known beforehand.Similarly, the hook has
beforeDonatepermission set to false. Since the protocol does not want non-KYC’d users to interact with the permissioned UniV4 pool, this action should also be restricted.Recommendation
Set both the
beforeInitializeandbeforeDonatepermissions to true.beforeInitializeshould allow only protocol admins to initialize the pool.beforeDonateshould either allow only KYC’d users, similar to other hooks, or disallow donations entirely, depending on the protocol’s preferences.Resolution
Manifest Team: Resolved.
-
L-01 Low Invalid Permits Due To Version Mismatch Validation Resolved
Description
The version in the
USHTokencontract is "1k", while the domain separator is calculated using the hardcoded version value "1".As a result, permit signatures that use the correct version cannot be validated because the signature digest is different.
Similarly, the
StakedUSHTokenBasecontract inheritsERC20Permit. However, token symbol is passed in the constructor instead of token name.Note that permit signatures can still be valid if users sign based on the public
DOMAIN_SEPARATOR, even though the versions and names do not match. However, it will be invalid if users sign it based on the correct version.Recommendation
Ensure that the constant version value matches the version used in the domain separator.
Resolution
Manifest Team: Resolved.
-
L-02 Low Inconsistent Behavior At Unstake Best Practices Resolved
Description
If
cooldownDurationis set to zero, all users can unstake immediately. However, if it is only reduced (not zero), users who started cooldown earlier remain subject to the longer duration, while new users benefit from the shorter one.This creates an unfair disadvantage for unaware users who don't recall
cooldownAssetsafter the parameter change.Recommendation
Store both the
block.timestampand thecooldownDurationactive at that time. When checking for unstake eligibility, allow users to unstake at the earlier of the two values (stored block.timestamp +stored cooldownDurationorstored block.timestamp + new cooldownDuration).This ensures fairness and consistency when cooldown parameters change.
Resolution
Manifest Team: Resolved.
-
L-03 Low No Cancellation Mechanism Informational Acknowledged
Description
When a user initiates a cooldown via
cooldownAssetsorcooldownShares, The contract records a futurecooldownEndtimestamp and increases theunderlyingAmountheld in the cooldown mapping.However, the current design does not allow users to undo or adjust these requests. Once a cooldown is started:
The expiry time can only be extended further by making additional cooldown calls. If a user changes their mind (they no longer want to unstake), there is no way to cancel or scale down the request. The system forces them to complete the full cooldown process.
Recommendation
Introduce functionality that lets users reverse or reduce cooldown commitments.
Resolution
Manifest Team: Acknowledged.
-
L-04 Low Unstake Cooldown Is Temporarily Denied DoS Acknowledged
Description
Both
cooldownAssetsandcooldownSharessend the assets to the silo as thereceiver:// StakedUSH.sol _withdraw(msg.sender, address(silo), msg.sender, assets, shares);In the base implementation, the auth layer enforces KYC on the receiver inside the shared withdraw path via
onlyAuthApproved(receiver) > authManager.checkUser(receiver).Since the Silo is a system contract that isn’t KYC’d, the call reverts until it has been granted KYC through the
grantKYCfunction, causing a temporary DoS when starting the cooldown.Recommendation
Ensure that the Silo contract is KYC’d by invoking the
grantKYCfunction, either within the deployment script or manually immediately after deployment.Resolution
Manifest Team: Acknowledged.
-
L-05 Low Discrepancy Between Similar Functions Unexpected Behavior Resolved
Description
The
getKycStatusfunction checks only the KYC status of an account and does not consider whether the account is banned.In contrast, the
batchGetKycStatusfunction first checks the banned status of the account and returns false regardless of the KYC status if the account is banned.As a result, the KYC status of the same account may appear differently depending on which function is used.
Recommendation
Consider updating one of the functions based on the intended behavior so that both functions behave consistently.
Resolution
Manifest Team: Resolved.
-
L-06 Low Unauthorized Users Can Perform Transfer Access Control Resolved
Description
The
transferFromfunction in theUSHTokencontract checks thefromandtoaddresses to ensure they are authorized. However, it does not checkmsg.sender.As a result, a banned or sanctioned user can still interact with the protocol and transfer other users’ funds if they have an allowance.
The same issue occurs in
_updatefunction ofStakedUSHtoken as well.Recommendation
Check
msg.senderas well and block their interaction with the protocol.Resolution
Manifest Team: Resolved.
-
L-07 Low initializeVault Can Be Frontrunned Frontrunning Resolved
Description
The
StakedUSHBasecontract has aninitializeVaultfunction that initially deposits 10,000USHTokens to prevent donation attacks. This function can only be called by the owner when thetotalSupplyis 0.However, there is no guarantee that this function will be the first deposit unless the deployment and
initializeVaultcall are performed atomically.A malicious user could front-run
initializeVaultand deposit only theMIN_SHARESamount of tokens. As a result,initializeVaultwould be blocked afterward because thetotalSupplywould no longer be zero.Although this scenario could occur, it does not result in the same consequences as a traditional donation attack, as the contract enforces
_checkMinSharesand prevents regular users from minting zero shares.Recommendation
Ensure that the deployment and
initializeVaultcall are performed atomically. Note that this requires the deployer address to be the_ownerspecified in the constructor.Alternatively, a check can be added to the
_depositfunction to revert iftotalSupplyis zero. This guarantees thatinitializeVaultis executed as the first deposit, since regular deposits cannot occur until the owner mints the initial shares.Resolution
Manifest Team: Resolved.
-
L-08 Low requiresMultiSigApproval Misses Interval Check Validation Resolved
Description
requiresMultiSigApproval()only checks whether the change exceeds the max percentage:return _exceedsMaxChange(currentPrice, newPrice);However, when the updater calls
updatePrice, it can also revert on the time-based interval:if (block.timestamp < s.lastUpdateTimestamp + s.minUpdateInterval) revert UpdateTooFrequent();Therefore,
requiresMultiSigApprovalmay return false (suggesting updater is fine) while an updater transaction would still revert due to the minimum interval check.Recommendation
Consider including the interval check in
requiresMultiSigApproval.Resolution
Manifest Team: Resolved.
-
L-09 Low Lost ECDSA Checks Enable Signature Malleability Validation Resolved
Description
The permit function in
SolmateERC20Upgradeableusesecrecoverto validate signatures without implementing checks for signature malleability. It has no validation that s is in the lower half of the curve's order.While the nonce mechanism (
nonces[owner]++) prevents signature replay attacks, the lack of proper ECDSA checks remains a deviation from best practices.Recommendation
It's recommended to require the
svalue to be in the lower half order.Resolution
Manifest Team: Resolved.
-
L-10 Low Reward Dilution Possible During transferInReward MEV Acknowledged
Description
The
StakedUSHBase.solcontract usestransferInRewardsto distribute rewards linearly over an 8-hour vesting period.This allows stakers to deposit before
transferInRewardsis called and withdraw after the vesting ends, effectively diluting rewards intended for long-term stakers.For this dilution to be effective:
cooldownDurationmust be set to0.cooldownDurationmust not be increased during the 8-hour vesting window.
Recommendation
Distribute rewards in smaller, more frequent intervals, making dilution gains negligible. Allow the vesting period to be configurable when calling
transferInRewards, enabling adjustments based on reward size.Resolution
Manifest Team: Acknowledged.
-
L-11 Low Missing Validation In Auth Batch Functions Validation Acknowledged
Description
The
AuthManagercontract correctly implements checks to prevent redundant state changes in its single-user functions likebanUser,removeBan,grantKyc, andrevokeKyc.But, the corresponding batch functions (
batchBan,batchRemoveBan,batchGrantKyc,batchRevokeKyc) lack these checks.They unconditionally write to storage for every user in the provided array, even if a user's status is already the intended state. This inconsistency leads to unnecessary
SSTOREoperations in a loop.Recommendation
Consider implementing the same check to skip the redundant state changes.
Resolution
Manifest Team: Acknowledged.
-
L-12 Low Circulating Supply Misreported Unexpected Behavior Resolved
Description
USHToken.solinherits fromSolmateERC20Upgradeable, whoseERC20implementation allows direct transfers toaddress(0)without decreasingtotalSupply.Meanwhile,
USHToken.solexposes acirculatingSupplyfunction that returnstotalSupply. This means that tokens transferred toaddress(0)are still counted, leading to an incorrect circulating supply calculation.Recommendation
Prevent transfers to
address(0)or updatecirculatingSupplyto exclude balances ataddress(0)from its calculation.Resolution
Manifest Team: Resolved.
-
L-13 Low Checks Can Be Bypassed Warning Acknowledged
Description
The
setAuthManagerfunction in theUSHTokencontract updates theauthManageraddress, which is used to determine authorized users.Although it is an owner-only function, sanctioned and banned checks can be bypassed during an update if the new
authManagercontract does not yet reflect the sanction and ban status maintained by the previous manager.If an update is to occur, the new contract should persist the state of the previous contract before being set via the
setAuthManagercall.Recommendation
Ensure that the new
authManagercontract preserves the state of the previous one before the update.Resolution
Manifest Team: Acknowledged.
-
L-14 Low setAuthManager Restarts Timelock Unexpected Behavior Acknowledged
Description
The
setAuthManagerfunction must be called twice to set the manager. The first call designates the pending manager and starts the timelock, while the second call, made after the timelock period, finalizes the manager.However, if the second call is made before the timelock ends, it resets the pending manager and restarts the timelock, regardless of whether the provided address is the same as the current pending manager or a different one.
Recommendation
While this is an
onlyOwnerfunction, consider adding a check in theelseblock so that_setAuthManagerReqTimestampis updated only if the provided address is different from_pendingAuthManager.Resolution
Manifest Team: Acknowledged.
-
L-15 Low hasValidAuth Does Not Account For Sanctions Validation Resolved
Description
In
AuthHook.sol, the internal_checkSenderfunction validates users by checking whether they are banned, sanctioned, or have KYC. This validation is enforced at_beforeSwap,_beforeRemoveLiquidity, and_beforeAddLiquidity.However, the external view function
hasValidAuthonly checks forhasKycandisBanned, not the sanctioned status. As a result,hasValidAuthcan returntruefor a sanctioned user, even though_checkSenderwould revert when the same user interacts with the pool.This creates an inconsistency between the view function and the actual logic.
Recommendation
Update the
hasValidAuthview function to also verify the sanctioned status of the user, ensuring consistency with the_checkSenderlogic.Resolution
Manifest Team: Resolved.
-
L-16 Low MIN_PRICE And MAX_PRICE Limit Price Updates Best Practices Resolved
Description
In
USHPriceOracle.sol, two constantsMIN_PRICEandMAX_PRICEdefine absolute lower and upper bounds for the price.The contract also enforces additional constraints, such as minimum/maximum update intervals and minimum/maximum percentage change per update.
Unlike these other constraints, which can be bypassed by the
multiSigAddress, theMIN_PRICEandMAX_PRICEbounds cannot be overridden.This design may lead to unwanted situations where the protocol cannot update the oracle price to reflect real market conditions, even if the value falls outside of the hard-coded thresholds.
If the primary security concern is preventing a compromised
priceUpdaterfrom setting arbitrary values, the interval and percentage checks already provide sufficient safeguards.In contrast, the hard-coded min/max values could unnecessarily block the protocol from setting a valid price in the future.
Recommendation
Consider making the
MIN_PRICEandMAX_PRICEthresholds adjustable, or allow them to be bypassed by themultiSigAddress. This ensures that the protocol can adapt to changing market conditions.Resolution
Manifest Team: Resolved.
-
L-17 Low New Cooldown Resets cooldownEnd Best Practices Acknowledged
Description
When cooldown is enabled, users must call
cooldownAssets/cooldownSharesto begin the unlock timer, and later callunstaketo withdraw once the timer expires.Both cooldown entry points overwrite the user’s single cooldown timer:
// in cooldownAssets / cooldownShares cooldowns[msg.sender].cooldownEnd = uint104(block.timestamp) + cooldownDuration; cooldowns[msg.sender].underlyingAmount + uint152(assets);This resets
cooldownEndto “now + duration” on every new request and aggregates the amounts. If a user starts a cooldown (e.g., for 100 tokens) and later starts another cooldown (another 100), the second call extends the wait for the entire balance (200) to the new, latercooldownEnd.Recommendation
Consider refactoring the cooldown behavior to allow multiple cooldowns, which requires decent refactoring or document the current behavior clearly for users.
Resolution
Manifest Team: Acknowledged.
-
I-01 Informational priceUpdater Can Set expectedIndex = Uint256.max Best Practices Resolved
Description
There is no limit on what value
expectedIndexcan take. It only checks that it is greater than the old index.However, the
priceUpdatercould set it totype(uint256).max, making it impossible to call theupdatePricefunction again.Recommendation
Ensure that
expectedIndexis greater than the old index, but only by 1.Resolution
Manifest Team: Resolved.
-
I-02 Informational Move _exceedsMaxChange Inside priceUpdater Gas Optimization Resolved
Description
The
_exceedsMaxChangefunction checks whethernewPricehas moved beyondmaxDailyChangePercentagecompared tocurrentPrice.It returns
trueif the limit is exceeded, otherwisefalse. However, the return value is only used when the caller ispriceUpdater, making it useless in the case ofmultiSigAddress.Recommendation
Move
_exceedsMaxChangeinside theifblock that checks whether the caller ispriceUpdaterResolution
Manifest Team: Resolved.
-
I-03 Informational State Change Validation In The USH Oracle Validation Resolved
Description
The contract includes checks to prevent redundant state changes in some setter functions like
setPriceUpdater,setMultiSigAddress, andsetEmergencyPauseby reverting with "AlreadyInThisState".This is a good practice as it avoids unnecessary storage writes and event emissions.
However,
setMaxDailyChangePercentageandsetMinUpdateIntervalfunctions do not have the same checks.Recommendation
Consider adding the check to
setMaxDailyChangePercentageandsetMinUpdateIntervalto ensure the new value is different from the existing one.Resolution
Manifest Team: Resolved.
-
I-04 Informational Overly Restrictive Approve Functionality Logical Error Resolved
Description
The approve function in the
USHTokencontract include thewhenNotPausedmodifier. While it makes sense to usewhenNotPausedfor actual transfer functions, applying it to approve overly restricts users.This is particularly important when users want to revoke their approvals during a paused period. In the event of a contract pause, users may reasonably want to revoke their approvals as a precautionary measure. Restricting this action during a paused state results in poor user experience.
The
OpenZeppelinextension ofERC20Pausableonly pauses the_updatefunction, which affects transfers and minting/burning, but does not affect approvals (Reference).Note that the same issue exists in the permit function as well.
Recommendation
Consider removing the
whenNotPausedmodifier fromapproveto allow users to revoke their approvals even during a paused state. Alternatively, ensure this behavior is explicitly documented to inform users.Resolution
Manifest Team: Resolved.
-
I-05 Informational Misleading Name Of maxDailyChangePercentage Error Resolved
Description
The variable
maxDailyChangePercentageinUSHPriceOracleenforces the maximum price change per update, not per day.The current name implies that the restriction applies over a 24-hour period, which is misleading.
Recommendation
Rename the variable to reflect its true behavior.
Resolution
Manifest Team: Resolved.
-
I-06 Informational Authentication Checked Twice For Msg.sender Gas Optimization Resolved
Description
In
StakedUSH.sol, the functionscooldownAssetsandcooldownSharesuse theonlyAuthApproved(msg.sender)modifier to check if the caller is approved. However, both functions later call_withdraw, which again validatescaller,owner, andreceiver.This results in redundant authentication checks, since the same validation is effectively performed twice.
Recommendation
Remove the
onlyAuthApproved(msg.sender)modifier fromcooldownAssetsandcooldownSharesto avoid redundant checks.Resolution
Manifest Team: Resolved.
-
I-07 Informational Redundant If-Else Branching In _update Gas Optimization Resolved
Description
In
StakedUSHBase.sol, the_updatefunction uses anif-elsestatement to handle zero-address transfers.This structure is redundant because
super._updateis always executed, and the only difference is whether restrictions are checked. The branching reduces readability without changing functionality.Recommendation
Refactor the function to use a single
ifstatement, checking restrictions only when bothfromandtoare not the zero address. This simplifies the flow and avoids unnecessary branching.Resolution
Manifest Team: Resolved.
-
I-08 Informational Batches Report Input Length Informational Acknowledged
Description
In the wallets batch updaters for example, the code emit
BatchAuthorizedWalletsAdded(length)/BatchAuthorizedWalletsRemoved(length)using the input array length, even if some entries are duplicates or already in the desired state.That means the event can claim “N added/removed” when fewer wallets actually changed.
Recommendation
Only count and emit per-wallet events when a flip actually occurs. The same issue exists in the batch-ban and batch-kyc functions.
Resolution
Manifest Team: Acknowledged.
-
I-09 Informational Redundant Code Comment Informational Acknowledged
Description
/** * @title IChainalysisOracle * @dev Interface for the official Chainalysis Oracle at 0x40c57923924b5c5c5455c48d93317139addac8fb */The comment above appears in lines 20–23 of the
SanctionsListcontract, but it should belong to theIChainalysisOracleinterface.Recommendation
Remove the comment from the
SanctionsListcontract and add it to theIChainalysisOracleinterface.Resolution
Manifest Team: Acknowledged.
-
I-10 Informational updateChainalysisOracle Should Set Enabled Informational Acknowledged
Description
The
updateChainalysisOraclefunction updates the oracle address but does not setchainalysisEnabledto true if it was previously false.When
chainalysisEnabledis false, setting a new oracle requires two calls:updateChainalysisOracleandsetChainalysisEnabled.Recommendation
Consider setting
chainalysisEnabledto true withinupdateChainalysisOraclewhen updating the oracle.Resolution
Manifest Team: Acknowledged.
-
I-11 Informational Warning About Burner Role Warning Acknowledged
Description
The
BURNER_ROLEin theUSHTokencontract can burn any amount from any user without restrictions. This behavior should be clearly documented for users.Note that the sole purpose of this finding is to inform users.
Recommendation
No fix is required, as this is a design choice of the protocol. It is recommended that this behavior be documented, and the finding can be acknowledged.
Resolution
Manifest Team: Acknowledged.
-
I-12 Informational Consider Overriding renounceRole In USHToken Unexpected Behavior Resolved
Description
The
StakedUSHcontract overrides therenounceRolefunction and does not allow this operation.However, this is not the case in the
USHTokencontract, and any role including the admin role can be renounced.Additionally, the contract does not check for a zero address when setting the
defaultAdminin theinitializefunction.Recommendation
Consider overriding
renounceRolefor at least thedefaultAdminrole, and adding a zero address check in the initialize functionResolution
Manifest Team: Resolved.
-
I-13 Informational Consider EIP Compliance Best Practices Resolved
Description
The protocol implements a namespaced storage layout for its upgradeable contracts. Each contract has a storage location derived from the keccak hash of its name, such as
keccak256("storaged.ush.token")orkeccak256("v1.storage.sanctions.list.manifest.finance").While this approach prevents storage collisions between contracts, it is considered best practice to use the storage location formula from ERC-7201 for namespaced storage layouts, which involves double hashing and masking with ~0xff.
Recommendation
Consider using the
ERC-7201formula to determine storage locations as a best practice.Resolution
Manifest Team: Resolved.
-
I-14 Informational AuthManager.sol Ignores maxBatchSize Best Practices Resolved
Description
In
AuthManager.sol, there is a storage variablemaxBatchSizethat defines the maximum number of updates allowed in a single batch.This variable is supposed to control batch limits across all
batchfunctions. However, the implementation directly uses the constantMAX_BATCH_SIZEinstead of using the storage variable.Recommendation
Make use of the
maxBatchSizestorage variable in all relevantbatchfunctions instead of theMAX_BATCH_SIZEconstant, ensuring smooth upgradability.Resolution
Manifest Team: Resolved.
-
I-15 Informational Unused Events/Errors Gas Optimization Resolved
Description
Unused errors in
IUSHToken.solinterface:TooFrequentBurnTooExcessiveBurnInsufficientBalance- Sanctioned
Unused events in
IUSHToken.solinterface:- Sanction
MintFailureBurnFailure
Unused errors in
AuthHook.solcontractOnlyAuthedUserUserBannedInvalidAuthManagerTimelockNotExpiredPendingManagerMismatch
Recommendation
Remove unused events/errors
Resolution
Manifest Team: Resolved.
-
I-16 Informational Redundant Decimals Function In USHToken Best Practices Resolved
Description
The
decimalsfunction inUSHTokenis redundant because the number of decimals is already set in storage by inheriting fromSolmateERC20Upgradeable, which already implements the decimals function that returns the stored value.Recommendation
Remove the
decimalsfunction fromUSHToken.Resolution
Manifest Team: Resolved.
-
I-17 Informational Misleading Comment Informational Resolved
Description
The comment on the
circulationSupplyfunction in theUSHTokencontract states, "Returns total supply less reserves," but this is misleading because the function always returns the total supply.Recommendation
Update the comment.
Resolution
Manifest Team: Resolved.
-
I-18 Informational Misleading PriceUpdated Event In Initialize Best Practices Acknowledged
Description
In
USHPriceOracle.sol, theinitializefunction emits theevent PriceUpdated(uint256 indexednewPrice, uint256 oldPrice, address indexed updater)event.However, the
updaterfield is set toaddress(this)instead of the actual caller (msg.sender) or the designatedowner/updateraddress.Emitting
address(this)as the updater does not provide meaningful information, since the contract itself cannot directly act as the updater.Recommendation
Update the event emission to use either
msg.senderor the explicitly providedupdater/owneraddress to ensure the event accurately reflects the responsible for initializing the price.Resolution
Manifest Team: Acknowledged.
-
I-19 Informational Consider Using ERC20Votes For Governance Informational Acknowledged
Description
According to the documentation of the protocol,
USATokenwill serve as the governance token.Additionally, it states that
USHTokenholders can vote to initiate the redemption process for real-world assets.However, both of these tokens are regular
ERC20tokens without theERC20Votesimplementation.While it is possible to use standard
ERC20tokens for governance voting through off-chain mechanisms, theERC20Votesimplementation provides additional features such as delegation, checkpoints, and historical voting power data.It is important to note that incorporating this feature would add complexity to the protocol, and the decision should ultimately depend on protocol-specific requirements.
Recommendation
Consider using
ERC20Votesif on-chain voting data and delegation are important for the protocol.Otherwise, ensure that the off-chain voting mechanism includes snapshots or other safeguards to prevent manipulations such as buy-vote-sell immediately.
Resolution
Manifest Team: Acknowledged.
Remediation Review
20 findings-
M-01 Medium KYC Bypass Via EIP7702 Validation Acknowledged
Description
With
EIP-7702, any KYC'd user can set their account code to allow non-KYC users to interact with the protocol, effectively bypassing the KYC check.Normally, non–KYC'd users can hold and transfer
USHToken, but they cannot interact with the permissioned Uniswap pool or perform swaps.However, this restriction can be bypassed with
EIP-7702. Consider the following scenario:- Alice is not KYC'd
- Bob is KYC'd
- Bob sets his account code using
EIP-7702to execute a swap on the Uniswap pool (Bob’s account
calls
UniversalRouter).- Alice calls Bob’s account and executes the swap through it.
- Since
msgSenderinUniversalRouteris Bob’s account, the KYC checks are bypassed.
In this way, a single KYC'd user can enable all other non-KYC users to interact with the protocol.
Recommendation
One option is to also check the KYC status of
tx.origin. However, this could introduce new restrictions and may lead to unexpected behaviors.Another option is to monitor users off-chain and ban those who behave in this way.
Resolution
Manifest Team: Acknowledged. We will track this off-chain.
-
M-02 Medium Even manifestAccounts Cannot Donate Validation Resolved
Description
The
_beforeDonatehook is designed to allowmanifestAccountsto donate while preventing any other accounts from doing so.The hook first checks whether the sender is one of the allowed accounts via
_checkAllowed, and then tries to get themsgSenderfrom it.However, none of the hardcoded addresses, including the
UniversalRouterandPositionManager, support the donate functionality. Reference.The only way to donate is by directly calling the
PoolManageror using custom routers. As a result,_checkAllowedalways reverts during_beforeDonate.Recommendation
If donations are expected to be allowed for
manifestAccounts, either a custom router must be implemented and verified in_checkAllowed, or themanifestAccountsshould directly call thePoolManager.In the latter case, the
sendershould not be checked via_checkAllowedbut should instead be validated directly usingcheckManifestAccount.Resolution
Manifest Team: The issue was resolved in commit 7cff6c8.
-
L-01 Low Hardcoded Addresses Restrict Future Expansion Suggestion Resolved
Description
The
AuthHookenforces a fixed set of hardcoded addresses as trusted senders. However, this restricts the protocol if expansion to other chains is required or if Uniswap introduces new routers.Recommendation
Consider storing the trusted senders in a mapping, and implementing owner-only setter functions for flexibility.
Resolution
Manifest Team: The issue was resolved in commit 7cff6c8.
-
L-02 Low Misleading Comment Regarding Price Oracle Compatibility Resolved
Description
The min and max prices in the
USHPriceOracleare based on 6-decimal values, and thepriceUpdateror multisig updates the prices using the same 6-decimal format.However, the comments in the
USHPriceOracleandUSHPriceOracleStoragecontracts, as well as in theIUSHPriceOracleinterface, still indicate that prices use 8 decimals, which is misleading for external integrators.Recommendation
Update all comments to ensure that integrators receive the correct price information.
If the prices are intended to use 8 decimals, like Chainlink, the previous issue remains, and the min/max prices need to be updated accordingly.
Resolution
Manifest Team: The issue was resolved in commit d1a4e3e.
-
L-03 Low Cast To Uint256 When Comparing Lower Half Validation Resolved
Description
As a fix for the previous L-09 issue, the
svalue is checked to ensure it is in the lower half of the curve order. However, this check is performed directly on thebytes32value without converting it touint256.As a result, a lexicographic comparison is used instead of a numeric one, which could lead to incorrect behavior if the byte order is misinterpreted.
Recommendation
Cast the s value to
uint256before comparing it, as done in the OpenZeppelin libraryif (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { revert }Resolution
Manifest Team: The issue was resolved in commit c70a1d0.
-
L-04 Low Non-Standard Events Break ERC20 Compatibility Compatibility Resolved
Description
TransferandApprovalevents inSolmateERC20Upgradeabledeclare the amount parameter as indexed. While syntactically valid, this deviates from theERC-20standard which defines amount as non-indexed.Many off-chain indexers, wallets, and analytics tools assume amount is encoded in the data section, not in a topic; this will break integrations and monitoring relying on the standard ABI encoding.
Recommendation
Change Transfer and Approval events to the
ERC-20standard form (Reference).Resolution
Manifest Team: Resolved.
-
L-05 Low ERC20Permit Domain Name Mismatch Compatibility Resolved
Description
The version mismatch in the
USHTokencontract from the previous L-01 issue has been fixed; however, the name mismatch inStakedUSHTokenBasestill persists.The
ERC20name is set to "Staked USH" whileERC20Permitis initialized with the name "sUSH".EIP-2612requires theEIP-712domain name to match theERC20name; a mismatch causes off-chain signatures to be computed over a different domain than the contract enforces, making permit signatures fail or be non-standard.Recommendation
Initialize
ERC20Permitwith the same name string used byERC20. For example:ERC20("StakedUSH", "sUSH"); ERC20Permit("Staked USH");Resolution
Manifest Team: The issue was resolved in commit a14393e.
-
L-06 Low Misleading ERC4626 View Methods On Deposit Compatibility Resolved
Description
Deposits are disallowed until the admin performs
initializeVault()because_depositrequirestotalSupply() = 0.However, standard
ERC4626views likemaxDeposit/previewDepositdo not reflect this and may suggest deposits are possible, causing integrators to attempt deposits that always revert.Recommendation
Override
maxDepositand/orpreviewDepositto return 0 or otherwise signal unavailability whentotalSupply() = 0.Alternatively, this can be acknowledged if the protocol intends to initialize the vault atomically or immediately after deployment.
Resolution
Manifest Team: Resolved. We handled this in the deployment script.
-
L-07 Low Missing View In IAuthManager Suggestion Resolved
Description
checkUserfunction inIAuthManagerinterface is declared without the view modifier, despite documentation explicitly requiring it remain view-only.Recommendation
Mark
IAuthManager.checkUseras external view in the interface.Resolution
Manifest Team: The issue was resolved in commit e914b89.
-
L-08 Low Positions Cannot Be Minted Via UniversalRouter Warning Acknowledged
Description
KYC'd users will interact with
UniversalRouterto perform swaps, whilemanifestAccountswill interact withPositionManagerto add or remove liquidity.However,
UniversalRouterallows minting positions viaPositionManagerbut does not support liquidity adjustments or position burns. (Reference1, Reference2).// should only call modifyLiquidities() to mint _checkV4PositionManagerCall(inputs); (success, output) = address(V4_POSITION_MANAGER).call{value: address(this).balance}(inputs);During this call,
UniversalRouteris themsg.senderof thePositionManagercontract. SincePositionManageris one of the allowed senders in theAuthHook,_getSender(sender)returns theUniversalRouteraddress, and the action reverts because that address is not amanifestAccount.However, adding this router to the
manifestAccountswould bypass the entire check and allow anyone, including non-KYC’d users, to mint positions through the router.The current behavior of disallowing position minting through
UniversalRouteris more aligned with the intended restriction logic; however,manifestAccountsshould be aware of this limitation.Recommendation
Be aware of this behavior and ensure that
manifestAccountsalways interact withPositionManagerdirectly, rather than throughUniversalRouter, for any liquidity-related actions.Additionally, ensure that
UniversalRouteris never added to the list ofmanifestAccounts, as this would bypass the intended restrictions.Resolution
Manifest Team: Acknowledged. UniversalRouter should never be granted ManifestAccount permissions.
-
L-09 Low Warning About KYC Restrictions Warning Acknowledged
Description
Only KYC'd users can swap tokens in the permissioned Uniswap pool. While the
SETTLE_ALLandTAKE_ALLactions use themsgSenderaddress, theSETTLEandTAKEactions accept arbitrary addresses.These addresses can be provided to the
UniversalRouterby the caller and then retrieved using the_mapPayerand_mapRecipientinternal functions on the router side. Reference.A KYC'd user can perform swaps on behalf of non-KYC'd users. While this is similar to a KYC'd user executing a swap themselves and then transferring the tokens to a non-KYC'd user, in this case, the funds are exchanged directly between Uniswap and the non-KYC'd user.
Recommendation
Be aware of this behavior. If this is not acceptable, unlike allowing a non-KYC'd user to hold and/or transfer tokens, this action may also need to be restricted.
However, the recipient address is not part of the
SwapParams, so this cannot be restricted in the hook. It is a feature of the router. As a result, if this action must be restricted, it needs to be enforced at the token transfer level.Resolution
Manifest Team: Acknowledged. We will track this off-chain.
-
I-01 Informational Division By Zero In _exceedsMaxChange Warning Resolved
Description
_exceedsMaxChangemay revert with a panic error if thecurrentPriceis 0. Previously, this was not possible becauseMIN_PRICEwas a constant.However, with the updates,
MIN_PRICEis now configurable and can be set to 0. While the owner is trusted, a compromised owner account could set this value to 0, which would break future price updates.Recommendation
Enforce that
newMinPriceis greater than 0 in theset_MIN_PRICEfunction.Resolution
Manifest Team: The issue was resolved in commit 865b7f9.
-
I-02 Informational Owner Can Set Inconsistent Bounds Validation Resolved
Description
The owner can set MIN and MAX constraints to inconsistent values (e.g.,
MIN_PRICE > MAX_PRICEorMIN_UPDATE_INTERVAL > MAX_UPDATE_INTERVAL).This makes all subsequent updates fail bound checks, effectively freezing oracle updates until the owner fixes the configuration.
Recommendation
Add validation in setters to ensure invariants hold, e.g.,
require(newMin < current MAX)andrequire(newMax > current MIN).Resolution
Manifest Team: The issue was resolved in commit 7aa8f89.
-
I-03 Informational Redundant Check In updatePrice Superfluous Code Resolved
Description
The
updatePricefunction checks whether the expected index is greater than the current update counter to prevent old updates. There are two checks for this:if (expectedIndex < s.updateCounter + 1) revert IndexMustBeGreater(); if (expectedIndex = s.updateCounter + 1) revert IndexMustGrowAsSequence();However, the first check is redundant, as the second check is more restrictive and requires the index to be exactly the next one.
Recommendation
if (expectedIndex < s.updateCounter + 1) revert IndexMustBeGreater()check can be removed.Resolution
Manifest Team: The issue was resolved in commit 728234b.
-
I-04 Informational Misleading NatSpec Comment On checkBanned Informational Resolved
Description
The
NatSpeccomment for thecheckBannedfunction, in both theAuthManagercontract and theIAuthManagerinterface, reads:“Reverts with
InvalidAddressif the account is the zero address, orUserNotPermittedif the user is banned.”However, the function returns when the address is 0 and does not revert.
Recommendation
Update the comment.
Resolution
Manifest Team: The issue was resolved in commit 8f62397.
-
I-05 Informational Incorrect Comment On maxChangePercentage Informational Resolved
Description
The comment on line 22 of the
USHPriceOracleStoragecontract and line 80 of theIUSHPriceOracleinterface still indicates that the change is daily:“Maximum allowed daily price change percentage.”
However, with the updates, this no longer refers to a daily price change but to the price change between each update.
Recommendation
Update comments.
Resolution
Manifest Team: The issue was resolved in commit 17e8705.
-
I-06 Informational Inconsistent Checks In Initialization Callback Validation Resolved
Description
In
AuthHook.sol, the_beforeInitializecallback enforces a different access control pattern than the other callbacks.Specifically, it calls
authManager.checkManifestAccount(sender)directly, without first validating the sender through_checkAllowedand thencheckManifestAccount(_getSender(sender)).The team’s intention is to allow pool initialization directly via the Uniswap
PoolManager.initializefunction when using a manifest account.However, if the
PositionManagercontract itself is ever added to the manifest accounts, this design unintentionally permits any user to initialize a pool with this hook, effectively bypassing intended restrictions.Recommendation
For uniformity and to prevent unauthorized pool initialization, apply the same validation logic in
_beforeInitializeas used in other callbacks.Resolution
Manifest Team: The issue was resolved in commit 7cff6c8.
-
I-07 Informational Pending Auth Manager Lacks Reset Option Best Practices Resolved
Description
In
AuthHook.sol, thesetAuthManagerfunction allows the owner to set a newpendingAuthManager, update it if called with a different address, or finalize the update once the timelock period has elapsed.However, there is currently no mechanism to cancel an existing pending authorization request.
Once a
pendingAuthManageris set, the only way to clear it is by replacing it with another address and waiting through the timelock.This could limit flexibility if the owner wants to abort an update without initiating a replacement.
Recommendation
Consider adding a method that allows the owner to cancel the current pending authorization request, resetting both
_pendingAuthManagerand_setAuthManagerReqTimestampto their default values.This would provide a clean way to abort an in-progress update safely.
Resolution
Manifest Team: The issue was resolved in commit a7c0a0a.
-
I-08 Informational Important Bound Changes Best Practices Resolved
Description
Owner-only setters that adjust the global bounds (MIN/MAX for price, interval, and change percentage) do not emit events.
Silent changes reduce transparency and can surprise off-chain systems. These setters should emit events as best practice.
Recommendation
Emit dedicated events for each adjustable parameter change, including old and new values.
Resolution
Manifest Team: The issue was resolved in commit e29f256.
-
I-09 Informational UniV4 Quotes Always Fail Unexpected Behavior Resolved
Description
The Quoter address is hardcoded as an allowed sender, and authorization is checked via
_getSender(sender). However, the Uniswap Quoter does not implement themsgSender()function.As a result, the try-catch will fail and default to treating the sender (i.e., the Quoter contract) as the user.
This leads to two scenarios. It is impossible to quote swaps without granting KYC to the Quoter contract.
However, if the Quoter is KYC’d, then everyone can interact with the protocol through the Quoter.
Recommendation
All official Quoter addresses on supported chains should be KYC’d in order to perform quotes. Also, be aware that there is no way to prevent non-KYC’d users from performing quotes without implementing your own Quoters.
Resolution
Manifest Team: Resolved. We handled this in the deployment script.
No findings match.
Invariants 49
The review's fuzzing suite asserted 49 invariants. 49 held.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
AM-01 | KYC Status Should Be True After grantKYC | Held |
AM-02 | Ban Status Should Not Change After grantKYC | Held |
AM-03 | KYC Status Should Be False After revokeKYC | Held |
AM-04 | Ban Status Should Not Change After revokeKYC | Held |
AM-05 | KYC Status Should Be True For All Accounts After batchGrantKYC | Held |
AM-06 | Ban Status Should Not Change After batchGrantKYC | Held |
AM-07 | KYC Status Should Be False For All Accounts After batchRevokeKYC | Held |
AM-08 | Ban Status Should Not Change After batchRevokeKYC | Held |
AM-09 | Ban Status Should Be True After banUser | Held |
AM-10 | Ban Status Should Be False After removeBan | Held |
AM-11 | Ban Status Should Be True For All Accounts After batchBan | Held |
AM-12 | Ban Status Should Be False For All Accounts After batchRemoveBan | Held |
AM-13 | Account Should Be In manifestAccounts after | Held |
AM-14 | addManifestAccount Account Should Not Be In manifestAccounts after | Held |
INV-IUC-01 | removeManifestAccount Incremental update count should increase by 1 after each successful | Held |
INV-BP-01 | price update Current price should always be within the defined min and max price bounds | Held |
INV-UT-01 | lastUpdatedTimestamp should be greater or equal to after a successful | Held |
INV-PUDEMC-01 | price update Price change percentage should not exceed maxChangePercentage when | Held |
INV-PUDEMUI-01 | updated by priceUpdater Price update should not occur before minUpdateInterval has passed from last update when updated by | Held |
INV-PU | priceUpdater Ensures that each price update reflects an actual price change | Held |
INV-PAC | Ensure that a Pending Manager proposal always has a corresponding request timestamp, and vice versa | Held |
INV-UAM | AuthManager can only update to previously pendingAuthManager | Held |
STAKE-01 | Deposit/mint should increase StakedUSH shares of the receiver | Held |
STAKE-02 | Deposit/mint should increase StakedUSH total supply | Held |
STAKE-03 | Deposit/mint should increase asset balance of the StakedUSH contract | Held |
STAKE-04 | Deposit/mint should decrease asset balance of the user | Held |
STAKE-05 | cooldownAssets/cooldownShares should decrease StakedUSH shares of the user | Held |
STAKE-06 | cooldownAssets/cooldownShares should decrease StakedUSH total supply | Held |
STAKE-07 | cooldownAssets/cooldownShares should decrease asset balance of the StakedUSH | Held |
STAKE-08 | contract cooldownAssets/cooldownShares should increase asset balance of the Silo contract | Held |
STAKE-09 | unstake should increase asset balance of the receive | Held |
STAKE-10 | unstake should decrease asset balance of the Silo contract | Held |
STAKE-11 | unstake should not change the total supply of StakedUSH | Held |
STAKE-12 | Withdraw/redeem should decrease StakedUSH shares of the user | Held |
STAKE-13 | Withdraw/redeem should decrease StakedUSH total supply | Held |
STAKE-14 | Withdraw/redeem should decrease asset balance of the StakedUSH contract | Held |
STAKE-15 | Withdraw/redeem should increase asset balance of the receiver | Held |
INV-CIO | Ensure cooldown duration is zero when a user withdraws from the vault | Held |
INV-UAC | Ensures the unstaked amount reflected in the silo's total holdings matches the user's underlying amount recorded during | Held |
INV-UCE | cooldown Ensures that the cooldown period has elapsed before allowing an unstake | Held |
INV-URUS | operation Ensures that after an unstake operation, all user cooldown fields are reset to zero | Held |
INV-CAC | Ensures that the staked amount reflected in the silo's total holdings matches the user's underlying amount recorded during | Held |
INV-CSBMA | cooldown Checks that the burned share amount correspond to the assets moved into | Held |
INV-RINU | cooldown Rescue shouldn't be called with USH Token | Held |
INV-LDU | Ensures the lastDistributionTimestamp is incrementing | Held |
INV-VAU | Ensures the vestingAmount got updated | Held |
INV-TIB | transferInRewards should increase the asset balance of StakedUSH contract | Held |
INV-RFZB | Ensures the address from has no balance after the redistribution | Held |
INV-RTB | Ensures the address to gets the redistributed balance | Held |
Put your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.