All incidents

Local Traders Price Takeover

Share
May 23, 2023 11:39 UTCAttackLoss: 32,650,797 LCTPending manual check5 exploit txWindow: 15m 37s
Estimated Impact
32,650,797 LCT
Label
Attack
Exploit Tx
5
Addresses
3
Attack Window
15m 37s
May 23, 2023 11:39 UTC → May 23, 2023 11:54 UTC

Exploit Transactions

TX 1BSC
0x57b589f631f8ff20e2a89a649c4ec2e35be72eaecf155fdfde981c0fec2be5ba
May 23, 2023 11:39 UTCExplorer
TX 2BSC
0xbea605b238c85aabe5edc636219155d8c4879d6b05c48091cf1f7286bd4702ba
May 23, 2023 11:39 UTCExplorer
TX 3BSC
0x49a3038622bf6dc3672b1b7366382a2c513d713e06cb7c91ebb8e256ee300dfb
May 23, 2023 11:39 UTCExplorer
TX 4BSC
0xf7d558407b6e1259d6bdf27bb9e99d58dced001dd4e782c0dc97eb9e70f2237d
May 23, 2023 11:52 UTCExplorer
TX 5BSC
0x042b8dc879fa193acc79f55a02c08f276eaf1c4f7c66a33811fce2a4507cea63
May 23, 2023 11:54 UTCExplorer

Victim Addresses

0xce3e12bd77dd54e20a18cb1b94667f3e697bea06BSC
0x303554d4d8bd01f18c6fa4a8df3ff57a96071a41BSC
0x5c65badf7f97345b7b92776b22255c973234efe7BSC

Loss Breakdown

32,650,797LCT

Similar Incidents

Root Cause Analysis

Local Traders Price Takeover

1. Incident Overview TL;DR

Local Traders on BNB Smart Chain exposed an ACT exploit because its live-price proxy at 0x303554d4d8bd01f18c6fa4a8df3ff57a96071a41 could be reinitialized by any non-admin caller. In block sequence 28460898 through 28461208, one unprivileged EOA first seized the proxy owner/admin slots, then set the quoted LCT price to 1 wei, then called LCTExchange.buyTokens() on 0xce3e12bd77dd54e20a18cb1b94667f3e697bea06 to buy 32,650,797 LCT for 32,650,797 wei, and finally dumped the tokens through PancakeRouter for 379.322744715267840729 BNB. The root cause is a constructor-style initializer, storeConstructor(address), left externally reachable behind a TransparentUpgradeableProxy, which let arbitrary callers overwrite the proxy storage that governed pricing.

2. Key Background

The ACT pre-state is BSC block 28460897, immediately before tx 0x57b589f631f8ff20e2a89a649c4ec2e35be72eaecf155fdfde981c0fec2be5ba entered block 28460898. At that point:

  • LCTExchange at 0xce3e12bd77dd54e20a18cb1b94667f3e697bea06 still held 32,650,798.7462 LCT.
  • The exchange trusted a live-price interface behind proxy 0x303554d4d8bd01f18c6fa4a8df3ff57a96071a41.
  • The proxy delegated to unverified implementation 0x312dc37075646c7e0dba21df5bdfe69e76475fdc.
  • The LCT/WBNB Pancake pool still had enough liquidity to absorb a full dump.

The verified exchange logic is simple and fully trusts the external price:

function buyTokens() external payable returns (uint256, uint256) {
    require(msg.value > 0, "Send ETH to buy some tokens");
    uint256 tokenAmount2 = msg.value / getLivePriceFromInheritance();
    uint256 tokenAmount = tokenAmount2 * 1e18;
    require(token.balanceOf(address(this)) >= tokenAmount, "Vendor contract has not enough tokens");
    bool sent = token.transfer(msg.sender, tokenAmount);
    require(sent, "Failed to transfer token to user");
}

Because the proxy is a standard TransparentUpgradeableProxy, non-admin callers reach the implementation through fallback delegation. That matters here because the implementation contained state-mutating logic that behaved like a constructor but remained callable after deployment.

3. Vulnerability Analysis & Root Cause Summary

This is an ATTACK root cause, not a pure MEV reordering event. The broken invariant is that only the intended Local Traders operator should be able to initialize or modify the live-price state consumed by LCTExchange.buyTokens(). Instead, the unverified implementation at 0x312dc37075646c7e0dba21df5bdfe69e76475fdc exposed storeConstructor(address) through the proxy, and that function rewrote proxy storage with caller-controlled values and a reset staticTokenPrice. Once the attacker became the stored owner, selector 0x925d400c let the attacker set staticTokenPrice to any value, including 1. From that point onward, the exchange price path was fully attacker-controlled, so buyTokens() converted wei directly into whole-token units at a 1:1 rate. The exploit remained permissionless as long as the exchange still held LCT inventory and downstream Pancake liquidity remained available for the unwind.

4. Detailed Root Cause Analysis

The decisive code-level breakpoint is the implementation behavior decoded from runtime bytecode:

// Behavior reconstructed from implementation 0x312dc37075646c7e0dba21df5bdfe69e76475fdc
function storeConstructor(address aggregator) external {
    slot0 = aggregator;        // aggregator
    slot1 = msg.sender;        // ltcAdmin
    slot2 = msg.sender;        // ltcOwner
    slot3 = 46280000000000;    // staticTokenPrice
}

function setStaticPrice(uint256 newPrice) external onlyOwner {
    slot3 = newPrice;
}

function getTokenPrice() external view returns (uint256) {
    return slot3;
}

The first exploit tx, 0x57b589f631f8ff20e2a89a649c4ec2e35be72eaecf155fdfde981c0fec2be5ba, called proxy 0x303554d4d8bd01f18c6fa4a8df3ff57a96071a41 with selector 0xb5863c10. The collector trace shows the proxy delegatecalling storeConstructor(address), and the storage writes line up with the decoded bytecode: slot 1 and slot 2 become the caller EOA, and slot 3 resets to 46280000000000. That is the point where the attacker seizes both the live-price owner and admin roles in proxy storage.

The second exploit tx, 0xbea605b238c85aabe5edc636219155d8c4879d6b05c48091cf1f7286bd4702ba, immediately used selector 0x925d400c against the same proxy. The trace shows another delegatecall into the same implementation and an SSTORE of 1 into slot 3. After that call, getTokenPrice() returned 1, so the exchange's price oracle was no longer a market input; it was direct attacker input.

The third exploit tx, 0x49a3038622bf6dc3672b1b7366382a2c513d713e06cb7c91ebb8e256ee300dfb, called buyTokens() with 32650797 wei. Because buyTokens() computes tokenAmount = (msg.value / price) * 1e18, the corrupted price produced exactly 32,650,797 * 1e18 units of LCT. The collector trace records the transfer:

0xcE3e12bD77DD54E20a18cB1B94667F3E697bea06::buyTokens{value: 32650797}()
  0x312DC37075646c7e0DBA21DF5BdFe69E76475fdc::getTokenPrice() [delegatecall]
  LocalTraders::transfer(attacker, 32650797000000000000000000)
  emit TokensPurchased(attacker, LCT, 32650797000000000000000000, 1)

The exchange started with 32,650,798.7462 LCT and, after the purchase, held only 1.7462 LCT. The exploit therefore did not mint or spoof tokens; it drained pre-funded exchange inventory by corrupting the pricing dependency.

Monetization was straightforward because the attacker already controlled the purchased inventory. The historical sender emitted three approval transactions between the buy and the dump, but only the PancakeRouter approval at tx 0xf7d558407b6e1259d6bdf27bb9e99d58dced001dd4e782c0dc97eb9e70f2237d is necessary for the minimal ACT path. The final swap tx, 0x042b8dc879fa193acc79f55a02c08f276eaf1c4f7c66a33811fce2a4507cea63, sold the full 32,650,797 LCT into the LCT/WBNB pair and unwrapped WBNB into native BNB:

PancakeRouter::swapExactTokensForETHSupportingFeeOnTransferTokens(32650797000000000000000000, 0, [LCT, WBNB], attacker, ...)
  0x64b266Cd63fF3239E6491d6c2c58A5B8552c8724::swap(0, 379322744715267840729, PancakeRouter, 0x)
  WBNB::withdraw(379322744715267840729)
  attacker::fallback{value: 379322744715267840729}()

5. Adversary Flow Analysis

The adversary strategy was a single-chain, multi-transaction oracle takeover followed by underpriced purchase and AMM monetization.

  1. Tx 0x57b589f631f8ff20e2a89a649c4ec2e35be72eaecf155fdfde981c0fec2be5ba at block 28460898: the attacker EOA 0xd771dfa8fa59bd2d1251a0481fca0cf216276dd7 reinitialized the proxy-backed live-price contract and became the stored owner/admin.
  2. Tx 0xbea605b238c85aabe5edc636219155d8c4879d6b05c48091cf1f7286bd4702ba at block 28460899: the attacker set staticTokenPrice = 1.
  3. Tx 0x49a3038622bf6dc3672b1b7366382a2c513d713e06cb7c91ebb8e256ee300dfb at block 28460900: the attacker paid 32650797 wei to buyTokens() and received 32650797000000000000000000 LCT.
  4. Txs 0xb50b100f139d5ea71f1bb0187271195cf0ff2d1b714dc001d1cc436bb6643151, 0xf7d558407b6e1259d6bdf27bb9e99d58dced001dd4e782c0dc97eb9e70f2237d, and 0x0611accca2dd8a66906601768bf3843a281d6049673573ffc1c440bb2d4acece: the historical sender approved routers; only the PancakeRouter approval is needed in a minimal reproduction.
  5. Tx 0x042b8dc879fa193acc79f55a02c08f276eaf1c4f7c66a33811fce2a4507cea63 at block 28461208: the attacker dumped the full LCT balance through PancakeRouter and received 379322744715267840729 wei of BNB.

The adversary-related account attribution is deterministic: the same EOA submitted the full nonce-ordered exploit sequence, received the stolen LCT from the exchange, approved the routers, and received the BNB payout from the final router unwind.

6. Impact & Losses

The immediate victim was LCTExchange, which lost 32650797000000000000000000 raw units of LCT, or 32,650,797 LCT at 18 decimals. The exchange inventory fell from 32,650,798.7462 LCT to 1.7462 LCT after the underpriced purchase. The broader Local Traders system was also compromised because the live-price proxy lost exclusive ownership and could no longer provide trustworthy pricing to the exchange.

The attacker realized the value by swapping the stolen LCT into the LCT/WBNB Pancake pool for 379.322744715267840729 BNB. Using the observed sender balance before nonce 0 (0.99655 BNB) and after nonce 6 (379.890044025235189932 BNB), and subtracting total gas of 0.42925069 BNB across the seven observed exploit-related transactions, the realized profit was 378.893494025235189932 BNB.

7. References

  • BSC tx 0x57b589f631f8ff20e2a89a649c4ec2e35be72eaecf155fdfde981c0fec2be5ba: proxy reinitializer call to 0x303554d4d8bd01f18c6fa4a8df3ff57a96071a41.
  • BSC tx 0xbea605b238c85aabe5edc636219155d8c4879d6b05c48091cf1f7286bd4702ba: owner-only static-price setter call to the same proxy.
  • BSC tx 0x49a3038622bf6dc3672b1b7366382a2c513d713e06cb7c91ebb8e256ee300dfb: LCTExchange.buyTokens() purchase trace and exchange inventory drain.
  • BSC tx 0x042b8dc879fa193acc79f55a02c08f276eaf1c4f7c66a33811fce2a4507cea63: PancakeRouter unwind trace and balance diffs showing BNB payout.
  • LCTExchange at 0xce3e12bd77dd54e20a18cb1b94667f3e697bea06: verified buyTokens() logic.
  • Live-price proxy 0x303554d4d8bd01f18c6fa4a8df3ff57a96071a41 and implementation 0x312dc37075646c7e0dba21df5bdfe69e76475fdc: decoded behavior from bytecode summary.
  • LCT token 0x5c65badf7f97345b7b92776b22255c973234efe7: verified ERC-20 used for the drained inventory and final AMM sale.