Skip to content
Cosmopediaby Unity Nodes
Documentationinformalsystems/auditsinformalsystems/audits › NeutronView on informalsystems/audits ↗

2025-07-30 Neutron Q3 2025 DEX Fractional Banking Audit Report Final

Security Audit Report

NEUTRON Q3 2025: DEX FRACTIONAL BANKING AUDIT

Last Revised Authors: 2025/07/30 Ivan Gavran, Aleksandar Stojanovic Neutron Q3 2025 DEX Fractional Banking Audit

Contents Audit overview 2 The project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Scope of this report . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Audit plan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Conclusions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2

Audit Dashboard 3 Severity Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3

System Overview 4

Threat Model 5 Definitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5

Threat Analysis 7

Findings 10 Migrate5to6 not registered . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Inefficient fractional balance loading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 PlaceMakerLimitOrder should take PrecDec . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 Not accounting for fractional debt when sending from accounts to the module . . . . . . . . . . . . . . . . . 14 Pool shares are still truncated, disregarding the fractional part . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 Miscellaneous code improvements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

Appendix: Vulnerability classification 17

Disclaimer 20

Informal Systems © 2025 < Table of Contents 1 Neutron Q3 2025 DEX Fractional Banking Audit

Audit overview The project In July 2025, Neutron engaged Informal Systems ↗ to conduct a security audit of the new feature, Fractional Banking, captured in PR#938 ↗. The auditors joined the work before the PR was fully finalized, thus the audit proceeded in two stages:

  1. Initial review and comments on commit 2a76a47 ↗
  2. Follow-up analysis on commit 2283112 ↗.

Scope of this report The scope of the report was analyzing the fractional banking feature as well as its consequences on the functioning of the whole DEX.

Audit plan The audit was conducted between July 7th, 2025, and July 16th, totalling to 8 person-days 2025 by the following personnel:

● Ivan Gavran ● Aleksandar Stojanovic

Conclusions We performed the audit combining manual code review and writing property-based tests. We found that overall the feature did not introduce serious adversarial consequences. We found some problems in the implementation, reducing precision or enabling DoS attacks (more details can be found in the “Findings” section). Our findings were promptly patched and reaudited.

Informal Systems © 2025 < Table of Contents 2 Neutron Q3 2025 DEX Fractional Banking Audit

Audit Dashboard Severity Summary

Finding Severity Number

Critical 0 High 0 Medium 2 Low 3 Informational 1 Total 6

Informal Systems © 2025 < Table of Contents 3 Neutron Q3 2025 DEX Fractional Banking Audit

System Overview Initially, the Neutron DEX operated using standard Cosmos SDK 18-digit decimal precision, which was adequate for manual trading where occasional rounding errors were tolerable, but became problematic as automated strategies emerged. The motivation for implementing high-precision calculations stems from the DEX’s architecture supporting auto- mated market making strategies. With liquidity providers able to place positions across numerous ticks (potentially every tick), small rounding errors from standard 18-digit-precision could accumulate rapidly, leading to significant value leakage over time. The DEX now uses 27-digit precision (10ˆ27) for all internal calculations while maintaining API compatibility. Users continue to interact with the system using standard integer token amounts, but internally:

  1. All DEX operations are performed using PrecDec with 27-digit precision
  2. Fractional balances are tracked separately in the KVStore via the FractionalBanker
  3. Automatic conversion occurs when fractional amounts accumulate to whole tokens, which are then trans- ferred to users’ standard bank balances This approach prevents precision loss while ensuring that no user value is lost to rounding errors.

Informal Systems © 2025 < Table of Contents 4 Neutron Q3 2025 DEX Fractional Banking Audit

Threat Model Here we list the desired properties of the system. We used those properties to analyze what are all possible ways in which they could be violated.

Definitions For an individual interaction with the system in which the user is getting funds from the system, and for a single chosen coin, let

● single_I (single transaction Ideal calculation) be the amount the user should get [give] if coins could be given in an arbitrary precision. (The full expression is single_I(c, tx, u), for a coin c, transaction tx, and user u) ● single_R (single transaction Real calculation) be the amount the user really gets [gives] (full: single_R(c, tx,

u))

Similarly, let

● total_I be the total amount the user should get [give] in all transactions by now. (The full expression is total_- I(c, t, u, [tx]), for coin c , timepoint t , user u, and a sequence of transactions [tx].) ● total_R be the total amount the user gets [gives] in all transactions by now. (Full: total_R(c, t, u, [tx]) )

Note, total_I and total_R get updated in two ways:

● when the user gets some amount (using SendFractionalCoinsFromModuleToAccount): contributes a positive amount. ● when the user gives some amount (usingSendFractionalCoinsFromAccountToModule): contributes a negative

amount.

Properties

  1. [SINGLE-TX-DRIFT] For each transaction-1 < single_I - single_R < 1.
  2. [USER-AMOUNT-RESPECTED] If the user is ready to deposit x amount of a coin, the exchange will not ask to transfer >x.
  3. [TOTAL-DRIFT] For the total withdrawn amount, at any point in time, 0 <= total_I - total_R < 1 . In particular, this implies that the DEX is at no point in time at a loss with respect to any of its users.
  4. [REMAINDER-CONSISTENCY] For each coin and user, the fractional amount stored for them stores the amount that the DEX owes them. In particular, for x being the fractional amount stored, it holds:
  5. Once the next withdrawing transaction takes place, x = total_I - total_R
  6. After each withdrawing transaction, 0<=x<1
  7. At any point, x >= 0 Note 1: [REMAINDER-CONSISTENCY] property implies eventual 1-off fairness: namely, that users can always enforce getting their tokens back (with the tolerance of 1 micro token) Note 2: [REMAINDER-CONSISTENCY] looks very similar to [TOTAL-DRIFT]. However, it contains added restrictions (a withdraw needs to happen) because the analysis showed that [TOTAL-DRIFT] did not hold, so

Informal Systems © 2025 < Table of Contents 5 Neutron Q3 2025 DEX Fractional Banking Audit

[REMAINDER-CONSISTENCY] was sketched as a replacement property. 5. [EXCHANGE-POSITIVE] Let S be the sum total amount on the exchange (for a particular coin). Let F be the sum total stored under fractional remainders. Finally, let Claims be the sum total of all possible claims (Withdraw and WithdrawFilledLimitOrder). At all time it holds that S - F >= Claims. 6. [NO DUST POSITIONS] At all times, no open position exists with a balance strictly smaller than 1 uToken 7. [INTERNAL-PRECISION] All calculations are done in PrecDec, and the conversion only happens when inter- facing with external accounts. 8. [BEHAVIOR-PRESERVATION] If a user was able for a transaction tx get value x before introducing fractional accounting, not (after introducing it) the user gets >=x.

Informal Systems © 2025 < Table of Contents 6 Neutron Q3 2025 DEX Fractional Banking Audit

Threat Analysis [SINGLE-TX-DRIFT] For each transaction -1 < single_I - single_R < 1. OK, NOT OK The property does not hold in its entirety since single_I - single_R can fall unboundedly due to the fact that SendFractionalCoinsFromAccountToModule will not take into account any debt, and will return it only eventually, when the user will be withdrawing for the first time (thus accummulating the debt, which causes single_I to grow, whereas single_R remains small). Let us focus on only those transactions where the user is getting coins from the module (single_I, single_R >= 0). The only sending (except for shares coins, which is the issue already raised) happens in the function SendFrac- tionalCoinsFromModuleToAccount.

● Fractional and Whole amounts are properly separated, zeros are not included. ● Whole amounts are sent to the address account. ● Fractional parts are stored using SetFractionalBalance.

[USER-AMOUNT-RESPECTED] If the user is ready to deposit x amount of a coin, the exchange will not ask to transfer >x. OK Sending with rounding up ↗ will always result in a number smaller than the initial number of coins the user wanted to send. Sending with rounding up is done at three places: at PlaceLimitOrder, MultiHopSwap, and Deposit.

  1. Let’s start by analyzing PlaceLimitOrder. When sending ↗ coins, it needs to hold that totalInCoin.amount <= amountIn. This corresponds to the requirement towards the Swap ↗ function that inAmount <= maxAm- ountTakerIn.
  2. LimitOrderTranche.Swap: inAmount is explicitly capped ↗ by maxAmountTakerIn.
  3. Pool.Swap: amountTakerIn (corresponding to inAmount) is explicitly capped ↗ by maxAmountTakerIn.
  4. In MultiHopSwap, the module sends exactly ↗ the user’s number of coins initialInCoin.
  5. In ExecuteDeposit, we have to make sure that SUM(amounts0) < totalInAMount0 and SUM(amounts1) < to- talInAMount1. If there is SwapOnDeposit, for a deposit that is behind-enemy-lines, a token may be reduced a bit when calling PerformSwapOnDepositSwap, but cannot increase. If autoswap was disabled, again inAmount0 or inAmount1 may be reduced (through the call to GreatesMatchingRatio. If it were enabled, the value is unchanged. In the end, totalInAmount0 (and symmetrically for totalInAmount1) is calculated by summing up all those potentially only reduced values. [TOTAL-DRIFT] At any point in time, 0 <= total_I - total_R < 1. OK, NOT OK Does not hold fully, for the same reasons as [SINGLE-TX-DRIFT]. [REMAINDER-CONSISTENCY] OK At each send (from the module to an account, and from an account to the module), the fractional part of the PrecDec number is stored, and is sent to the user at the first next send from the module. [EXCHANGE-POSITIVE] OK The property holds. [NO DUST POSITIONS] OK

Informal Systems © 2025 < Table of Contents 7 Neutron Q3 2025 DEX Fractional Banking Audit

The property holds. [INTERNAL-PRECISION] All calculations are done in PrecDec, and the conversion only happens when interfacing with external accounts.

  1. Deposit: OK, NOT OK ● The value of issued shares is truncated ↗ into math.Int , reducing the amount that the user would be able to claim at a later point. Since SharesIssued/SharesToWithdraw are mentioned in the interface, it is understandable that they themselves remain of type Int. The user loss exists here and it scales with number of deposits. ● Other than that, the interface is correct: all internal calculations are in PrecDec, which only gets converted

to Int when transferring from user to the module ↗ 2. Withdraw: OK ● Shares (Int) are turned into PrecDec reserves in the RedeemValue function ↗. ● The same comment as for Deposit: shares amounts could also be augmented with fractional parts

  1. PlaceLimitOrder: OK, NOT OK ● Placing a limit order still takes an Int ↗. This is not forced, since what we want to place is amountLeft ↗, which is a PrecDec as a result of the internal calculation.
  2. WithdrawFilledLimitOrder: OK
  3. CancelLimitOrder: OK
  4. MultiHopSwap: OK [BEHAVIOR-PRESERVATION] If a user was able for a transaction tx get value x before introducing fractional accounting, not (after introducing it) the user gets >=x.
  5. Liquidity iteration stops prematurely or iterates unnecessarily OK The threat that was inspected hear was if very small maker amounts would remained uncleaned, potentially causing either stopping too early (before exhausting all the taker amount) or iterating over those negligible amounts. In some sense, there is a premature stopping, when the remaining taker denom cannot get us more than a single micro coin, though this is acknowledged and by design ↗. We verified that it does not hurt, but it is indeed unnecessary. Following are the reasons why the threat does not materialize:

● the outer loop of liquidity iteration breaks either when there are no more liquidity sources ↗ or when the limitPrice is crossed ↗. In either case, unrelated to maker reserves. ● the individual swap will clear the whole amount in the reserves ↗, as long as the taker amount suffices. When it

does not suffice, a small amount may remain, but the next swap will clear it.

[IMPLEMENTATION]

  1. Panic in safeAdd ↗ stemming from SendFractionalCoinsFromModuleToAccount or SendFractionalCoins- FromAccountToModule will not happen, since all coins are either a single-element array, or created through NewPrecDecCoins. Since they enter the storage sorted, they remain sorted after doing the balance.Add(tokens...) because of the sort at return ↗. OK
  2. We analyzed for unused code remaining in the codebase after changes. A couple of instances were found and reported in the Miscellaneous code improvements finding. OK
  3. Migration implementation OK, NOT OK We analyzed the migration implementation and found the following
  4. store was initialized correctly, in particular dec_ values.
  5. a migration was not registered, reported in the Migrate5to6 not registered finding.

Informal Systems © 2025 < Table of Contents 8 Neutron Q3 2025 DEX Fractional Banking Audit

  1. Validation OK The new coin (PrecDecCoin) contains the necessary validation

● PrecDecCoin validation ↗ does denom validation, nil and negative amount checks. ● PredDecCoins validation ↗ is also present. ● Same or similar validations are also done for sdk.Coin (code ref ↗).

  1. Users funds strictly separated OK, NOT OK

● Each fractional token balance is stored under a key ↗ that consists of user address and token denom. ● The value stored represents the actual fractional balanace for that specific user and token combination. ● When sending withdraw and other proto messages, the creator of the message must be the signer (code

ref ↗) and is subsequently used as the callerAddr from which the balance is deducted (code ref ↗). ● This design ensures there is no possibility that other users can initiate withdrawals, cancellations, or any

other unwilling actions on behalf of other users. ● The authentication mechanism prevents unauthorized access to other users’ balances by requiring the

message creator to be the signer.

  1. grpc queries are updated OK
  2. Tests are updated OK

● TODO for adding precdec fields left here ↗.

Informal Systems © 2025 < Table of Contents 9 Neutron Q3 2025 DEX Fractional Banking Audit

Findings Finding Type Severity Status

Migrate5to6 not registered Implementation Medium Resolved

Inefficient fractional balance loading Protocol Medium Resolved

PlaceMakerLimitOrder should take Implementation Low Acknowledged PrecDec

Not accounting for fractional debt Protocol Low Resolved when sending from accounts to the module

Pool shares are still truncated, Protocol Low Risk Accepted disregarding the fractional part

Miscellaneous code improvements Implementation Informational Resolved

Informal Systems © 2025 < Table of Contents 10 Neutron Q3 2025 DEX Fractional Banking Audit

Migrate5to6 not registered Severity Medium Impact 3 - High Exploitability 1 - Low Type Implementation Status Resolved

Description Migration from v5 to v6 is not registered ↗ in module.go. Migrate5to6 is never called.

Problem scenarios Upon migration, new store variables won’t be initialized.

Recommendation Register migration in module.go:RegisterServices.

Informal Systems © 2025 < Table of Contents 11 Neutron Q3 2025 DEX Fractional Banking Audit

Inefficient fractional balance loading Severity Medium Impact 1 - Low Exploitability 3 - High Type Protocol Status Resolved

Involved artifacts ● x/dex/keeper/fractional_banker.go ↗

Description The SendFractionalCoinsFromModuleToAccount function in the FractionalBanker loads all fractional balances for a user address on every transaction, regardless of which specific tokens are being processed. This creates an inefficient pattern where:

  1. GetFractionalBalance(ctx, address) retrieves all fractional coins for the user.
  2. New tokens are added to balance and whole new balance is processed through RoundDownToWholeToken- Amounts.
  3. The complete fractional balance is stored back, even when only a subset of tokens changed. The current implementation stores fractional balances as a single FractionalBalance struct containing a slice of all PrecDecCoin for a user, requiring full loading and saving on every operation.

Problem scenarios Contracts acting on behalf of multiple users (like Mars integrations) will trigger this inefficient loading pattern frequently for potentially large number of users. A malicious user may create a large number of token pairs permissionlessly, thereby either

  1. Harming other users of the same contract (who would have to pay for that gas), or
  2. Blocking the contract from performing any operations (by exceeding the total gas allowance for a transaction).

Recommendation Store fractional balances per token-user pair instead of per user:

  1. Use KVStore with keys user_address + token_denom and values as individual PrecDec amounts.
  2. Only load the specific token balance of user needed for each operation.
  3. Update only the relevant token balance instead of the entire user balance.

Informal Systems © 2025 < Table of Contents 12 Neutron Q3 2025 DEX Fractional Banking Audit

PlaceMakerLimitOrder should take PrecDec Severity Low Impact 1 - Low Exploitability 1 - Low Type Implementation Status Acknowledged

Description The function PlaceMakerLimitOrder still takes an Int ↗. This is not necessary, since what we want to place is amountLeft ↗, which is a PrecDec as a result of the internal calculation (which subsequently gets truncated to Int).

Problem scenarios Loss of precision.

Recommendation Change the interface of the PlaceMakerLimitOrder function.

Informal Systems © 2025 < Table of Contents 13 Neutron Q3 2025 DEX Fractional Banking Audit

Not accounting for fractional debt when sending from accounts to the mod- ule Severity Low Impact 1 - Low Exploitability 1 - Low Type Protocol Status Resolved

Description There is an asymmetry when between two functions of the FractionalBalance struct:

● SendFractionalCoinsFromModuleToAccount takes into account any existing fractional debt that needs to be sent to the user ● SendFractionalCoinsFromAccountToModule does not take into account existing debt.

This results in fractional balances potentially accumulating to values greater than 1.

Problem scenarios Loss of precision.

Recommendation Account for the debt in both functions.

Informal Systems © 2025 < Table of Contents 14 Neutron Q3 2025 DEX Fractional Banking Audit

Pool shares are still truncated, disregarding the fractional part Severity Low Impact 1 - Low Exploitability 1 - Low Type Protocol Status Risk Accepted

Description The value of shared issued after depositing is truncated ↗ into math.Int, reducing the amount that the user would be able to claim at a later point. Since SharesIssued/SharesToWithdraw are

Excerpt (19996 of 30827 characters). Read the whole page on informalsystems/audits ↗