Blockchain bridges security & common cross-chain vulnerabilities

What to consider when developing a secure cross-chain protocol? Let’s draw conclusions from some of the past cross-chain exploits.

Jakub Zmysłowski 2022.07.05   –   8 MIN read
blockchain bridge security

The dynamic development of various blockchains with their own added value, such as Polygon, Avalanche, BSC, Fantom or Solana, was a response to the growing users’ demand for desirable features. However, each of them acting separately would not be able to fully use its potential. This generates the need to interconnect each other so users could choose the most suitable one for their needs, collaborate between different ecosystems and create interoperable Dapps. Here comes the cross-chain bridge technology. 

Cross-chain bridge, in short, is a technology which allows communication between two separate blockchain networks. Transferring and swapping assets, calling functions in contracts from other blockchains and more. The evolving cross-chain ecosystem has allowed its users to get what’s best from specific blockchains. Some of the desired factors are: 

  • Reducing transaction fees and speeding up transactions’ confirmation process (which often comes with lower security and decentralization),
  • Utilizing arbitrary functionalities which are accessible on different networks,
  • Transferring assets to blockchains with larger liquidity pools.

However, bridges, like any early-staged component, introduce new threats to the system. 

Security of consensus model in blockchain 

There is one sentence that I would like you to remember after reading this chapter. Do not rely on a decentralized consensus algorithm if your infrastructure is not decentralized. 

Currently, the Ronin exploit brought the largest financial loss to the DeFi world. As an Ethereum sidechain, Ronin provides faster and cheaper transactions, which were utilized in the p2e game – Axie Infinity. 

Sidechains are one of the blockchain scaling solutions, which require a bridge to communicate with different chains. They let users lock up their ETH and reissue the wrapped tokens. Ronin is using the Proof of Authority model which is employed to approve transactions – here it was a consensus of 5 out of 9 validators. However, four PoA validators were operated by the same company behind the project (Sky Mavis). 

In November 2021, due to heavy network traffic, the Axie DAO set up a gas-free RPC node. They allowlisted Sky Mavis keys to sign transactions on their behalf. The arrangement was supposed to last only for a month, but the access list was never revoked. Unfortunately, the attacker (allegedly Lazarus Group) who had compromised four Sky Mavis’ keys also found a vulnerability in the RPC node, leading to the Axie DAO signature leak. The attacker signed a forged withdrawal transaction by hacked keys and came into possession of the $625M. 

The Multisig system used by Ronin to sign off transactions was redundant and didn’t provide a sufficient decentralization level. It concentrated the majority of governance in the individual entity. To protect yourself from this kind of attack it is recommended to increase the number of validators and store their keys in separate, secure places. A good, additional way to eliminate this kind of single point of compromise is to introduce multi-part computing (MPC) to protect private keys. In this method, private keys are fragmented between the nodes and selected ones are required to sign a transaction. 

The security of the verification process in both source and target chains has to be taken into consideration. Additionally, Enterprise Ethereum Alliance reported several security factors of concern in the case of the consensus layer. 

  • Are the specified security requirements of the consensus algorithm considered? 
    • PoW blockchains with low hash rate are susceptible to manipulations by malicious nodes. PoA  chains are in need of really trustworthy entities to confirm transactions. The PoS model should not be based only on a few of the most wealthy users.
  • Is the blockchain finality taken into account?
    • While a transaction is executed in a source blockchain, an associated event is transferred by a relayer to the target blockchain. This mechanism is used to trigger a corresponding, bridged action. The process requires that the transaction cannot change – the block containing it has to be final. A reasonable number of block confirmations to ensure the finality has to be determined. Chains without Immediate Confirmation algorithm implementation are recommended to consider the proper block confirmation amount (e.g., in Ethereum, it takes between six and twelve confirmed blocks for a transaction to be finalized). 

Incorrect input validation in cross-chain protocol

From our observations of various blockchain bridges, we come up with a conclusion to always validate and constrain users’ input.

For example: Qubit Finance is a platform enabling lending and borrowing assets that operates across Ethereum and Binance Smart Chain. As a typical use case, users can deposit their ERC-20 token to the protocol’s bridge and receive BEP-20 in return, which may be used now on BSC. In January 2022, the attacker abused logical error from the smart contract’s code and stole about $80M.

The exploiter input malicious data that allowed him to withdraw tokens on BSC without any deposit on Ethereum chain. It was possible because a QBridge contract did not properly verify if the required amount of ETH was locked, providing malicious proof of non-existent token. In a nutshell, the attacker used the fact that the `safeTransferFrom` function does not revert a transaction if the provided token is an EOA address (in this case address(0)). 

 function deposit(uint8 destinationDomainID, bytes32 resourceID, bytes calldata data) external payable notPaused {
    	require(msg.value == fee, "QBridge: invalid fee");
    	address handler = resourceIDToHandlerAddress[resourceID];
    	require(handler != address(0), "QBridge: invalid resourceID");
    	uint64 depositNonce = ++_depositCounts[destinationDomainID];
    	IQBridgeHandler(handler).deposit(resourceID, msg.sender, data);
    	emit Deposit(destinationDomainID, resourceID, depositNonce, msg.sender, data);
	}
  1. The attacker executed the deposit function with parameters, including resourceID (token contract address is derived from this parameter) and data (encoded amount of ETH that he wanted to transfer to another chain). The ETH value attached to the transaction was sufficient to pass the first requirement.
  2. The IQBridgeHandler(handler) address was derived from provided resourceID and the deposit function was called on it.
function deposit(bytes32 resourceID, address depositer, bytes calldata data)
external override onlyBridge { 
uint option; 
uint amount; 
(option, amount) = abi.decode(data, (uint, uint)); 
address tokenAddress = resourceIDToTokenContractAddress[resourceID]; 
require(contractWhitelist[tokenAddress], "provided tokenAddress is not whitelisted"); 
     if (burnList[tokenAddress]) { 
         require(amount >= withdrawalFees[resourcelD], "less than withdrawal fee"); 
         QBridgeToken(tokenAddress).burnFrom(depositer, amount);
 	} else {
         require(amount >= minAmounts[resourceID][option], "less than minimum amount"); 
        tokenAddress.safeTransferFrom(depositer, address(this), amount); 
 	}
 }
  1. This deposit function verifies if:
  • tokenAddress is whitelisted – it passes because resourceID corresponds to the ETH chain and the tokenAddress is an address(0) which is allowlisted by the contract,
  • the provided amount is greater than the value from withdrawalFees and minAmounts mappings – it passes,
  • safeTransferFrom function is executed correctly – it passes, because an address(0) is EOA, thus underlying low-level call returns true.
  1. The Deposit event is emitted causing the QBridge contract to believe that ETH cross-chain deposit was made – an arbitrary amount of qXETH tokens were minted on the BSC chain for the exploiter.

The attack stems from the fact that the same event was emitted for the ETH coin and ERC-20 token deposit. So, the attacker was allowed to trigger a deposit function for the ERC-20 giving the data appropriate for generating proof used to withdraw ETH on the Binance Smart Chain.  

The remediation, in this case, is to ensure correct validation. All the data from untrusted (and in BC case all inputted data is untrusted) sources have to be subject to validation. It is highly recommended to stick to the following rules described in the Smart Contract Security Verification Standard V6 Malicious input handling chapter:

  • Verify that if the input (function parameters) is validated, the positive validation approach (allowlisting) is used where possible,
  • Verify that the length of the address being passed is determined and validated by a smart contract,
  • Verify that there are no vulnerabilities associated with malicious input handling.

Follow the described recommendations and always separate operations for native ETH coins and ERC-20 tokens.

Lack of cross-contract access control in blockchain bridges

Restricting users from directly calling highly privileged contracts is not sufficient. We highly suggest double-checking cross-contract calls. 

As an example we use a Poly Network which is a bridge platform for ERC-20 tokens that link several blockchains such as Ethereum, BSC, Polygon, Switcheo or Ontology (where the described attack begins). In August 2021, the inappropriate access control between Poly’s contracts led to the temporary loss of $611M. Temporary – because the exploiter returned most of the stolen amount. 

For better understanding, let’s go through the typical, simplified use case related to cross-chain assets between Ethereum and BSC blockchains:

  1. A user executes the lock function from the LockProxy contract (LockProxy.sol#L64) on the Ethereum chain (source chain).
  2. The LockProxy contract calls the crossChain function (LockProxy.sol#L86) on the EthCrossChainManager contract. It generates specified raw parameters required for the cross-chain transaction (EthCrossChainManager.sol#L102).
  3. The EthCrossChainManager contract emits events in order to trigger off-chain ETH relayers. They verify previously generated raw parameters, sync the block header and commit transaction proof to the Poly chain (relay chain).
  4. The contract on the Poly chain verifies the raw parameters again and emits the event in order to trigger off-chain signers (keepers) to sign the transaction. They sign it and send it to the same contract on the Poly chain. 
  5. Cross-chain Manager on Poly chain confirms the keepers’ signatures and executes the cross-chain transaction by emitting the event to trigger BSC chain relayers.
  6. BSC relayers verify raw parameters and invoke verifyHeaderAndExecuteTx function (EthCrossChainManager.sol#L127) on the BSC chain (destination chain). 
  7. Related assets are unlocked and sent to the designated user.

So, as we already understand the basics, let’s look into the real attack scenario:

  1. The attacker skipped the first step and directly called the external crossChain function in order to construct the required raw parameters from the given input. The event was emitted in order to trigger off-chain relayers.
  2. The verifyHeaderAndExecuteTx function (EthCrossChainManager.sol#L127) was invoked by relayers. It verifies if:
    • The inputted block header is correct by checking the signature, 
    • The transaction was included in a block from designated Merkle proof
  1. The internal function _executeCrossChainTx  (EthCrossChainManager.sol#L166) was called and made another call to the target contract.
    • It checks if the called address is a contract, not an EOA (EthCrossChainManager.sol#L185),
    • …but does not prevent the attacker from calling the EthCrossChainData contract, which has a putCurEpochConPubKeyBytes method (EthCrossChainData.sol#L45) responsible for modifying the keepers’ public keys.
  1. The _executeCrossChainTx function would execute an arbitrary defined function from the target contract, because of the following line:
(success, returnData) = _toContract.call(abi.encodePacked(bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))), abi.encode(_args, _fromContractAddr, _fromChainId)));
  1. The called contract has to correspond with the signature hash:
bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))

So, the _method parameter (which is user input) would be concatenated with “(bytes,bytes,uint64)” string. The attacker figured out the suitable value of _method parameter and that the hashed result was the same as the sighash (first four bytes) of the putCurEpochConPubKeyBytes function.

  1. The attacker replaced the legitimate keeper public key with the crafted one, so the attacker became the only keeper. 
  2. The attacker used their corresponding private key to execute arbitrary transactions on the destination chain.

The basic cause of this attack was that the EthCrossChainManager was the owner of the EthCrossChainData contract. Highly privileged contracts that deal with sensitive functions and data should be treated with a lot of consciousness and separated from cross-chain relay calls. Make sure that users cannot use cross-chain messages to interact with those specific contracts. However, in the case when it is necessary, verify that only the desired functions are allowlisted.

Summary – How to secure your blockchain bridge?

No man is an island. Neither are blockchains. As the cross-chain systems are gaining more and more importance in the crypto world, there is a huge room for improvement for this technology in the security field. If you are developing a cross-chain protocol, remember about past attacks and stick to the requirements described in  Smart Contract Security Verification Standard C6 Bridge chapter. Moreover, I encourage you not to forget about a brief conclusion of those attacks:

  • Do not rely on a decentralized consensus algorithm if your infrastructure is not decentralized,
  • Always validate and constrain users’ input.
  • Restricting users from directly calling highly privileged contracts is not sufficient. Remember about cross-contract calls. 

According to Vitalik Buterin, the future will be related to multi-chains blockchain ecosystem rather than bridges. As the main reason, he emphasized fundamental security limits that this mechanism has. It definitely expands the available attack surface, adding the additional level of complexity. Nevertheless, cross-chain adoption keeps increasing given the main course associated with protection improvements.  

If you enjoyed my articles and want to chat about blockchain or smart contracts security, feel free to reach me out. You can find me on Twitter or Linkedin.

Jakub Zmysłowski
Jakub Zmysłowski IT Security Consultant