Nashpoint engaged Guardian to review the security of their Nashpoint Contracts. From the 27th of October to the 13th of November, a team of 5 auditors reviewed the source code in scope.
- Published
- Review window
- October 27 to November 13, 2025
- Rounds
- Main Review, Remediation Review
- Language
- Solidity
- Chains
- Arbitrum
- Sector
- Real-world assets, Yield and vaults
- 0 Critical
- 2 High
- 14 Medium
- 30 Low
- 20 Informational
Scope
Overview
Nashpoint engaged Guardian to review the security of their Nashpoint Contracts. From the 27th of October to the 13th of November, a team of 5 auditors reviewed the source code in scope.
Findings 66
Main Review
60 findings-
H-01 High Nodes Cannot Be Used In Defi Protocols Compatibility Resolved
Description
Proof of concept: PoC
The
transfer,transferFrom, andapprovefunctions in the Node contract are supposed to return boolean values.However, these overridden functions do not explicitly return any value and always return false, even though
super.transfer/super.transferFromexecutes successfully.As a result, Nodes can never be used in DeFi protocols that utilize safe libraries like Solmate’s
SafeTransferLiborOpenZeppelin’sSafeERC20, which breaks a core feature of Nodes.Recommendation
Return true in these functions if they execute successfully.
Resolution
Nashpoint Team: The issue was resolved in commit 48b7373.
-
H-02 High Locked Tokens In Adapter Due To Duplicate Hash DoS Resolved
Description
Proof of concept: PoC
The
SubRedManagementcontract enables multi-token deposits from investors in exchange for security tokensstToken. TheNashpointprotocol handles this via the beacon proxy pattern, deploying different instances ofDigiftAdaptercontracts.Once
DigiftAdapterrequest a deposit or redemption, an admin from theSubRedManagementcontract finalizes the process by callingsettleSubscriberorsettleRedemption. At this point, asettleSubscriberorsettleRedemptionevent is emitted, which the Manager role from Nashpoint verifies throughDigiftEventVerifier.verifySettlementEventfunction.The event verifier prevents double spending by hashing the
blockHash,receiptsRoot,transaction index,log indexand reverting if the hash was already used.The issue is that multiple
DigiftAdaptercontracts can have the same used hash logged for a valid transaction if they are included in theinvestorListof the same event. However, since the hash has already been used, this will completely DoS the event verification for the latter adapters, resulting in locked funds.Consider the following example:
Two
DigiftAdaptercontracts exist:Adapter AandAdapter B.requestDepositis executed on both adapters for20,000 USDeach. Admin forwards this requestforwardRequestsToDigifttoSubRedManagement.SubRedManagementfinalizes this request viasettleSubscriber, emitting theSettleSubscriberevent which includes both Adapter A and Adapter B investment and share token quantities.To finalize this settlement, the
Managerstarts with callingsettleDepositonAdapter A, providing the offchain and onchain parameters to verify that theSettleSubscriberevent was emitted to includeAdapter Ain the list of investors and share tokens minted.The event is then verified,
vars.logHash = _hashLog(vars.blockHash, vars.receiptsRoot, fargs.txIndex, i);is calculated, andusedLogs[vars.logHash] = true;is updated.Next, the
ManagercallssettleDepositonAdapter B. Since Adapter B's subscription event was grouped with Adapter A's subscription event, it will contain the same block hash, receipt root, transaction index, and log index i. This means that it will produce the exact same log hash that was calculated forAdapter A, causingrevert LogAlreadyUsed();.Thus, the event cannot be verified, and funds are subsequently locked in the
SubRedManagementcontract.Recommendation
Include the
DigiftAdapteraddresses in the hash to ensure adapter uniqueness.Resolution
Nashpoint Team: The issue was resolved in commit db926f6.
-
M-01 Medium Wrong requestRedeem Authorization Per Spec Compatibility Resolved
Description
The current
_validateOwnerimplementation incorrectly requires operators to haveERC-20approval and rejects approved spenders who aren't operators whenrequestRedeem, violatingERC-7540.Per the EIP-7540 spec, operators should be able to execute redemptions without allowance restrictions, while approved spenders (non-operators) should be able to request redemptions within their approved share limit.
Recommendation
Spend allowance only when the spender is not operator.
function _validateOwner(address owner, uint256 shares) internal { if (owner != msg.sender && !isOperator[owner][msg.sender]) { _spendAllowance(owner, msg.sender, shares); }Resolution
Nashpoint Team: The issue was resolved in commit 7ff2c88.
-
M-02 Medium Wrong maxClaimableAssets Can DoS Fulfillments DoS Resolved
Description
In
erc7540Router.fulfillRedeemRequest, when no component can fully coverassetsRequested, the router enters the partial path and computes:claimableShares = claimableRedeemRequest(0, node)maxClaimableAssets = convertToAssets(claimableShares)convertToAssetscan return an inflated amount (cause it calculates the asset with current shares exchange rate, which will be higher than the value of shares at settlement often) vs the actual withdrawable assets(maxWithdraw(node)), somin(assetsRequested, maxClaimableAssets)will exceedmaxWithdraw(node), causing the withdrawal to revert when callingcomponents.withdrawand blocking fulfillment.Recommendation
Use
maxWithdrawinstead ofconvertToAssets(claimableShares)Resolution
Nashpoint Team: The issue was resolved in commit 2c458ec.
-
M-03 Medium Rebalance Deadlock Due To Management Fee DoS Resolved
Description
startRebalancealways attempts to pay accrued management fees from the Node’s on-hand reserve before opening the rebalance window.There isn't a balance or reserve ratio enforcement in Nodes, and some Nodes may have all of their balance invested in other components and have zero reserves.
If the Node's balance is less than the computed
feeForPeriod,_payManagementFees()andstartRebalancereverts, preventing the rebalance window from ever opening.Since routers can only move funds during the rebalance window, operators cannot free up reserve to pay the fee, causing a deadlock where actions dependent on rebalancing are DoS’d.
Recommendation
Consider setting a minimum reserve ratio or buffer amount that cannot be invested in components, ensuring sufficient funds are available to cover fees.
Alternatively, consider a mechanism that pulls from components to cover fees when the balance is insufficient (e.g. atomically liquidate remainder during
startRebalance).Resolution
Nashpoint Team: The issue was resolved in commit 5715448.
-
M-04 Medium User Can Claim Others’ Queued ERC7540 DoS Acknowledged
Description
Once users request withdrawals from the Node contract, the
Rebalancerrole can proceed to request a withdrawal from theERC7540vault (viaERC7540Router.requestAsyncWithdrawal), if an ERC7540 vault is used for investments by the Node.After the request is finalized by the external
ERC7540 vaultcontract (which can take days), theRebalancerrole can proceed to fulfill the redemption request of users viaERC7540Router::fulfillRedeemRequest.The problem is that a user can timely execute a withdrawal request just before the
Rebalancerrole callsERC7540Router.fulfillRedeemRequest, taking up other users' requested redemption for that rebalancing window, causing another withdrawal delay for users which can be at least another 1-2 days, or more.Consider the following example:
- Bob and Alice both request a withdrawal from the Node contract, each requesting
100 sharesto withdraw. - Rebalancer role initiates a rebalance via
startRebalance, then proceeds to call
ERC7540Router.requestAsyncWithdrawalwith200 sharesspecified.- The component takes
48 hoursto finalize the request. - Bob decides instead of withdrawing only
100 shares, he wants to withdraw a full200 shares. Rather than waiting for
the
Rebalancerto callERC7540Router.fulfillRedeemRequestfollowed by another100 shareswithdrawal request, he realizes Alice has already queued100 shares.- Bob takes advantage and timely requests another withdrawal via
Node.requestRedeemspecifying another100
sharesjust before the next rebalancing window (as the rebalancing time is public).- Now when the rebalancing window is initiated and
RebalancercallsERC7540Router.fulfillRedeemRequestwith Bob's
address, Bob will receive the full
200 sharesthat was queued for withdrawal, because now his total assets requested is updated.- Therefore, Alice will receive nothing.
Alice now has to wait for another
ERC7540Router.requestAsyncWithdrawalcall followed byERC7540Router.fulfillRedeemRequestfor her100 sharewithdrawal, which can take days or longer.Recommendation
A potential solution could be to separate withdrawal requests into epochs denoted by an
epochId, so if any user attempts to request a withdrawal last second, it will reflect the withdrawal request for the next epoch rather than the current one.Resolution
Nashpoint Team: Acknowledged.
- Bob and Alice both request a withdrawal from the Node contract, each requesting
-
M-05 Medium Owner Can Drain All Funds Via rescueTokens Trust Assumptions Resolved
Description
rescueTokens()allows the owner to recover accidentally sent tokens, blocking only the underlyingassetand component share tokens. The implementation assumes that the share address of any component is the component address itself:function rescueTokens(address token, address recipient, uint256 amount) external onlyOwner { if (token == asset) revert ErrorsLib.InvalidToken(); >> if (_isComponent(token)) revert ErrorsLib.InvalidToken(); IERC20(token).safeTransfer(recipient, amount); emit EventsLib.RescueTokens(token, recipient, amount); }However, for
ERC7540components, the share token of a component (obtained viaIERC7575(component).share()) can differ from the component address itself, as seen inERC7540Router._requestRedeem(), so the owner can withdraw shares for any component that has a share token different from its address.Example how owner can drain all users funds:
- Owner whitelists an
ERC7540component whereshare() != component - Sets allocation to 100% and invests all node funds into that component
- Calls
rescueTokens(shareToken, owner, balance)to sweep all share tokens (bypasses the
_isComponentcheck)- All user funds are drained
Recommendation
Add a loop in
rescueTokens()to also blockIERC7575(components[i]).share()for each component , you may consider doing it via try catch to avoid reverts from components that doesn't haveshare()functionResolution
Nashpoint Team: The issue was resolved in commit d54c056.
- Owner whitelists an
-
M-06 Medium Whitelist Bypass Via EIP7702 Gaming Resolved
Description
With
EIP-7702, a whitelisted user can set their code to allow non-whitelisted users to interact with the protocol, effectively bypassing theGatePolicy.Consider this scenario:
- Bob is whitelisted and Alice is not.
- Bob sets his account to interact with a permissioned Node like a router.
- Bob's account takes Alice's assets, deposits to Node, and transfers shares to Alice.
- Since the
msg.senderin this case is Bob, whitelist checks pass and any non-whitelisted user can
interact with Nodes.
Note that this scenario does not work with
TransferPolicy, as the funds must ultimately be transferred to or from a non-whitelisted user, which the policy prevents.Recommendation
One option is to recommend using
TransferPolicyandGatePolicyconcurrently for all node owners, asGatePolicyalone does not prevent end-to-end interaction.Another option would be to use
tx.originas well along withmsg.senderfor the whitelist check inGatePolicy. However, this may be overly restrictive or introduce new issues, especially if some node owners want to implement custom routers that are whitelisted for their own nodes.Alternatively, consider tracking users off-chain and removing from the whitelist those who allow others via
EIP-7702.Resolution
Nashpoint Team: The issue was resolved in commit 37f402f.
-
M-07 Medium Cannot Support Hybrid Asynchronicity DoS Acknowledged
Description
The
_getErc7540Assetsfunction in theERC7540Routercontract assumes that allERC7540components are fully asynchronous in both deposits and redemptions, and directly calls thependingRedeemRequest,claimableRedeemRequest,pendingDepositRequest, andclaimableDepositRequestfunctions.However, according to the
ERC7540specifications, implementations can choose whether to include asynchronous flows for deposits, redemptions, or both.For example, the Node itself is
ERC7540-compatiblebut does not implement thependingDepositRequestorclaimableDepositRequestfunctions.Directly calling all four of these functions in
_getErc7540Assetscould cause a DoS when the component is partially asynchronous, as is the case with the Node.Recommendation
Use
try/catchwhen callingERC7540components in theERC7540Router. Alternatively, ensure all components supported by the protocol are fully asynchronous and never include hybrid components.Resolution
Nashpoint Team: Acknowledged.
-
M-08 Medium Wrong ERC-7540 Claimable Share Valuation Logical Error Resolved
Description
_getErc7540Assets() values claimable redeem shares at the current share price via
convertToAssets(), but claimable shares represent a locked-in redemption value from when the request was processed. From erc7540 spec:"The assets that will be received on redeem or withdraw MAY NOT be equivalent to the value of
convertToAssets(shares)at the time of Request, as the price can change between Pending and Claimed"Example:
- Node requests redeem of 100 shares when share price = 1.0 → locked value = 100 assets
- Request processed and becomes claimable (100 shares → 100 assets)
- Component NAV increases → new share price = 1.2
_getErc7540Assets computes: convertToAssets(100 claimable shares) = 120 assets- Actual claimable via
maxWithdraw(): 100 assets - Overvaluation: 20 assets (20%)
This can inflate or reduce the actual
totalAsset, leading to incorrect share valuation and erroneous node share calculations.Recommendation
Consider using
maxWithdraw()for claimable shares. However, there is a tradeoff here and keep in mind that it may underestimate assets if vault has some withdrawal restrictions (e.g., withdraw cap per transaction, paused vault returns 0).Resolution
Nashpoint Team: The issue was resolved in commit 91db03d.
-
M-09 Medium Owner Can Drain User Funds Via Swing Pricing Trust Assumptions Resolved
Description
Proof of concept: PoC
Owner can drain all users funds by exploiting the bonus mechanism , and the fact that he can switch swing pricing on and off at will with other important params.
Attack Steps: 1. Set
rebalanceCooldown = 0andrebalanceWindow = 1 sec(no lower bounds enforced) : this to faster the attack 2. Invest all reserves to vault (0% reserve ratio) 3. Enable max swing pricing (99%) and set extreme reserve target (90%) 4. Owner's secondary address deposits to capture ~49.5% bonus shares (dilutes existing users) 5. Disable swing pricing to exit without penalty 6. Liquidate vault and redeem owner's shares at inflated value 7. Repeat each block until node is drainedRecommendation
You may consider queueing activation/deactivation of swing pricing to take effect in future to limit the owner power, so users can opt out and exit the system.
Additionally, consider having global lower bounds for
rebalanceCooldownandrebalanceWindowto decrease the likelihood of such attack by malicious owner.Resolution
Nashpoint Team: The issue was resolved in commit 6259916.
-
M-10 Medium Users May Be Over-penalized For Withdrawals Logical Error Resolved
Description
When swing pricing is enabled, users' withdrawal may face the penalty. According to https://nashpoint.gitbook.io/nashpoint/swing-pricing-calculations, when reserves fall below target, withdrawals receive progressively worse pricing.
The problem here is that if one withdrawal starts when the reserve ratio is larger than the target reserve ratio, and ends with the reserve ratio is less than the target reserve ratio, the whole withdrawal assets will be punished.
For example: 1. Current reserve cache ratio is 100%. Target reserve ratio is 10%. 2. Alice wants to request all assets, Then all assets will be punished with the final swing factor. In fact, when Alice requests withdrawal for the first 90% asset, the node should not charge some punish fees.
When users deposit assets, users may get some bonus when the reserve ratio is below the target reserve ratio. Similar issue in deposit, if one deposit operation will raise the reserve ratio from one ratio below the target reserve ratio to one ratio above the target ratio, the system will return the min of deposit and asset shortfall to avoid overpaying the deposit bonus.
When users request withdrawal, one similar mechanism should be applied.
Recommendation
When one withdrawal request will drop the reserve ratio from the ratio which is above target ratio to the ratio which is below the target ratio, the punish amount should be calculated in the range from target reserve ratio to the actual reserve ratio after the withdrawal.
Resolution
Nashpoint Team: The issue was resolved in commit 6259916.
-
M-11 Medium Split Withdrawals To Reduce Penalty Math Resolved
Description
Proof of concept: PoC
When swing pricing is enabled, users' withdrawal may be punished when the reserves fall below target.
However, the penalty calculation is non-linear. When users split their withdrawal into several smaller transactions, they end up paying a lower total penalty than expected.
Recommendation
Refactor the penalty calculation.
Resolution
Nashpoint Team: The issue was resolved in commit 6259916.
-
M-12 Medium Redeem Asset Impacted Math Resolved
Description
Proof of concept: PoC
When swing pricing is enabled, users' redeem request may face some punishment when current reserve ratio is below than the target ratio.
When the rebalancer finalizes a redemption request, all related shares are burned, and the actual return asset is calculated based on
sharesAdjusted. After the redemption is finalized, the share price increases slightly.Therefore, the order of redemption finalizations is important. In this case, later redemption finalizations will receive a higher price. Because the order has importance, the rebalancer should finalize redemption requests in the order they were originally submitted.
The problem is that a single user can submit multiple redemption requests, and the Node contract merges all of them together. When the rebalancer attempts to fulfill redemptions for a controller, it becomes impossible to finalize these requests in their original submission order.
Recommendation
Do not merge a single user’s multiple redemption requests. The rebalancer should fulfill redemption requests in the order they were originally submitted.
Resolution
Nashpoint Team: The issue was resolved in commit 6259916.
-
M-13 Medium Incorrect reserveImpact Math Resolved
Description
The
reserveImpactis computed using the pre-penalty asset value, but the penalty reduces the assets. Therefore, the actual reserveImpact after the redemption differs from the one used during the penalty calculation. As a result, users are always over-penalized.//... } else { reserveImpact = int256(Math.mulDiv(reserveCash - assets, WAD, totalAssets - assets)); } assets = Math.mulDiv(assets, (WAD - _getSwingFactor(reserveImpact, maxSwingFactor, targetReserveRatio)), WAD); //@audit returned asset value is decreased.For this calculation to be precise, the asset value after the penalty is applied should be the same value used when calculating the reserveImpact.
Recommendation
Consider
assetsToReturnas the final value. We need to solve the equation where:reserveImpact = int256(Math.mulDiv(reserveCash - assetsToReturn, WAD, totalAssets - assetsToReturn)); assetsToReturn = Math.mulDiv(assets, (WAD - _getSwingFactor(reserveImpact, maxSwingFactor, targetReserveRatio)), WAD);Note that this introduces additional complexity, as the
reserveImpactneeded to compute the final value is itself unknown and circularly dependent on the final value.If the additional complexity is undesirable and the current version will be used, this should be documented, as it creates unfair situations for high-value redemption requests.
Resolution
Nashpoint Team: The issue was resolved in commit 6259916.
-
M-14 Medium Share Price May Become Inflated Rounding Resolved
Description
Proof of concept: PoC
When the swing pricing is enabled, users' withdrawal operation may be punished, the protocol will burn all shares for this withdrawal and leave some assets in the node contract as one punishment.
If the last user requests to redeem all shares, and the rebalancer role will start rebalance and finalize the redeem. After the redeem finalization,
shareamount will be 0, andcacheTotalAssetswill not be reduced to 0.Based on current condition, if another user wants to deposit, the share's price will be very high(current share = 0,
cacheTotalAssetsmight be larger than 1e18).In Nashpoint, the impact of rounding (down or up) on calculations is typically around 1 wei when the share price is not inflated, which is acceptable. However, if the share price is inflated, such rounding can result in significant losses for users.
Recommendation
Consider enforcing Node owners to mint some dead shares upon Node deployment.
Resolution
Nashpoint Team: The issue was resolved in commit 6259916.
-
L-01 Low Missing Slippage Control Validation Acknowledged
Description
When swing price is enabled and the vault's liquidity reserve is less than the
targetReserveRatio, users are incentivized to deposit via a deposit bonus, which mints users more shares than normal.However, the bonus calculation uses various factors that can change before function execution such as cash after redemptions,
totalAssets(), and owner configurable parameters such asmaxSwingFactorandtargetReserveRatio. This can cause users to receive a lower bonus than expected, or receive no bonus at all.Slippage when exact output amount is not received for deposit/withdraw is also recommended under "Security Considerations" within EIP-4626 documentation.
Similarly, the
requestRedeemfunction does not have slippage control, and users may encounter unexpected penalties when swing pricing is enabled.Recommendation
Add slippage control for these functions.
Resolution
Nashpoint Team: Acknowledged.
-
L-02 Low Users May Pay More Management Fee Logical Error Acknowledged
Description
Management fees are paid at the beginning of each rebalance period after
cacheTotalAssetsare updated. For a default 24-hour rebalancing node, this uses the end-of-day asset value for the entire 24-hour period, regardless of when deposits are made during the day.If a large deposit is made just a second before rebalancing, the system still charges fees as if the funds were managed for the entire 24-hour period.
While the current behavior is similar to fund management in traditional finance, and fees are regularly paid during rebalancing, there is no guarantee that rebalancing will occur frequently enough to make this discrepancy negligible.
Since nodes are permissionless and rebalancing periods can be set by their owners, the issue can be more pronounced, especially when nodes have longer rebalancing periods, such as weeks.
Recommendation
Ideally, every state change that updates
cacheTotalAssetsshould charge fees based on the latest cached value and the period since thelastPayment. However, this would increase complexity.If the end-of-day value will still be used for fee payments, document this behavior. Additionally, consider enforcing maximum bounds for rebalancing cooldowns or adding a function that allows a protocol-owned rebalancer to charge fees more frequently, reducing discrepancies even if the node has a longer rebalancing period.
Resolution
Nashpoint Team: Acknowledged.
-
L-03 Low Fee Calculation Oversight Logical Error Acknowledged
Description
The
setAnnualManagementFeefunction updates the fee rate without first calculating and paying accrued fees for the period fromlastPaymenttill now at the old rate. This results in miscalculated fees using the new rate for past periods.A similar situation occurs when updating protocol management fee with
setProtocolManagementFeefunction as well.Recommendation
Pay management fees with previous fee percent and update the last payment before setting the new fee
Resolution
Nashpoint Team: Acknowledged.
-
L-04 Low Indexed Dynamic Arrays In Events Not Supported Events Resolved
Description
PoliciesAddedandPoliciesRemovedevents declarebytes4[]indexed sigs. However, Solidity does not support indexed dynamic array parameters in events. When indexed attribute is used in these types, hash of the value is stored in topics.Reference: "A topic can only hold a single word (32 bytes) so if you use a reference type for an indexed argument, the
Keccak-256hash of the value is stored as a topic instead."As a result, these events are emitted with the hash of the sigs array, and might be unexpected for offchain listeners.
For example, the emitted event in the
test_addPoliciestest in theNode.t.solfile is:emitPoliciesAdded(sigs:0xfb5baaecab62c516763cea2dfba17fbbc24907e4e3b0be426bde71be89af495f,policies: [0x0000000000000000000000000000000000000012])Recommendation
Consider removing the indexed attribute from these events or emitting per-element events.
Resolution
Nashpoint Team: The issue was resolved in commit 500d450.
-
L-05 Low Whitelist And Blacklist Flags Can Conflict Logical Error Acknowledged
Description
The admin setters in
BaseComponentRouterallow a component to be both whitelisted and blacklisted simultaneously. This inconsistent state can cause policy bypass in downstream code that only checks one flag and makes the intended component status ambiguous.Recommendation
Enforce mutual exclusivity in setters: when setting blacklist to true, force whitelist to false (and vice versa).
Resolution
Nashpoint Team: Acknowledged.
-
L-06 Low Owner Updates Should Effect In Future Unexpected Behavior Acknowledged
Description
The node owner can change the rebalance window during an active rebalance or adjust the cooldown period while in an active cooldown.
However, ideally, these changes should only affect future rebalance windows and cooldown periods, and the owner should not be able to increase or decrease the current period without notice.
Recommendation
Do not allow the owner to change the rebalance window or cooldown period when the Node is already in that particular state.
Resolution
Nashpoint Team: Acknowledged.
-
L-07 Low Malicious Rebalancer Can Steal Incentive Tokens Trust Assumptions Acknowledged
Description
The
swap()function inOneInchV6RouterV1allows the rebalancer to setminAssetsOutto any value, including zero, with no oracle price validation, this allow the rebalancer to steal all incentive tokens.Example:
- Node has 1000 USDC worth of incentive tokens accumulated
- Malicious rebalancer manipulates swap
pool[asset-incentivetoken]via flash loan - Malicious rebalancer calls
swap()withminAssetsOut = 0 - Swap executes at manipulated rate, node receives ~0 assets back
- Rebalancer back run to swap back , extract the whole incentive , and return the flashloan
- Loss: up to 100% of incentive value stolen
Recommendation
Either use whitelisted oracles to validate the
minAmountOut, or maybe only owner should be able to do swap (since owner can sweep incentive tokens anyway , so i assume he's trusted on that )Resolution
Nashpoint Team: Acknowledged.
-
L-08 Low Same Price Deviation For Different Assets Oracle Resolved
Description
The
DigiftAdaptercontract uses the samepriceUpdateDeviationparameter for both the asset token and the Digift token.However, these tokens may have different update frequencies, especially when real-world asset price updates are considered.
While it may be reasonable for Digift tokens to have an update deviation of over 24 hours, or even several days, such a deviation could render the asset token’s price data completely outdated.
Recommendation
Consider using different deviation values for different tokens.
Resolution
Nashpoint Team: The issue was resolved in commit 1ff5440.
-
L-09 Low Manager Can Mis-assign Funds Censoring Acknowledged
Description
In
digiFTAdapter, whenforwardRequestsToDigiftfunction is called, it snapshotsaccumulatedDepositintopendingDepositRequestglobally but does not lock or record which specific nodes contributed to that batch.While a pending settlement exists, new nodes can still call
requestDeposit(), which adds toaccumulatedDeposit(separate from the pending amount).At settlement via
settleDeposit(nodes, ...), the only check is that the sum ofnode.pendingDepositRequestacross the providednodes[]equalsglobalPendingDepositRequest. It does not verify that these are the same nodes whose deposits were actually forwarded. As a result, assets can be assigned to other nodes as long as the total sum remains equal.Example scenario:
- Node1 deposits 100 tokens → manager forwards to DigiFT (pending = 100)
- Node2 deposits 60, Node3 deposits 40 (accumulated = 100, but pending still = 100 from Node1)
- Manager can settle the DigiFT response to Node2 and Node3 instead of Node1, because 60 + 40 =
100
- Node1 remains with pending status even though his batch was settled
Note that the same issue exist with redeem settlements.
Recommendation
Use an auto-incrementing batchId (bumped on each forward); on deposit, snapshot which batchId each node contributed to; at settlement, enforce that returns apply only to nodes tied to that exact batchId.
Resolution
Nashpoint Team: Acknowledged.
-
L-10 Low Overestimated ComponentAssets For Fee Vault Logical Error Resolved
Description
The use of
convertToAssetsinERC4626Router.solcan overestimate the actual asset value of a component's shares. PerERC-4626spec,convertToAssetsdoes not account for fees or slippage that would apply during a real redemption.This can inflate the reported
totalAssetsin the Node, leading to incorrect share minting. As a result, users may redeem more assets than they should (causing insolvency), or receive fewer assets than they are entitled to.Recommendation
Consider using
previewRedeeminstead orconvertToAssets, as it's inclusive of fees per spec.Resolution
Nashpoint Team: The issue was resolved in commit 2cd9444.
-
L-11 Low Liquidation Queue Can Have Inactive Component Error Resolved
Description
The protocol docs state that one of the requirements of the liquidation queue is that all addresses must be active components. This is correctly enforced when the Node owner calls
setLiquidationQueue.However, if the node owner decides to remove the component via
removeComponentfunction, the deleted component is then not removed from the queue, thus breaking this requirement.The impact is that, in addition to a broken protocol invariant, this will DoS liquidation orders in
_enforceLiquidationOrder:try IRouter(router).getComponentAssets(candidate, true) returns (uint256 assets) { candidateAssets = assets; }routerfor the removed candidate will beaddress(0), and since a return value is expected, this will not go to thecatchblock and instead entirely revert.Recommendation
Upon removing a component, consider also removing it from the liquidation queue, or check if the owner cleared it from the queue first.
Resolution
Nashpoint Team: The issue was resolved in commit fc79a10.
-
L-12 Low Liquidation Queue Incorrect Ordering Logical Error Resolved
Description
The protocol allows Node Owners to configure liquidation ordering to prioritize components based on, for example, liquidity costs and withdrawal delays.
The following example is provided in the documentation:
For queue [A, B, C] where A is ERC7540 and B, C are ERC4626: Must check A's claimable balance first Can only use B if A's claimable balance insufficient Can only use C if both A and B insufficient Pending (non-claimable) balances in A don't block using B or CThere's a case where all of A,B,C may have insufficient claimable assets. In that case, the correct way should be to perform a partial (maximum) withdrawal on A first, followed by partial withdrawal on B, and then C.
However, a direct partial withdrawal on B without following the correct order can succeed with the current implementation.
For example, assume total claimable assets are
A= 50tokenA,B = 30tokenA, andC = 30 tokenA. The requested withdrawal is60 tokenA. The rebalancer role callsERC4626Router.fulfillRedeemRequestfor componentB. This callsNode::enforceLiquidationOrderwithcomponent = Band60 tokenA._enforceLiquidationOrderwill loop through the entire queue list ofA,B,C, and since they all hold insufficient tokens, the call succeeds and a partial withdrawal on component B is executed using the minimal balance int256 componentShares = Math.min(IERC4626(component).convertToShares(assetsRequested),IERC20(component).balanceOf(address(node)));.This can be a common occurrence as investment portfolios are diversified across various components, and the intention of the Node Owner is to perform a partial withdrawal in the queued order.
Recommendation
Within
_enforceLiquidationOrder, if each component has insufficient funds, ensure thecomponentspecified is equal to the candidate at the top of the queue. This will ensure partial withdrawals can happen in the correct order as the Node Owner intends.Resolution
Nashpoint Team: The issue was resolved in commit fc79a10.
-
L-13 Low Insufficient Minimum Threshold Check Logical Error Partially resolved
Description
Node Owners can configure a
maxDeltaparameter for each allocated component, which is responsible for ensuring that the amount of deposited assets during investments are above this threshold. As the protocol docs state, this is to ensure unnecessary transactions are minimized.The
maxDeltaparameter is enforced within theBaseComponentRouter:// Validate deposit amount exceeds minimum threshold if (depositAmount < Math.mulDiv(totalAssets, INode(node).getComponentAllocation(component).maxDelta, WAD)) { revert ErrorsLib.ComponentWithinTargetRange(node, component); } // limit deposit by reserve ratio requirements // _validateReserveAboveTargetRatio() ensures currentCash >= idealCashReserve depositAmount = Math.min(depositAmount, currentCash - idealCashReserve); // subtract execution fee for protocol depositAmount = _subtractExecutionFee(depositAmount, node);Notice how the
depositAmountis checked against themaxDeltathreshold (to ensure it is not too small), but thedepositAmountis subsequently updated to a potentially lower value in the next lines of code, followed by a fee deduction. This updateddepositAmount, which is the actual deposit amount, is not checked against the minimum threshold.The impact is that unnecessary/small investment transactions will continue to occur, breaking protocol invariant.
Recommendation
Check against the
minimum thresholdafter updating thedepositAmountand deducting feesResolution
Nashpoint Team: The issue was resolved in commit ae4a663.
-
L-14 Low Partial Withdrawals Are Blocked DoS Acknowledged
Description
The
ERC7540routercan requests a partial async withdrawal usingMath.min(assetsRequested,maxClaimableAssets)in thefulfillRedeemRequestfunction:assetsReturned = _executeAsyncWithdrawal(node, component, Math.min(assetsRequested, maxClaimableAssets));if the component is
DigiftAdapterand the amount requested isassetRequestedwhich is less thanmaxWithdrawthe tx will always revert, asdigiFTadapter enforces full-withdraw-only semantics and reverts unless the requested assets equalsnode.maxWithdrawexactly. Which will block a request fulfilment.Recommendation
Consider relaxing the strict requirement in
digiFTAdapterand allowing partial withdraw.Resolution
Nashpoint Team: Acknowledged.
-
L-15 Low Malicious Owner Can Lock Users' Assets DoS Acknowledged
Description
The Node Owner has the ability to block user withdrawals via the following methods:
- Changing
setRebalanceCooldown(orsetRebalanceWindow) to extend cooldown so rebalancer
cannot call
startRebalance()to initiate redemption from components.- Implement incorrect component ratio causing
startRebalance()to fail due to
validateComponentRatios()returning false. Since rebalancing fails, withdrawals from components will also fail.- Utilize policies to, for example, pause the router/rebalancer address for the withdrawal selectors.
This is dangerous as the Node Owner is a permissionless role, thus causing harm to innocent users and trust for the protocol.
Recommendation
- Set strict limits on how much the owner can change rebalance cooldown and window
- Enforce component ratio validation upon adding a new component in
Node::addComponent - Ensure that withdrawals cannot be blocked in policies
Alternatively, document these behaviors for users to inform them of the owner’s capabilities.
Resolution
Nashpoint Team: Acknowledged.
- Changing
-
L-16 Low Node Owner Can Steal Funds Censoring Acknowledged
Description
When there is something wrong in one component, the protocol owner will set this component into the blacklist. The node owner is expected to withdraw assets from this component and then remove this component via
removeComponent.Normally, component tokens are not allowed to be withdrawn in
rescueTokens. However, a malicious owner can remove a blacklisted component directly by force and then directly transfer these component tokens.function removeComponent(address component, bool force) external onlyOwner onlyWhenNotRebalancing { if (force && !IRouter(router).isBlacklisted(component)) { revert ErrorsLib.NotBlacklisted(); } } function rescueTokens(address token, address recipient, uint256 amount) external onlyOwner { if (token == asset) revert ErrorsLib.InvalidToken(); if (_isComponent(token)) revert ErrorsLib.InvalidToken(); IERC20(token).safeTransfer(recipient, amount); emit EventsLib.RescueTokens(token, recipient, amount); }Recommendation
Consider documenting this behavior. Alternatively, do not allow blacklisted component shares to be rescued either, which forces the owner to withdraw the underlying funds through the regular liquidation flow.
Resolution
Nashpoint Team: Acknowledged.
-
L-17 Low Settlements May Use Incorrect Prices Warning Acknowledged
Description
The
settleDepositandsettleRedeemfunctions in thedigiftAdaptercontract use the latest oracle prices to determine the settlement value.While the expected time between the actual settlement event on the DigiFT side and the settlement on the adapter side can be up to 256 blocks, the latest price at the time of adapter settlement may differ from the price at the time of DigiFT settlement.
This discrepancy can become more pronounced if a time gap occurs due to unexpected downtime in the backend or event listeners, requiring manual intervention after 256 blocks. In this case, the real settlement value and the calculated settlement value may differ significantly.
Recommendation
Be aware of this situation. Alternatively, consider using the valid price at the time the event is emitted on DigiFT to determine the settlement value.
Resolution
Nashpoint Team: Acknowledged.
-
L-18 Low Users Can Claim Incentra Rewards Directly Logical Error Acknowledged
Description
In
IncentraRouter, the rebalancer can claim some rewards from Incentra on behalf of the node.The Incentra distributor on Arb is deployed on:
https://arbiscan.io/address/0x273d0d19eaC2861FCF6B21893AD6d71b018E25aB#code.
Users can claim node's rewards on behalf of the node directly via below function.
function claimAll(address earner, address[] calldata campaignAddrs) public { for (uint256 i = 0; i < campaignAddrs.length; i++) { IRewardContract(campaignAddrs[i]).claim(earner); } }In the current design, only the node rebalancer is allowed to claim rewards. However, regular users can claim rewards directly on behalf of the node, giving them a way to increase the asset amount directly.
e.g. 1. Alice deposits asset with current cache price. 2. Alice claims Incentra rewards via function claim. Although the current share price does not change, this ensures that the share price will be affected after the next rebalance. 3. After the next rebalance, the cache price will be updated, and the user can request withdraw with new price.
If the rebalancers claim these rewards frequently in every rebalancing period, the impact would be minimal. However, if rebalancers do not claim rewards, users could anticipate a larger change in share value in the future and deposit before the value increases.
Recommendation
Rebalancers should claim rewards as frequently as possible to minimize the impact of claimed rewards on the share value, since there is no way to prevent regular users from claiming on behalf of the Node.
Resolution
Nashpoint Team: Acknowledged.
-
L-19 Low Griefing Rebalancers Via Dust Requests Censoring Acknowledged
Description
Share owners create redemption requests, which are expected to be fulfilled by the rebalancer during the next rebalancing period. By default, rebalancers have one hour to perform multiple tasks, such as claiming rewards and fulfilling redemption requests.
Share owners can create redemption requests for any amount and for any controller, and these requests are stored per controller.
It is possible to create thousands of small redemption requests across different controller addresses, which increases the rebalancer’s workload in the next rebalancing period.
This not only results in griefing the rebalancer, but can also cause legitimate users’ redemption requests to be delayed for several rebalancing periods.
Recommendation
Consider enforcing a minimum request amount to prevent such griefing.
Additionally, consider disabling the creation of requests for arbitrary controllers. If this feature is necessary at the node level, the restriction can be implemented through a custom policy, allowing node owners to maintain greater control.
Resolution
Nashpoint Team: Acknowledged.
-
L-20 Low Inaccurate Calculation Of Shares Logical Error Acknowledged
Description
totalAssets()usescacheTotalAssets(refreshed atstartRebalance) to price deposits/mints. Deposits are allowed while the cache can be stale, soconvertToSharesoften uses an outdated denominator to calculate the number of shares to mint.Depositors can be over-minted when current actual
totalAssetsis greater thancacheTotalAssets(yield accumulated fromlastUpdateuntil now), or under-minted when current actualtotalAssetsis less thancacheTotalAssets(strategy loss from lastcacheTotalAssetsupdate until now). Example:Cache = 1,000,000; actual
totalAssetswith accumulated yield from all components = 1,050,000;- User deposits 100,000 → receives 100,000 shares
- Shortly after, rebalancer calls
startRebalance, cache gets updated to 1,150,000 - User shares worth: 100,000 * 1,150,000 / 1,100,000 = 104,545, a 4.5k gain extracted from existing
holders without actually contributing to the yield
- This gain for the depositing user is a loss for others who have been staking their assets in the node
- Same applies for strategy losses
- if total assets are frequently updated by rebalancer, impact will minimal
Recommendation
Consider refreshing
totalAssetbefore any deposit.Resolution
Nashpoint Team: Acknowledged.
-
L-21 Low Unbounded Settlement Loops Gas Griefing Acknowledged
Description
The
DigiftAdaptersupports multiple nodes depositing/redeeming in the same batch. When settlement occurs viasettleDeposit(nodes, ...)orsettleRedeem(nodes, ...), the manager is enforced to provide all contributor nodes to that batch and the function loops through all of them to distribute shares/assets proportionally.There is no cap on how many nodes can contribute to a single batch. If too many nodes deposit/redeem for the same batch , the settlement transaction could exceed block gas limits and revert, making it impossible to settle and permanently locking all pending funds, unless upgrade
Recommendation
Enforce a maximum number of nodes that can be whitelisted per adapter instance (depending on gas analysis).
Resolution
Nashpoint Team: Acknowledged.
-
L-22 Low Missing cacheTotalAssets Update After Swap Logical Error Acknowledged
Description
In Nashpoint, the Node may gain some rewards. The rebalancer can swap these incentive tokens into node asset in the rebalancing window.
One normal rebalancing order may be like as below: 1. Start rebalance. 2. Claim rewards. 3. Swap rewards to node asset token. 4. Fulfill redemptions. 5. Invest into vaults.
The problem here is that when the incentive token is a node asset, or when other incentive tokens are swapped into a node asset, the Node does not update the claimed or swapped node assets in
cacheTotalAssets.Then, when the rebalancer fulfills a redemption, the claimed rewards will not be included in the asset calculation, which is unfair to the user who redeemed their shares, as they are also entitled to a portion of these earned incentives. These assets will only be considered in the next rebalancing period.
Recommendation
When the rebalancer claims node assets or swaps other incentive tokens to acquire node assets, these assets should be added to
cacheTotalAssets. If this is the intended behavior, document this for users.Resolution
Nashpoint Team: Acknowledged.
-
L-23 Low Can Invest In Blacklisted Components Warning Resolved
Description
The whitelist and blacklist status of a component is only checked when adding or removing a component from a Node. There is no check for the blacklist status when investing in components.
A blacklisted component must be removed from the Node. However, until it is removed, it is still considered a valid Node component.
The rebalancer might unintentionally continue investing in blacklisted components without noticing that a component has been blacklisted by the registry owner, especially if the rebalancing process is automated.
Recommendation
Deposits into blacklisted components should be prevented. This can be achieved by checking the blacklist status in the
_computeDepositAmountfunction of theBaseComponentRouter, which is used by all investment flows.Resolution
Nashpoint Team: The issue was resolved in commit 65ac649.
-
L-24 Low Unchecked Deviation Upper Bound Validation Resolved
Description
The
withinRangefunction inMathLibreverts with an underflow error when theallowedDeviationvalue exceeds WAD. TheallowedDeviationvalues passed to this function are thepriceDeviationandsettlementDeviationvalues from theDigiftAdaptercontract.While the setter functions for these values enforce that the value is less than or equal to WAD, there is no such check during initialization, so these values can exceed WAD, causing reverts during
withinRangecall.Recommendation
Ensure that
args.priceDeviationandargs.settlementDeviationininitializeare less than or equal to WAD.Resolution
Nashpoint Team: The issue was resolved in commit 221274d.
-
L-25 Low Fee Bypass Via Low Decimal Tokens Rounding Acknowledged
Description
The protocol earns fees via the following logic which is executed during investments and swaps:
uint256 executionFee = transactionAmount * registry.protocolExecutionFee() / WAD;The
executionFeerounds down due to Solidity’s default built-in rounding down. With WAD math, a fee as low as 0.0001% would meanregistry.protocolExecutionFee() = 1e12. BecauseexecutionFee =floor(amount * 1e12 / 1e18), anyamount < 1e6base units rounds the fee down to zero.This is especially problematic for some high value tokens such as WBTC, where 1e6 of the token is currently worth ~$1110.
In addition, for higher decimal tokens, users can batch swap/investment transactions into small amounts to round down the
executionFeeto 0 due to a lowertransactionAmount.The investment/swap calls would proceed as normal since the execution fee function always returns when fee calculated is 0:
if (executionFee == 0) { return transactionAmount; }Another option could be for users to create wrappers for high decimal tokens (i.e 18 decimals) and turn it into low decimal (i.e 2-6 decimals), allowing fee bypass.
The result is loss of funds/revenue for the protocol.
Recommendation
Consider rounding up the
executionFeeor using fractional protocol fee accounting.Resolution
Nashpoint Team: Acknowledged.
-
L-26 Low Mint And Withdraw Returns Incorrect Values Compatibility Resolved
Description
DigiftAdapter.mint()andDigiftAdapter.withdraw()return values that don't match the actual assets consumed or shares burned, creating inconsistency with their emitted events andERC-4626/7575semantics.mint(): ReturnsclaimableDepositRequest(gross assets) but actually consumesassets -
assetsToReimburse(net assets). TheDepositevent correctly emits the net amount, but the return value overstates consumption.withdraw(): ReturnsclaimableRedeemRequest(gross shares) but actually burnsshares -
sharesToReimburse(net shares). TheWithdrawevent correctly emits the net amount, but the return value doesn't reflect what was burned. TheNatSpecalso states "returns (uint256shares) The number of shares burned", which is wrong.While the current
ERC7540Routerdoes not rely on these return values (it tracks via balance deltas), it is preferable to remain aligned with the specification for any future interactions.Recommendation
Consider returning net amounts (after reimbursement) to match actual state changes and emitted events.
Resolution
Nashpoint Team: The issue was resolved in commit c8cca04.
-
L-27 Low Inaccurate Liquidation Queue Enforcement Logical Error Resolved
Description
Liquidation order enforcement uses inflated component capacity
ERC4626Router.getComponentAssets()ignores theclaimableOnlyparameter and always returns
convertToAssets(shareBalance), which does not account for withdrawal limits- When Node's
_enforceLiquidationOrder()callsgetComponentAssets(candidate, true)it expects the
actual claimable assets, not all the asset invested in a component, but in case of
ERC4626Routerit may receive a value that can be more than what actually can be withdrawn from the component (maxWithdraw), thus it may enforce a wrong liquidation queue .Recommendation
Return
maxWithdraw(node)whenclaimableOnly == trueinERC4626Router.getComponentAssets()Resolution
Nashpoint Team: The issue was resolved in commit fc79a10.
-
I-01 Informational finalizeRedemption Lacks Rebalancing Modifier Logical Error Resolved
Description
The rebalancer role has two ways of finalizing user requested redemptions:
Node::fulfillRedeemFromReserve: Utilizes the reserves within the Node contract to finalize
redemptions.
Router::fulfillRedeemRequest(which callsNode::finalizeRedemption): Executes withdrawals from
components, such as
ERC4626vaults, to finalize redemptions.The first method,
fulfillRedeemFromReserveensures that theRebalancerrole can only execute this after first callingstartRebalance(). This ensures that the total assets are up to date via (_updateTotalAssets) and that fees are distributed (via_payManagementFees) using the current cached balance.The problem is that the second option,
fulfillRedeemRequest, does not have the requirement that the rebalancing window must have been initiated first (viastartRebalance()). This means that when redemptions are finalized, an outdatedcacheTotalAssetsmay be used, causing an incorrect share to asset calculation, thus causing a loss for users.In addition, this flow will subsequently skip fee payment (to protocol and node owner). Since
finalizeRedemptiondeducts thecacheTotalAssets, the fee payment for the withdrawal is skipped permanently, as the fees calculated depend on thecacheTotalAssetsvalue. This will cause a loss to the protocol and node owner.Recommendation
Apply the
onlyWhenRebalancingmodifier toNode::finalizeRedemptionResolution
Nashpoint Team: The issue was resolved in commit e959571.
-
I-02 Informational Possible Different Share Price Via Deposit/Mint Informational Acknowledged
Description
In Node, users can choose to deposit assets via deposit or mint. When swing pricing is enabled, users may receive a deposit bonus through deposit, but they will not receive a bonus through mint.
Recommendation
Document this behavior to inform users about the difference between deposit and mint.
Resolution
Nashpoint Team: Acknowledged.
-
I-03 Informational rescueTokens Should Restrict Incentive Tokens Trust Assumptions Acknowledged
Description
The
rescueTokensfunction can be used to withdraw tokens from the Node, except for the Node's asset token and component tokens. However, this restriction does not apply to incentive tokens claimed from external integrations.These incentive tokens should also not be withdrawable by the Node owner, similar to the Node’s asset tokens, as they belong to depositors and are intended to be swapped into asset tokens during rebalancing.
Recommendation
Restrict incentive tokens from being withdrawn via the
rescueTokensfunction. However, this would require additional functionality to track all incentive token addresses when the Node joins a campaign.Alternatively, document this behavior for users.
Resolution
Nashpoint Team: Acknowledged.
-
I-04 Informational Inconsistent Liquidation Ordering Unexpected Behavior Resolved
Description
The order of liquidation set by the Node Owner is correctly enforced within
ERC7540Router::fulfillRedeemRequest,ERC4626Router::fulfillRedeemRequest. These functions execute requested withdrawals and update storage for user withdrawal requests.However, the
Rebalancerrole can directly withdraw from the components viaERC4626Router::liquidateandERC7540Router::executeAsyncWithdrawal, which do not check the liquidation order.The impact is that withdrawals can happen in a complete different order than what the Node Owner specified, potentially withdrawing from more riskier vaults first.
Recommendation
Consider enforcing the liquidation order in
ERC4626Router::liquidateandERC7540Router::executeAsyncWithdrawalResolution
Nashpoint Team: The issue was resolved in commit fc79a10.
-
I-05 Informational totalAssets() Can Revert Violating ERC7575 Compatibility Acknowledged
Description
The
totalAssets()function callsconvertToAssets(totalSupply()), which internally fetches prices via_getAssetPrice()and_getPrice(). These price functions enforce staleness and deviation checks that can revert.ERC7575spec requirement: "totalAssets()MUST NOT revert"Recommendation
Make price fetches in view functions non-reverting by returning cached/last-known values when fresh data is unavailable, or clearly document that this implementation does not fully conform to
ERC7575view function requirementsResolution
Nashpoint Team: Acknowledged.
-
I-06 Informational Division By 0 DoS During Deposit Bonus Math Resolved
Description
QuoterV1executes the following when calculating deposit bonus:uint256 targetReserveAssets = investedAssets.mulDiv(targetReserveRatio, WAD - targetReserveRatio); ... uint256 deltaClosedPct = Math.mulDiv(deltaClosed, WAD, targetReserveAssets);In case the
investedAssetsis a low amount (i.e after liquidations) and for exampletargetReserveRatiois set to 5% (0.05e18),targetReserveAssetscan round down to 0. This would cause a revert due to division by 0 when calculatingdeltaClosedPctduring the deposit bonus calculation, causing DoS to deposits completely.This edge case can be triggered if
getCashAfterRedemptionsis depleted due to large withdrawals causing(Math.mulDiv(getCashAfterRedemptions(), WAD, totalAssets()) < targetReserveRatio)thus executing the deposit bonus branch during deposits.Recommendation
Return safely if
targetReserveAssets == 0when calculating deposit bonusResolution
Nashpoint Team: The issue was resolved in commit 2c6e5b9.
-
I-07 Informational Unbounded Loops In Event Verification Gas Optimization Resolved
Description
DigiftEventVerifier.verifySettlementEvent()iterates through all logs in the transaction receipt to locate the settlement event, and then iterates through all investors in the event to find the caller's index.A transaction can emit multiple logs, and a log can include many investors, which may result in excessive gas consumption or even hit block limits in some cases.
Recommendation
Add
logIndexandinvestorIndextoOffchainArgsto allow direct access instead of O(n) scans, which will save a significant amount of gas.Resolution
Nashpoint Team: The issue was resolved in commit a0ee7e7.
-
I-08 Informational Unused Named Return Value Informational Resolved
Description
The
_getInvestmentSizefunction in theERC4626RouterandERC7540Routercontracts defines the return value asuint256 depositAssets. However, thisdepositAssetsvalue is never used, and delta is returned instead.Recommendation
Remove unused named value.
Resolution
Nashpoint Team: The issue was resolved in commit c1a5fda.
-
I-09 Informational Warning About Virtual Functions Warning Resolved
Description
Both
getComponentAssetsand_getInvestmentSizefunction inBaseComponentRoutercontract are declared virtual but have empty bodies that implicitly return zero. These functions must be overridden in the inheriting router contract.While this is not an issue at the moment, forgetting to override these functions when adding new routers causes calls to succeed but return 0, which can lead to unexpected behaviors such as incorrect valuations.
Recommendation
Consider making these virtual functions revert by default, which would prevent unexpected behaviors and enable early detection if a new router fails to override them.
Resolution
Nashpoint Team: The issue was resolved in commit 59402ff.
-
I-10 Informational _safeApprove Can Revert Informational Acknowledged
Description
The helper function approves a spender directly to a non-zero amount without first resetting the allowance to zero. Some tokens require that the allowance be set to zero before assigning a new non-zero allowance.
Since all flows in the codebase approve an amount and use the same amount immediately afterward, allowances are expected to be cleared after execution. However, care should be taken when adding components or new routers, as this may cause unexpected reverts in edge cases.
Examples include an underlying component being incompatible with
ERC4626orERC7540despite expectations (e.g.,requestDepositusing a different asset value than provided), or a new router not utilizing the full allowance.Although these are extreme edge cases, they can leave unused allowances, and cause reverts in subsequent actions.
Recommendation
Be aware of this situation and ensure that all components and routers always use the full allowance during execution. Alternatively, consider resetting the allowance to 0 first if any remaining allowance exists.
Resolution
Nashpoint Team: Acknowledged.
-
I-11 Informational Users May Be Unable To Withdraw From Adapter Documentation Acknowledged
Description
DigiftAdapterenforces minimum amounts forrequestDepositandrequestRedeem.Assume the minimum for
deposit = 1000e6 USDCand minimum forredeem = 10e18 shares(as configured in tests), users may have deposits where their total shares represent less than the minimum required to request a redemption from the component.Following the intended design, the rebalancer will be unable to execute a redemption request for these users, unless an admin changes the minimum amount or other users queue redemptions, which will allow the total request to exceed the minimum.
Recommendation
Consider documenting this clearly to ensure users are aware of it.
Resolution
Nashpoint Team: Acknowledged.
-
I-12 Informational Unused State Variables In QuoterV1 Informational Resolved
Description
QuoterV1declares three state variables that are never written to or read from anywhere in the codebase:/* STATE */ mapping(address => bool) public isErc4626; mapping(address => bool) public isErc7540; bool public isInitialized;Recommendation
Consider removing unused state variables.
Resolution
Nashpoint Team: The issue was resolved in commit 6259916.
-
I-13 Informational Missing Public Functions In Multiple Interfaces Informational Acknowledged
Description
Several interfaces are incomplete compared to their implementations.
Missing functions include:
INode(9 methods): Ownable functions,multicall(), rebalance getters, swing pricing gettersINodeRegistry(9 methods): Ownable functions, UUPS upgrade methods, config gettersIQuoterV1(4 methods): Type checks, initialization state, registry referenceINodeFactory(2 methods): Implementation and registry getters
Recommendation
Consider adding these methods or documenting why they're excluded.
Resolution
Nashpoint Team: Acknowledged.
-
I-14 Informational Misleading Comment Regarding deltaClosedPct Informational Resolved
Description
The
deltaClosedPctvalue represents the percentage of the delta relative to thetargetReserveAssets:deltaClosedPct = Math.mulDiv(deltaClosed, WAD, targetReserveAssets).However, the comments “It is the inverse of the percentage of the reserve assets shortfall closed by the deposit” and “Get
reserveImpactas a measure of how much the deposit helps to close any asset shortfall” gives the impression thatdeltaClosedPctwas meant to represent the percentage of the shortfall, not of thetargetReserveAssets.Recommendation
Update the comments.
If the intention was to calculate the reserve impact based on the shortfall percentage,
deltaClosedPctshould beMath.mulDiv(deltaClosed, WAD, shortfall)Resolution
Nashpoint Team: The issue was resolved in commit 6259916.
-
I-15 Informational Incorrect Comment In DigiftEventVerifier Informational Resolved
Description
In line 234 of the
DigiftEventVerifier, the comment "// Structure: (stToken,investorList,quantityList,currencyTokenList,amountList, timestamp)" is incorrect, as the last parameter in the structure isfeeList, not timestamp.Recommendation
Update the comment.
Resolution
Nashpoint Team: The issue was resolved in commit d48b89f.
-
I-16 Informational Warning Regarding Dust Accumulation In Digift Warning Acknowledged
Description
The
settleDepositandsettleRedeemfunctions inDigiftAdapteraccumulate the dust amount to the last node in the loop. The adapter definesminDepositAmountandminRedeemAmount, which are set to non-trivial values in the tests.Accumulating dust to the last node is not an issue under normal conditions where the minimum amounts are set. However, if these minimum amounts are ever set to 0, it becomes possible to request only 1 wei of shares.
In that case,
sharesToReimbursemay exceed 1 wei due to the dust accumulated on the last node, which can cause an underflow later.Note that this is an extreme edge case scenario where the minimum amounts must be set to 0, a node must have a request with an extremely low value, and that node must also be the last one during settlement.
Recommendation
Be aware of this and do not set minimum amounts to 0.
Resolution
Nashpoint Team: Acknowledged.
-
I-17 Informational Zero Share Minting Allowed Warning Resolved
Description
The Node contract does not revert when the resulting share amount is 0 during a deposit or mint operation. This can cause users to lose their assets without receiving any shares, especially when the share price is inflated.
Recommendation
Do not allow zero share minting.
Resolution
Nashpoint Team: The issue was resolved in commit 93cc526.
Remediation Review
6 findings-
L-01 Low Can Invest In Blacklisted Components Validation Resolved
Description
The fix for L-23 introduces whitelist status in
_computeDepositAmountand prevents deposits when a component is not whitelisted.However, since issue L-05 is acknowledged, whitelist and blacklist statuses can still conflict, allowing a blacklisted component to also remain whitelisted.
Blacklisting does not automatically remove a component from the whitelist, and the issue persists until the component is manually de-whitelisted or removed by the owner.
Recommendation
Check the blacklist status in
_computeDepositAmountas well or always ensure blacklisting and de-whitelisting happens together.Resolution
Nashpoint Team: The issue was resolved in commit 2a6584f.
-
L-02 Low Unsafe External Call In NodeFactory Validation Resolved
Description
SetupCallsare introduced to theNodeFactoryto allow Node deployers to configure their Nodes during deployment. This is mainly intended for performing policy calls that are protected by theonlyNodeOwnermodifier, since the owner at the time of these calls is the factory contract itself.However, there is no guarantee that these setup calls will be performed only on policies. Any target address can be called with any payload, without any protection. They could even be used for malicious actions, such as funding an exploiter through the factory contract or interacting with OFAC-sanctioned accounts.
While there is no incentive to perform such actions and the likelihood is extremely low, it could still be legally binding for Nashpoint in the future because, on paper, it would appear that the ‘Nashpoint-owned factory’ is the entity carrying out these actions.
Recommendation
Do not allow arbitrary target addresses to be called through the factory. Keep a list of policies that can be called during setup, and allow only those addresses as valid targets.
Resolution
Nashpoint Team: The issue was resolved in commit 4fe012b.
-
L-03 Low Users May Avoid Paying Performance Fees Logical Error Acknowledged
Description
In the fix for M-03, the management fee payment can be bypassed if there is not enough balance in the Node. This will make sure that
startBalanceoperation will not be blocked.This will raise one new issue here: The users may avoid paying performance fees.
e.g.
- The node owner starts rebalance in timestamp X.
rebalanceWindow = 1 hour,rebalanceCooldown
= 23 hours.- Alice deposits 1000 USDC in timestamp X + 1.
- In timestamp X + 1 hour, the rebalancer invests 1000 USDC into one component. Assume that the
target reserve ratio is 0.
- In timestamp X + 1 hour + 1, Alice requests redeem.
- In timestamp X + 24 hour, the node rebalancer will start another round of rebalance. The balance
in the node is 0. The management fee will be bypassed here.
- The rebalancer triggers
fulfillRedeemRequest. Alice can get back her asset without paying any
fees here.
Recommendation
In
startRebalance, if the balance is not enough to pay the management fee, record the management fee as debt, and update thelastPaymenttimely.Resolution
Nashpoint Team: Acknowledged.
- The node owner starts rebalance in timestamp X.
-
I-01 Informational Consider Early Return In _fulfillRedeemFromReserve Informational Acknowledged
Description
The
_fulfillRedeemFromReservefunction setsbalance = max(currentBalance, 1)and, ifassetsToReturn > balance, setsassetsToReturn = balance. When actual reserve is 0, this forces assetsToReturn = 1, which later fails withExceedsAvailableReservein_finalizeRedemptionfunction.function _fulfillRedeemFromReserve(address controller) internal { ... uint256 balance = Math.max(IERC20(asset).balanceOf(address(this)), 1); uint256 assetsToReturn = convertToAssets(request.pendingRedeemRequest); ... if (assetsToReturn > balance) { sharesPending = (sharesPending * balance - 1) / assetsToReturn + 1; assetsToReturn = balance; // balance may be 0, but forced to 1 above } _finalizeRedemption(controller, assetsToReturn, sharesPending); }Recommendation
Although the flow eventually reverts when the balance is zero, consider reverting or returning early in that case.
Resolution
Nashpoint Team: Acknowledged.
-
I-02 Informational Tradeoffs Related To Share Valuation In M-08 Warning Acknowledged
Description
The fix for the previous M-08 introduces a mixed approach in which the
maxWithdrawvalue at the locked rate is used for the normal case, while the current-rate share conversion is used when withdrawals are paused.While this approach fixes the issue in most cases and the likelihood of the paused state is low, it is important to note that it will still result in incorrect valuation when withdrawals are paused.
Unfortunately, this is one of the trade-offs of asynchronous share valuations. In a paused scenario, the protocol must either intentionally undervalue the total underlying assets by continuing to use
maxWithdrawand keeping claimable assets at 0, or accept the risk of overvaluation by using the current rate.From a security perspective, undervaluing total assets may be the safer option, as it would prevent insolvency if everyone attempted to redeem their shares in an extremely rare scenario. However, this also means that new depositors would receive more shares than their deposits justify.
Recommendation
Note that this issue aims to highlight possible edge-cases after the fix of M-08. The decision to use the current rate when withdrawals are paused should be made deliberately, as it still carries some risk of overvaluation, and it is important to consider the trade-offs of both scenarios.
Resolution
Nashpoint Team: Acknowledged.
-
I-03 Informational Blocked Revoke Actions In Policies With Blacklist Logical Error Acknowledged
Description
The updated policies now include a blacklisting feature as well. Both the whitelist and blacklist variants apply actor checks to the spender on every approval, regardless of the amount.
This also blocks
approve(spender, 0)calls intended to revoke existing allowances if the spender has since become blacklisted.Similarly, removing an operator via a
setOperator(operator, false)call is also blocked once the operator becomes blacklisted. Ability to revoke approvals and operators when they become blacklisted is crucial and should not be blocked.Recommendation
Allow revoking approvals and removing operators when using
GatePolicywith blacklisting, and do not performactorCheckon spender/operator in these special cases.Resolution
Nashpoint Team: Acknowledged.
No findings match.
Invariants 113
The review's fuzzing suite asserted 113 invariants. 113 held.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
DIGIFT-01 | Global Pending Deposit Must Match Forwarded Amount | Held |
DIGIFT-02 | Global Pending Redeem Must Match Forwarded Amount | Held |
DIGIFT-03 | No Pending Deposits Must Remain After Settle | Held |
DIGIFT-04 | No Pending Redemptions Must Remain After Settle | Held |
DIGIFT-05 | Max Mintable Shares Must Be Non-Zero After Settle Deposit | Held |
DIGIFT-06 | Max Withdrawable Assets Must Be Non-Zero After Settle Redeem | Held |
DIGIFT-07 | Total Max Withdrawable Must Match Expected Assets | Held |
DIGIFT-08 | Withdraw Assets Must Match Max Withdraw Before | Held |
DIGIFT-09 | Node Balance Must Not Decrease After Withdraw | Held |
DIGIFT-10 | Max Withdraw Must Be Zero After Withdraw | Held |
DIGIFT-11 | Pending Redeem Must Increase After Request | Held |
DIGIFT-12 | Balance Must Not Increase After Request Redeem | Held |
FACTORY-01 | Deployed Node Address Must Not Be Zero | Held |
FACTORY-02 | Deployed Escrow Address Must Not Be Zero | Held |
FACTORY-03 | Node Escrow Link Must Match Deployed Escrow | Held |
FACTORY-04 | Node Asset Must Match Init Args Asset | Held |
FACTORY-05 | Node Owner Must Match Init Args Owner | Held |
FACTORY-06 | Node Total Supply Must Be Zero After Deploy | Held |
FACTORY-07 | Node Must Be Registered In Registry After Deploy | Held |
NODE-01 | User Share Balance Must Increase After Successful Deposit/Mint | Held |
NODE-02 | Escrow Share Balance Must Increase By The Redemption Request Amount | Held |
NODE-03 | Escrow Share Balance Must Decrease After A Redeem Is Finalized | Held |
NODE-04 | User Asset Balance Must Increase By Requested Asset Amount After Withdraw | Held |
NODE-05 | Escrow Asset Balance Must Be >= Sum Of All claimableAssets | Held |
NODE-06 | Component's Asset Ratio Should Not Exceed Target After Invest | Held |
NODE-07 | Node's Reserve Should Not Decrease Below Target After Invest | Held |
NODE-08 | Receiver Share Balance Must Increase By Minted Shares After Deposit | Held |
NODE-09 | Node Asset Balance Must Increase By Deposited Assets | Held |
NODE-10 | Node Total Assets Must Increase By Deposited Assets | Held |
NODE-11 | Node Total Supply Must Increase By Minted Shares | Held |
NODE-12 | Receiver Share Balance Must Increase By Minted Shares | Held |
NODE-13 | Receiver Asset Balance Must Decrease By Assets Spent | Held |
NODE-14 | Node Total Assets Must Increase By Assets Spent | Held |
NODE-15 | Node Total Supply Must Increase By Requested Shares | Held |
NODE-16 | Owner Share Balance Must Decrease By Requested Shares | Held |
NODE-17 | Pending Redeem Must Increase By Requested Shares | Held |
NODE-18 | Claimable Redeem Must Remain Unchanged After Request | Held |
NODE-19 | Claimable Assets Must Remain Unchanged After Request | Held |
NODE-20 | Pending Redeem Must Decrease After Fulfill | Held |
NODE-21 | Claimable Redeem Must Increase After Fulfill | Held |
NODE-22 | Claimable Assets Must Decrease By Withdrawn Amount | Held |
NODE-23 | Escrow Asset Balance Must Decrease By Withdrawn Amount | Held |
NODE-24 | Pending Redeem Must Decrease By Finalized Shares | Held |
NODE-25 | Claimable Redeem Must Increase By Finalized Shares | Held |
NODE-26 | Claimable Assets Must Increase By Returned Assets | Held |
NODE-27 | Escrow Asset Balance Must Increase By Returned Assets | Held |
NODE-28 | Node Asset Balance Must Decrease By Returned Assets | Held |
NODE-29 | Claimable Redeem Must Decrease By Redeemed Shares | Held |
NODE-30 | Claimable Assets Must Decrease By Returned Assets | Held |
NODE-31 | Receiver Asset Balance Must Increase By Returned Assets | Held |
NODE-32 | Escrow Asset Balance Must Decrease By Returned Assets | Held |
NODE-33 | Component Must Be Registered After Add | Held |
NODE-34 | Component Must Be Unregistered After Remove | Held |
NODE-35 | Node Balance Must Decrease By Rescued Amount | Held |
NODE-36 | Recipient Balance Must Increase By Rescued Amount | Held |
NODE-37 | Policy Must Be Registered After Add | Held |
NODE-38 | Policy Must Be Unregistered After Remove | Held |
NODE-39 | Component Balance Must Increase By Delta After Gain Backing | Held |
NODE-40 | Component Balance Must Decrease By Delta After Lose Backing | Held |
NODE-41 | Shares Exiting Must Not Exceed Total Supply | Held |
ONEINCH-01 | Asset Token Balance Of Node Must Increase After Successful Swap | Held |
ONEINCH-02 | All Incentive Token Input Must Be Used During Swap | Held |
ONEINCH-03 | Node Must Receive At Least 99% Of Min Assets Out | Held |
ONEINCH-04 | Node Must Spend Exact Incentive Amount | Held |
ONEINCH-05 | Executor Must Receive At Least Incentive Amount | Held |
POOL-01 | Pending Deposits Must Not Increase After Process | Held |
POOL-02 | Pending Redemptions Must Be Zero After Process | Held |
REGISTRY-01 | Protocol Fee Address Must Match Set Value | Held |
REGISTRY-02 | Protocol Management Fee Must Match Set Value | Held |
REGISTRY-03 | Protocol Execution Fee Must Match Set Value | Held |
REGISTRY-04 | Policies Root Must Match Set Value | Held |
REGISTRY-05 | Registry Type Status Must Match Set Value | Held |
REGISTRY-06 | Owner Must Match After Transfer | Held |
RWD-FLD-01 | Claim Recipient Must Be Node Address | Held |
RWD-FLD-02 | Claim Cumulative Amount Must Match Params | Held |
RWD-FLD-03 | Claim Position ID Must Match Params | Held |
RWD-FLD-04 | Claim Cycle Must Match Params | Held |
RWD-FLD-05 | Claim Proof Hash Must Match Params | Held |
RWD-INC-01 | Last Earner Must Be Node Address | Held |
RWD-INC-02 | Campaign Addresses Hash Must Match | Held |
RWD-INC-03 | Rewards Hash Must Match | Held |
RWD-MKL-01 | Users Hash Must Match Params | Held |
RWD-MKL-02 | Tokens Hash Must Match Params | Held |
RWD-MKL-03 | Amounts Hash Must Match Params | Held |
RWD-MKL-04 | Proofs Hash Must Match Params | Held |
ROUTER-01 | Blacklist Status Must Match Set Value | Held |
ROUTER-02 | Whitelist Status Must Match Set Value | Held |
ROUTER-03 | Tolerance Value Must Match Set Value | Held |
ROUTER4626-01 | Invest Must Return Non-Zero Deposit Amount | Held |
ROUTER4626-02 | Node Component Shares Must Not Decrease After Invest | Held |
ROUTER4626-03 | Node Asset Balance Must Not Increase After Invest | Held |
ROUTER4626-04 | Liquidate Must Return Non-Zero Assets When Expected | Held |
ROUTER4626-05 | Node Component Shares Must Not Increase After Liquidate | Held |
ROUTER4626-06 | Node Asset Balance Must Not Decrease After Liquidate | Held |
ROUTER4626-07 | Fulfill Must Return Non-Zero Assets | Held |
ROUTER4626-08 | Escrow Balance Must Not Decrease After Fulfill | Held |
ROUTER4626-09 | Node Asset Balance Must Not Increase After Fulfill | Held |
ROUTER7540-01 | Invest Must Request Non-Zero Assets | Held |
ROUTER7540-02 | Pending Deposit Must Not Decrease After Invest | Held |
ROUTER7540-03 | Node Asset Balance Must Not Increase After Invest | Held |
ROUTER7540-04 | Node Component Shares Must Increase By Received Shares After Mint | Held |
ROUTER7540-05 | Claimable Must Not Increase After Mint | Held |
ROUTER7540-06 | Pending Redeem Must Not Decrease After Request Withdrawal | Held |
ROUTER7540-07 | Component Share Balance Must Not Increase After Request Withdrawal | Held |
ROUTER7540-08 | Execute Withdrawal Must Return Non-Zero Assets | Held |
ROUTER7540-09 | Execute Withdrawal Assets Must Match Max Withdraw Before | Held |
ROUTER7540-10 | Claimable Must Not Increase After Execute Withdrawal | Held |
ROUTER7540-11 | Node Asset Balance Must Not Decrease After Execute Withdrawal | Held |
ROUTER7540-12 | Max Withdraw Must Be Zero After Execute Withdrawal | Held |
ROUTER7540-13 | Fulfill Redeem Must Return Non-Zero Assets When Expected | Held |
ROUTER7540-14 | Escrow Balance Must Not Decrease After Fulfill Redeem | Held |
ROUTER7540-15 | Node Asset Balance Must Not Increase After Fulfill Redeem | Held |
ROUTER7540-16 | Component Shares Must Not Increase After Fulfill Redeem | Held |
More from Nashpoint
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.