We do not have a reliable USD price for the recorded assets yet.
0x31d3231cda62c0b7989b488ca747245676a32d81BSCGROKD’s staking reward proxy at 0x31d3231cda62c0b7989b488ca747245676a32d81 allowed any caller to overwrite pool 0’s reward tuple and then harvest rewards computed from that attacker-written state. In tx 0x383dbb44a91687b2b9bbd8b6779957a198d114f24af662776f384569b84fc549, the adversary bought GROKD, added GROKD/WBNB liquidity, and deposited exactly 1 LP token into the proxy. Fourteen blocks later, tx 0x8293946b5c88c4a21250ca6dc93c6d1a695fb5d067bb2d4aed0a11bd5af1fb32 called selector 0xd2beb00a, changed the reward parameter from 187500000000000000 to 969437458174368938037660232, and then called reward() to transfer 1346001915309486235668340 GROKD to the attacker-controlled helper contract.
The root cause is a direct state-integrity failure in the staking implementation behind the proxy. Recovered implementation code at 0xec61196d3e2ae276eecf070110075118abd1f63e shows that selector 0xd2beb00a is a public setter over the pool tuple with no owner or role check, while reward() later reloads that same tuple to calculate payout. Any unprivileged actor able to stake a positive LP amount could reproduce the same drain while the proxy retained GROKD inventory.
GROKD is a BEP-20 token at 0xa4133fed73ea3361f2f928f98313b1e1e5049612. Its verified source creates Pancake pair 0x8af65d9114dfccd050e7352d77eec98f40c42cfd against WBNB and applies 3.5% buy and sell fees in . That token-side behavior explains the taxed swaps and why the first attacker transaction mints only LP tokens after adding liquidity.
_update()2.852666716250696018The reward system itself is unverified on BscScan. Users interact with ERC1967 proxy 0x31d3231cda62c0b7989b488ca747245676a32d81, which delegates to implementation 0xec61196d3e2ae276eecf070110075118abd1f63e. Because source was unavailable, the auditor recovered Solidity decompilation, Yul decompilation, and disassembly, and those artifacts are necessary to prove the internal storage layout used by both the unrestricted setter and reward().
The relevant public surface is deposit(address,uint256), userInfo(address), poolInfo(uint256), lastRewardBlock(), reward(), and undocumented selector 0xd2beb00a. The attacker used PancakeSwap for setup and public proxy methods for execution. No privileged role, private key compromise, or hidden off-chain data is required.
This incident is an ATTACK-class invariant break caused by attacker-controlled mutation of reward configuration. The critical invariant is that pool reward parameters must remain protocol-controlled and must not be writable by arbitrary callers. Recovered victim code shows that selector 0xd2beb00a violates that invariant. In the recovered Solidity output, Unresolved_d2beb00a(uint256,uint256,uint256,uint256) checks only that the pool index is in range and then writes three consecutive storage words at keccak256(7) + 3*pid, +1, and +2. No msg.sender comparison, owner-slot read, or authorization branch exists in that function.
The recovered code also shows how the overwritten storage becomes exploitable payout state. poolInfo(uint256) reads the same three-word tuple from the same storage base, and reward() later reloads that tuple during reward calculation. The seed trace confirms that immediately after the overwrite, reward() transfers 1346001915309486235668340 GROKD to the attacker and advances lastRewardBlock() to the exploit block. Because payout logic trusts mutable storage that any caller can rewrite, even a 1 LP position is enough to claim incident-scale rewards from protocol inventory.
Recovered victim code is the decisive evidence. In the recovered Solidity decompilation, selector 0xd2beb00a appears as:
function Unresolved_d2beb00a(uint256 arg0, uint256 arg1, uint256 arg2, uint256 arg3) public {
require(arg0 < store_a);
var_b = 0x07;
storage_map_b[(0x03 * arg0) + keccak256(var_b)] = var_g;
storage_map_c[(0x03 * arg0) + keccak256(var_b)] = var_h;
storage_map_d[(0x03 * arg0) + keccak256(var_b)] = var_i;
}
The corresponding Yul recovery shows the same write pattern directly:
mstore(0, 0x07)
sstore(add(0, add(mul(0x03, calldataload(0x04)), sha3(0, 0x20))), mload(add(mload(0x40), 0)))
sstore(add(0x01, add(mul(0x03, calldataload(0x04)), sha3(0, 0x20))), mload(add(mload(0x40), 0x20)))
sstore(add(0x02, add(mul(0x03, calldataload(0x04)), sha3(0, 0x20))), mload(add(mload(0x40), 0x40)))
This absence of authorization is meaningful because privileged functions in the same recovered implementation do show explicit permission checks. For example, transferOwnership(address) contains an owner comparison before updating the owner slot. That contrast demonstrates that the decompiler does recover access-control logic when it exists, and no such logic exists in 0xd2beb00a.
poolInfo(uint256) reads the same pool tuple from the same mapping base:
function poolInfo(uint256 arg0) public view returns (bytes memory) {
require(arg0 < store_a);
var_a = 0x07;
return abi.encodePacked(
storage_map_aj[arg0 * 0x03],
storage_map_ak[(arg0 * 0x03) + keccak256(var_a)],
storage_map_al[(arg0 * 0x03) + keccak256(var_a)]
);
}
reward() then reloads that tuple for the active pool, again by anchoring to storage slot 0x07 and reading offsets +0, +1, and +2:
mstore(0, 0x07)
mstore(0xa0, sload(add(add(mul(0x03, sload(0x05)), sha3(0, 0x20)), 0)))
mstore(0xc0, sload(add(add(mul(0x03, sload(0x05)), sha3(0, 0x20)), 0x01)))
mstore(0xe0, sload(add(add(mul(0x03, sload(0x05)), sha3(0, 0x20)), 0x02)))
The second seed trace shows this logic executing live. poolInfo(0) first returns (37565180, 37997180, 187500000000000000). The attacker then calls d2beb00a, which overwrites storage slot 0xa66cc928b5edb82af9bd49922954155ab7b0942694bea4ce44661d9a8736c68a. Immediately afterward, reward() transfers 1346001915309486235668340 GROKD from the proxy to the attacker and updates lastRewardBlock() from 37622464 to 37622478. The exploit is therefore deterministic: the contract computes rewards from a tuple that any caller can rewrite.
The adversary flow is short and completely permissionless.
In tx 0x383dbb44a91687b2b9bbd8b6779957a198d114f24af662776f384569b84fc549 on BSC block 37622464, EOA 0x2f5181aaab8776d8c67d4505ff47213391908b34 sends 0.2 BNB to helper contract 0x98aa55463d2d4d957a53e9f8cc1efd39c4003a74. The helper swaps 0.1 BNB for GROKD through router 0x10ed43c718714eb63d5aa57b78b54704e256024e, adds GROKD/WBNB liquidity to pair 0x8af65d9114dfccd050e7352d77eec98f40c42cfd, receives 2.852666716250696018 LP tokens, approves the staking proxy, and deposits exactly 1e18 LP units. The trace shows the proxy’s LP balance increasing by 1000000000000000000, and later userInfo(attacker).amount reads back as 1000000000000000000.
In tx 0x8293946b5c88c4a21250ca6dc93c6d1a695fb5d067bb2d4aed0a11bd5af1fb32 on block 37622478, the same helper contract reads lastRewardBlock(), poolInfo(0), and userInfo(attacker), then calls selector 0xd2beb00a with pool id 0, the existing start and end values, and an attacker-chosen reward parameter 969437458174368938037660232. The proxy delegates the call to the implementation, which performs the observed storage write.
After the overwrite, the helper calls reward(). The trace records GROKD::transfer(attacker, 1346001915309486235668340) from the proxy and shows the proxy’s GROKD balance dropping to 592991783799382637245, below the 1,000 GROKD dust threshold. No additional contracts, hidden calldata, or privileged entrypoints are required beyond these two transactions.
The measurable loss is a direct drain of the staking proxy’s GROKD reward inventory.
{
"token_symbol": "GROKD",
"amount": "1346001915309486235668340",
"decimal": 18
}
Before the harvest transaction, the proxy held 1346594907093285618305585 GROKD. After the harvest, it held only 592991783799382637245 GROKD. Nearly the entire reward inventory was therefore removed in a single permissionless harvest, leaving only dust. The impact generalizes to any unprivileged actor able to acquire and stake a positive LP amount while selector 0xd2beb00a remains callable and the proxy still holds GROKD inventory.
0x383dbb44a91687b2b9bbd8b6779957a198d114f24af662776f384569b84fc5491e180x8293946b5c88c4a21250ca6dc93c6d1a695fb5d067bb2d4aed0a11bd5af1fb32d2beb00a overwrite, reward() call, GROKD transfer, and checkpoint advance0xa4133fed73ea3361f2f928f98313b1e1e50496120xec61196d3e2ae276eecf070110075118abd1f63e0xec61196d3e2ae276eecf070110075118abd1f63e0xec61196d3e2ae276eecf070110075118abd1f63e