Calculated from recorded token losses using historical USD prices at the incident time.
0xac6f716c57bbb1a4c1e92f0a9531019ea2ecfcaea67794bbd27115d400ae9b410x8befc1d90d03011a7d0b35b3a00ec50f8e014802Base0xd971fd39d9714d5eb1b54b931790170a0630f131Base0x4200000000000000000000000000000000000006Base0xba12222222228d8ba445958a75a0704d566bf2c8Base0x8bEfC1d90d03011a7d0b35B3a00eC50f8E014802 fronting an implementation MPRORewardStake at 0xd971fD39D9714d5eb1B54B931790170A0630f131.protocol_bug (missing access control on a value-bearing withdrawal function).In a single adversary-crafted transaction on Base, the attacker uses a Balancer Vault WETH flash loan to temporarily inflate the MPRO staking proxy's WETH balance and then calls a publicly callable unwrapWETH(uint256,address) function on the proxy. Because the implementation behind the proxy has no access control on this function and forwards ETH to an arbitrary recipient, the call converts both the pre-existing reward WETH pot and the flash-loaned WETH into native ETH, sends that ETH to an attacker-controlled helper contract, repays the flash loan, and leaves the residual ETH as attacker profit.
The underlying MPRORewardStake implementation exposes unwrapWETH(uint256,address) as an externally callable function that (a) checks only that the requested amount is positive and that the contract's WETH balance is at least that amount, (b) calls WETH9.withdraw(amount), and (c) transfers the resulting ETH to an arbitrary recipient address. With the staking proxy holding a shared reward pot in WETH, any unprivileged caller with temporary WETH liquidity (via a flash loan) can drain that pot to an arbitrary address.
30210274.0x8bEfC1d90d03011a7d0b35B3a00eC50f8E014802 is configured as a TransparentUpgradeableProxy delegating to MPRORewardStake at 0xd971fD39D9714d5eb1B54B931790170A0630f131.3.981326901636573675 WETH.0x4200000000000000000000000000000000000006 on Base with standard deposit/withdraw semantics.0xBA12222222228d8Ba445958a75a0704d566BF2C8 and exposes permissionless flash loans in WETH.0x5CC162c556092fE1d993b95D1b9E9CE58a11dBC9, and0x0c6A8c285d696d4D9b8dD4079a72a6460A4dA05F.This pre-state is reconstructed from seed metadata, traces, prestate balance diffs, and contract source/decompilation.
There is a single adversary-crafted transaction b that realizes the exploit from sigma_B:
0xac6f716c57bbb1a4c1e92f0a9531019ea2ecfcaea67794bbd27115d400ae9b410x5CC162c556092fE1d993b95D1b9E9CE58a11dBC90x0c6A8c285d696d4D9b8dD4079a72a6460A4dA05F100852657473363426325 WETH.unwrapWETH(104833984375000000000, 0x0c6A8c285d696d4D9b8dD4079a72a6460A4dA05F) via the proxy (delegatecall into MPRORewardStake).104.833984375 WETH (which includes the pre-existing 3.981326901636573675 WETH reward pot) into ETH.100852657473363426325 wei of ETH to re-mint WETH and repay the flash loan.Snippet – Seed transaction trace for exploit tx 0xac6f… (cast run -vvvvv)
0x0c6A8c285d696d4D9b8dD4079a72a6460A4dA05F::flashLoanCallback(...)
├─ BalancerVault.flashLoan(..., WETH9, 100852657473363426325, ...)
│ ├─ WETH9.transfer(0x8bEfC1d90d03011a7d0b35B3a00eC50f8E014802, 100852657473363426325)
│ └─ ...
├─ 0x8bEfC1d90d03011a7d0b35B3a00eC50f8E014802::unwrapWETH(104833984375000000000, 0x0c6A8c2...)
│ ├─ 0xd971fD39D9714d5eb1B54B931790170A0630f131::unwrapWETH(104833984375000000000, 0x0c6A8c2...) [delegatecall]
│ │ ├─ WETH9.balanceOf(0x8bEfC1d90d03011a7d0b35B3a00eC50f8E014802)
│ │ ├─ WETH9.withdraw(104833984375000000000)
│ │ └─ ETH sent to 0x0c6A8c2...
├─ WETH9.deposit{value: 100852657473363426325}()
├─ WETH9.transfer(0xBA12222222228d8Ba445958a75a0704d566BF2C8, 100852657473363426325)
└─ ETH residual sent to 0x5CC162c556092fE1d993b95D1b9E9CE58a11dBC9
profit0x5CC162c556092fE1d993b95D1b9E9CE58a11dBC93.980180099247068721 ETH (3,980,180,099,247,068,721 wei)3.981326901636573675 ETH (3,981,326,901,636,573,675 wei)The profit computation is grounded in the prestate-tracer balance diff for the exploit tx.
Snippet – Native balance deltas for exploit tx (prestate tracer)
{
"native_balance_deltas": [
{
"address": "0x4200000000000000000000000000000000000006",
"delta_wei": "-3981326901636573675"
},
{
"address": "0x5cc162c556092fe1d993b95d1b9e9ce58a11dbc9",
"delta_wei": "3980180099247068721"
},
{
"address": "0x4200000000000000000000000000000000000011",
"delta_wei": "1146620000000000"
},
{
"address": "0x420000000000000000000000000000000000001a",
"delta_wei": "17596017272"
},
{
"address": "0x4200000000000000000000000000000000000019",
"delta_wei": "164793487682"
}
]
}
The attacker EOA gains 3.980180099247068721 ETH, WETH9's ETH backing decreases by 3.981326901636573675 ETH, and the small difference is accounted for by system fee-collector addresses.
The following context is needed to understand the incident:
MPRO staking architecture on Base
TransparentUpgradeableProxy at 0x8bEfC1d90d03011a7d0b35B3a00eC50f8E014802 that delegates to MPRORewardStake at 0xd971fD39D9714d5eb1B54B931790170A0630f131.External dependencies: WETH9 and Balancer Vault
0x4200000000000000000000000000000000000006 is a standard wrapped-ETH contract with deposit() and withdraw(uint256) that burns WETH and releases the same amount of native ETH to the caller.0xBA12222222228d8Ba445958a75a0704d566BF2C8 provides permissionless flash loans in WETH on Base, allowing an unprivileged contract to borrow and repay WETH within a single transaction.These components together make it possible for an attacker to temporarily swell the proxy's WETH balance and then unwrap that balance into ETH.
The core vulnerability is that MPRORewardStake exposes a publicly callable unwrapWETH(uint256,address) function that performs only a balance check and then unconditionally unwraps WETH and sends the resulting ETH to an arbitrary recipient address. There is no access control tying this operation to stakers, admins, or protocol-controlled recipients.
The decompiled MPRORewardStake implementation at 0xd971fD39D9714d5eb1B54B931790170A0630f131 contains the following unwrapWETH logic:
Snippet – Decompiled unwrapWETH implementation (MPRORewardStake)
function unwrapWETH(uint256 amount, address recipient) public returns (bool) {
require(recipient == address(recipient));
require(amount > 0);
require(address(recipient) != address(0));
require(address(rewardToken) != address(0));
// Check contract's WETH balance
uint256 bal = rewardToken.balanceOf(address(this));
require(bal >= amount, "MPRORewardStake: Insufficient WETH balance");
// Unwrap WETH to ETH
rewardToken.withdraw(amount); // WETH9.withdraw(amount)
// Send ETH to arbitrary recipient
(bool success, ) = recipient.transfer(amount);
require(success, "MPRORewardStake: ETH transfer failed");
return true;
}
Key properties:
onlyOwner, onlyStaker, or similar modifier; any caller can invoke unwrapWETH through the proxy.rewardToken.balanceOf(address(this)) >= amount. It does not distinguish between long-term staking rewards and transient balances (such as flash-loaned WETH).recipient address with no restriction that it be the proxy owner, a staking rewards contract, or any other protocol-controlled account.As a result:
amount is less than or equal to the current WETH balance.amount that includes both the reward pot and the flash-loaned WETH.withdraw call converts this entire amount to ETH; the attacker can then repay the flash loan out of the same ETH and keep the residual as profit.rewardToken.balanceOf(address(this)) and is therefore fully exposed to this mechanism.0x8bEfC1d90d03011a7d0b35B3a00eC50f8E014802 – a TransparentUpgradeableProxy front-end that routes calls (including unwrapWETH) to the implementation.0xd971fD39D9714d5eb1B54B931790170A0630f131 – specifically the unwrapWETH(uint256,address) function.For the exploit to succeed, all of the following must hold:
rewardToken balance in WETH. In this incident, the relevant pot is 3.981326901636573675 WETH.MPRORewardStake at 0xd971fD39D9714d5eb1B54B931790170A0630f131 with the vulnerable unwrapWETH logic.rewardToken.balanceOf(address(this)) is at least the desired amount for unwrap.unwrapWETH(uint256,address) function must be callable by arbitrary addresses without role checks, and the recipient parameter must be unconstrained.rewardToken.balanceOf(address(this)) reflects legitimate, protocol-owned reward funds, which is not true under flash-loan adversaries.The adversary executes a single transaction that:
unwrapWETH(uint256,address) function on the proxy with an amount that includes both the existing reward pot and the flash-loaned WETH.WETH9.withdraw(amount) at a helper contract.Adversary cluster:
0xac6f716c….unwrapWETH via the proxy.Victim-related contracts:
0x8bEfC1d90d03011a7d0b35B3a00eC50f8E0148020xd971fD39D9714d5eb1B54B931790170A0630f1310x42000000000000000000000000000000000000060xBA12222222228d8Ba445958a75a0704d566BF2C80x29eb782b8707227fac7620ee7b3ab8c6a34f074b and user activity configure the staking system and fund the reward pot.3.981326901636573675 WETH as rewardToken.104.833984375 WETH) and the flash-loaned amount (100.852657473363426325 WETH).This pre-existing reward pot becomes the funding source for the attacker's profit when combined with the flash-loaned WETH.
0x3acfcb1d8fef75bdaf88c9f8a49043937d5ce664631f9b5e1e5f0880b6cc2a7726181727 on Base0x5CC162c556092fE1d993b95D1b9E9CE58a11dBC90x0c6A8c285d696d4D9b8dD4079a72a6460A4dA05F, which exposes methods that orchestrate the Balancer flash-loan and unwrapWETH call path.This establishes a reusable entry point the attacker later uses in the exploit transaction.
0xac6f716c57bbb1a4c1e92f0a9531019ea2ecfcaea67794bbd27115d400ae9b41302102740x5CC162c556092fE1d993b95D1b9E9CE58a11dBC90x0c6A8c285d696d4D9b8dD4079a72a6460A4dA05FFlow:
100852657473363426325 WETH (token 0x4200…0006).unwrapWETH(uint256,address) function with amount = 104833984375000000000 and recipient = 0x0c6A8c2….MPRORewardStake.unwrapWETH, which:
104.833984375 WETH (which holds because the balance is the sum of the pre-existing 3.981326901636573675 WETH reward pot and the 100.852657473363426325 WETH flash loan),WETH9.withdraw(104833984375000000000), burning that much WETH and causing 104.833984375 ETH to be sent to the proxy,100852657473363426325 wei of ETH into WETH via WETH9.deposit and transfers exactly that amount of WETH back to Balancer Vault, fully repaying the flash loan.3.981326901636573675 ETH is forwarded to the attacker EOA, which realizes a net profit of 3.980180099247068721 ETH after protocol-level fees.After the exploit, the proxy is upgraded to a new implementation at 0x8127D4532D0DA08C2DB6c36e18160d8343265b93.
0xf91d6c36def77da30431d6648e77fdd2c17ef51d7da9fdfcc95efdae0291083a0xFF8DaC673883f2BC40454c940f5f07DEe68424240x8bEfC1d90d03011a7d0b35B3a00eC50f8E014802unwrapWETH(1000, 0xFF8DaC673883f2BC40454c940f5f07DEe6842424) via the proxy.Snippet – Post-incident unwrapWETH attempt (debug_traceTransaction callTracer)
{
"from": "0xff8dac673883f2bc40454c940f5f07dee6842424",
"to": "0x8befc1d90d03011a7d0b35b3a00ec50f8e014802",
"input": "0xe16d9ce5...000003e8...",
"error": "execution reverted",
"revertReason": "MPRORewardStake: Amount must be 0",
"calls": [
{
"type": "DELEGATECALL",
"to": "0x8127d4532d0da08c2db6c36e18160d8343265b93",
"revertReason": "MPRORewardStake: Amount must be 0"
}
]
}
This shows that in the patched implementation, unwrapWETH reverts when called with a nonzero amount, effectively disabling the vulnerable withdrawal path that was abused in the incident.
3.981326901636573675 ETHDuring the exploit transaction:
3.981326901636573675 WETH is consumed as part of the 104.833984375 WETH unwrapped via unwrapWETH.WETH9.withdraw converts this WETH into ETH, which is then split between repaying the flash loan and the attacker profit.0x5CC162c556092fE1d993b95D1b9E9CE58a11dBC93.980180099247068721 ETHThe difference between WETH9's ETH loss and the attacker's ETH gain is paid to Base system fee-collector addresses and does not represent further protocol-user loss.
This report focuses on protocol-level impact:
It does not attempt to apportion the drained amount across individual staker wallets or external protocols; that per-staker attribution is out of scope for this ACT-focused root-cause report.
0xac6f716c57bbb1a4c1e92f0a9531019ea2ecfcaea67794bbd27115d400ae9b410x3acfcb1d8fef75bdaf88c9f8a49043937d5ce664631f9b5e1e5f0880b6cc2a770xf91d6c36def77da30431d6648e77fdd2c17ef51d7da9fdfcc95efdae0291083a0xac6f716c57bbb1a4c1e92f0a9531019ea2ecfcaea67794bbd27115d400ae9b41 (cast run with verbose call tree).MPRODoubleRewardAutoStake and decompiled MPRORewardStake at 0xd971fD39D9714d5eb1B54B931790170A0630f131, including the unwrapWETH(uint256,address) logic.0xf91d6c36def77da30431d6648e77fdd2c17ef51d7da9fdfcc95efdae0291083a, demonstrating the patched implementation's Amount must be 0 revert.