Oasys blockchain report study. Everybody goes to jail

Intro

What is Oasys?

It is both L2 blockchain and ecosystem that is based on forked Ethereum protocol. It is super-limited, with no possibility to deploy custom contracts, and is specifically aimed to solve blockchain gaming stuff. I guess they are actually based in Japan, as they have events in Tokyo and Japanese partnerships, like Bandai Namco (guys who own Pacman and Gundam) and DoubleJump.japan.

Their bug bounty was present at Immunefi in first half of 2023; sadly, they decided to - temporarily, I hope - stop their Bug Bounty program. As far as I know, I am the last person who was paid with this BB program due to very long and hard negotiation process. Last chapter in this report study has lot of details about this negotiation process to focus on bug itself.

I am not aware if this report is only Critical-grade across all reports to Oasys, but I personally consider this bug, along with “download any dataset from Ocean”, as most impactful I ever found, and this definitely deserves to be scored as Critical, and I assume this one is probably the most impactful one across all Oasys got; at least I did not see fixes of vulnerabilities that impactful in their GitHub. However, I had to fight with Oasys for 7 months to accept it

At first glance looks so easy

Let’s talk first about how Oasys DPoS work.

I do not want to say anything bad, but I really do not get the idea behind their implementation.

Some tech nuances made me laugh (probably due to my weird sense of engineering humour). However, I cannot stop myself from sharing with everyone the fact that Oasys is running not over the EVM PoS or any DPoS available, but created EVM PoW with forced complexity 1 and cross-validation of block producer between validators. I understand that deep in the core this basically works as PoS, but I cannot stop laughing from the fact that validators are actually miners at Oasys Blockchain. Just other validators just behave like a bunch of snobs in the code. “You mined this block, but you mined it without our approval”.

Okay, jokes aside, let’s dive in tech details.

Oasys is managed by bunch of pre-deployed system smart contracts, such as StakeManager.sol and Environment.sol. They are considered “system-ish” contracts. Anyone can read them, but calling them is mostly available within specific set of rules.

First of all, only current validator can execute calls that are impactful, that is protected via onlyCoinbase modifier:

modifier onlyCoinbase() {
    if (msg.sender != block.coinbase) revert OnlyBlockProducer();
    _;
}

I already know after explaining this vulnerability for several times that I need to explain one more extra detail: block.coinbase is just ordinary address. Only limitation is that it should be EOA, so it could sign blocks. Yes, block validators uses same private keys just like ordinary EOA accounts. Thus, they can make smart contract calls, where this equation will be valid: msg.sender == block.coinbase.

To understand the attack I will focus on just 2 functions under this onlyCoinbase protection: Environment.updateValue and StakeManager.slash. Initializers are also protected, but this is not important for us here.

Environment.updateValue, basically, controls amount of rewards and some configs in next block. It will be used in very end, and now let’s focus on StakeManager.slash.

Despite just being called “slash”, this function is also able to jail validator if he gets enough slashes, kicking him from next epoch validation participation and obtaining the validator rewards.

Sounds easy crit, eh? You just need to slash everyone while you are validating the block.

Numbers are also looking doable. At 2023 summer, configuration was following:

  • 24 validators

  • 500 slashes to jail

  • 5760 blocks per epoch (15s blocks in 24 hrs)

  • around $50k in OAS staked to join validators club

This means you need to make 12 000 calls (24 validators * 500 slashes) to StakeManager.slash within one epoch while you are coinbase to jail everybody. Sadly, we need to do it directly (and also we cannot deploy contracts), so one slash = 1 transaction.

How much is it in blocks? My theoretical calculation was 500+ txs per block, however, when I ran oasys-geth to test the concept (damn, it was hard), I was able to make merely up to 300+ txs per block. I assume that deeper dive in block production can fix this, but since we’re talking about PoC, it’s not that important.

So, we have 300 txs block capacity and 12000 to run. This is in 40 (12000 / 300) blocks in single epoch to be controlled by attacker. Or, like, 0.7% of epoch (5760 blocks). You do not even need to make some significant deposit for that.

Is this that easy?

Not that easy

It should not be that simple, right? Yes. Let’s dive deep into oasys-geth repository. Of course, there is a protection from such a smart guys.

It is called “system transactions” and is partially emulating the PoS beacon chain functionality over the general chain.

How it works? First, there is IsSystemTransaction function in consensus/oasys/contract.go that checks if transaction is “system”.

Provided code is simplified a lot (please, Go developers, don’t kill me, I know this code is invalid but it is at least readable) to preserve the idea. This is latest version of the code, and it already contains the fix for my report:

func (c *Oasys) IsSystemTransaction(tx *types.Transaction, header *types.Header) (bool) {
	if tx.To == nil 
		return false
	if tx.Signer != header.Coinbase 
		return false

	for contract, methods := range systemMethods {
		if contract == tx.To && methods.has(tx.MethodName)
			return true
	}

	return false
}

What are those “System Transactions”?

If you want to see them in the code - they are mainly used in consensus/oasys/oasys.go.

System transactions are ejected from ordinary block transactions and processed separately as part of consensus logic. All of them have to be “consumed” here and there by various internal calls, such as initialization and slashing. You cannot simply add new System Transaction, either block finalisation or consensus will fail.

If you will look at Finalize function, you will see how they are used: they are passed to everything merely related to block production functionality, and slowly consumed. In the end of finalisation, there should be 0 System Transactions not consumed yet.

So you cannot just begin a slaughterhouse and slash everyone you see. Your blocks will be simply rejected if you will try to act like a crazy and will try to slash everyone.

But, there’s a nuance!

This is how this function looked like (again, VERY simplified) when I reported this bug:

func (c *Oasys) IsSystemTransaction(tx *types.Transaction, header *types.Header) (bool, error) {
	if tx.To == nil 
		return false
	if tx.Signer != header.Coinbase 
		return false

	if genesisContracts[*tx.To()] && tx.GasPrice() == 0
		return true
	return false
}

Let me repeat the diff part again just in case you missed it:

// new, fixed
	for contract, methods := range systemMethods
		if contract == tx.To && methods.has(tx.MethodName)
			return true
// at the moment of report, with vulnerability
	if genesisContracts[*tx.To()] && tx.GasPrice() == 0
		return true

I’m personally agree with genesisContracts[*tx.To()] check, but look at this: tx.GasPrice() == 0.

You know how to bypass it? Just send transaction with gas!

And, since you are the validator pre-1559 miner with respects from other guys in the club, you do not need to follow the gas price rules, and you can just take transactions with gas price as low as 1 wei (just do not make them 0) and all other validators will accept this block with killing spree.

Even easier

When you will try to replicate this attack in Hardhat (on pre-april commit), you will not even detect the protection, as Hardhat will automatically give you some non-zero gas price and you will just bypass it.

So, let’s put all things together and make full attack description:

  • attacker deposits around $50k and becomes a small validator in Oasys chain

  • When he considers that his time has come (and he has at least 40-50 blocks to validate remaining within the epoch), he start to fill blocks - ones he is validating - with calls of StakeManager.slash against everyone else. Only thing he needs is to ensure that gas price is not zero (and, optionally, as cheap as possible).

  • In the end of the epoch everyone else gets jailed, and he becomes the only validator in the network.

What can he do with this ultimate power?

  • first of all, obviously, he can stop the network. And this will require a hardfork to remove everything he did. Additionally, he can validate blocks for few more days or weeks to make it even more painful

  • secondly, he can continue to maintain the network for whatever reason he wants - and what is important, he can reject any attempts to become new validators, so network will be fully his kingdom. This means he can e.g. block the deposits to the exchanges, so he will be only one who is able to sell something

  • thirdly, you remember Environment.updateValue? This is a configurator for block rewards and stuff that is operated via consensus of validators. Attacker can literally set any amount of rewards he want for himself and start minting OAS tokens infinitely, getting as much as he wants for his definitely honest and hard work đź‘€

Absolutely hard

Sadly, Oasys acted in a very questionable manner during whole report discussion. I usually say only good words about projects - even when I was angry as Ocean, it was 100% about me, not about them. Anger is boost for me, so I cannot say anything bad about Ocean either now or then, as they were absolutely correct and polite in their behavior.

I cannot express how grateful I am to Immunefi for mediation of this process. Without their help it would probably end with nothing, but it ended as $200k payout instead.

This is non-default sum of BB program, but a result of agreement between me and Oasys: we both consider that problem lays between smart contracts and blockchain logic (however, we disagree on nuances), so we found the solid ground between their $150k Smart Contract Critical and $250k Blockchain/DLT critical.

I will just leave a timeline of my report to make it less biased.

April 15. Report created.

April 18. Report closed with comment. Immunefi meditation was requested. On request from Immunefi, full-scale PoC (500+ lines of code) was provided in a few days later.

Here’s their comment. They will ignore me until July after it:

This is not a security bug report because staking contract takes slashing on every day, and slashed validator just don't take reward from validation. All validator needs to stake for joining, slashing makes no reason beneficial for other validator who jailed. So, basically, jailing makes no harm for whole consensus system for validator.

Due to this reason, we are closing the submission and no reward will be issued.

April 26. Oasys shadow-fixes the bug in this commit (https://github.com/oasysgames/oasys-validator/commit/35f9ad2456e4f2cabb50332141698ce0d466dbae)

July 6. Oasys insists that bug is invalid. I provide them the proofs of fix in 10 days after the report. I asked them for a proofs that it was task from internal backlog - I understand that it was, though sad for me, possible thing to happen - but did not get an answer. Guess it was shadow-fixing.

July 26. Oasys attempts to lower the severity from “critical” to “high” due to missing recommendations on how to fix. I point them that severity lowering is happening only on mutual agreement (https://immunefisupport.zendesk.com/hc/en-us/articles/4415200339601-Lowering-Severity-). Since I just want to end this story, I also explicitly asks them if it is about money or severity, intending to propose just lowering the payout if everything is all around the money. In parallel, I inform the Immunefi that I’m open to negotiation and OK in advance (without asking me in future) to lower the sum to $200k in any stablecoins without any extra discussion, as I just do not want to exchange OAS that was used for payouts - and also want to finally close this report.

July 31. Oasys continues to argue. Also, they propose me a Go/Solidity developer position (sic!). I kindly ask them to end with report first, also reminding about Immunefi core rules (https://immunefi.com/rules/), with 3 points already broken:

  • Abusing the "no fix, no pay" rule by stealth fixing the bug later without providing full payment to the whitehat

  • Breaking SLAs regarding responsiveness and bug report resolution (again, it’s already July, and report was sent in April)

  • Soliciting whitehats on Immunefi for commercial projects or private bug bounty programs (I think that includes job offers)

August 14. Immunefi mediation is complete. This was super-slow, but I understand the guys - checking the blockchain code is really hard, so no bad feelings towards Immunefi team.

September 13. Oasys disagrees with Immunefi mediation (sic!!!). Immunefi guys write me “it is now between us and them, please just chill a bit”.

September 20. Oasys finally agrees on vuln and severity. However, it starts to argue about “Smart Contract vs Blockchain” vulnerability. In money terms, it’s $150k vs $250k. Somewhy, again the argument about the bugfix advice appears. Their comment:

I believe that accepting my previous offer, which categorized the issue as critical severity in the contract category, is a reasonable compromise.

Since we've compromise to downgrade the severity level (in fact, our team identified the root cause), I kindly ask that you also compromise on the category.

September 23 - October 6. Immunefi provides a second mediation result, insisting that it is blockchain bug. Oasys still argues.

November 1. Everything is finally confirmed. Immunefi team completed the negotiation process with Oasys and I will receive my bounty in few days (specifically, November 6).

Subscribe to my Twitter for more web3 security stuff:

Subscribe to Merkle Bonsai
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.