The vaults.sx contract on EOS mainnet has been exploited through a re-entrancy attack. 1,180,142.5653 EOS (~13M USD) and 461,796.8968 USDT were stolen making this the biggest hack on EOS.

Vaults.sx is a yield aggregator where users can deposit EOS or USDT in return for interest-bearing SXEOS/SXUSDT tokens. The deposited tokens are then available in the flash.sx contract for flashloans and aggregate fees. Finally, SX tokens can be redeemed for a pro-rata share of the underlying funds + aggregated fees again.

EOS actions execution order

The eosio.token notifies both the sender and the recipient using require_recipient:

void token::transfer( const name&    from,
const name& to,
const asset& quantity,
const string& memo )
{
// ...

require_recipient( from );
require_recipient( to );

// ...
}

Imagine both parties having a contract deployed that each creates a log inline action:

[[eosio::on_notify("*::transfer")]]
void tester::on_transfer(const name from, const name to, const asset quantity, const string memo )
{
log_action log( get_self(), { get_self(), "active"_n });
log.send( "hello from " + get_self().to_string() );
}

When sending a token from A to B using token.transfer(A, B, 1.0000 EOS, "") one might expect the following sequence of execution:

  1. token.transfer receiver:token
  2. token.transfer receiver:A
  3. A.log receiver:A
  4. token.transfer receiver:B
  5. B.log receiver:B

But this is not the case. EOSIO always executes all notification handlers first before processing normal inline actions. After all notifications have been processed the created inline actions are processed depth-first. This means the actual execution order is:

  1. token.transfer receiver:token
  2. token.transfer receiver:A
  3. token.transfer receiver:B
  4. A.log receiver:A
  5. B.log receiver:B

The consequence is that malicious inline actions can run between receiving the notification (token.transfer receiver:B) and creating the log inline action:

[[eosio::on_notify("*::transfer")]]
void tester::on_transfer(const name from, const name to, const asset quantity, const string memo )
{
// not guaranteed to execute next (!)
// if another contract received the same notification earlier and created an inline action
// it'll be executed before this one
log_action log( get_self(), { get_self(), "active"_n });
log.send( "hello from " + get_self().to_string() );
}

The exploit

  1. token.transfer(attacker, vaults.sx): Deposit tokens to receive SX tokens
  2. sxtoken.transfer(attacker, vaults.sx)@sxtoken Redeem half of the received SX tokens again by transferring them to the vault
  3. sxtoken.transfer(attacker, vaults.sx)@attacker: This notifies the sender (attacker) who creates 1) an inline action to call the vaults.sx update function and 2) another inline action redeeming the second half of the SX tokens.
  4. sxtoken.transfer(attacker, vaults.sx)@vaults.sx The receiver (vaults.sx) is notified, pays out the fair share of underlying tokens and correctly saves the new fund balance left in the contract:

5. The vault.update inline action created in step 3) is now executed which overwrites the update in step 4) as the balance of the vaults.sx contract has not changed yet, because the inline action performing the redeem transfer to the attacker still has to be executed.

At some point, the second redeem of the other half of the SX tokens is performed using the original funds (instead of the funds — first redeem output). The share is then redeemed on an inflated total supply which leads to a bigger payout than their actual pro-rata share. The attacker then goes on and repeats these steps several times until all funds are trained.

In reality, the attack is more complicated as the attacker cannot directly call vault.update but needs to take a tiny flash.sx flashloan which then triggers the vault update.

This is similar to the attack on Coral Finance that also exploited the unexpected execution order of notifications.

An example transaction performing this can be seen here The stolen tokens are currently held in the aquudqdmxesw account.

Full Stack Software Engineer #javascript #EOS. Into Recreational Math / CS 🤯 Just message me about anything, my mind is open.