PRXVT Staking Transfer Accounting Flaw Drains Reward Pool
Exploit Transactions
0x88610208c00f5d5ca234e45205a01199c87cb859f881e8b35297cba8325a5494Victim Addresses
0xdac30a5e2612206e2756836ed6764ec5817e6fffBase0xc2ff2e5aa9023b1bb688178a4a547212f4614bc0Base0x702980b1ed754c214b79192a4d7c39106f19bce9Base0xf3fe57d25ef1a7e370f0f50a223cf98a48db410cBase0x43873934f4dfc12ccd51f6b0604b4178dae07303Base0xb263c3c5fc32f63a82d3f18b7fb5448d6fcbec0eBase0x7407f9bdc4140d5e284ea7de32a9de6037842f45Base0x000000000000000000000000000000000000deadBaseLoss Breakdown
Similar Incidents
MPRO Staking Proxy unwrapWETH Flash-Loan Exploit (Base)
26%TSURUWrapper onERC1155Received bug mints unbacked tokens and drains WETH
22%MineSTM LP-burn MEV drains USDT from STM liquidity
22%DOGGO/WETH cross-pool arbitrage MEV extracts WETH spread
21%DysonVault / Thena Overnight LP Unwind MEV
21%YziLabs pool accounting flaw drains WBNB liquidity
21%Root Cause Analysis
PRXVT Staking Transfer Accounting Flaw Drains Reward Pool
On Base (chainid 8453), an adversary-controlled orchestrator contract at 0x702980b1ed754c214b79192a4d7c39106f19bce9 exploited a design flaw in the PRXVT staking system.
The staking contract PRXVTStaking at 0xdac30a5e2612206e2756836ed6764ec5817e6fff mints a transferable receipt token stPRXVT (an ERC20) to represent staked positions in the reward token AgentTokenV2 at 0xc2ff2e5aa9023b1bb688178a4a547212f4614bc0.
Because the contract does not update Synthetix-style reward accounting when stPRXVT is transferred, an orchestrator can shuttle a single large stPRXVT position through many short-lived helper contracts.
Each helper temporarily holds the entire stake, calls earned() and claimReward(), receives a full historical reward payout, and returns the principal stPRXVT back to the orchestrator.
By repeating this pattern across many helpers and transactions, the adversary drains the PRXVTStaking reward pool while paying only the configured burn fee on each claim and gas costs, and finally consolidates principal plus extracted rewards back to the controlling EOA.
Key Background
PRXVTStaking (0xdac30a5e2612206e2756836ed6764ec5817e6fff) is a staking contract on Base that accepts a PRXVT-like ERC20 token AgentTokenV2 (0xc2ff2e5aa9023b1bb688178a4a547212f4614bc0) and mints stPRXVT 1:1 as an ERC20 receipt token. Stakers earn rewards linearly over time, with rewards distributed at a configurable rewardRate and a configurable burn fee applied on each claimReward() call. The contract follows a Synthetix-style pattern where a global rewardPerToken is accumulated over time and per-account mappings track how much of that reward stream each address has already accounted for.
The core reward accounting is implemented as follows in artifacts/root_cause/seed/8453/0xdac30a5e2612206e2756836ed6764ec5817e6fff/src/PRXVTStaking.sol:
function rewardPerToken() public view returns (uint256) {
if (_totalStaked == 0) {
return rewardPerTokenStored;
}
return
rewardPerTokenStored
+ ((lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * PRECISION)
/ _totalStaked;
}
function earned(address account) public view returns (uint256) {
uint256 baseReward =
(balanceOf(account) * (rewardPerToken() - userRewardPerTokenPaid[account]))
/ PRECISION
+ rewards[account];
BoostInfo storage boost = _boosts[account];
if (boost.expiresAt > block.timestamp && boost.multiplier > PRECISION) {
return (baseReward * boost.multiplier) / PRECISION;
}
return baseReward;
}
Staking and withdrawal are wired correctly through the updateReward modifier so that when a user calls stake or withdraw, rewards[account] and userRewardPerTokenPaid[account] are synchronized with the current rewardPerToken:
modifier updateReward(address account) {
rewardPerTokenStored = rewardPerToken();
lastUpdateTime = lastTimeRewardApplicable();
if (account != address(0)) {
rewards[account] = earned(account);
userRewardPerTokenPaid[account] = rewardPerTokenStored;
}
_;
}
function stake(uint256 amount)
external
nonReentrant
whenNotPaused
updateReward(msg.sender)
{
require(amount > 0, "Cannot stake 0");
if (balanceOf(msg.sender) == 0) {
require(amount >= minimumStake, "Below minimum stake");
}
_totalStaked += amount;
_mint(msg.sender, amount);
prxvtToken.safeTransferFrom(msg.sender, address(this), amount);
emit Staked(msg.sender, amount);
}
function withdraw(uint256 amount)
public
nonReentrant
updateReward(msg.sender)
{
require(amount > 0, "Cannot withdraw 0");
require(balanceOf(msg.sender) >= amount, "Insufficient balance");
_totalStaked -= amount;
_burn(msg.sender, amount);
prxvtToken.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}
Rewards are claimed via claimReward, which applies a burn fee and sends the remainder of the reward tokens to the caller:
function claimReward()
public
nonReentrant
whenNotPaused
updateReward(msg.sender)
{
uint256 reward = rewards[msg.sender];
require(reward > 0, "No rewards to claim");
rewards[msg.sender] = 0;
uint256 burnAmount = (reward * burnFeePercent) / 10_000;
uint256 userAmount = reward - burnAmount;
totalBurned += burnAmount;
if (burnAmount > 0) {
prxvtToken.safeTransfer(BURN_ADDRESS, burnAmount);
emit RewardBurned(msg.sender, burnAmount);
}
if (userAmount > 0) {
prxvtToken.safeTransfer(msg.sender, userAmount);
}
emit RewardPaid(msg.sender, userAmount);
}
Crucially, PRXVTStaking inherits from OpenZeppelin ERC20 and exposes a fully transferable stPRXVT token, but it does not override transfer or transferFrom to hook into updateReward. As a result, the reward accounting logic is implicitly assuming that staking positions remain associated with one address, while the token implementation allows them to move freely between accounts without any reward state being updated.
The reward pool itself is funded and bounded in a way that assumes reward claims are tied to genuine time-weighted exposure. The contract uses a parameter rewardRate and a rewardsDuration to spread a finite supply of rewards over time. However, the failure to synchronize accounting on transfers allows an attacker to make far more claims than intended for a single economic position.
At the protocol level, the relevant parties and contracts are:
- EOA
0x7407f9bdc4140d5e284ea7de32a9de6037842f45— the adversary’s controlling externally owned account on Base. - Orchestrator contract
0x702980b1ed754c214b79192a4d7c39106f19bce9— a custom contract that holds the stakedstPRXVT, deploys helper contracts, and orchestrates repeated reward claims. - Helper contracts such as
0xF3FE57d25eF1A7E370F0f50a223Cf98a48DB410c,0x43873934F4dfc12CCD51f6b0604B4178DAe07303, and0xB263C3c5fc32f63A82D3F18b7fb5448d6Fcbec0e— short-lived contracts that temporarily receive the fullstPRXVTbalance, call intoPRXVTStakingto claim rewards, and return principal plus rewards to the orchestrator. PRXVTStaking(0xdac30a5e…) — the staking contract that holds the reward pool and enforces the reward/burn logic.AgentTokenV2(0xc2ff2e…) — the PRXVT-like ERC20 reward token being distributed by the staking contract.
The ACT opportunity is defined at block height 40230817 on Base. The pre-state includes the balances and contract code for the EOA, orchestrator, PRXVTStaking, and AgentTokenV2. Evidence includes:
artifacts/root_cause/seed/8453/0xdac30a5e2612206e2756836ed6764ec5817e6fff/src/PRXVTStaking.solartifacts/root_cause/seed/8453/0xc2ff2e5aa9023b1bb688178a4a547212f4614bc0/src/virtualPersona/AgentTokenV2.solartifacts/root_cause/data_collector/iter_1/contract/8453/0x702980b1ed754c214b79192a4d7c39106f19bce9/decompile/0x702980b1ed754c214b79192a4d7c39106f19bce9-decompiled.solartifacts/root_cause/data_collector/iter_1/contract/8453/0xF3FE57d25eF1A7E370F0f50a223Cf98a48DB410c/decompile/0xF3FE57d25eF1A7E370F0f50a223Cf98a48DB410c-decompiled.sol- Etherscan txlists for the EOA and orchestrator around the relevant block range.
From a security perspective, the key violated assumptions are:
- Rewards must be aligned with economic exposure over time; here, the same stake position can collect historical rewards multiple times.
- The intended conservation of rewards (bounded via rewardRate and
balance - _totalStaked) is bypassed because the same underlying stake is counted repeatedly. - Using a freely transferable ERC20 as a reward-bearing receipt without integrating transfers into the reward accounting logic breaks the invariants assumed by the Synthetix-inspired design.
Vulnerability Analysis
Vulnerable Mechanism
The core vulnerability arises from the combination of:
- A Synthetix-style reward accounting model that relies on per-address state (
userRewardPerTokenPaid[account]andrewards[account]) to track how much of the global reward stream has already been accrued; and - An ERC20 staking receipt (
stPRXVT) that is fully transferable, withouttransferortransferFromupdating the reward accounting state for sender and receiver.
In a correct Synthetix-style design, transferring staking receipts from address A to B would typically:
- Call
updateReward(A)andupdateReward(B), fixing both addresses’rewardsanduserRewardPerTokenPaidto the currentrewardPerToken. - Move the staked balance from A to B.
This ensures that B does not retroactively gain access to rewards that were already “spent” by A.
In PRXVTStaking, there is no override of transfer / transferFrom, and therefore no call to updateReward on transfers. The observable behavior is:
- A legacy holder’s reward state is only updated when they call
stake,withdraw,claimReward, or another function gated byupdateReward. - A newly introduced holder who receives
stPRXVTvia a simple ERC20transferwill:- Have a non-zero
balanceOf(account)inPRXVTStaking, and - Have
userRewardPerTokenPaid[account]left at its default (typically zero), - So
earned(account)will treat them as if they had been staked for the entire period since the reward distribution started.
- Have a non-zero
This allows an adversary to create a fresh helper address, transfer a fully matured stPRXVT balance into that address, and immediately call earned(helper) and claimReward() to realize the historical rewards again, even if the same economic stake has already collected rewards at other addresses.
Code Evidence of Missing Transfer Hooks
PRXVTStaking overrides totalSupply but does not override transfer or transferFrom:
// ============ ERC20 Overrides ============
function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
return _totalStaked;
}
All other ERC20 behavior, including transfer and transferFrom, is inherited unchanged from OpenZeppelin ERC20. The OpenZeppelin implementation does not know about userRewardPerTokenPaid or rewards; it simply updates balances and emits Transfer events. There is no updateReward call in that path.
Combined with the earned function shown earlier, this guarantees that a newly created holder of stPRXVT with a large balance and a zeroed userRewardPerTokenPaid will be treated as if they owned that balance for the entire reward period.
Constraints and Reward Pool Capacity
The reward pool is funded and bounded through the standard Synthetix pattern: rewardRate is set based on the available reward tokens and rewardsDuration, with checks that prevent setting rewardRate so high that rewards exceed the contract’s balance over the configured duration. However, these checks assume each unit of stake corresponds to a unique entitlement path for rewards.
Because the same staked tokens can collect rewards repeatedly under different addresses, the actual total rewards paid out per unit of stake can significantly exceed what is implied by nominal APR calculations, as long as the reward pool holds enough tokens to cover repeated claims plus burn fees.
Detailed Root Cause Analysis
ACT Opportunity and Success Predicate
The ACT opportunity is defined at Base block 40230817, capturing the state before the first key transaction in the exploit sequence. The pre-state includes:
- EOA
0x7407f9bdc4140d5e284ea7de32a9de6037842f45with a largeAgentTokenV2balance and sufficient ETH for gas. - Orchestrator contract
0x702980b1ed754c214b79192a4d7c39106f19bce9with no initialstPRXVTor reward token balance. - PRXVTStaking contract
0xdac30a5e2612206e2756836ed6764ec5817e6fffwith a significantAgentTokenV2balance in its reward pool. - AgentTokenV2 token contract
0xc2ff2e5aa9023b1bb688178a4a547212f4614bc0.
The success predicate is profit-based with AgentTokenV2 taken as the reference asset. The adversary cluster is:
cluster{EOA 0x7407f9bdc4140d5e284ea7de32a9de6037842f45, orchestrator 0x702980b1ed754c214b79192a4d7c39106f19bce9 and helper contracts it deploys}.
Key quantified facts:
- Fees are paid in native ETH; no fees are paid in AgentTokenV2.
- Before the first
e6d7db7eexploit transaction, the orchestrator’s AgentTokenV2 balance is zero. The EOA’s AgentTokenV2 balance decreases by2.3e24in theprepare()transaction as principal is staked. - For representative exploit tx
0x91d8e05b9ca9b708606878643310fa0385ae698c0c0802aace04ec6d396681f1, the balance diff shows:- PRXVTStaking’s AgentTokenV2 balance decreases by
-229742725742797866000000(~2.2974e23), - The burn address gains
+22974272574279786600000(~2.2974e22), - The orchestrator gains
+206768453168518079400000(~2.067684531685180794e23).
- PRXVTStaking’s AgentTokenV2 balance decreases by
This is visible in artifacts/root_cause/data_collector/iter_3/tx/8453/0x91d8e05b9ca9b708606878643310fa0385ae698c0c0802aace04ec6d396681f1/balance_diff.json:
{
"erc20_balance_deltas": [
{
"token": "0xc2ff2e5aa9023b1bb688178a4a547212f4614bc0",
"holder": "0xdac30a5e2612206e2756836ed6764ec5817e6fff",
"delta": "-229742725742797866000000",
"contract_name": "AgentTokenV2"
},
{
"token": "0xc2ff2e5aa9023b1bb688178a4a547212f4614bc0",
"holder": "0x000000000000000000000000000000000000dead",
"delta": "22974272574279786600000",
"contract_name": "AgentTokenV2"
},
{
"token": "0xc2ff2e5aa9023b1bb688178a4a547212f4614bc0",
"holder": "0x702980b1ed754c214b79192a4d7c39106f19bce9",
"delta": "206768453168518079400000",
"contract_name": "AgentTokenV2"
}
]
}
The adversary cluster’s net delta in this tx is therefore a deterministic +206,768,453,168,518,079,400,000 AgentTokenV2 tokens after fees. Repeating this pattern across many e6d7db7e exploit calls yields a large positive cumulative profit while preserving the staked principal.
The valuation summary is:
- Reference asset: AgentTokenV2.
- Tx
0x7cf17521041f4370f79710b5106d4d8382bb12794fd06711764a649694b8a5a8(prepare) moves2.3e24tokens from the EOA to PRXVTStaking and mints an equalstPRXVTposition to the orchestrator. - Exploit transactions
0xe1a6c6781990c68b099bf53e114820d502ca57e5cf3fafc9e50ab2227d7747a8,0x91d8e05b9ca9b708606878643310fa0385ae698c0c0802aace04ec6d396681f1, and0x04c1826c909ad78ec5cdf772f14357c937883fcc003da6daea502fcc72a9b7eaconsistently show the orchestrator gaining roughly2.06e23AgentTokenV2 per exploit tx, while only a ~10% burn fee is paid. - Gas costs in ETH are small relative to the ERC20 reward scale.
withdraw()tx0x20094a7c6d7b5f201ebc545ae62efc6c6ef618a22f87fd39e35d4710b62d7d10consolidates the orchestrator’s holdings back to the EOA without creating new rewards.
These facts are sufficient to prove a deterministic net positive value change for the adversary cluster in the reference asset, even if not every exploit tx in the full campaign is individually enumerated.
Orchestrator and Helper Behavior
The orchestrator at 0x7029… is decompiled in artifacts/root_cause/data_collector/iter_1/contract/8453/0x702980b1ed754c214b79192a4d7c39106f19bce9/decompile/0x702980b1ed754c214b79192a4d7c39106f19bce9-decompiled.sol. It exposes several key functions:
/// @custom:selector 0xcb577480
/// @custom:signature prepare(uint256 arg0) public payable
function prepare(uint256 arg0) public payable {
require(arg0 == arg0);
require(address(msg.sender) == 0x7407f9bdc4140d5e284ea7de32a9de6037842f45);
// transfer AgentTokenV2 from EOA to PRXVTStaking and approve staking
// ...
}
/// @custom:selector 0xe6d7db7e
/// @custom:signature Unresolved_e6d7db7e(uint256 arg0) public payable returns (bytes memory)
function Unresolved_e6d7db7e(uint256 arg0) public payable returns (bytes memory) {
require(arg0 == arg0);
require(address(msg.sender) == 0x7407f9bdc4140d5e284ea7de32a9de6037842f45);
// loop deploying helpers and transferring stPRXVT, then invoking helper execute
// ...
}
/// @custom:selector 0x3ccfd60b
/// @custom:signature withdraw() public payable
function withdraw() public payable {
require(address(msg.sender) == 0x7407f9bdc4140d5e284ea7de32a9de6037842f45);
// read stPRXVT and AgentTokenV2 balances,
// transfer AgentTokenV2 to the EOA,
// call PRXVTStaking.withdraw(amount) to redeem principal
// ...
}
Every key function includes a strict require(msg.sender == 0x7407...), which firmly ties control of the orchestrator to the EOA in question. However, the code is not privileged in the protocol sense: any adversary can deploy an equivalent contract with different hard-coded EOA values.
The helper pattern is captured by the decompiled helper contract at 0xF3FE57d25eF1A7E370F0f50a223Cf98a48DB410c (artifacts/root_cause/data_collector/iter_1/contract/8453/0xF3FE57d25eF1A7E370F0f50a223Cf98a48DB410c/decompile/...-decompiled.sol):
/// @custom:selector 0xe65add95
/// @custom:signature execute(address arg0, address arg1, address arg2) public payable
function execute(address arg0, address arg1, address arg2) public payable {
// arg0: PRXVTStaking, arg1: AgentTokenV2, arg2: orchestrator
// check balances in staking and reward token,
// transfer AgentTokenV2 rewards to orchestrator,
// and finally call PRXVTStaking.claimReward()
// ...
(bool success, bytes memory ret0) = address(arg0).claimReward(); // call
}
This logic matches the on-chain trace patterns, where each helper:
- Receives the full
stPRXVTbalance viaPRXVTStaking::transfer(orchestrator -> helper). - Calls
execute(PRXVTStaking, AgentTokenV2, orchestrator). - Within
execute, callsPRXVTStaking::earned(helper)andPRXVTStaking::claimReward()to extract rewards for the entire historical stake. - Transfers the claimed AgentTokenV2 from helper to orchestrator.
- Transfers the entire
stPRXVTbalance back from helper to orchestrator.
This cycle is repeated with many fresh helper addresses per exploit transaction.
Trace Evidence of the Multi-Helper Cycle
A representative exploit trace is artifacts/root_cause/data_collector/iter_3/tx/8453/0x04c1826c909ad78ec5cdf772f14357c937883fcc003da6daea502fcc72a9b7ea/trace.cast.log. A snippet of the trace around a single helper cycle shows:
0x702980b1Ed754C214B79192a4D7c39106f19BcE9::e6d7db7e(...)
├─ PRXVTStaking::balanceOf(0x7029...) → 2300000000000000000000000 [2.3e24]
├─ new helper 0x43873934F4dfc12CCD51f6b0604B4178DAe07303(...)
├─ PRXVTStaking::transfer(0x7029... -> 0x85A09952...) value 2.3e24 stPRXVT
├─ helper::execute(PRXVTStaking, AgentTokenV2, 0x7029...)
│ ├─ PRXVTStaking::earned(0x85A0...) / claimReward()
│ │ ├─ AgentTokenV2::transfer(PRXVTStaking -> 0x0000...dEaD, ~1.15e21)
│ │ ├─ AgentTokenV2::transfer(PRXVTStaking -> 0x85A0..., ~1.03e22)
│ └─ ...
├─ PRXVTStaking::transfer(0x85A0... -> 0x7029...) value 2.3e24 stPRXVT
├─ AgentTokenV2::transfer(0x85A0... -> 0x7029..., ~1.03e22)
This confirms:
- The full
stPRXVTstake moves from orchestrator to helper and back in each cycle. claimReward()is called with the helper as the beneficiary, burning ~10% to0x000000000000000000000000000000000000dEaDand paying ~90% to the helper, which then remits it to the orchestrator.- The principal
stPRXVTremains intact under the orchestrator’s control after each cycle.
Because each helper starts with userRewardPerTokenPaid[helper] == 0, earned(helper) treats the full stPRXVT balance as having been staked for the entire reward period, allowing rewards to be repeatedly double-counted.
End-to-End Attack Sequence (ACT Transaction Sequence b)
The ACT transaction_sequence_b is:
-
Tx 1 (prepare)
- Chain: Base (8453).
- Tx hash:
0x7cf17521041f4370f79710b5106d4d8382bb12794fd06711764a649694b8a5a8. - Type: adversary-crafted.
- EOA
0x7407...callsorchestrator.prepare(2.3e24)with selector0xcb577480. - Balance diff (
artifacts/.../0x7cf175.../balance_diff.json) shows:- EOA loses
2.3e24AgentTokenV2, - PRXVTStaking gains
2.3e24AgentTokenV2, - Orchestrator gains
2.3e24stPRXVT.
- EOA loses
- This primes a large staked position under the orchestrator.
-
Tx 2 (exploit)
- Chain: Base (8453).
- Tx hash:
0x91d8e05b9ca9b708606878643310fa0385ae698c0c0802aace04ec6d396681f1. - Type: adversary-crafted.
- EOA
0x7407...callsorchestrator.Unresolved_e6d7db7e(0x927c0)with selector0xe6d7db7e. - Within this tx, the orchestrator:
- Repeatedly transfers the full
2.3e24stPRXVTposition to fresh helper contracts. - Each helper calls
execute(PRXVTStaking, AgentTokenV2, orchestrator), which issuesearned(helper)andclaimReward()calls to PRXVTStaking and then returns the principalstPRXVTand AgentTokenV2 rewards to the orchestrator.
- Repeatedly transfers the full
- The balance diff for this tx (shown above) demonstrates:
- PRXVTStaking loses
~2.2974e23AgentTokenV2, - Dead address gains
~2.2974e22AgentTokenV2, - Orchestrator gains
~2.067684531685180794e23AgentTokenV2.
- PRXVTStaking loses
- This confirms the double-counting of rewards with principal intact.
-
Tx 3 (withdraw)
- Chain: Base (8453).
- Tx hash:
0x20094a7c6d7b5f201ebc545ae62efc6c6ef618a22f87fd39e35d4710b62d7d10. - Type: adversary-crafted.
- EOA
0x7407...callsorchestrator.withdraw()with selector0x3ccfd60b. - The decompiled
withdraw()function:- Requires
msg.sender == 0x7407.... - Transfers all AgentTokenV2 held by the orchestrator to the EOA.
- Calls
PRXVTStaking.withdraw(amount)to redeemstPRXVTback into underlying tokens for the EOA.
- Requires
- Even without a full trace, tx metadata and code confirm this step consolidates principal plus extracted rewards into the EOA.
Additional exploit transactions 0x88610208c00f5d5ca234e45205a01199c87cb859f881e8b35297cba8325a5494, 0xe1a6c6781990c68b099bf53e114820d502ca57e5cf3fafc9e50ab2227d7747a8, and 0x04c1826c909ad78ec5cdf772f14357c937883fcc003da6daea502fcc72a9b7ea follow the same pattern as Tx 2, with repeated helper cycles, consistent reward extraction, and preserved principal.
Adversary Flow Analysis
Adversary Strategy Summary
The adversary’s strategy is:
- Use the EOA
0x7407...to stake a very large AgentTokenV2 position into PRXVTStaking under the orchestrator’s address viaprepare(), creating a largestPRXVTposition controlled by the orchestrator. - Repeatedly cycle that single
stPRXVTstake through a chain of fresh helper contracts in exploit transactions, with each helper:- Temporarily holding the full
stPRXVTbalance, - Calling
earned()andclaimReward()to collect a full historical reward payout, - Returning the principal back to the orchestrator and forwarding rewards.
- Temporarily holding the full
- After many such exploit transactions, call
withdraw()from the EOA to pull both principal and accumulated rewards back from the orchestrator into the EOA.
Because the staking contract does not adjust reward baselines on stPRXVT transfers, each helper behaves as though it had been staked from the beginning, enabling multiple full reward claims for the same underlying stake.
Adversary-Related Accounts and Victim Contracts
Adversary cluster:
-
0x7407f9bdc4140d5e284ea7de32a9de6037842f45- Chain: Base (8453), EOA.
- Sends all
prepare(),e6d7db7e(), andwithdraw()transactions to the orchestrator. - Orchestrator bytecode hard-codes
require(msg.sender == 0x7407...)for all critical functions, proving that this EOA controls the orchestrator and receives final withdrawals.
-
0x702980b1ed754c214b79192a4d7c39106f19bce9- Chain: Base (8453), contract (unverified on explorer).
- Receives the seed and exploit transactions, holds the large
stPRXVTstake, deploys helper contracts, and orchestrates repeated transfers and reward claims. - Decompile shows all key entrypoints gated by
require(msg.sender == 0x7407...)and calls into PRXVTStaking and AgentTokenV2 as described.
-
0xF3FE57d25eF1A7E370F0f50a223Cf98a48DB410c- Chain: Base (8453), contract.
- A helper-style contract with function
execute(address,address,address)that:- Calls
PRXVTStaking::earned(helper), - Calls
PRXVTStaking::claimReward(), - Transfers reward and principal back to the orchestrator.
- Calls
- Used in early exploit txs, including the seed transaction
0x88610208....
-
0x43873934F4dfc12CCD51f6b0604B4178DAe07303- Chain: Base (8453), contract.
- One of many short-lived helper contracts deployed within exploit tx
0x04c1826c.... - Receives the full
2.3e24stPRXVTposition, callsearned()andclaimReward(), receives reward tokens, and returnsstPRXVTto the orchestrator, as shown in the mid-sequence trace.
-
0xB263C3c5fc32f63A82D3F18b7fb5448d6Fcbec0e- Chain: Base (8453), contract.
- Another helper contract in the same pattern as
0x4387..., deployed and used within0x04c1826c.... - Behavior is representative of the broader helper set under adversary control.
Victim candidates:
-
PRXVTStaking(0xdac30a5e2612206e2756836ed6764ec5817e6fff)- Chain: Base (8453), verified contract.
- Holds the staking pool, mints/burns
stPRXVT, and pays out AgentTokenV2 rewards. - Its flawed handling of reward accounting on transfers is the root cause of the exploit.
-
AgentTokenV2 (PRXVT-like reward token)(0xc2ff2e5aa9023b1bb688178a4a547212f4614bc0)- Chain: Base (8453), verified contract.
- ERC20 reward token distributed by PRXVTStaking and drained by the adversary.
Adversary Lifecycle Stages
-
Adversary staking setup and priming
- Chain: Base (8453), tx:
0x7cf17521041f4370f79710b5106d4d8382bb12794fd06711764a649694b8a5a8, block40230817. - Mechanism: stake via helper/orchestrator.
- EOA
0x7407...callsorchestrator.prepare(2.3e24). - Effect: transfers
2.3e24AgentTokenV2 from the EOA to PRXVTStaking and mints an equalstPRXVTposition to the orchestrator. - Evidence: balance diff for this tx (
artifacts/.../0x7cf175.../balance_diff.json) and PRXVTStakingstake()logic confirm the movement of principal and creation of the staked position.
- Chain: Base (8453), tx:
-
Adversary exploit execution with multi-helper reward claims
- Chain: Base (8453), txs:
0x88610208c00f5d5ca234e45205a01199c87cb859f881e8b35297cba8325a5494, block40230828.0x91d8e05b9ca9b708606878643310fa0385ae698c0c0802aace04ec6d396681f1, block40230830.0x04c1826c909ad78ec5cdf772f14357c937883fcc003da6daea502fcc72a9b7ea, block40230956.
- For each
e6d7db7e(tx)from the EOA to0x7029...:- The orchestrator repeatedly transfers the full
stPRXVTposition to a fresh helper contract. - The helper calls
PRXVTStaking::earned(helper)andclaimReward(), receiving rewards for the entire historical stake. - The helper then transfers
stPRXVTback to the orchestrator and forwards rewards to it.
- The orchestrator repeatedly transfers the full
- Effect: the reward token AgentTokenV2 is transferred from PRXVTStaking to helpers and then to the orchestrator, while a fraction (burn fee) is permanently burned. This pattern repeats many times per transaction and across dozens of
e6d7db7etransactions, extracting large cumulative rewards while leaving the principal stake intact. - Evidence:
- Seed trace
artifacts/root_cause/seed/8453/0x88610208.../trace.cast.logand mid-sequence traceartifacts/root_cause/data_collector/iter_3/tx/8453/0x04c1826c.../trace.cast.logshow repeated sequences of:PRXVTStaking::balanceOf,PRXVTStaking::transfer(orchestrator -> helper),helper::execute(PRXVTStaking, AgentTokenV2, orchestrator),PRXVTStaking::earned(helper),PRXVTStaking::claimReward(),PRXVTStaking::transfer(helper -> orchestrator).
- Balance diffs for
0xe1a6c6...,0x91d8e0..., and0x04c1826c...show consistent patterns where PRXVTStaking loses ~2.3e23AgentTokenV2 per helper cycle, the burn address gains ~10% of that amount, and the orchestrator gains ~90%.
- Seed trace
- Chain: Base (8453), txs:
-
Adversary profit realization and withdrawal
- Chain: Base (8453), tx:
0x20094a7c6d7b5f201ebc545ae62efc6c6ef618a22f87fd39e35d4710b62d7d10, block40231078. - EOA
0x7407...callsorchestrator.withdraw(). - Effect: the decompiled
withdraw()function transfers all AgentTokenV2 held by the orchestrator to the EOA and callsPRXVTStaking::withdraw()to redeem the orchestrator’sstPRXVTback into underlying tokens, consolidating both principal and extracted rewards into the EOA. - Evidence: tx metadata (selector
0x3ccfd60b) and thewithdraw()decompile show it reading balances, transferring AgentTokenV2 to the EOA, and callingPRXVTStaking.withdraw(amount). Combined with prior balance diffs, this is sufficient to attribute the accumulated profit to the EOA–orchestrator cluster.
- Chain: Base (8453), tx:
Impact & Losses
Token-Level Impact
The primary impacted asset is AgentTokenV2 on Base:
- Token: AgentTokenV2 (
0xc2ff2e5aa9023b1bb688178a4a547212f4614bc0). - Loss magnitude: at least several multiples of approximately
2.06e23tokens pere6d7db7eexploit transaction across dozens of such transactions, plus a permanent burn of roughly2.3e22tokens per helper cycle to the dead address0x000000000000000000000000000000000000dEaD.
Because the exact number of helper cycles and exploit transactions is large, the exact total extraction is campaign-dependent. However, sampled balance diffs show consistent per-tx extraction patterns of approximately:
- ~
2.3e23AgentTokenV2 removed from PRXVTStaking per exploit transaction, - ~10% of that amount burned to
0x0000...dEaD, - ~90% credited to the orchestrator and ultimately to the EOA.
This is sufficient to classify the incident as a high-severity protocol bug with significant reward pool depletion.
Protocol-Level Impact
- PRXVTStaking’s reward pool is drained substantially by repeated reward claims on a single underlying stake.
- Other honest stakers see their expected rewards reduced or exhausted despite their stake, violating economic fairness.
- Tokenomics assumptions around staking APY, burn schedules, and reward sustainability are broken because a single actor extracts disproportionately large rewards without corresponding economic exposure.
- The incident demonstrates that transferable reward-bearing receipt tokens must be coupled with robust accounting that handles transfers, or else complex orchestrator/helper patterns can systematically exploit double-counting of rewards.
References
Key evidence and artifacts:
-
[1] Seed tx trace for
0x88610208c00f5d5ca234e45205a01199c87cb859f881e8b35297cba8325a5494(Base 8453)
artifacts/root_cause/seed/8453/0x88610208c00f5d5ca234e45205a01199c87cb859f881e8b35297cba8325a5494/trace.cast.log -
[2] Balance diff for prepare() tx
0x7cf17521041f4370f79710b5106d4d8382bb12794fd06711764a649694b8a5a8
artifacts/root_cause/data_collector/iter_2/tx/8453/0x7cf17521041f4370f79710b5106d4d8382bb12794fd06711764a649694b8a5a8/balance_diff.json -
[3] Balance diff for early exploit tx
0xe1a6c6781990c68b099bf53e114820d502ca57e5cf3fafc9e50ab2227d7747a8
artifacts/root_cause/data_collector/iter_2/tx/8453/0xe1a6c6781990c68b099bf53e114820d502ca57e5cf3fafc9e50ab2227d7747a8/balance_diff.json -
[4] Balance diff for representative exploit tx
0x91d8e05b9ca9b708606878643310fa0385ae698c0c0802aace04ec6d396681f1
artifacts/root_cause/data_collector/iter_3/tx/8453/0x91d8e05b9ca9b708606878643310fa0385ae698c0c0802aace04ec6d396681f1/balance_diff.json -
[5] Balance diff and trace for mid-sequence exploit tx
0x04c1826c909ad78ec5cdf772f14357c937883fcc003da6daea502fcc72a9b7ea
artifacts/root_cause/data_collector/iter_3/tx/8453/0x04c1826c909ad78ec5cdf772f14357c937883fcc003da6daea502fcc72a9b7ea/ -
[6] PRXVTStaking.sol source code
artifacts/root_cause/seed/8453/0xdac30a5e2612206e2756836ed6764ec5817e6fff/src/PRXVTStaking.sol -
[7] AgentTokenV2.sol source code
artifacts/root_cause/seed/8453/0xc2ff2e5aa9023b1bb688178a4a547212f4614bc0/src/virtualPersona/AgentTokenV2.sol -
[8] Orchestrator contract 0x7029... decompile
artifacts/root_cause/data_collector/iter_1/contract/8453/0x702980b1ed754c214b79192a4d7c39106f19bce9/decompile/0x702980b1ed754c214b79192a4d7c39106f19bce9-decompiled.sol -
[9] Helper contract 0xF3FE... decompile
artifacts/root_cause/data_collector/iter_1/contract/8453/0xF3FE57d25eF1A7E370F0f50a223Cf98a48DB410c/decompile/0xF3FE57d25eF1A7E370F0f50a223Cf98a48DB410c-decompiled.sol