Notional Finance hacked for $1.73M due to integer overflow vulnerability

iconMetaEra
Share
AI summary iconSummary
On September 4, 2026, a $1.73 million exploit targeted Notional Finance, as attackers leveraged an integer overflow in the Escrow contract. A vulnerability in the ExchangeRate._convertToETH function enabled the creation of fCash positions without adequate collateral. The incident underscores the critical importance of maintaining a strong risk-to-reward ratio in DeFi strategies. Traders using technical analysis for crypto should remain vigilant, as such vulnerabilities can rapidly alter market dynamics.

Author: Jiujiu

Edit: 77

Context

On September 4, 2026, the well-known decentralized lending platform Notional Finance was attacked, resulting in a loss of approximately $1.73 million. Below is the detailed analysis by the SlowMist Security Team regarding this incident:

Prerequisite Knowledge

In Notional Finance V1, fCash can be understood as a cash claim with a maturity date: the cash receiver receives payment at maturity, while the cash payer makes payment at maturity. The protocol records both positions in the Portfolio and assesses an account’s health for opening positions through free collateral checks.

The safeTransferFrom function in ERC1155Trade handles asset accounting here. When the transferred asset type belongs to the cash receiver, the contract calls Portfolios.mintfCashPair, recording a liability for the payer and an equal claim for the receiver. This call appears to be an ERC1155 transfer, but it results in the minting of an fCash pair.

After a new position is written to the Portfolio, free collateral must be calculated and verified. The RiskFramework contract first aggregates the payer's fCash liabilities into a signed int256, then converts all currency balances into ETH for summation. Negative values represent cash owed, while positive values represent cash or claims held in the account. This calculation determines whether the transaction is permitted.

After the position expires, Portfolio calls the Escrow's portfolioSettleCash function to convert the expired fCash into cash balance. The account then withdraws the corresponding DAI or USDC assets from Escrow using the withdrawal function.

Root cause

The core vulnerability in this attack lies in the ExchangeRate._convertToETH function used by the Escrow implementation contract, where a line of code directly converts the absolute value of a signed integer to a uint128.

The attacker first created two liability positions with the same payer, with amounts of 1 and uint128.max. The sum of these two liabilities equals 2^128. Since the payer's payment is recorded as a negative value in risk calculations, the key parameter passed to Escrow's convertBalancesToETH becomes -2^128.

However, the Solidity version used in this contract is 0.6.x, so the absolute value 2^128, when forcibly cast to uint128, overflows to 0, causing this liability to be excluded from the ETH-denominated result.

Finally, in the mintfCashPair function, the check only requires the payer's free collateral to be greater than or equal to zero; thus, after an underflow results in zero, this check passes, allowing an account with insufficient assets to cover its liabilities to still open a position.

Attack Step Analysis

1. In the preceding transaction (0xe1589a19…d60a), the attacker first created multiple auxiliary contracts and called the setApprovalForAll function of the ERC1155Trade contract to grant approval to these auxiliary contracts; additionally, the attacker pre-queried the expiration dates of two CashMarket contracts and calculated the required values for the three AssetId parameters needed in subsequent operations.

2. Immediately afterward, the attacker calls the safeTransferFrom function of the ERC1155Trade contract to mint one unit of an fCash pair, with a cashGroupId of 2 and an expiration timestamp of 1788480000 (September 4, 2026, 8:00 AM). This triggers the internal _upsertAsset function to update the liabilities and expected income for both the from address (the attacker contract) and the to address (Receiver Contract 1).

After the pair is minted, Portfolios immediately checks the attacker contract's free collateral; however, due to the small initial minting amount, when converted to ETH under the current exchange rate and precision, it rounds down to zero, allowing the first check to pass.

3. The attacker then called the safeTransferFrom function of the ERC1155Trade contract again to mint an fCash pair with an amount of uint128.max, where the corresponding bond asset's cashGroupId is 2, but the expiration timestamp is 1796256000 (December 3, 2026, at 8:00 AM).

There is a detail here: the attacker minted bond assets with different maturity dates to two separate addresses. This is because, when updating the liability for the from address, if the bond assets were identical, the system would directly accumulate them, causing a revert during addition due to overflow (the contract uses the SafeMath library).

4. After updating liabilities and expected income for the from address (attack contract) and to address (receiving contract 2) during the second minting, the mintfCashPair function calls the freeCollateral function to calculate the net collateral position of the from address and verifies that it is greater than or equal to zero to ensure it is healthy.

First, the getRequirement function in the RiskFramework contract will sum the total liabilities from the two minting operations for the from address:

The final result is -2^128, after which the Escrow contract’s convertBalancesToETH function is called to convert this value into ETH, and convertBalancesToETH in turn calls the _convertToETH function from the ExchangeRate library for computation:

By tracing into the _convertToETH function, it can be observed that it first takes the absolute value of balance and then converts the 256-bit value to a 128-bit value using uint128. The total liability from the above "from" address is -2^128; when its absolute value, 2^128, is forcibly converted via uint128(), it overflows due to exceeding the upper limit, resulting in 0. This causes the protocol to incorrectly assume that the "from" address's free collateral position is 0 and therefore healthy, allowing it to pass the final check in the mintfCashPair function.

5. Subsequently, Contract 2 received the position and split it further between two auxiliary contracts. These two safeTransferFrom calls still enter the mintfCashPair function. However, since the payer at this point is Contract 2—the receiver from the second minting—it holds the large expected income position acquired in the previous step. The risk calculation treats this as a positive claim, allowing it to pass the collateral check; as a result, the two auxiliary contracts obtain receiver fCash that can be converted to cash upon maturity.

6. The attacker then initiated a second legitimate profit-taking transaction, settling the claims associated with the two open positions from the previous transaction, increasing the cash balances of the corresponding assets, and subsequently calling the Escrow contract’s withdraw function to withdraw and realize the profits.

Summary

The key to this attack was that the attacker first used two separate positions to sum the payer’s liabilities to 2^128, then had the Escrow convert this liability amount into ETH. By exploiting an overflow vulnerability in the type conversion, the result was truncated to zero, bypassing the risk checks.

The SlowMist Security Team recommends that project teams perform range checks before type conversion, directly rejecting any results exceeding type(uint128).max. Additionally, boundary tests should be implemented for all operations involving signed amounts, precision scaling, and asset denominations, with special emphasis on testing edge cases such as uint128.max, uint128.max + 1, and the absolute values of negative numbers. Consider referencing or using OpenZeppelin’s SafeCast library for safe type conversions.

Disclaimer: The information on this page may have been obtained from third parties and does not necessarily reflect the views or opinions of KuCoin. This content is provided for general informational purposes only, without any representation or warranty of any kind, nor shall it be construed as financial or investment advice. KuCoin shall not be liable for any errors or omissions, or for any outcomes resulting from the use of this information. Investments in digital assets can be risky. Please carefully evaluate the risks of a product and your risk tolerance based on your own financial circumstances. For more information, please refer to our Terms of Use and Risk Disclosure.