How I Could Drain an Entire Blockchain: Post Mortem on a Bug in Godwoken Chain

Yaron Velner
Risk DAO
Published in
4 min readNov 16, 2022

Last month, I accidentally found that it is possible to undo the effect of EVM reverts in the Godwoken blockchain. This would allow me to flash loan its entire local DEX TVL without paying it back, and effectively drain the entire $7m TVL of the Godwoken chain.

I immediately reported the issue to the Godwoken team, the bug was quickly patched, and a bug bounty was awarded. In this post, we describe the chain of events that led to the discovery of the bug. We illustrate how it could have been exploited, and how it was quickly patched.

Timeline

The events took place at the date of 18/10/2022, while I was debugging the integration of B.Protocol with the Hadouken Finance lending market.

11:21:45 UTC I deployed a smart contract that would call B.Protocol liquidate function, and return a different value if the function reverts. The contract was executed, and it returned a value of 8.

try yyy.liquidateBorrow{gas: gasLimit}(borrower, amount, collateral) {
return 7;
}
catch {
return 8;
}

12:43:29 UTC Hadouken Finance team made a direct call to B.Protocol smart contract and it reverted with error: “ReentrancyGuard: reentrant call”. This type of error suggested a potential bug in the reentrancy guard mechanism, however this piece of code was authored by open-zeppelin and it is extremely battle tested.

13:48:00 UTC In an attempt to narrow down the complexity of the setup, I deployed two simple contracts, namely, DebugAAA and DebugBBB, depicted below:

contract DebugAAA {
uint public state = 1;
function test() external {
state = 2;
revert();
}
}
contract DebugBBB {
function test(DebugAAA a) external returns(uint) {
try a.test{gas: 100000}() {
return 8;
}
catch {
return 9;
}
}
}

I deployed both contracts, and then called BBB.test(a), and read a.state(), it returned a value of 2, which indicated that the effect of the revert function was cancelled.

async function testAAA() {
const a = await artifact.require("DebugAAA").new()
const b = await artifact.require("DebugBBB").new()

console.log((await a.state()).toString()) // 1

await b.test(a.address)

console.log((await a.state()).toString()) // 2
}

13:52:01 UTC I reported the findings to Godwoken devs.

14:40:39 UTC Godwoken confirmed the bug.

2022–10–19 14:19:19 UTC The bug was patched.

Severity of the bug

At the time of disclosure, the Godwoken chain had one popular dapp, namely Yokaiswap, with over $7m TVL (according to DeFi Llama). In addition, Hadouken Finance was about to launch its lending market on Godwoken.

Yokaiswap is a Uniswap v2 fork, and the Hadouken code is based on Aave v2. With the bug we found, it was possible to drain both of these dapps in the following way:

Exploiting the bug to drain Uniswap v2 forks

Uniswap v2’s swap function implements an exchange between tokenIn and tokenOut.

However, it first transfers the out token(s) to the user:

170 if(amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
180 if(amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens

and only then it verifies the user transferred the in token(s) to the reserve:

182 require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K`);

The following piece of code would drain the entire reserve balance.

try uniReserve.swap(
token0.balanceOf(address(uniReserve)),
token1.balanceOf(address(uniReserve)),
0xMe, bytes(0)
)

Indeed in lines 170 and 171 (of the Uniswap code) the reserve will transfer its entire balance to the attacker. It will revert in line 182. However, due to the bug the state will not be reverted, and the attacker balance will contain the transferred tokens.

Exploiting the bug to drain Aave v2 forks

Aave v2 has a built-in flashloan function that could have been exploited in the same way as in the uniswap attack. However, this feature was disabled by Hadouken Finance. A different attack vector was to exploit the repay function. Aave v2’s repay function only pulls the user funds at the end of the execution. After that it only does one additional call, namely, handleRepayment, however, this function has an empty logic. Hence, the repay function would revert only at the end of the execution, and thus, when wrapping it with a try/catch clause, it would still wipe the user’s debt.

The draining is completed by repeatedly borrowing more after each (fake) debt repay call.

Patching the bug

The root cause for the bug is that evmone (the EVM implementation Godwoken is using) requires the ‘host’ to revert the call failure (include logs, account state, and other state changes make inside the call), in the buggy version they ignored this mechanism (and assumed the evmone will do the reverting job between calls), and only handled the whole transaction level reverting.

In less than 24 hours after my initial report, the Godwoken dev team was able to find the root cause, and quickly patched it with a workaround that reverts the entire tx whenever an internal tx fails (and thus, effectively nullifies the try/catch sematics).

The team is working on a complete fix, which may come out in a few days:

  1. Design two Godwoken syscalls that help control the state revert logic.

2. The two syscalls will be called around an inner EVM call if needed.

About Risk DAO

Risk DAO is a service DAO focused on providing a new, open-source risk assessment framework, associated audits, and dashboards to stress test, monitor, and manage risk in DeFi lending and borrowing protocols as well as L1 and L2 networks.

You can follow us on Twitter here. You can join our Discord here.

--

--