Zunami Protocol-Post Mortem

Introduction

In August 2023 the Zunami protocol lost about $2.1 million in a price manipulation attack. This was caused by donations that resulted in incorrect prices.

Flash loan attacks are used a lot in Defi to hack protocols. In this case, the attacker was able to strategically borrow funds from UniSwap and balancer to manipulate Zunami’s pool. Flash loans give people opportunities to borrow funds without putting down any collateral or going through any credit checks.

Timeline of events

The hack was recognized

Shortly after Zunami protocol responded with this

PeckShield followed up with this information in regards to the hack

On August 15, 2023, Zunami Protocol posted a Post-Mortem describing the attack and the plan going forward.

Flash loan

A flash loan is where a user can borrow money with no upfront collateral and return the borrowed asset within the same transaction.

Here is an example of how a flash loan works

All of these steps are done in one transaction. This is why flash loans are a hacker's tool of choice. It is a way to make a huge profit in one transaction.

Incident Analysis

The attack occurred by a price manipulation attack that was caused by a flawed calculation of the LP price and this was found in the function of totalHoldings().

Here is what the attacker did in the exploit:

  1. Borrowed $7 million USDT from UniswapV3, $ 7 million USDC and 10,011 WETH from Balancer.

  2. The attacker added liquidity into CurveFinance Swap with USDC and minted crvFRAX, then swapped crvFrax for UZD and USDC for UZD in the Curve.

  3. The balanceOf() function depended on the incorrect price of the cache.

function balanceOf(address account) public view virtual override returns (uint256) {
  if (!containRigidAddress(account)) return super.balanceOf(account);

  return _balancesRigid[account];
}

function convertFromNominalWithCaching(uint256 nominal, Math.Rounding rounding) internal virtual returns (uint256 value) {
  if (nominal == type(uint256).max) return type(uint256).max;
  cacheAssetPriceByBlock();
  return nominal.mulDiv(assetPriceCached(), DEFAULT_DECIMALS_FACTOR, rounding);
}

function assetPriceCached() public view virtual returns (uint256) {
  return _assetPriceCached;
}

zETH(LP) is dependent on CRV price and exchange rate that was calculated from the ETH and CRV balances from the CurveConvexStratBase contract. The attacker was able to manipulate the balance of CRV and the price and that in turn increased _assetPriceCached

function cacheAssetPrice() public virtual {
  blockCached = block.number;
  uint256 currentAssetPrice = assetPrice();
  if (assetPriceCached < currentAssetPrice) {
    assetPriceCached = currentAssetPrice;
    emit CachedAssetPrice(blockCached, _assetPriceCached);
  }
}

function assetPrice() public view override returns (uint256) {
  return priceOracle.lpPrice();
}

function lpPrice() external view returns (uint256) {
  return (totalHoldings() * 1e18) / totalSupply();
}

function totalHoldings() public view returns (uint256) {
  uint256 length = poolInfo.length;
  uint256 totalHold = 0;
  for (uint256 pid = 0; pid < length; pid++) {
    totalHold += poolInfo[pid].strategy.totalHoldings();
  }
  return totalHold;
}

function totalHoldings() public view virtual returns (uint256) {
  uint256 crvLpHoldings = (cvxRewards.balanceOf(address(this)) getCurvePoolPrice()) / CURVE_PRICE_DENOMINATOR;

 
uint256 crvEarned = cvxRewards.earned(address(this));

  uint256 cvxTotalCliffs = config.cvx.totalCliffs();
  uint256 cvxRemainCliffs = cvxTotalCliffs - config.cvx.totalSupply() / config.cvx.reductionPerCliff();

  uint256 amountIn = (crvEarned cvxRemainCliffs) / cvxTotalCliffs + config.cvx.balanceOf(address(this));
  uint256 cvxEarningsUSDT = priceTokenByExchange(amountIn, config.cvxToUsdtPath);

  amountIn = crvEarned + config.crv.balanceOf(address(this));
  uint256 crvEarningsUSDT = priceTokenByExchange(amountIn, config.crvToUsdtPath);

  uint256 tokensHoldings = 0;
  for (uint256 i = 0; i < 3; i++) {
    tokensHoldings += config.tokens[i].balanceOf(address(this)) decimalsMultipliers[i];
  }

  return tokensHoldings + crvLpHoldings + (cvxEarningsUSDT + crvEarningsUSDT) decimalsMultipliers[ZUNAMI_USDT_TOKEN_ID];
}

  1. The SDT balance affects UZD price; the attacker managed to swap 11 WETH for 55,981 SDT in the Curve, then donated all of the SDT into the MIMCurveStakeDao. The last of the 10,000 WETH was swapped for 55,043 SDT and the remaining $ 7 million USDT was swapped for 2,154 WETH in the Sushiswap.

The attacker noticed a flaw in the design of the calculation of the LP price and took out a flash loan and manipulated the prices for a huge profit.

PeckShield notified Zunami protocol stating that they noticed an ongoing attack and they encrypted the hash to protect themselves. Zunami followed up shortly after stating that they would be investigating the matter. PeckShield was prompt in notifying the pro

Root Cause

The root cause of the hack was manipulation of prices which occurred because of incorrect computation of LP prices. The specific cause was found in the totalHoldings function. This is where inflation occurred with sdt and sdtPrice.

The flaw was found in the totalHoldings function which was a flawed calculation of the LP price

One article noted that slow mist made Zunami aware of the vulnerability months before the hack. The protocol did not make any changes at the time. An attacker must have seen the TVL locked in the protocol and decided to see if there were any vulnerabilities. The attacker was able to find the vulnerability and manipulate the price.

Technicality of the hack

The cause of the hack was due to price manipulation. The attacker took advantage of the way that UDZ was calculated, so they were able to inflate their value.

The way that a user balance is calculated is by

Balance[address] * assetPriceCached()/DEFAULT_DECIMALS_FACTOR

The attacker updated the variable _assetPriceCached by calling the function cacheAssetPrice()

Impact Assessment

The Zunami protocol lost $2.1M and caused the zStables to depeg by 85% and 99%. The funds were transferred through Tornando Cash, which is a crypto mixing service platform.

When Zunami protocol realized that “zStables” pools on curveFinance were hacked they warned their users not to purchase Zunami Ether. Learning that Zunami protocol had a security firm warn them about the vulnerability about 2 months prior is not a good thing. I think as a user that does not provide security or trust. As an investor certain check marks should be met to ensure security is a priority.

Response and Mitigation

The protocol immediately notified its users of the hack and let the users know not to purchase any Zunami Ether until further investigation was conducted. After further investigation had taken place around August 29, 2023, the protocol gave refunds to the collateral holders of UZD and zETH for the block before the hack.

According to Immunefi, price manipulation is one of the top 10 most common vulnerabilities. This was the issue with this particular hack.

Calculating the token value was the insecure code in this particular contract. Using an Oracle like Chainlink would have been better instead of relying on internal calculation.

Recommendations:

Zunmai should have taken precautions when Slowmist which is an audit firm reached out to them about a potential vulnerability they found in the protocol.

According to Immunefi, price manipulation is one of the top 10 most common vulnerabilities. This was the issue with this particular hack.

Calculating the token value was the insecure code in this particular contract. Using an Oracle like Chainlink would have been better instead of relying on internal calculation. Chainlink has various nodes working to collect various data, so the data is decentralized and is used by many different protocols.

According to the documentation, Zunami had an audit conducted by Ackee Blockchain. They had the protocol audited on February 22, 2022, UZD stable coin was audited on October 1, 2022, and February 2023 UZD v1.2 was audited by HashEx. The protocol definitely should have had more than one audit conducted and used different audit approaches such as Code4rena or private audits. It is beneficial to have multiple audits especially when you have a lot of TVL in your protocol.

Conclusion

Price oracle manipulation attacks are one of the top hacks that occur in Defi. Protocols with a lot of TVL must take the necessary steps to ensure that security is a top priority. Humans make errors and that is why it is crucial to prioritize security by getting audits using tools such as formal verification, invariant testing, 100% test coverage in unit tests and checking the absence of undesired functionality.

Resources

neptunemutual.com/blog/how-was-the-zunami-p..

certik.com/resources/blog/3iea5hcDLs77TkOMQ..

https://www.halborn.com/blog/post/explained-the-zunami-protocol-hack-august-2023