

Discover more from Zircon Finance
On Saturday, March 18, Zircon was hacked on both its Moonriver and BSC deployments.
The attacker, with address 0xfe7a244cf06e9296c3233f47b315a720bad542f7 on both chains, deployed an exploit contract and began systematically attacking every pool.
When we discovered that something was off, the majority of the liquidity was already drained by the hacker. We quickly paused the Pylon contracts, but this was futile, as the attack did not involve the Pylon at all. It was instead an exploit of a vulnerability in a modification we introduced to the Uniswap V2 core, specifically the burnOneSide function.
The function simulates removing liquidity through the classic two-sided method, then selling the half that the user doesn’t need against what remains in the pool. To do so, the function would calculate the amount0 and amount1 the user would receive normally, and subtract this amount from the reserves before simulating the swap.
The key error was a mismatch between using the balance of tokens present in the pool vs. the internal reserves value. Reserves are updated at the end of every operation, while balance reads the effective balance of token0 and token1 in the pool during the call.
We derived the user’s share to be burned by reading the balances at the start, but subtracted the amounts from the reserves, not the balances. Applying either of these consistently would’ve prevented the issue.
function burnOneSide(address to, bool isReserve0) external lock returns (uint amount) {
//cut
uint amount0;
uint amount1;
uint balance0 = IUniswapV2ERC20(_token0).balanceOf(address(this));
uint balance1 = IUniswapV2ERC20(_token1).balanceOf(address(this));
uint liquidity = balanceOf[address(this)];
//cut
uint _totalSupply = totalSupply;
amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
if (isReserve0) {
amount0 += getAmountOut(amount1, _reserve1 - amount1, _reserve0 - amount0, _liquidityFee);
//cut
Here is a general description of the attack, based on a sample tx with the MOVR/ETH pool:
Borrow 6254 MOVR in a flash loan from Solarbeam
Swap 865 MOVR against our pool to obtain 1.94 ETH
Supply 5600 MOVR in the pool through the mintOneSide() method (a custom macro that simulates selling half of the MOVR for ETH and then adding liquidity with the result)
The step of supplying liquidity was necessary to obtain some amount of pool tokens that could then be burned through the vulnerable burnOneSide method
The attacker donates a carefully crafted amount of ETH (1.364324934225575566) to the pool such that the calculation liquidity.mul(balance0)/_totalSupply would be just 1e-18 less than the ETH reserves variable. This “removes” nearly all ETH in the pool, making it extremely “valuable” for the pool.
When the getAmountOut function simulates the swap, it’s reading as if the user is swapping amount0 (1.55 ETH) against a pool that “contains” just 1e-18 ETH (1 wei). Since ETH is considered incredibly valuable now, the swap takes out nearly all MOVR in the pool, or 7158 MOVR.
Since the pool is now drained of nearly all MOVR, it is MOVR that’s extremely valuable. The hacker swaps less than 1 gwei of MOVR to get back the ETH he donated
The hacker returns the original flash loan to Solarbeam, pocketing a profit of 888 MOVR and 3.492 ETH (or, basically, the pool’s entire liquidity).
Why did this happen?
We’ve identified a number of contributing factors for this incident:
We’ve implemented this feature at an early stage of development and at a lower level of experience. The first commit of this function was in February 2022, and it saw almost no modifications since then.
We failed to make deep enough tests, including fuzz tests that attempt to supply bogus inputs to the function.
The function was audited as part of our initial release, but the auditors failed to catch the issue.
Because of the auditing and the simplicity of the changes, we were complacent and too confident in the Uniswap core. We never reviewed it for potential bugs like we did with the Pylon, where we believed most of the attack vectors could come from.
Because of our misguided confidence, we did not implement any flash loan protection or pause mechanism for the Uniswap core.
We had poor monitoring setups and missed the timing on setting up protection by CounterStrike.ai, with whom we had just signed a partnership. This increased the damage caused by the hacker.
While there were many small contributing causes and individual mistakes, the true root cause of this hack (and basically all others) was insufficient Human Factors preparation. Human factors is the study of human behavior with complex systems, dealing with human error and how to design systems that minimize it.
The discipline has allowed engineers to make air travel the safest form of travel, because it recognizes that humans will always make mistakes. The right approach is to work around them and minimize the circumstances that lead to mistakes.
In practice this means relying on multiple independent audits and crowd-sourced bounties, holding regular internal reviews through dedicated staff, crafting top-notch monitoring and firewall systems (such as CounterStrike), using Test-Driven Development and fuzzing techniques to uncover these types of bugs.
All of these things required a lot of developers and money, which we never had. We’re a small bootstrapped team hoping to innovate in a competitive space, but unfortunately this is not a recipe for success.
Hack-proof protocols require teams who are aware of the human factors issues (including the fact that, for example, audits alone are insufficient and somewhat overhyped as a security precaution), and have the resources to implement proper controls.
What’s next?
The attack did not involve the Pylon system, which is now paused. According to our estimates, there should be approximately $20,000 worth of assets in the Pylon.
We will redistribute this money to the non-ZRG LPs, as an advance on a future repayment plan. This initial distribution will occur proportionally on a USD value basis as a snapshot taken during the hack.
The easiest way is likely to handle the repayment manually through a claim form. We expect to begin this process by the end of the week.
As for the remainder of the liability, estimated to be about $350,000 worth currently (we chose to also include whatever was “reduced” by the omega before the hack), we commit to a continuous repayment plan from the revenue of a revamped and relaunched Zircon protocol. The specific percentages are to be determined by a future DAO.
The debt will likely be represented by Pylon pool tokens you currently own, hence it’s important to keep them safe for the duration of the repayment period.
We expect to repay the debts in the original tokens. If this is unsatisfactory, each LP will have the option to convert their credit to a snapshot in the current equivalent USD value.
Once relaunched, we will also set up markets to trade these claims and give a chance to realize them at a discounted price earlier on. Depending on the situation and the effective revenue, the future DAO might choose to modify this plan to speed the recovery up (provided it does not jeopardize the financial position of the protocol).
We’re also working to potentially trace the attacker. However for now we operate on the assumption that 100% of the funds are irretrievable.
What about ZRG?
Since the value of ZRG has been completely wiped out, the obvious choice is to sunset this specific token and relaunch with a new one.
We will take a snapshot of the combined wallet + Float LP balances (pre-hack), and launch an airdrop with the new token. We will exclude “insider” wallets such as the Zircon Labs or Zircon DAO wallets, and any ZRG still in unclaimed farms. The new token is likely to have standard allocations for team, company and DAO — the “fair launch” experiment can be considered over. So the % of total supply is likely to be somewhat different than before, but the specifics will need to be arranged in the future.
The Reconstruction
We’ve pondered a lot over the weekend on how someone can bounce back from a devastating hack like this. For most protocols and teams, hacks or code errors of similar scope are pretty much deadly.
However, this is also a chance at a clean slate. Taking the experience and the supporters gathered along the way to rebuild the protocol. And do everything the right way from the beginning.
The plan for the new Zircon is currently in motion. This week will be key to define how it will look like, and we welcome community input on this.
The steps are fairly simple: rebrand, fundraise, invest a lot in security. However, implementing them won’t be easy and we appreciate any support we can get.
We’ll keep you posted on the progress. For now, let us sort out the mess and finalize the plan.