Taking a Closer Look at Ocean Exploit

4 min read

Learn how Ocean was exploited on the BNB chain, resulting in a loss of $505,000.

TL;DR#

On July 18, 2023, Ocean (BNO token) was exploited on the BNB chain, resulting in a loss of $505,000.

Introduction to Ocean#

Ocean is a marketplace to discover, create, collect, and sell distinctive NFTs.

Vulnerability Assessment#

The root cause of the exploit is due to the presence of a vulnerable function where the reward debt is updated after NFT transfers without the related bonus, making it smaller than the actual reward claimed.

Steps#

Step 1:

We attempt to analyze the attack transaction executed by the exploiter.

Step 2:

The pool contract supports both the ERC20 and NFT stakes; however, the `emergencyWithdraw` function allows users to withdraw their ERC20 stakes but doesn't process the NFT stake records.

function emergencyWithdraw() public {
  pledgeAddress.safeTransfer(address(msg.sender), userInfo[msg.sender].allstake);
  userInfo[msg.sender].allstake = 0;
  userInfo[msg.sender].rewardDebt = 0;
}

Step 3:

The `rewardDebt` is set to zero, but the `nftAddition` value from the `pendingFit` function is not reset to zero. Therefore, it puts the additional token gained from `unstakeNft` via `pendingFit` into the next pledge, resulting in an obvious increase in reward calculation.

function unstakeNft(uint256[] memory tokenIds) public payable notPause {
  require(msg.value >= withdrawalFee, "Please pay the withdrawal fee");
  uint256 pending = pendingFit(msg.sender);
  if (pending > 0) {
    payable(wallet).transfer(msg.value);
    safeGoodTransfer(msg.sender, pending);
    emit Withdraw(msg.sender, pending);
  } else {
    payable(wallet).transfer(msg.value);
  }

  require(tokenIds.length > 0, "At least one token ID must be provided");
  for (uint256 i = 0; i < tokenIds.length; i++) {
    require(nftContract.ownerOf(tokenIds[i]) == address(this), "There is no such NFT on the contract");

    (bool isIn, uint256 index) = firstIndexOf(userNft[msg.sender], tokenIds[i]);
    require(isIn, "This is not your NFT");
    removeByIndex(msg.sender, index);
    userInfo[msg.sender].nftAmount = userInfo[msg.sender].nftAmount.sub(1);
    nftContract.safeTransferFrom(address(this), msg.sender, tokenIds[i]);

    poolInfo.nftSupply = poolInfo.nftSupply.sub(1);
    emit UnStakeNft(msg.sender, tokenIds[i]);
  }

  updatePool();
  userInfo[msg.sender].rewardDebt =
    (userInfo[msg.sender].allstake.add(userInfo[msg.sender].nftAddition)).mul(poolInfo.accPerShare).div(1e12);
}
function pendingFit(address _user) public view returns (uint256) {
  uint256 curBlock = poolInfo.endBlock < block.number ? poolInfo.endBlock : block.number;
  UserInfo storage user = userInfo[_user];
  uint256 accPerShare = poolInfo.accPerShare;

  if (curBlock > poolInfo.lastRewardBlock && poolInfo.stakeSupply != 0) {
    uint256 multiplier = getMultiplier(poolInfo.lastRewardBlock, curBlock);
    uint256 fitReward = multiplier.mul(perBlock);
    accPerShare = accPerShare.add(fitReward.mul(1e12).div(poolInfo.stakeSupply.add(poolInfo.nftAddition)));
  }

  uint256 userreward = (user.allstake.add(user.nftAddition)).mul(accPerShare).div(1e12).sub(user.rewardDebt);
  return userreward;
}

Step 4:

The attacker repeatedly staked the same two Ocean NFTs and claimed their share of rewards, which resulted in the loss of 1.84 million BNO tokens, worth approximately $505,000.

Step 5:

At the time of this writing, all of the stolen funds have been transferred to this address controlled by the attacker.

Aftermath#

The price of the BNO token dropped by over 99% following the occurrence of the exploit.

Solution#

The exploit resulted from erroneous calculations in both the `emergencyWithdraw` and `unstakeNft` functions. These vulnerabilities were exploited by the attacker to manipulate their reward amounts, resulting in a substantial financial loss. However, this exploit could have been avoided with a few crucial alterations in the codebase.

Primarily, it is essential to revise the `emergencyWithdraw` function to ensure accurate handling of both ERC20 and NFT stakes, which involves accommodating the `nftAddition` variable in the affected functions.

The next crucial update pertains to the `unstakeNft` function. As it stands, the function facilitates an inflation of the `nftAddition` value post-withdrawal, which must be remedied. Additionally, the `pendingFit` function needs to be restructured for accurate user reward computation.

Lastly, to prevent the re-staking of the same NFTs by users, a checkpoint mechanism needs to be incorporated into the contract. These modifications would prevent such exploits in the future.

Despite the most scrupulous preventative measures, certain attacks can still find their way through. This is precisely the scenario where solutions like Neptune Mutual become invaluable in mitigating the consequences of such breaches. If the team associated with Ocean Marketplace had established a dedicated cover pool with Neptune Mutual, the repercussions of this exploit could have been considerably lessened. Neptune Mutual provides coverage to users who have suffered losses of funds or digital assets due to vulnerabilities in smart contracts, underpinned by their pioneering parametric policies.

When a user purchases a Neptune Mutual parametric cover policy, they do not need to furnish proof of losses to claim their payouts. Instead, following the confirmation and resolution of an incident via Neptune Mutual's comprehensive incident resolution framework, payouts can be claimed immediately. Our marketplace is available on multiple popular blockchain networks, including EthereumArbitrum, and the BNB chain. This broad reach allows us to serve a diverse array of DeFi users, offering them protection from potential vulnerabilities and bolstering their confidence in the ecosystem.

Reference Source BlockSec

By

Tags