THORChain started with a BEP2 token (RUNE-B1A) and ERC20 RUNE. To "upgrade" RUNE to native, a user sends BEP2 RUNE-B1A to the BNB vault with memo switch:<rune address>
.
There is a bug where 90 billion fake BEP2 RUNE can be sent and redeemed for real RUNE, then swapped for 100% of assets.
Firstly an attacker needs to create a BEP2 token, exactly RUNE-67C
on mainnet. This is available because it is only registered on Binance testnet (as of writing!). The token suffix -XXX
is selected by Binance chain as the first 3 digits of the transaction hash.
https://github.com/binance-chain/BEPs/blob/master/BEP2.md
Normally this is essentially "random" and would require average 23,000 "guesses" to get, worst case 46,000, costing 10 BNB each. However a script can be written fairly easily to ensure the transaction hash begins with 67C
in order to get the desirable BEP2 token suffix. One way would be to randomly generate private keys until the transaction hash is correct, or slightly modifying the amount of BNB, for example 10.00000001
. Several tokens exist with the popular -000
and -888
suffix which proves this is feasible. This disclosure does not go into detail here, suffice to say it's possible to generate the RUNE-67C token on mainnet with around $3500 to $20,000 funds and 1-2 days work.
The attacker then sends 90 billion fake RUNE into the BNB vault address with memo switch:<attackers thor.rune address>
. THORChain mints 90 billion native RUNE which the attacker then does SWAP and/or ADD+WITHDRAW to steal 100% of pool assets.
Critical
Without network rate limiting: 100% of all pools.
With network rate limiting and fast validator "halt" response: around $1m.
Difficulty
Easy-ish: A script needs to be written to generate the RUNE-67C
asset, which is achievable, and at least 10 BNB (Binance token issue fee).
In handler_switch.go
the observed attackers BNB switch transaction comes in and passes ValidateBasic
and validateCurrent
.
The handleCurrent
function then checks for THORChainHalted
mimir and immediately checks the asset:
if !msg.Tx.Coins[0].IsNative() && msg.Tx.Coins[0].Asset.IsRune() {
return h.toNativeV56(ctx, msg)
}
The IsRune()
function is defined as follows:
// IsRune is a helper function ,return true only when the asset represent RUNE
func (a Asset) IsRune() bool {
return a.Equals(Rune67CAsset) || a.Equals(RuneB1AAsset) || a.Equals(RuneNative) || a.Equals(RuneERC20Asset) || a.Equals(RuneERC20TestnetAsset)
}
The Asset .Equals
is defined as:
a.Chain.Equals(a2.Chain) && a.Symbol.Equals(a2.Symbol) && a.Ticker.Equals(a2.Ticker) && a.Synth == a2.Synth
And the symbol constants are:
Rune67CAsset = Asset{Chain: BNBChain, Symbol: "RUNE-67C", Ticker: "RUNE", Synth: false} // testnet asset on binance ganges
// RuneB1AAsset RUNE on Binance main net
RuneB1AAsset = Asset{Chain: BNBChain, Symbol: "RUNE-B1A", Ticker: "RUNE", Synth: false} // mainnet
// RuneNative RUNE on thorchain
The fake mainnet shitcoin RUNE-67C
passes the test and leads to the holy grail: h.mgr.Keeper().MintAndSendToAccount
.
I did not investigate the feasibility of generating a fake ETH contract address for the testnet version as it is not required given that it is much easier to create the desired BNB token.
This was found stepping through all of the handlers. For each handler asking how would I abuse the priveleges in this handler, then looking for ways to exploit. The switch function goal is clearly to pass an attacker controlled coin amount and address into the mint function. The main stumbling block is to pass the IsRune()
test, which upon further research on BNB token creation, revealed that RUNE-67C
does not exist on Binance mainnet and it is possible to use a script to mint exactly RUNE-67C
fairly cheaply by generating transaction hashes. Otherwise a "random" generations at 10 BNB each would cost $50-120m before finding the right address, which would be unrealistic.
This was reported to @leena on Discord at 2231h AEST, 28 Jul 2021. A video conference was organised with @leena and @heimdall, and also reported on Discord to @Son of Odin who confirmed the vulnerability and immediately began a PR.
This vulnerability will not be publicly disclosed until 90 days or until fixed in THORChain with all ACTIVE validators running the patched code.
Remove the testnet tokens from the IsRune()
check for THORChain mainnet build.
The network is safe whilst halted, and can be brought up safely with HaltTHORChain
prior to 100% of nodes running the new code.
More beer coolers ?!
Update: actually I think I could extract more by not being greedy and minting say 500M tokens, sending 5M to a wallet, perform the attack mint switch to native, then do swaps in various pools. This would appear like a whale dumping, which is a legitimate activity and could increase the block sign out rate even with a limiting system. There would probably be more confusion and discussion in #mccn than usual as to whether it’s malicious because it look like a switch (legit) and swaps (legit). I think this ruse could get away with $5-20m before people got spooked or dug deep enough to realise that it wasn’t RUNE-B1A