All incidents

Telcoin Wallet Reinitialization Drain

Share
Dec 25, 2023 17:23 UTCAttackLoss: 141,755,751.88 TEL, 4,249.98 MATICPending manual check1 exploit txWindow: Atomic
Estimated Impact
141,755,751.88 TEL, 4,249.98 MATIC
Label
Attack
Exploit Tx
1
Addresses
1
Attack Window
Atomic
Dec 25, 2023 17:23 UTC → Dec 25, 2023 17:23 UTC

Exploit Transactions

TX 1Polygon
0x35f50851c3b754b4565dc3e69af8f9bdb6555edecc84cf0badf8c1e8141d902d
Dec 25, 2023 17:23 UTCExplorer

Victim Addresses

0x56bcadff30680ebb540a84d75c182a5dc61981c0Polygon

Loss Breakdown

141,755,751.88TEL
4,249.98MATIC

Similar Incidents

Root Cause Analysis

Telcoin Wallet Reinitialization Drain

1. Incident Overview TL;DR

On Polygon block 51546496, attacker EOA 0xdb4b84f0e601e40a02b54497f26e03ef33f3a5b7 used a single transaction, 0x35f50851c3b754b4565dc3e69af8f9bdb6555edecc84cf0badf8c1e8141d902d, to re-initialize Telcoin wallet proxies that had already been deployed and funded. Each successful re-initialization replaced the proxy's beacon with attacker contract 0x10e5c8d3537856f141272e1c39befdab4dd8bde0, then delegatecalled attacker-controlled logic that transferred TEL and, in some cases, native POL/MATIC out of the victim wallet.

The root cause is a storage-layout collision in the wallet proxy architecture. The proxy implementation 0x10d0e9755c67ab37089acb4f51e8b4ee407fe853 records its initialization state in storage slot 0, but the delegatecalled wallet implementation 0x50b18f323237e38fc308134efff3a83163572c85 also writes the wallet state word to slot 0. The first deployment therefore clobbers the proxy's Initializable bookkeeping back to a value whose low byte is zero, allowing any unprivileged caller to run initialize(address,bytes) again later. The attacker exploited that reopened initializer to swap beacons and drain 49 funded wallets.

2. Key Background

Telcoin wallets are deployed as beacon-style proxies. A representative wallet, 0x56bcadff30680ebb540a84d75c182a5dc61981c0, was created in transaction 0x1a31cb6f417d30fe8769328b3412bfb0d70247a82009ef28dfab5730c82acd05 by factory 0x4cf353974155a600f87d73222f9df0548e2b740e. Its initial beacon was the legitimate Telcoin beacon 0x3f443c31dab3dab882f9fe621f97b74c3c14837d.

The verified wallet implementation at 0x50b18f323237e38fc308134efff3a83163572c85 is an assembly-heavy wallet contract whose initialize(uint256,address,address,address) entrypoint sets owner and gatekeeper roles and writes the packed wallet state to slot 0. It also uses slot 0xaa as its own one-time initialization flag.

The proxy implementation at 0x10d0e9755c67ab37089acb4f51e8b4ee407fe853 is not source-verified, but its runtime bytecode and traces show standard beacon-proxy behavior. It exposes initialize(address,bytes), calls implementation() on the supplied beacon, writes the EIP-1967 beacon slot 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50, and delegatecalls the caller-supplied initialization bytes. The runtime bytecode also contains the OpenZeppelin-style revert strings Initializable: contract is already initialized, ERC1967: new beacon is not a contract, and ERC1967: beacon implementation is not a contract.

3. Vulnerability Analysis & Root Cause Summary

This incident is an ATTACK, not a MEV-only opportunity. The vulnerable condition is that a supposedly one-time proxy initializer remained callable after deployment because proxy state and delegated wallet state were not storage-isolated. During the original deployment flow, the proxy briefly stored its own initialization marker in slot 0, then delegatecalled the wallet implementation, which overwrote slot 0 with the wallet's packed state word. That state word ends in 00, so the proxy no longer remembered that it had been initialized. Once that happened, any unprivileged caller could call initialize(address,bytes) again, supply an attacker-controlled beacon, and run arbitrary delegatecall logic in the victim wallet's storage context. The concrete invariant failure is that a deployed wallet proxy should have a permanently locked beacon and initialization status, but this design let a public call replace both.

4. Detailed Root Cause Analysis

The verified wallet implementation makes the slot collision explicit. Its initialization routine stores the wallet state at slot 0:

function initialize() {
    let init := sload(0xaa)
    if eq(init, 1) { invalid() }
    sstore(0xaa, 1)

    let _state := calldataload(0x4)
    let _gatekeeperA := calldataload(0x24)
    let _gatekeeperB := calldataload(0x44)
    let _owner := calldataload(0x64)

    sstore(_owner, 1)
    sstore(0, _state)
    sstore(_gatekeeperA, 3)
    sstore(_gatekeeperB, 3)
}

The deployment trace for 0x1a31cb6f417d30fe8769328b3412bfb0d70247a82009ef28dfab5730c82acd05 shows the proxy initializer and the delegated wallet initializer writing the same slot in sequence:

0x10d0E9755C67Ab37089aCb4f51E8b4eE407FE853::initialize(...) [delegatecall]
  emit BeaconUpgraded(param0: 0x3F443C31dAb3DAB882f9Fe621F97b74c3C14837D)
  Wallet::fallback(...) [delegatecall]
    storage changes:
      @ 170: 0 -> 1
      @ 0: 257 -> 0xa260d3506d4bae68003000000000000000000000000000000000000000000000
  storage changes:
    @ 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50: 0 -> 0x0000000000000000000000003f443c31dab3dab882f9fe621f97b74c3c14837d
    @ 0: 0 -> 257

This trace is the decisive breakpoint. The proxy first writes slot 0 to decimal 257 (0x0101), which is consistent with OpenZeppelin Initializable bookkeeping for _initialized = 1 and _initializing = true. The delegatecalled wallet initializer then overwrites that same slot with the Telcoin wallet state word 0xa260...0000, whose low byte is 0x00. The proxy therefore loses the information that initialization already happened.

Direct RPC reads confirm the vulnerable pre-state that remained on-chain for the representative wallet 0x56bcadff30680ebb540a84d75c182a5dc61981c0 immediately before the exploit. At block 51546495, the validator observed:

  • Beacon slot: 0x0000000000000000000000003f443c31dab3dab882f9fe621f97b74c3c14837d
  • Slot 0: 0xa260d3506d4bae68003000240001000000000000000001000040001000000000

That low-byte-zero slot value is why initialize(address,bytes) remained reachable.

The attacker transaction then exercised the reopened initializer. The trace for 0x35f50851c3b754b4565dc3e69af8f9bdb6555edecc84cf0badf8c1e8141d902d shows the representative wallet accepting a second initialization call:

0x56BCADff30680EBB540a84D75c182A5dC61981C0::initialize(
  0x10E5C8d3537856F141272E1C39BeFdab4Dd8BdE0,
  0xa8b89898
)
  0x10d0E9755C67Ab37089aCb4f51E8b4eE407FE853::initialize(...) [delegatecall]
    emit BeaconUpgraded(param0: 0x10E5C8d3537856F141272E1C39BeFdab4Dd8BdE0)
    0x10E5C8d3537856F141272E1C39BeFdab4Dd8BdE0::a8b89898() [delegatecall]
  storage changes:
    @ 0: 0xa260d3506d4bae68003000240001000000000000000001000040001000000000
      -> 0xa260d3506d4bae68003000240001000000000000000001000040001000000001
    @ 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50:
      0x0000000000000000000000003f443c31dab3dab882f9fe621f97b74c3c14837d
      -> 0x00000000000000000000000010e5c8d3537856f141272e1c39befdab4dd8bde0

After the beacon swap, the delegated attacker routine queried TEL balances and transferred the entire victim balance to the attacker. For the representative wallet, the validator trace and balance diff both show a pre-state TEL balance of 601829675 raw units and a post-state TEL balance of 0. The attacker-side balance diff for the full incident shows a gain of 14175575188 raw TEL units and a net native gain of 4249835172122553405600 wei after fees, while the victim wallets lost 4249980000760000000000 wei in aggregate.

The exploit is therefore deterministic: a publicly callable proxy initializer, plus a slot-0 collision during the original deployment flow, let anyone replace the beacon of a funded wallet proxy and run arbitrary delegatecall logic as the wallet.

5. Adversary Flow Analysis

The attacker used contract 0x10e5c8d3537856f141272e1c39befdab4dd8bde0 as both the transaction target and the malicious beacon. Transaction 0x35f50851c3b754b4565dc3e69af8f9bdb6555edecc84cf0badf8c1e8141d902d supplied an array of 50 wallet addresses and caused the attacker contract to call each wallet's public initialize(address,bytes) entrypoint with beacon 0x10e5c8d3537856f141272e1c39befdab4dd8bde0 and payload selector 0xa8b89898.

For each successful wallet:

  1. The wallet proxy accepted the second initialization call because slot 0 no longer encoded a completed initializer state.
  2. The proxy checked implementation() on the attacker beacon, wrote the attacker's address into the EIP-1967 beacon slot, and emitted BeaconUpgraded.
  3. The proxy delegatecalled the attacker routine a8b89898() in the wallet's storage context.
  4. The attacker routine queried TEL.balanceOf(wallet), transferred the full TEL balance to attacker EOA 0xdb4b84f0e601e40a02b54497f26e03ef33f3a5b7, and forwarded native POL/MATIC when present.

The repeated pattern is visible throughout the exploit trace. One attempted wallet, 0x191c6ca4429860c9d029230c4d7538cd7d643734, reverted with ReentrancySentryOOG, so 49 of the 50 attempts succeeded. The attack did not require privileged keys, prior approvals from wallet owners, or attacker-specific predeployment artifacts beyond the attacker's own public beacon contract.

6. Impact & Losses

The attacker drained assets from 49 Telcoin wallet proxies in a single Polygon transaction. The measurable losses established by the collected balance diffs are:

  • TEL: 14175575188 raw units, with token decimals 2
  • MATIC: 4249980000760000000000 wei, with token decimals 18

The attacker EOA finished the exploit transaction with a strictly positive portfolio delta even after gas. The validator-confirmed balance diff shows attacker 0xdb4b84f0e601e40a02b54497f26e03ef33f3a5b7 increasing from 0 to 14175575188 raw TEL units and from 195087677523038417579 wei to 4444922849645591823179 wei, for a net native gain of 4249835172122553405600 wei after transaction fees.

7. References

  • Polygon tx 0x1a31cb6f417d30fe8769328b3412bfb0d70247a82009ef28dfab5730c82acd05: representative wallet deployment and first initialization
  • Polygon tx 0x35f50851c3b754b4565dc3e69af8f9bdb6555edecc84cf0badf8c1e8141d902d: attacker re-initialization and drain transaction
  • Proxy implementation 0x10d0e9755c67ab37089acb4f51e8b4ee407fe853: public initialize(address,bytes) beacon proxy logic
  • Wallet implementation 0x50b18f323237e38fc308134efff3a83163572c85: verified source showing sstore(0, _state) in initialize()
  • Representative victim wallet 0x56bcadff30680ebb540a84d75c182a5dc61981c0
  • Legitimate beacon 0x3f443c31dab3dab882f9fe621f97b74c3c14837d
  • Attacker beacon/orchestrator 0x10e5c8d3537856f141272e1c39befdab4dd8bde0