All incidents

Carrot Public Hook Backdoor

Share
Oct 10, 2022 12:53 UTCAttackLoss: 31,318.18 USDTPending manual check1 exploit txWindow: Atomic
Estimated Impact
31,318.18 USDT
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Oct 10, 2022 12:53 UTC → Oct 10, 2022 12:53 UTC

Exploit Transactions

TX 1BSC
0xa624660c29ee97f3f4ebd36232d8199e7c97533c9db711fa4027994aa11e01b9
Oct 10, 2022 12:53 UTCExplorer

Victim Addresses

0xcff086ead392ccb39c49ecda8c974ad5238452acBSC
0xf34c9a6aaac94022f96d4589b73d498491f817faBSC

Loss Breakdown

31,318.18USDT

Similar Incidents

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

  1. Seed exploit transaction 0xa624660c29ee97f3f4ebd36232d8199e7c97533c9db711fa4027994aa11e01b9 on BNB Chain block 22055612, including opcode-level trace and balance diff.
  2. Verified Carrot token source for 0xcff086ead392ccb39c49ecda8c974ad5238452ac, especially transReward(bytes), _beforeTransfer, and transferFrom.
  3. Decompiled helper contract for 0x6863b549bf730863157318df4496ed111adfa64f, especially selector 0xbf699b4b.
  4. Helper setup transaction history for 0x6863b549bf730863157318df4496ed111adfa64f, showing deployer-owned initialization and token registration.
  5. Wrapper decompilation for 0x5575406ef6b15eec1986c412b9fbe144522c45ae, showing delegation to implementation 0xc422f23102bf2eeb237a8f7789be6a6be3e4a251.
  6. Implementation creation transaction 0x1379263a554e6969d1d3d06061d1212aa3aef62def30258be70924cdf92b0494, tying the exploit logic to attacker EOA 0xd11a93a8db5f8d3fb03b88b4b24c3ed01b8a411c.