We do not have a reliable USD price for the recorded assets yet.
0x88610208c00f5d5ca234e45205a01199c87cb859f881e8b35297cba8325a54940xdac30a5e2612206e2756836ed6764ec5817e6fffBase0xc2ff2e5aa9023b1bb688178a4a547212f4614bc0Base0x702980b1ed754c214b79192a4d7c39106f19bce9Base0xf3fe57d25ef1a7e370f0f50a223cf98a48db410cBase0x43873934f4dfc12ccd51f6b0604b4178dae07303Base0xb263c3c5fc32f63a82d3f18b7fb5448d6fcbec0eBase0x7407f9bdc4140d5e284ea7de32a9de6037842f45Base0x000000000000000000000000000000000000deadOn 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.
PRXVTStaking (0xdac30a5e2612206e2756836ed6764ec5817e6fff) is a staking contract on Base that accepts a PRXVT-like ERC20 token AgentTokenV2 () and mints 1:1 as an ERC20 receipt token. Stakers earn rewards linearly over time, with rewards distributed at a configurable and a configurable burn fee applied on each call. The contract follows a Synthetix-style pattern where a global is accumulated over time and per-account mappings track how much of that reward stream each address has already accounted for.
0xc2ff2e5aa9023b1bb688178a4a547212f4614bc0stPRXVTrewardRateclaimReward()rewardPerTokenThe 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:
0x7407f9bdc4140d5e284ea7de32a9de6037842f45 — the adversary’s controlling externally owned account on Base.0x702980b1ed754c214b79192a4d7c39106f19bce9 — a custom contract that holds the staked stPRXVT, deploys helper contracts, and orchestrates repeated reward claims.0xF3FE57d25eF1A7E370F0f50a223Cf98a48DB410c, 0x43873934F4dfc12CCD51f6b0604B4178DAe07303, and 0xB263C3c5fc32f63A82D3F18b7fb5448d6Fcbec0e — short-lived contracts that temporarily receive the full stPRXVT balance, call into PRXVTStaking to 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.solFrom a security perspective, the key violated assumptions are:
balance - _totalStaked) is bypassed because the same underlying stake is counted repeatedly.The core vulnerability arises from the combination of:
userRewardPerTokenPaid[account] and rewards[account]) to track how much of the global reward stream has already been accrued; andstPRXVT) that is fully transferable, without transfer or transferFrom updating the reward accounting state for sender and receiver.In a correct Synthetix-style design, transferring staking receipts from address A to B would typically:
updateReward(A) and updateReward(B), fixing both addresses’ rewards and userRewardPerTokenPaid to the current rewardPerToken.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:
stake, withdraw, claimReward, or another function gated by updateReward.stPRXVT via a simple ERC20 transfer will:
balanceOf(account) in PRXVTStaking, anduserRewardPerTokenPaid[account] left at its default (typically zero),earned(account) will treat them as if they had been staked for the entire period since the reward distribution started.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.
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.
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.
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:
0x7407f9bdc4140d5e284ea7de32a9de6037842f45 with a large AgentTokenV2 balance and sufficient ETH for gas.0x702980b1ed754c214b79192a4d7c39106f19bce9 with no initial stPRXVT or reward token balance.0xdac30a5e2612206e2756836ed6764ec5817e6fff with a significant AgentTokenV2 balance in its reward pool.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:
e6d7db7e exploit transaction, the orchestrator’s AgentTokenV2 balance is zero. The EOA’s AgentTokenV2 balance decreases by 2.3e24 in the prepare() transaction as principal is staked.0x91d8e05b9ca9b708606878643310fa0385ae698c0c0802aace04ec6d396681f1, the balance diff shows:
-229742725742797866000000 (~2.2974e23),+22974272574279786600000 (~2.2974e22),+206768453168518079400000 (~2.067684531685180794e23).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:
0x7cf17521041f4370f79710b5106d4d8382bb12794fd06711764a649694b8a5a8 (prepare) moves 2.3e24 tokens from the EOA to PRXVTStaking and mints an equal stPRXVT position to the orchestrator.0xe1a6c6781990c68b099bf53e114820d502ca57e5cf3fafc9e50ab2227d7747a8, 0x91d8e05b9ca9b708606878643310fa0385ae698c0c0802aace04ec6d396681f1, and 0x04c1826c909ad78ec5cdf772f14357c937883fcc003da6daea502fcc72a9b7ea consistently show the orchestrator gaining roughly 2.06e23 AgentTokenV2 per exploit tx, while only a ~10% burn fee is paid.withdraw() tx 0x20094a7c6d7b5f201ebc545ae62efc6c6ef618a22f87fd39e35d4710b62d7d10 consolidates 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.
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:
stPRXVT balance via PRXVTStaking::transfer(orchestrator -> helper).execute(PRXVTStaking, AgentTokenV2, orchestrator).execute, calls PRXVTStaking::earned(helper) and PRXVTStaking::claimReward() to extract rewards for the entire historical stake.stPRXVT balance back from helper to orchestrator.This cycle is repeated with many fresh helper addresses per exploit transaction.
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:
stPRXVT stake moves from orchestrator to helper and back in each cycle.claimReward() is called with the helper as the beneficiary, burning ~10% to 0x000000000000000000000000000000000000dEaD and paying ~90% to the helper, which then remits it to the orchestrator.stPRXVT remains 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.
The ACT transaction_sequence_b is:
Tx 1 (prepare)
0x7cf17521041f4370f79710b5106d4d8382bb12794fd06711764a649694b8a5a8.0x7407... calls orchestrator.prepare(2.3e24) with selector 0xcb577480.artifacts/.../0x7cf175.../balance_diff.json) shows:
2.3e24 AgentTokenV2,2.3e24 AgentTokenV2,2.3e24 stPRXVT.Tx 2 (exploit)
0x91d8e05b9ca9b708606878643310fa0385ae698c0c0802aace04ec6d396681f1.0x7407... calls orchestrator.Unresolved_e6d7db7e(0x927c0) with selector 0xe6d7db7e.2.3e24 stPRXVT position to fresh helper contracts.execute(PRXVTStaking, AgentTokenV2, orchestrator), which issues earned(helper) and claimReward() calls to PRXVTStaking and then returns the principal stPRXVT and AgentTokenV2 rewards to the orchestrator.~2.2974e23 AgentTokenV2,~2.2974e22 AgentTokenV2,~2.067684531685180794e23 AgentTokenV2.Tx 3 (withdraw)
0x20094a7c6d7b5f201ebc545ae62efc6c6ef618a22f87fd39e35d4710b62d7d10.0x7407... calls orchestrator.withdraw() with selector 0x3ccfd60b.withdraw() function:
msg.sender == 0x7407....PRXVTStaking.withdraw(amount) to redeem stPRXVT back into underlying tokens for 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.
The adversary’s strategy is:
0x7407... to stake a very large AgentTokenV2 position into PRXVTStaking under the orchestrator’s address via prepare(), creating a large stPRXVT position controlled by the orchestrator.stPRXVT stake through a chain of fresh helper contracts in exploit transactions, with each helper:
stPRXVT balance,earned() and claimReward() to collect a full historical reward payout,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 cluster:
0x7407f9bdc4140d5e284ea7de32a9de6037842f45
prepare(), e6d7db7e(), and withdraw() transactions to the orchestrator.require(msg.sender == 0x7407...) for all critical functions, proving that this EOA controls the orchestrator and receives final withdrawals.0x702980b1ed754c214b79192a4d7c39106f19bce9
stPRXVT stake, deploys helper contracts, and orchestrates repeated transfers and reward claims.require(msg.sender == 0x7407...) and calls into PRXVTStaking and AgentTokenV2 as described.0xF3FE57d25eF1A7E370F0f50a223Cf98a48DB410c
execute(address,address,address) that:
PRXVTStaking::earned(helper),PRXVTStaking::claimReward(),0x88610208....0x43873934F4dfc12CCD51f6b0604B4178DAe07303
0x04c1826c....2.3e24 stPRXVT position, calls earned() and claimReward(), receives reward tokens, and returns stPRXVT to the orchestrator, as shown in the mid-sequence trace.0xB263C3c5fc32f63A82D3F18b7fb5448d6Fcbec0e
0x4387..., deployed and used within 0x04c1826c....Victim candidates:
PRXVTStaking (0xdac30a5e2612206e2756836ed6764ec5817e6fff)
stPRXVT, and pays out AgentTokenV2 rewards.AgentTokenV2 (PRXVT-like reward token) (0xc2ff2e5aa9023b1bb688178a4a547212f4614bc0)
Adversary staking setup and priming
0x7cf17521041f4370f79710b5106d4d8382bb12794fd06711764a649694b8a5a8, block 40230817.0x7407... calls orchestrator.prepare(2.3e24).2.3e24 AgentTokenV2 from the EOA to PRXVTStaking and mints an equal stPRXVT position to the orchestrator.artifacts/.../0x7cf175.../balance_diff.json) and PRXVTStaking stake() logic confirm the movement of principal and creation of the staked position.Adversary exploit execution with multi-helper reward claims
0x88610208c00f5d5ca234e45205a01199c87cb859f881e8b35297cba8325a5494, block 40230828.0x91d8e05b9ca9b708606878643310fa0385ae698c0c0802aace04ec6d396681f1, block 40230830.0x04c1826c909ad78ec5cdf772f14357c937883fcc003da6daea502fcc72a9b7ea, block 40230956.e6d7db7e(tx) from the EOA to 0x7029...:
stPRXVT position to a fresh helper contract.PRXVTStaking::earned(helper) and claimReward(), receiving rewards for the entire historical stake.stPRXVT back to the orchestrator and forwards rewards to it.e6d7db7e transactions, extracting large cumulative rewards while leaving the principal stake intact.artifacts/root_cause/seed/8453/0x88610208.../trace.cast.log and mid-sequence trace artifacts/root_cause/data_collector/iter_3/tx/8453/0x04c1826c.../trace.cast.log show repeated sequences of:
PRXVTStaking::balanceOf,PRXVTStaking::transfer(orchestrator -> helper),helper::execute(PRXVTStaking, AgentTokenV2, orchestrator),PRXVTStaking::earned(helper),PRXVTStaking::claimReward(),PRXVTStaking::transfer(helper -> orchestrator).0xe1a6c6..., 0x91d8e0..., and 0x04c1826c... show consistent patterns where PRXVTStaking loses ~2.3e23 AgentTokenV2 per helper cycle, the burn address gains ~10% of that amount, and the orchestrator gains ~90%.Adversary profit realization and withdrawal
0x20094a7c6d7b5f201ebc545ae62efc6c6ef618a22f87fd39e35d4710b62d7d10, block 40231078.0x7407... calls orchestrator.withdraw().withdraw() function transfers all AgentTokenV2 held by the orchestrator to the EOA and calls PRXVTStaking::withdraw() to redeem the orchestrator’s stPRXVT back into underlying tokens, consolidating both principal and extracted rewards into the EOA.0x3ccfd60b) and the withdraw() decompile show it reading balances, transferring AgentTokenV2 to the EOA, and calling PRXVTStaking.withdraw(amount). Combined with prior balance diffs, this is sufficient to attribute the accumulated profit to the EOA–orchestrator cluster.The primary impacted asset is AgentTokenV2 on Base:
0xc2ff2e5aa9023b1bb688178a4a547212f4614bc0).2.06e23 tokens per e6d7db7e exploit transaction across dozens of such transactions, plus a permanent burn of roughly 2.3e22 tokens per helper cycle to the dead address 0x000000000000000000000000000000000000dEaD.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.3e23 AgentTokenV2 removed from PRXVTStaking per exploit transaction,0x0000...dEaD,This is sufficient to classify the incident as a high-severity protocol bug with significant reward pool depletion.
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