Understanding the XBridge Exploit

4 min read

Learn how XBridge was exploited, resulting in a loss of assets worth approximately $1.44 million.

TL;DR#

On April 24, 2024, XBridge was exploited on the Ethereum Mainnet and the BNB chain due to a smart contract vulnerability, which collectively resulted in a loss of assets worth approximately $1.44 million. 

Introduction to XBridge#

XBridge, built by SaitaChain, is a cross-chain platform connecting the Ethereum Mainnet and the BNB chain.

Vulnerability Assessment#

The root cause of the exploit is a faulty smart contract implementation caused by a lack of regulated access control.

Steps#

Step 1:

We attempt to analyze one of the attack transactions executed by the exploiter.

Step 2:

The attacker invoked a call to the withdrawTokens function of the vulnerable contract and was able to seamlessly take away the Saitachain: STC Token from the protocol.

undefined

Step 3:

When a user makes an external call to the listToken function of this exploited contract, if the base token matches the corresponding token, then without any prior verification of the token owner or the caller of the contract, anyone would be able to set the token owner to themselves.

/**
 * @dev token owner can list the pair of their token with their corresponding chain id
 * @param baseToken struct that contains token address and its corresponding chain id
 * @param correspondingToken struct that contains token address and its corresponding chain id
 * @param _isMintable if corresponding token address is mintable then its `true` otherwise `false`
 */
function listToken(tokenInfo memory baseToken, tokenInfo memory correspondingToken, bool _isMintable) external payable {
  address _baseToken = baseToken.token;
  address _correspondingToken = correspondingToken.token;
  require(_baseToken != address(0), "INVALID_ADDR");
  require(_correspondingToken != address(0), "INVALID_ADDR");
  require(
    tokenToTokenWithChainId[baseToken.chain][correspondingToken.chain][_baseToken] == address(0) &&
      tokenToTokenWithChainId[baseToken.chain][correspondingToken.chain][_correspondingToken] == address(0),
    "THIS_PAIR_ALREADY_LISTED"
  );

  isMintableWithChainId[baseToken.chain][correspondingToken.chain][_baseToken][_correspondingToken] = _isMintable;
  isMintableWithChainId[baseToken.chain][correspondingToken.chain][_correspondingToken][_baseToken] = _isMintable;
  isMintableWithChainId[correspondingToken.chain][baseToken.chain][_baseToken][_correspondingToken] = _isMintable;
  isMintableWithChainId[correspondingToken.chain][baseToken.chain][_correspondingToken][_baseToken] = _isMintable;

  tokenToTokenWithChainId[baseToken.chain][correspondingToken.chain][_baseToken] = _correspondingToken;
  tokenToTokenWithChainId[baseToken.chain][correspondingToken.chain][_correspondingToken] = _baseToken;
  tokenToTokenWithChainId[correspondingToken.chain][baseToken.chain][_baseToken] = _correspondingToken;
  tokenToTokenWithChainId[correspondingToken.chain][baseToken.chain][_correspondingToken] = _baseToken;

  if (_isMintable) {
    isWrappedWithChainId[baseToken.chain][correspondingToken.chain][_correspondingToken] = true;
    isWrappedWithChainId[correspondingToken.chain][baseToken.chain][_correspondingToken] = true;
    isWrapped[_correspondingToken] = true;
  }

  tokenOwnerWithChainId[baseToken.chain][correspondingToken.chain][_baseToken][_correspondingToken] = msg.sender;
  tokenOwnerWithChainId[baseToken.chain][correspondingToken.chain][_correspondingToken][_baseToken] = msg.sender;
  tokenOwnerWithChainId[correspondingToken.chain][baseToken.chain][_baseToken][_correspondingToken] = msg.sender;
  tokenOwnerWithChainId[correspondingToken.chain][baseToken.chain][_correspondingToken][_baseToken] = msg.sender;

  if (_baseToken == _correspondingToken) _tokenOwner[_baseToken] = msg.sender;
  else {
    if (_baseToken.code.length > 0) _tokenOwner[_baseToken] = msg.sender;
    else _tokenOwner[_correspondingToken] = msg.sender;
  }

  if (!excludeFeeFromListing[msg.sender]) transferListingFee(listingFeeCollector, msg.sender, msg.value);

  emit TokenListed(_baseToken, baseToken.chain, _correspondingToken, correspondingToken.chain, _isMintable, msg.sender);
}

Step 4:

With this ownership privilege in hand, the attacker then invoked the withdrawTokens function to extract and take away the base tokens directly.

/**
 * @dev token lister can withdraw tokens
 * @param token token address to withdraw from bridge contract
 * @param receiver address to recive the withdrawn tokens
 * @param amount amount of tokens to deposit in bridge contract
 */
function withdrawTokens(address token, address receiver, uint256 amount) external {
  // require(token.code.length > 0, "TOKEN_NOT_DEPLOYED_ON_THIS_CHAIN");
  // address _correspondingToken = tokenToTokenWithChainId[srcId][dstId][token];
  require(token != address(0), "TOKEN_NOT_LISTED");
  require(amount > 0, "AMOUNT_CANT_BE_ZERO");
  address user = msg.sender;
  require(user == _tokenOwner[token], "ONLY_TOKEN_LISTER_CAN_WITHDRAW");
  // require(user == tokenOwnerWithChainId[srcId][dstId][token][_correspondingToken], "ONLY_TOKEN_LISTER_CAN_WITHDRAW");

  if (token != native) {
    require(amount <= (IERC20(token).balanceOf(address(this)) - tokenTax[token]), "WITHDRAW_LESS");

    if (isWrapped[token]) revert("CANT_WITHDRAW_WRAPPED_TOKENS");

    IERC20(token).transfer(receiver, amount);
  } else {
    require(amount <= address(this).balance, "WITHDRAW_LESS");
    (bool success, ) = payable(receiver).call{value: amount}("");
    require(success, "WITHDRAW_FAILED");
  }

  emit TokenWithdrawn(user, receiver, amount);
}

Step 5:

At the time of this writing, this address of the attacker has a hold of approximately $1,247,151 worth of assets.

Address on the Ethereum Mainnet: $830,920
Address on the BNB Chain: $416,231

Step 6:

On the Ethereum Mainnet, the exploiter has already transferred 20 ETH worth approximately $63,264 into Tornado Cash. Likewise, the address of the exploiter on the BNB chain has already laundered 230.1 BNB worth $135,340 into Tornado Cash.

Aftermath#

The team acknowledged the occurrence of the exploit and stated that their technical team will be investigating the matter to contain the impact caused by the exploit.

Solution#

To address the vulnerabilities and secure the XBridge platform from similar exploits in the future, a comprehensive approach involving both immediate fixes and longer-term security practices is required. The first step requires pausing the bridge protocol to ensure that the damage caused by or during the exploit is contained. Following this, the smart contracts require immediate patches, particularly improving access control in the vulnerable function to ensure only legitimate token owners can make changes and enhancing validation in the affected function to verify the legitimacy of withdrawals.

A comprehensive security audit by a reputable third party is crucial to uncovering any other potential vulnerabilities. Regular code reviews and automated analysis should become routine, complemented by continuous security training for developers to keep pace with evolving blockchain security standards.

Even with stringent security measures in place, the complete eradication of risks related to vulnerability exploits remains challenging. In such scenarios, partnering with Neptune Mutual is invaluable. If the XBridge team had established a dedicated cover pool with us, the financial repercussions of such security breaches could have been mitigated. Neptune Mutual specializes in providing coverage against losses due to smart contract vulnerabilities through bespoke parametric policies designed to address these specific risks.

Collaborating with Neptune Mutual simplifies the recovery process for users by minimizing the requirement for detailed proof of loss documentation. Following the verification and definite resolution of an incident via our detailed incident resolution protocol, we focus on promptly delivering compensation and financial support to the affected parties. This approach guarantees rapid assistance to users suffering from security breaches.

Our services are accessible through our marketplace  across various leading blockchain platforms, including EthereumArbitrum, and the BNB chain, which allows us to offer extensive support to a diverse range of DeFi participants. This widespread availability significantly strengthens our ability to safeguard against smart contract vulnerabilities, thereby enhancing the security framework for our extensive user base.

Reference Source Cyvers

By

Tags