Reproduced Exploit

DefiApp — sale tokens are not scaled to 18 decimals

_computeTokens divides a 6-decimal USDC amount by an 18-decimal price and stops there. The comment says to multiply by 1e12 so the result matches an 18-decimal sale token. That multiply is missing. At price 0.01e18, 1000 USDC buys a fair 1e23 raw HOME (100,000 tokens). The function returns 1e11, wh…

Jan 2025•Other•arithmetic•3 min read

Chain

Other

Category

arithmetic

Date

Jan 2025

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 49882-h-01-sale-token-amount-is-not-adjusted-per-its-decimals-pash. Standalone Foundry PoC and full write-up: 49882-h-01-sale-token-amount-is-not-adjusted-per-its-decimals-pash_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/arithmetic/decimal-mismatch · vuln/logic/price-calculation

Reproduction: self-contained Foundry PoC, no fork and no cheatcodes in the exploit. Trace: output.txt.

Key info#

ImpactHIGH — a buyer who pays 1000 USDC (1000000000 raw) receives 1e11 HOME instead of the fair 1e23
ProtocolDefiApp PublicSale
Vulnerable codePublicSale._computeTokens
Bug classMissing decimal scale on the sale-token output
FindingPashov Audit Group — DefiApp, 2025-01-08 · H-01 · #49882
ReportDefiApp-security-review_2025-01-08.md
SourceAuditVault
Audited codedefi-app/defi-app-contracts@b64eb41a74dc02655eb3e10ef2b6fe34b4ff51c6 src/token/PublicSale.sol
Compilersolc 0.8.27, cancun, via_ir

TL;DR#

_computeTokens divides a 6-decimal USDC amount by an 18-decimal price and stops there. The comment says to multiply by 1e12 so the result matches an 18-decimal sale token. That multiply is missing. At price 0.01e18, 1000 USDC buys a fair 1e23 raw HOME (100,000 tokens). The function returns 1e11, which is 1e12 times too small. The sale then transfers that dust amount into the buyer's vesting balance.

The vulnerable code#

SOLIDITY
function _computeTokens(uint256 _amountUSDC, uint256 _price) private pure returns (uint256) {
    // _price = price * 10^18 --> precision scaling
    // _amount = (input_amount * 10^6 (USDC/T)) * 10^18 (_price)
    // (_amount * 1e18) / _price = (10^6 * 10^18) / 10^18 = 10^6 precision
    // 10^6 * 10^12 = 10^18 --> scale for future token's decimals
    // @> VULN: the 1e12 scale named in the comment is never applied
    return ((_amountUSDC * 1e18) / _price);
    // FIX: return ((_amountUSDC * 1e18) / _price) * 1e12;
}

Root cause#

USDC has 6 decimals and the price is scaled by 1e18. (amount * 1e18) / price lands on 6-decimal precision. An 18-decimal token needs another 10^(18-6) = 1e12. The function returns the unscaled value, and depositUSDC transfers that value out of the sale inventory.

Preconditions#

The sale token has 18 decimals. The price is the report's example, 0.01e18. The buyer deposits 1000 USDC (1000 * 10^6). No other role is required.

Attack walkthrough#

  1. Buyer pays 1000000000 raw USDC (1000 USDC).
  2. _computeTokens(1000e6, 0.01e18) returns (1000e6 * 1e18) / 1e16 = 1e11.
  3. The sale transfers 100000000000 raw HOME to the buyer.
  4. The fair 18-decimal amount is 1e11 * 1e12 = 1e23 (100000 ether). The buyer is short by a factor of 1e12.

Control: multiplying the same expression by 1e12 delivers 1e23 HOME for the same 1000 USDC.

Diagrams#

flowchart TD A["Buyer deposits 1000 USDC, raw 1000000000"] --> B["Price is 0.01e18"] B --> C["VULN: _computeTokens returns amount times 1e18 over price"] C --> D["Result is 1e11 instead of 1e11 times 1e12"] D --> E["Buyer receives 1e11 HOME"] E --> F["Fair payout for an 18-decimal token is 1e23 HOME"]

Impact#

The buyer pays 1000 USDC and is credited 1e11 sale tokens instead of 1e23. That is 100,000 tokens of dust versus 100,000 whole tokens. Every purchaser of an 18-decimal sale token is underpaid by 1e12.

Remediation#

Scale the result by 10^(saleToken.decimals - 6). For the 18-decimal HOME token that factor is 1e12:

SOLIDITY
return ((_amountUSDC * 1e18) / _price) * 1e12;

Sources#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.