All incidents

Astrid Counterfeit Token Drain

Share
Oct 28, 2023 10:41 UTCAttackLoss: 64.18 stETH, 39.17 rETH +1 morePending manual check1 exploit txWindow: Atomic
Estimated Impact
64.18 stETH, 39.17 rETH +1 more
Label
Attack
Exploit Tx
1
Addresses
1
Attack Window
Atomic
Oct 28, 2023 10:41 UTC → Oct 28, 2023 10:41 UTC

Exploit Transactions

TX 1Ethereum
0x8af9b5fb3e2e3df8659ffb2e0f0c1f4c90d5a80f4f6fccef143b823ce673fb60
Oct 28, 2023 10:41 UTCExplorer

Victim Addresses

0xbAa87546cF87b5De1b0b52353A86792D40b8BA70Ethereum

Loss Breakdown

64.18stETH
39.17rETH
20cbETH

Similar Incidents

Root Cause Analysis

Astrid Counterfeit Token Drain

1. Incident Overview TL;DR

In Ethereum mainnet transaction 0x8af9b5fb3e2e3df8659ffb2e0f0c1f4c90d5a80f4f6fccef143b823ce673fb60 at block 18448168, EOA 0x792ec27874e1f614e757a1ae49d00ef5b2c73959 deployed exploit contract 0xB2E855411f67378C08F47401eACFf37461e16188, created three counterfeit restaked-token contracts, and used Astrid's proxy at 0xbAa87546cF87b5De1b0b52353A86792D40b8BA70 to redeem Astrid-held stETH, rETH, and cbETH. The attacker then monetized the drained assets and ended with a net native-ETH gain of 127635668939942863577 wei after gas.

The root cause is a direct trust-boundary failure in AstridProtocol.withdraw, AstridProtocol._processWithdrawals, and AstridProtocol.claim. Astrid accepted an arbitrary caller-supplied IRestakedETH contract, then trusted that unverified contract to define both the real payout token (stakedTokenAddress()) and the redeemable amount (scaledBalanceToBalance(...)). That let an unprivileged attacker burn attacker-issued fake IOUs and receive Astrid's real underlying assets.

2. Key Background

Astrid maintains a mapping from real staked assets to protocol-issued restaked tokens. On deposit, the protocol is supposed to accept only whitelisted staked assets, transfer the real asset into Astrid, and mint the corresponding Astrid restaked token. On withdrawal, Astrid should only honor those same protocol-issued restaked tokens and should derive redemption amounts from protocol-controlled accounting.

The critical interface is IRestakedETH, which exposes stakedTokenAddress(), scaledBalanceOf(), scaledBalanceToBalance(), mint(), and burn(). Astrid's withdrawal path calls those methods on whatever contract address the caller supplies. The incident occurred while processWithdrawalsOnWithdraw was enabled, so withdrawal requests were processed immediately and could be claimed in the same permissionless transaction.

At the relevant pre-state, Astrid's proxy already held transferable balances of stETH, rETH, and cbETH. Those balances were publicly observable through RPC and sufficient to satisfy the attacker-crafted redemption requests.

3. Vulnerability Analysis & Root Cause Summary

This was an ATTACK-class ACT opportunity, not an admin-key compromise or privileged operation. Astrid correctly whitelisted the real staked assets on the deposit side, but it did not enforce the same trust boundary on the withdrawal side. Instead, withdraw only checked that the supplied _restakedTokenAddress had bytecode, that the caller held a balance, and that the caller had approved Astrid. Once those superficial checks passed, _processWithdrawals trusted the attacker-supplied contract to tell Astrid which real staked token should be released and how many real tokens were owed. claim repeated that trust by again resolving the payout token through the attacker-controlled contract and then safeTransfering the real asset out of Astrid.

The broken invariant is straightforward: only Astrid-issued restaked tokens bound to Astrid's configured stakedTokens mapping should ever authorize redemption of Astrid-held assets, and the redeemable amount should come from Astrid-controlled accounting rather than from arbitrary external token logic. The concrete breakpoint is the pair of calls to IRestakedETH(_restakedTokenAddress).stakedTokenAddress() and IRestakedETH(_restakedTokenAddress).scaledBalanceToBalance(...) inside _processWithdrawals, followed by the same stakedTokenAddress() lookup in claim.

4. Detailed Root Cause Analysis

The trust asymmetry is visible directly in Astrid's verified implementation. Deposit enforces the whitelist and the configured restaked-token mapping, while withdraw does not:

function deposit(address _stakedTokenAddress, uint256 amount) public nonReentrant whenNotPaused {
    require(Utils.contractExists(_stakedTokenAddress), "AstridProtocol: Contract does not exist");
    require(amount > 0, "AstridProtocol: Amount must be greater than 0");
    StakedTokenMapping memory stakedTokenMapping = stakedTokens[_stakedTokenAddress];

    require(stakedTokenMapping.whitelisted, "AstridProtocol: Staked token not whitelisted");
    ...
    IRestakedETH(stakedTokenMapping.restakedTokenAddress).mint(msg.sender, amount);
}

function withdraw(address _restakedTokenAddress, uint256 amount) public nonReentrant whenNotPaused {
    require(Utils.contractExists(_restakedTokenAddress), "AstridProtocol: Contract does not exist");
    require(amount > 0, "AstridProtocol: Amount must be greater than 0");
    require(IERC20(_restakedTokenAddress).balanceOf(msg.sender) >= amount, "AstridProtocol: Insufficient balance of restaked token");
    require(IERC20(_restakedTokenAddress).allowance(msg.sender, address(this)) >= amount, "AstridProtocol: Insufficient allowance of restaked token");
    ...
}

The rest of the exploit path is enabled by _processWithdrawals and claim:

address _stakedTokenAddress = IRestakedETH(_restakedTokenAddress).stakedTokenAddress();
uint256 requestedAmount = IRestakedETH(_restakedTokenAddress).scaledBalanceToBalance(request.requestedRestakedTokenShares);
...
IRestakedETH(_restakedTokenAddress).burn(address(this), requestedAmount);
totalClaimableWithdrawals[_stakedTokenAddress] += requestedAmount;
...
address _stakedTokenAddress = IRestakedETH(request.restakedTokenAddress).stakedTokenAddress();
...
bool sent = Utils.payDirect(msg.sender, request.claimableStakedTokenAmount, _stakedTokenAddress);

That logic means any contract that:

  1. has bytecode,
  2. behaves like an ERC-20 well enough to satisfy balanceOf, allowance, and transferFrom,
  3. returns a chosen real token from stakedTokenAddress(), and
  4. returns a chosen redeemable amount from scaledBalanceToBalance(...)

can be treated by Astrid as if it were a legitimate Astrid-issued restaked token.

The seed trace shows exactly that happening. For the stETH leg, Astrid accepts attacker contract 0x09F2544778001407bA1dFf71Fa22C37F77abB186, queries it for the real payout token and amount, burns the fake token, and then transfers real stETH:

AstridProtocol::withdraw(0x09F2544778001407bA1dFf71Fa22C37F77abB186, 64176037513415148812)
  0x09F254...::stakedTokenAddress() -> 0xae7ab96520de3a18e5e111b5eaab095312d7fe84
  0x09F254...::scaledBalanceToBalance(...) -> 64176037513415148812
AstridProtocol::claim(0)
  0x09F254...::stakedTokenAddress() -> 0xae7ab96520de3a18e5e111b5eaab095312d7fe84
  Lido::transfer(0xB2E855411f67378C08F47401eACFf37461e16188, 64176037513415148812)

The same pattern repeats for rETH via counterfeit contract 0x18d06Dbf8E0926110C9202136A78c3AE151461db and for cbETH via counterfeit contract 0xe26D01D3c0167D668A74276B18D9AF89E84cD910. Because Astrid already held the real assets, the attacker did not need any privileged state change or off-chain secret. The only required exploit conditions were:

  • Astrid held redeemable staked-token balances.
  • An attacker could deploy fresh helper contracts that implemented the expected interface.
  • withdraw and claim remained publicly callable.
  • processWithdrawalsOnWithdraw was enabled for same-transaction extraction, or an admin would later process the requests.

The attack sequence breaks three basic security principles at once: never trust caller-supplied external contracts as authoritative sources for asset identity or redemption math; enforce whitelist and issuance checks symmetrically on deposit and withdrawal; and never let burning an attacker-issued IOU authorize release of protocol-held real assets.

5. Adversary Flow Analysis

The entire exploit happened inside one adversary-crafted transaction.

First, EOA 0x792ec27874e1f614e757a1ae49d00ef5b2c73959 created exploit contract 0xB2E855411f67378C08F47401eACFf37461e16188. During construction, that contract deployed three counterfeit IRestakedETH implementations:

  • 0x09F2544778001407bA1dFf71Fa22C37F77abB186 for stETH
  • 0x18d06Dbf8E0926110C9202136A78c3AE151461db for rETH
  • 0xe26D01D3c0167D668A74276B18D9AF89E84cD910 for cbETH

It minted a large fake balance of each token to itself, approved Astrid, and then ran three withdraw/claim cycles. After those cycles, Astrid had released:

  • 64176037513415148811 wei of stETH to the attacker, with 1 wei left in Astrid because of Lido share-rounding dust
  • 39165842900855519099 wei of rETH
  • 20000407064566313222 wei of cbETH

The attacker then monetized the drained assets. The trace shows stETH being routed through the Curve stETH pool and rETH/cbETH through Uniswap V3, after which the exploit contract held 63638748240147758427 wei of WETH, withdrew it to ETH, and sent 127797499079942863577 wei back to the originating EOA:

WETH9::withdraw(63638748240147758427)
0x792eC27874E1F614e757A1ae49d00ef5B2C73959::fallback{value: 127797499079942863577}()

Transaction successfully executed.
Gas used: 5394338

The adversary cluster identified from the trace is:

  • 0x792ec27874e1f614e757a1ae49d00ef5b2c73959: sender EOA and final ETH profit recipient
  • 0xB2E855411f67378C08F47401eACFf37461e16188: exploit contract
  • 0x09F2544778001407bA1dFf71Fa22C37F77abB186: counterfeit restaked token for stETH
  • 0x18d06Dbf8E0926110C9202136A78c3AE151461db: counterfeit restaked token for rETH
  • 0xe26D01D3c0167D668A74276B18D9AF89E84cD910: counterfeit restaked token for cbETH

6. Impact & Losses

Astrid's proxy at 0xbAa87546cF87b5De1b0b52353A86792D40b8BA70 lost its immediately available balances of three major liquid-staking assets:

  • stETH: 64176037513415148811 wei (64.176037513415148811 stETH)
  • rETH: 39165842900855519099 wei (39.165842900855519099 rETH)
  • cbETH: 20000407064566313222 wei (20.000407064566313222 cbETH)

The collector's balance-diff artifact confirms the protocol-side depletion for rETH and cbETH to zero and the profit-side token increases on the attacker-controlled recipients. It also confirms the sender EOA's native balance increase from 1108389279355874825 wei to 128744058219298738402 wei, for a net delta of 127635668939942863577 wei after paying 161830140000000000 wei of gas.

7. References

  • Seed transaction: 0x8af9b5fb3e2e3df8659ffb2e0f0c1f4c90d5a80f4f6fccef143b823ce673fb60
  • Victim protocol proxy: 0xbAa87546cF87b5De1b0b52353A86792D40b8BA70
  • Victim assets:
    • stETH: 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84
    • rETH: 0xae78736Cd615f374D3085123A210448E74Fc6393
    • cbETH: 0xBe9895146f7AF43049ca1c1AE358B0541Ea49704
  • Astrid implementation source: https://repo.sourcify.dev/contracts/full_match/1/0x4d5b4b9ccf52bbcfe7b71b3038d8577293779e0c/sources/AstridProtocol.sol
  • IRestakedETH interface: https://repo.sourcify.dev/contracts/full_match/1/0x4d5b4b9ccf52bbcfe7b71b3038d8577293779e0c/sources/interfaces/IRestakedETH.sol
  • Seed trace artifact: artifacts/collector/seed/1/0x8af9b5fb3e2e3df8659ffb2e0f0c1f4c90d5a80f4f6fccef143b823ce673fb60/trace.cast.log
  • Seed metadata artifact: artifacts/collector/seed/1/0x8af9b5fb3e2e3df8659ffb2e0f0c1f4c90d5a80f4f6fccef143b823ce673fb60/metadata.json
  • Seed balance diff artifact: artifacts/collector/seed/1/0x8af9b5fb3e2e3df8659ffb2e0f0c1f4c90d5a80f4f6fccef143b823ce673fb60/balance_diff.json