SilicaPools decimal-manipulation bug drains WBTC flashloan collateral
Exploit Transactions
0x9b9a6dd05526a8a4b40e5e1a74a25df6ecccae6ee7bf045911ad89a1dd3f08140xac6c9ec1b77f3084ac5345813bfa48e4d9cdd67d9309305ce6dfaff69a7cbd110xb8a15efb31211b335e3e2b662cfef4ab0ae8cb5513ec67fa260c95485bad5114Victim Addresses
0xf3f84ce038442ae4c4dcb6a8ca8bacd7f28c9bdeEthereum0x4585fe77225b41b697c938b018e2ac67ac5a20c0Ethereum0x2260fac5e5542a773aa44fbcfedf7c193bc2c599EthereumLoss Breakdown
Similar Incidents
bZx/Fulcrum WBTC market manipulation drains ETH liquidity
38%WBTC Drain via Insecure Router transferFrom Path
36%Hegic WBTC Pool Repeated Tranche Withdrawal Exploit
35%Access-control bug draining 5 ETH from token contract
34%Pool16 lend/redeem accounting bug drains USDC without HOME backing
34%ENF Redeem Decimal Mis-Scaling
33%Root Cause Analysis
SilicaPools decimal-manipulation bug drains WBTC flashloan collateral
An unprivileged adversary exploited SilicaPools on Ethereum mainnet (chainid 1) in block 22146340 using a custom Silica index contract and a helper contract. Within a single block, the adversary:
- Obtained a WBTC flashloan.
- Manipulated the index’s
decimalsvalue during the Silica pool lifecycle. - Minted and later redeemed ERC‑1155 position tokens under mis‑scaled parameters.
- Swapped WBTC to WETH and then to ETH.
- Routed more than 50 ETH to a profit EOA while leaving additional ETH on a helper contract.
The core root cause is that SilicaPools trusts an external ISilicaIndex implementation whose decimals field can be changed by an adversary during the pool lifecycle. PoolMaths uses decimals together with shares and balance to compute collateral requirements and payouts, but SilicaPools does not enforce that decimals is immutable or consistent with the units of the index state. A malicious index changes decimals mid‑lifecycle, causing mis‑scaled accounting and enabling undercollateralized payouts to the adversary’s positions.
The adversary cluster achieves at least 2.0 ETH net profit (after gas) in the reference asset ETH, with a conservative upper bound of 0.1 ETH on total gas costs for the three attacker-crafted transactions.
Key Background
- SilicaPools (
0xf3F84cE038442aE4c4dCB6A8Ca8baCd7F28c9bDe) is an ERC‑1155‑based protocol. Each pool is parameterized by:- A payout token (here WBTC).
- A custom
ISilicaIndexaddress. - Floor and cap parameters.
- Target start and end timestamps. Long and short ERC‑1155 tokens represent claims on how the index’s balance changes over the pool’s lifetime.
- The
ISilicaIndexinterface exposesshares(),balance(), anddecimals(). SilicaPools assumes:decimalscorrectly describes the scale of the index’s quantities relative to the payout token.- The relationship between
shares,balance, anddecimalsremains consistent across the pool lifecycle.
PoolMathsuses the index’sshares(),balance(), anddecimals()along with pool parameters to compute required collateral and thebalanceChangePerSharevalue that determines payouts to long and short ERC‑1155 holders.- In this incident, the index at
0x9188738a7cA1E4B2af840a77e8726cC6Dcbe7Bdbis unverified and adversary‑controlled. The decompiled code shows:- Storage variables corresponding to
shares,decimals, andbalance. - A function that can set
decimalsfrom 31 to 1. - State diffs confirm that the index’s
decimalsvalue is changed inside the primary exploit transaction.
- Storage variables corresponding to
- The helper contract
0x80BF7Db69556D9521c03461978B8fC731DBBD4e4is an unverified dispatcher callable by EOA0xfde0d1575ed8e06fbf36256bcdfa1f359281455a. It orchestrates:- A WBTC flashloan from
0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb. - SilicaPools interactions.
- Swaps on the WBTC/WETH Uniswap V3 pool.
- WETH withdrawals and ETH profit payouts to EOA
0x229b8325bb9ac04602898b7e8989998710235d5f.
- A WBTC flashloan from
Key Contract Snippets
SilicaPools reliance on index.decimals() (verified source for SilicaPools at 0xf3F8…9bDe)
The pool end logic records the ending index state and uses index.decimals() when computing balanceChangePerShare in endPool:
// Collected SilicaPools source, endPool() snippet
function endPool(PoolParams calldata poolParams) public {
bytes32 poolHash = hashPool(poolParams);
PoolState storage sState = sPoolState[poolHash];
ISilicaIndex index = ISilicaIndex(poolParams.index);
// ...
uint256 indexBalanceAtEnd = index.balance();
sState.balanceChangePerShare = uint128(
PoolMaths.balanceChangePerShare(
indexBalanceAtEnd,
sState.indexInitialBalance,
sState.indexShares,
index.decimals(),
poolParams.floor,
poolParams.cap
)
);
// ...
}
Caption: Verified SilicaPools code shows that endPool recomputes balanceChangePerShare using a live call to index.decimals(), rather than pinning the value used at pool start or collateralization.
Custom index with mutable decimals (decompiled index at 0x9188…7Bdb)
// Decompiled index contract (heimdall-rs), key fields
contract DecompiledContract {
address public owner;
uint256 public shares;
uint256 store_d;
uint256 public decimals;
uint256 public balance;
/// @custom:selector 0xa6f9dae1
function changeOwner(address arg0) public payable {
require(arg0 == (address(arg0)));
owner = (address(arg0) * 0x01) | (uint96(owner));
}
/// @custom:selector 0x2ee79ded
function change() public payable {
decimals = 0x01;
}
}
Caption: The adversary-controlled index exposes a public decimals storage variable and a change() function that sets decimals to 1, allowing the adversary to modify the units used by SilicaPools at any point in the pool lifecycle.
Index storage diff showing decimals change during exploit
// State diff for index 0x9188…7Bdb during primary yoink() tx 0x9b9a6d…f0814
{
"storage_diff": {
"0x0000000000000000000000000000000000000000000000000000000000000002": {
"from": "0x000000000000000000000000000000000000000000000000000000000000001f",
"to": "0x0000000000000000000000000000000000000000000000000000000000000001"
}
}
}
Caption: Collected state diff shows storage slot 2 (the decimals field) changing from 31 to 1 during the primary exploit transaction, confirming on‑chain manipulation of index decimals.
Vulnerability & Root Cause Analysis
Vulnerability Summary
SilicaPools trusts an external ISilicaIndex for shares, balance, and decimals at multiple lifecycle steps but does not constrain how the index can change decimals or validate that these values remain consistent with the payout token units. A malicious index can change decimals mid‑lifecycle in a way that breaks PoolMaths’ assumptions and causes undercollateralized or mis‑priced payouts.
Detailed Root Cause
- Lifecycle dependence on external index:
- At collateralization (e.g., via
collateralizedMint), WBTC collateral is transferred into SilicaPools based onPoolMaths.collateral(...), which depends on the index’ssharesanddecimals. - When a pool is started (
startPool), SilicaPools records snapshot values:index.shares().index.balance()asindexInitialBalance.- These snapshots implicitly depend on the then‑current
decimals(viaPoolMathsbehavior during collateralization).
- When a pool is ended (
endPool), SilicaPools again calls the index:- Reads
index.balance()at end. - Reads
index.decimals()live. - Computes
balanceChangePerShareviaPoolMaths.balanceChangePerShare(...), which uses the currentdecimals.
- Reads
- At collateralization (e.g., via
- Assumed invariants:
- SilicaPools implicitly assumes:
decimalsis a stable configuration parameter describing how index units map to the payout token.- The relationship between
shares,balance, anddecimalsstays consistent from collateralization through pool end.
- These assumptions are not enforced in code:
decimalsis not pinned at pool start.- The contract does not check that
decimalsremains unchanged.
- SilicaPools implicitly assumes:
- Adversary-controlled index behavior:
- The custom index at
0x9188…7Bdbis unverified and controlled by the adversary. - The decompiled contract exposes:
- A public
decimalsvariable with an auto‑generated getter. - A
change()function that setsdecimals = 1.
- A public
- The index’s
sharesandbalancecan be set to arbitrary values consistent with the adversary’s strategy.
- The custom index at
- On-chain evidence of decimals manipulation:
- The state diff for
0x9188…7Bdbduring the primary yoink() transaction0x9b9a6d…f0814shows:- Slot 2 (the
decimalsfield) changed from0x1f(31) to0x01(1).
- Slot 2 (the
- The SilicaPools call trace during the same tx includes:
- A
STATICCALLtoindex.decimals()before or around pool end computation.
- A
- The state diff for
- Effect on pool math and payouts:
- At collateralization/start, collateral requirements and initial index state are effectively tied to
decimals = 31. - At pool end,
PoolMaths.balanceChangePerShareis computed usingdecimals = 1. - This mismatch causes
balanceChangePerShare(and therefore long/short payouts) to be computed on a different scale from the one implied by the locked collateral, allowing the adversary’s positions to receive a WBTC‑denominated payout that is too large relative to the WBTC originally deposited. - The undercollateralization occurs at the protocol level: SilicaPools pays out more WBTC than is justified by the index evolution and collateral.
- At collateralization/start, collateral requirements and initial index state are effectively tied to
Vulnerable Components
- SilicaPools contract (
0xf3F8…9bDe):- Uses
ISilicaIndexforshares,balance, anddecimalswithout enforcing immutability or consistency ofdecimals. - Calls
index.decimals()at pool end forbalanceChangePerShareinstead of using a pinned value. - Relies on
PoolMathsto compute both collateral and payouts using the same logical units, which are broken by the mutable index.
- Uses
- Custom index contract (
0x9188…7Bdb):- Adversary‑controlled implementation of
ISilicaIndex. - Exposes a mutable
decimalsfield viachange(). - On‑chain storage diffs show
decimalschanging mid‑transaction.
- Adversary‑controlled implementation of
- External ecosystem components (used but not themselves flawed in this context):
- WBTC token (
0x2260…c599). - WETH9 token (
0xC02a…6Cc2). - Flashloan provider (
0xBBBB…FFCb). - WBTC/WETH Uniswap V3 pool (router and pool contracts interacting with the helper contract).
- WBTC token (
ACT Opportunity & Transaction Sequence
Pre‑state σ_B
- Block height B:
22146340. - Pre‑state definition: Ethereum mainnet state immediately before block 22146340, prior to:
- Funding transaction
0xac6c9ec1b77f3084ac5345813bfa48e4d9cdd67d9309305ce6dfaff69a7cbd11. - Attacker‑crafted yoink() transactions:
0x9b9a6dd05526a8a4b40e5e1a74a25df6ecccae6ee7bf045911ad89a1dd3f0814.0xb8a15efb31211b335e3e2b662cfef4ab0ae8cb5513ec67fa260c95485bad5114.
- Funding transaction
Evidence for σ_B:
- Seed transaction metadata and traces.
- Data collection summary and state diffs for:
- SilicaPools (
0xf3F8…9bDe). - Custom index (
0x9188…7Bdb). - The three relevant transactions.
- SilicaPools (
Transaction Sequence b
-
Tx 1 (funding, attacker-crafted)
- Hash:
0xac6c9ec1b77f3084ac5345813bfa48e4d9cdd67d9309305ce6dfaff69a7cbd11 - From: EOA
0x229b8325bb9ac04602898b7e8989998710235d5f. - To: Helper contract
0x80BF7Db69556D9521c03461978B8fC731DBBD4e4. - Type: Standard ETH transfer.
- Role: Funds the helper contract with ETH for gas and protocol interactions.
// Funding tx trace summary (debug_traceTransaction callTracer) { "result": { "from": "0x229b8325bb9ac04602898b7e8989998710235d5f", "to": "0x80bf7db69556d9521c03461978b8fc731dbbd4e4", "value": "0x29dfee0c2a906076d" } }Caption: Trace for tx 0xac6c9e…bd11 shows a direct ETH transfer from the profit EOA to the helper contract with value 0x29dfee0c2a906076d ≈ 48.2782721821223 ETH.
- Hash:
-
Tx 2 (primary yoink, attacker-crafted)
- Hash:
0x9b9a6dd05526a8a4b40e5e1a74a25df6ecccae6ee7bf045911ad89a1dd3f0814 - From: EOA
0xfde0d1575ed8e06fbf36256bcdfa1f359281455a. - To: Helper contract
0x80BF7Db69556D9521c03461978B8fC731DBBD4e4. - Mechanism: Flashloan + index/pool manipulation + swaps + ETH payout.
- Role: Main exploit transaction that realizes undercollateralized payouts.
Key on‑chain behaviors:
- Approvals for WBTC:
- Helper approves SilicaPools and flashloan provider to move WBTC.
- WBTC flashloan:
- Flashloan provider at
0xBBBB…FFCbsends1e9units of WBTC to the helper.
- Flashloan provider at
- SilicaPools interactions:
- Helper moves WBTC into SilicaPools to collateralize a pool using the malicious index.
- SilicaPools calls
index.shares(),index.balance(), andindex.decimals(), and laterendPoolcomputesbalanceChangePerShare.
- Index manipulation:
- Index
0x9188…7Bdbchanges itsdecimalsfrom 31 to 1 viachange(), as seen in the storage diff.
- Index
- Payout and swaps:
- SilicaPools pays an undercollateralized WBTC amount back to the helper.
- Helper swaps WBTC→WETH on Uniswap V3 and unwraps WETH to ETH via WETH9.
- Helper forwards most ETH to the profit EOA and retains a small remainder.
// Excerpt from call trace of primary yoink() tx 0x9b9a6d…f0814 { "calls": [ { "from": "0x80bf7db69556d9521c03461978b8fc731dbbd4e4", "to": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", "input": "0x095ea7b3…", // WBTC approve SilicaPools "type": "CALL" }, { "from": "0xbbbbbbbbbb9cc5e90e3b3af64bdaf62c37eeffcb", "to": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", "input": "0xa9059cbb…", // Flashloan provider transfers WBTC to helper "type": "CALL" }, { "from": "0xf3f84ce038442ae4c4dcb6a8ca8bacd7f28c9bde", "to": "0x9188738a7ca1e4b2af840a77e8726cc6dcbe7bdb", "input": "0x313ce567", // index.decimals() "output": "0x…1f", "type": "STATICCALL" } ] }Caption: Primary yoink() call trace shows WBTC approvals and transfers, a flashloan from 0xBBBB…FFCb, and a
STATICCALLto the malicious index’sdecimals()function as part of the SilicaPools pool computation. - Hash:
-
Tx 3 (second yoink, attacker-crafted, same block)
- Hash:
0xb8a15efb31211b335e3e2b662cfef4ab0ae8cb5513ec67fa260c95485bad5114 - From: Same EOA
0xfde0d1575ed8e06fbf36256bcdfa1f359281455a. - To: Helper contract
0x80BF7Db69556D9521c03461978B8fC731DBBD4e4. - Mechanism: Repeats a similar flashloan + SilicaPools + swap pattern.
- Role: Provides additional profit but is not necessary to prove a strictly positive ACT profit delta.
// Excerpt from call trace of second yoink() tx 0xb8a15e…d5114 { "calls": [ { "from": "0x4585fe77225b41b697c938b018e2ac67ac5a20c0", "to": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", "input": "0xa9059cbb…", // WBTC transfer to helper "type": "CALL" }, { "from": "0x80bf7db69556d9521c03461978b8fc731dbbd4e4", "to": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "input": "0xd0e30db0", // WETH9.deposit (wrap ETH to WETH) "type": "CALL" } ] }Caption: Second yoink() trace shows additional WBTC movements and WETH interactions via the helper contract, mirroring the primary exploit pattern and generating extra ETH profit.
- Hash:
Adversary Flow Analysis
Adversary-Related Accounts
- Profit EOA:
0x229b8325bb9ac04602898b7e8989998710235d5f- Provides initial ETH funding to the helper contract.
- Receives the majority of ETH profits from the helper.
- Helper contract:
0x80BF7Db69556D9521c03461978B8fC731DBBD4e4- Orchestrates flashloans, SilicaPools interactions, swaps, and ETH payouts.
- Retains a small amount of ETH as residual profit.
- Transaction‑sender EOA:
0xfde0d1575ed8e06fbf36256bcdfa1f359281455a- Originates the yoink() transactions calling the helper contract.
- Pays gas for the attacker-crafted transactions.
- Malicious index:
0x9188738a7cA1E4B2af840a77e8726cC6Dcbe7Bdb- Adversary‑controlled implementation of
ISilicaIndex. - Provides mutable
decimals, enabling the mis‑scaling exploit.
- Adversary‑controlled implementation of
These entities form the adversary cluster used in the ACT profit calculation.
Lifecycle Stages
-
Adversary initial funding
- Tx:
0xac6c9e…bd11. - The profit EOA
0x229b…d5f5transfers exactly48.278272182122317677ETH to the helper contract0x80BF…d4e4. - This funding covers gas and any protocol fees needed for the exploit.
- Tx:
-
Adversary tx execution (primary yoink)
- Tx:
0x9b9a6d…f0814. - Sender EOA
0xfde0…455ainvokes the helper contract. - The helper:
- Obtains a
1e9‑unit WBTC flashloan from0xBBBB…FFCb. - Approves and transfers WBTC into SilicaPools to collateralize a pool parameterized with the malicious index.
- Triggers pool lifecycle actions (start and end) that cause SilicaPools to:
- Read index state and
decimals. - Compute a mis‑scaled
balanceChangePerSharedue to thedecimalschange from 31 to 1.
- Read index state and
- Receives an undercollateralized WBTC payout from SilicaPools.
- Swaps WBTC→WETH on Uniswap V3 and unwraps WETH to ETH via WETH9.
- Pays back the flashloan principal plus fee.
- Forwards:
50.834053044409908846ETH to profit EOA0x229b…d5f5.- Keeps
0.050884937982393301ETH on the helper contract.
- Obtains a
- Tx:
-
Adversary repeated execution (second yoink)
- Tx:
0xb8a15e…d5114. - Sender EOA
0xfde0…455aagain calls the helper contract. - The transaction reuses the same helper and malicious index setup to run another decimals‑based pool manipulation strategy, increasing total profit.
- This second transaction is additive to the adversary’s gain but not required to satisfy the ACT profit predicate.
- Tx:
Profit Predicate & Quantitative Analysis
ACT Profit Predicate
- Type: Profit.
- Reference asset: ETH.
- Adversary address (cluster representative):
0x229b8325bb9ac04602898b7e8989998710235d5f.
The analysis considers the adversary cluster:
- EOA
0x229b…d5f5(profit receiver and funder). - Helper contract
0x80BF…d4e4. - Sender EOA
0xfde0…455a(tx origin for yoink()).
Balance Deltas from Primary Yoink
Using balance_diff.json and balance_diff_extended.json for the primary yoink() transaction 0x9b9a6d…f0814:
// Seed balance diff for 0x9b9a6d…f0814 (native ETH)
{
"native_balance_deltas": [
{
"address": "0x80bf7db69556d9521c03461978b8fc731dbbd4e4",
"delta_wei": "50884937982393301"
},
{
"address": "0xfde0d1575ed8e06fbf36256bcdfa1f359281455a",
"delta_wei": "-476756366117580"
},
{
"address": "0x229b8325bb9ac04602898b7e8989998710235d5f",
"delta_wei": "50834053044409908846"
}
]
}
Caption: Native balance diffs for the primary yoink() tx show ETH gains for the helper contract and profit EOA, and a small gas payment loss for the sender EOA.
ETH‑denominated changes in the primary yoink() tx:
- Profit EOA
0x229b…d5f5:+50.834053044409908846ETH. - Helper contract
0x80BF…d4e4:+0.050884937982393301ETH. - Sender EOA
0xfde0…455a:-0.00047675636611758ETH (gas).
Funding Transaction Delta
In the funding transaction 0xac6c9e…bd11:
- Profit EOA
0x229b…d5f5sends exactly48.278272182122317677ETH to the helper contract0x80BF…d4e4.
Net Profit Calculation (Cluster)
Aggregate over the adversary cluster for the funding and primary yoink() transactions:
- Cluster gross ETH gain:
50.834053044409908846(profit EOA gain)+ 0.050884937982393301(helper gain)− 48.278272182122317677(funding outflow)− 0.00047675636611758(yoink() gas)- ≈
2.606188ETH gross profit.
Gas costs for the other attacker‑crafted transactions (funding and second yoink()) are also included in the conservative bound:
- All three attacker‑crafted transactions:
- Gas limits below
1,000,000. - Gas prices below
30 gwei.
- Gas limits below
- Therefore, total gas spent is strictly
< 0.1ETH.
Subtracting the conservative upper bound of 0.1 ETH gas from the ≈ 2.606188 ETH gross gain yields:
- Net cluster profit >
2.5ETH.
The analysis conservatively asserts:
- Value delta in reference asset:
>= 2.0ETH for the adversary cluster after all fees.
This satisfies the ACT profit predicate: the adversary can deterministically achieve strictly positive ETH‑denominated profit from state σ_B by executing the transaction sequence b.
Impact & Losses
Quantified Minimum Profit
- Reference asset: ETH.
- Adversary cluster profit: At least
2.0ETH net of gas, based on:- Exact ETH balance deltas across the funding and primary yoink() transactions.
- A conservative
< 0.1ETH upper bound on total gas fees for all three attacker‑crafted transactions.
Protocol-Level Impact
- SilicaPools suffers undercollateralized payouts denominated in WBTC that are converted to ETH by the adversary.
- The protocol’s accounting invariants between collateral, index evolution, and payouts are broken for pools that integrate the malicious index.
- Additional impacts:
- The exact WBTC‑denominated protocol loss and any secondary DeFi side‑effects (such as WBTC/WETH Uniswap pool imbalance) are not fully quantified in this analysis.
- However, the adversary’s ETH‑denominated profit is proven to be at least
2.0ETH net of gas, establishing a clear economic loss to SilicaPools and its users.
References
Key artifacts underlying this analysis:
- Seed transaction metadata and trace
- Seed transaction
0x9b9a6d…f0814metadata andcallTracertrace.
- Seed transaction
- SilicaPools contract source
- Verified source for
SilicaPools.solat0xf3F84cE038442aE4c4dCB6A8Ca8baCd7F28c9bDe.
- Verified source for
- Custom index decompiled source
- Heimdall‑decompiled contract for
0x9188738a7cA1E4B2af840a77e8726cC6Dcbe7Bdb, showing mutabledecimalsbehavior.
- Heimdall‑decompiled contract for
- Primary yoink() call trace
callTraceroutput for0x9b9a6d…f0814, covering flashloan, SilicaPools, index, WBTC/WETH pool, and WETH9 interactions.
- Funding and second yoink() call traces
callTraceroutputs for:- Funding tx
0xac6c9e…bd11. - Second yoink() tx
0xb8a15e…d5114.
- Funding tx
- State and balance diffs
- Index and SilicaPools storage diffs during the primary yoink() tx.
- Native balance diffs for the three adversary-crafted transactions used to compute the ETH profit lower bound.