TrustPad receiveUpPool Inflation
Exploit Transactions
0x2490368b43951caa8bf6f730bf0aaa0bcc2657d6f64fdcc3b0372b6500d0dcfc0x191a34e6c0780c3d1ab5c9bc04948e231d742b7d88e0e4f85568d57fcdc031820xea5bb62b8a151917a732d4114d716c7e6c087af8b3c0b3416c9dbc37c59f04daVictim Addresses
0xE613c058701C768E0d04D1bf8e6a6dc1a0C6d48ABSC0xADCFC6bf853a0a8ad7f9Ff4244140D10cf01363CBSCLoss Breakdown
Similar Incidents
BankrollNetworkStack self-buy dividend inflation exploit
32%Belt beltBNB Share Inflation
31%Ocean Pool NFT Reward Drain
31%Helio Plugin Donation Inflation
30%Bitpaid Mature-Lock Top-Up Exploit
30%QiQi Reward Quote Override Drain
30%Root Cause Analysis
TrustPad receiveUpPool Inflation
1. Incident Overview TL;DR
TrustPad's staking pool at 0xE613c058701C768E0d04D1bf8e6a6dc1a0C6d48A was drained through a permissionless reward-inflation attack on BSC. Across transactions 0x2490368b43951caa8bf6f730bf0aaa0bcc2657d6f64fdcc3b0372b6500d0dcfc, 0x191a34e6c0780c3d1ab5c9bc04948e231d742b7d88e0e4f85568d57fcdc03182, and 0xea5bb62b8a151917a732d4114d716c7e6c087af8b3c0b3416c9dbc37c59f04da, the attacker EOA 0x1a7b15354e2f6564fcf6960c79542de251ce0dc9 funded and drove helper contract 0x1694d7fAbf3B28f11D65DEEb9f60810DAa26909a to mint fake fixed-APR rewards, stake them, and cash out 110691360001 raw TPAD units.
The root cause is a public migration function, receiveUpPool(address,uint256), trusting caller-controlled lock state from msg.sender and updating reward accounting before principal is credited. That lets any unprivileged helper contract replay a full-lock APR reward on unchanged TPAD principal by spoofing isLocked() and depositLockStart().
2. Key Background
The victim pool is TrustPad's LaunchpadLockableStaking proxy at 0xE613c058701C768E0d04D1bf8e6a6dc1a0C6d48A, running verified implementation 0x129f4ac88b0446f9b46b176c93531e6cf4687657. At block 33260104, historical reads show the pool was configured with TPAD as both staking and reward token, fixedApr = 300, lockPeriod = 94608000, waitForRewardMaturity = true, and a large funded TPAD balance.
The reward model is fixed-APR and time-based. getFixedAprPendingReward(account) computes rewards from two pieces of state: depositLockStart[account] and user.amount, with elapsed time capped at the full lock period. Under honest operation that means a staker only earns the full reward after the configured lock duration has really elapsed.
receiveUpPool(address,uint256) is intended as a migration path from another staking pool. The dangerous assumption is that msg.sender is a trusted sibling pool implementing the same interface. In reality the function is public, so any contract can call it and answer the victim pool's isLocked(account) and depositLockStart(account) queries with arbitrary values.
3. Vulnerability Analysis & Root Cause Summary
This is an access-control and accounting-ordering bug in a staking migration path. The protocol exposes receiveUpPool() publicly, but the function treats msg.sender as a trusted staking contract and asks it for lock metadata. If the target account is not already locked in the victim pool, the victim reads LaunchpadLockableStaking(msg.sender).isLocked(account) and LaunchpadLockableStaking(msg.sender).depositLockStart(account) without authenticating the caller. It then calls updateDepositLockStart(account,newLockStartTime) before increasing user.amount by the transferred principal. Inside updateDepositLockStart(), the pool snapshots pending rewards, overwrites the lock start, and recomputes reward debt using the new timestamp. Because getFixedAprPendingReward() caps elapsed time at the full lock period, spoofing a very old lock start instantly realizes the full fixed-APR reward on the existing principal. Repeating receiveUpPool() and withdraw() on the same tokens compounds pendingRewards while leaving the principal available for reuse.
The invariant that fails is straightforward: fixed-APR rewards should only accrue from trusted internal lock state over real elapsed time, and a migration should never let an untrusted caller re-realize a completed lock-period reward on unchanged principal.
The critical code path is visible in the verified implementation:
function receiveUpPool(address account, uint256 amount) external {
uint256 newLockStartTime;
if (isLocked(account)) {
newLockStartTime = depositLockStart[account];
} else {
newLockStartTime = LaunchpadLockableStaking(msg.sender).isLocked(account)
? LaunchpadLockableStaking(msg.sender).depositLockStart(account)
: block.timestamp;
}
updateDepositLockStart(account, newLockStartTime);
...
user.amount += amount;
}
function updateDepositLockStart(address account, uint256 lockStart) internal {
updateUserPending(account);
depositLockStart[account] = lockStart;
updateUserDebt(account);
}
function getFixedAprPendingReward(address account) public view returns (uint256) {
uint256 passedTime = block.timestamp >= depositLockStart[account] + lockPeriod
? lockPeriod
: block.timestamp - depositLockStart[account];
return passedTime * getRewardPerSecond(account);
}
4. Detailed Root Cause Analysis
The attacker first needed a small amount of TPAD principal. In tx 0x2490368b43951caa8bf6f730bf0aaa0bcc2657d6f64fdcc3b0372b6500d0dcfc, the attacker EOA sent 0.02 BNB into helper contract 0x1694d7fAbf3B28f11D65DEEb9f60810DAa26909a, which bought 41295609937 raw TPAD units on PancakeSwap. Receipt-backed accounting shows this setup transaction used 603651 gas at 3 gwei, costing 1810953000000000 wei in BNB.
The exploit happens in tx 0x191a34e6c0780c3d1ab5c9bc04948e231d742b7d88e0e4f85568d57fcdc03182. The helper repeatedly calls the victim pool's public receiveUpPool(helper, 41295742091) and withdraw(41295742091) routines. The trace shows the victim pool delegating into the implementation, then immediately querying the helper for lock state:
LaunchpadLockableStaking::receiveUpPool(0x1694d7f..., 41295742091)
0x1694d7f...::isLocked(0x1694d7f...) -> true
0x1694d7f...::depositLockStart(0x1694d7f...) -> 1
Returning 1 as the lock start is enough to make the victim believe nearly the entire 94608000 second lock period has already elapsed. updateDepositLockStart() snapshots rewards using the old position, overwrites the timestamp with the spoofed value, and resets reward debt from getFixedAprPendingReward(). When the helper immediately withdraws the same principal, the principal returns to the helper but the manufactured pendingRewards remain in the victim pool's accounting.
That loop is repeated until the pool has minted enough fake rewards. The trace then shows a final receiveUpPool(helper, 1) where the helper returns a near-mature lock timestamp derived from the pool's own lock period, followed by stakePendingRewards():
LaunchpadLockableStaking::receiveUpPool(0x1694d7f..., 1)
0x1694d7f...::depositLockStart(0x1694d7f...) -> 1604678573
...
LaunchpadLockableStaking::stakePendingRewards()
emit StakedPending(user: 0x1694d7f..., amount: 110691360000)
Historical reads confirm the victim pool recorded the helper with amount = 110691360001 and pendingRewards = 0 at block 33260396. That is the inflated staking position created entirely from replayed lock rewards on the same seed principal.
The final tx, 0xea5bb62b8a151917a732d4114d716c7e6c087af8b3c0b3416c9dbc37c59f04da, is the cash-out. Once the spoofed lock has matured, the helper withdraws the full inflated stake. Historical reads at block 33260397 show the helper's TPAD balance rose to 151987127703 raw units while the victim pool balance fell from 29420091579116815 to 29419980887756815, a loss of exactly 110691360001 raw TPAD units for the pool.
The fee accounting is deterministic. Across the three exploit transactions the attacker spent 7093976 gas at 3 gwei, for total BNB gas cost 21281928000000000 wei. Because the success predicate is denominated in TPAD, fees_paid_in_reference_asset is correctly 0 TPAD while the BNB gas cost is documented separately.
5. Adversary Flow Analysis
Stage 1: Seed TPAD acquisition
- Tx
0x2490368b43951caa8bf6f730bf0aaa0bcc2657d6f64fdcc3b0372b6500d0dcfc - EOA
0x1a7b15354e2f6564fcf6960c79542de251ce0dc9 - Sends
0.02BNB into helper contract0x1694d7fAbf3B28f11D65DEEb9f60810DAa26909a - Helper swaps into
41295609937raw TPAD units on PancakeSwap
Stage 2: Fake migration loops and reward manufacturing
- Tx
0x191a34e6c0780c3d1ab5c9bc04948e231d742b7d88e0e4f85568d57fcdc03182 - The helper approves the victim pool, spoofing the migration-source surface expected by
receiveUpPool() - The helper repeatedly:
- calls
receiveUpPool(helper, 41295742091) - returns
isLocked = trueanddepositLockStart = 1 - immediately calls
withdraw(41295742091)
- calls
- After enough loops, the helper performs one final
receiveUpPool(helper, 1)with a near-mature spoofed timestamp and then callsstakePendingRewards() - The victim pool records the helper with an inflated TPAD stake of
110691360001
Stage 3: Cash-out
- Tx
0xea5bb62b8a151917a732d4114d716c7e6c087af8b3c0b3416c9dbc37c59f04da - After maturity, the helper withdraws its full staked amount
- The helper ends with
151987127703raw TPAD units, versus41295609937raw TPAD units after setup
The attack is ACT because every required component is permissionless: the attacker only needs an unprivileged helper contract, a small amount of TPAD principal, public liquidity, and the victim pool's public receiveUpPool() entrypoint.
6. Impact & Losses
The direct victim is TrustPad's LaunchpadLockableStaking pool at 0xE613c058701C768E0d04D1bf8e6a6dc1a0C6d48A. The stolen asset is TPAD token 0xADCFC6bf853a0a8ad7f9Ff4244140D10cf01363C.
The measured loss is:
- TPAD lost by the victim pool:
110691360001raw units (110.691360001TPAD with 9 decimals) - Attacker helper TPAD balance after setup:
41295609937 - Attacker helper TPAD balance after final withdrawal:
151987127703 - Net attacker TPAD increase:
110691517766raw units
The slight difference between the victim's loss and the helper's TPAD increase is consistent with reflection-token side effects during the setup and exploit sequence. The exploit still deterministically drains more than 110 TPAD from the pool and leaves the attacker materially profitable in TPAD terms.
7. References
- TrustPad LaunchpadLockableStaking proxy:
0xE613c058701C768E0d04D1bf8e6a6dc1a0C6d48A - Verified LaunchpadLockableStaking implementation:
0x129f4ac88b0446f9b46b176c93531e6cf4687657 - TrustPad token:
0xADCFC6bf853a0a8ad7f9Ff4244140D10cf01363C - Exploit tx
0x2490368b43951caa8bf6f730bf0aaa0bcc2657d6f64fdcc3b0372b6500d0dcfc(seed principal acquisition) - Exploit tx
0x191a34e6c0780c3d1ab5c9bc04948e231d742b7d88e0e4f85568d57fcdc03182(fake migration loops and reward staking) - Exploit tx
0xea5bb62b8a151917a732d4114d716c7e6c087af8b3c0b3416c9dbc37c59f04da(cash-out) - Historical victim and attacker balances from validator-reviewed RPC observations in
artifacts/auditor/iter_0/rpc_observations.json - Receipt-backed gas accounting in
artifacts/auditor/iter_1/fee_accounting.json