On August 6, blockchain security audit company Beosin Alert monitored that the Ronin Bridge project had abnormal cross-chain asset extraction behavior. According to the analysis of the Beosin security team, the root cause of this abnormal behavior is that when the project upgraded the contract, it did not properly initialize the operator weight required for cross-chain transaction confirmation, resulting in the minimumVoteWeight parameter in the contract being zero, so that anyone's signature can pass the cross-chain verification.
Attack transaction link:
https://etherscan.io/tx/0x2619570088683e6cc3a38d93c3d98899e5783864e15525d5f5810c11189ba6cb
Beosin is currently working with the project party to handle this incident. In this long article, we will also analyze the two points of abnormality in this transaction:
First, the withdrawal amount is too high. In Ronin Bridge, there is a limit on the amount of cross-chain withdrawals. If the amount of cross-chain withdrawals is too large, it needs to be transferred to manual confirmation. The limit of the cross-chain asset WETH in this transaction is 4000. This transaction withdrew 3996. Of course, this is not a loophole, but it is enough to attract attention.
Secondly, after checking this withdrawal transaction, we found that there is only one cross-chain validator Operator, and the corresponding operation weight is 0, which means that there is a problem with this transaction. Because in the Ronin Bridge project, users need to obtain signatures from multiple Operators to withdraw cross-chain assets, and the accumulated weight must reach the specified threshold.
Event Analysis:
After analyzing the on-chain contracts and data, we found that the contract Operator is independently managed by the corresponding Manager contract, and it is a governance contract specifically used to manage Ronin Bridge. After checking its transaction records, we found that the most recent transaction was to upgrade the Ronin Bridge contract, and it was just before the abnormal transaction. Therefore, the general aspects of the vulnerability are basically clear, and it should be a problem caused by the project party upgrading the contract.
Going deeper, by comparing the codes before and after the Ronin Bridge upgrade, we found that the key parameter “_totalOperatorWeight” used by the event was newly added in this upgrade, and it was necessary to call the initializeV3 function in the upgrade to initialize the “OperatorWeight” newly added in the V3 version.
Unfortunately, in the transaction to upgrade the contract, the initializeV3 function was not called, but the initializeV4 function was mistakenly called to initialize the V4 version.
At this point, the principle of the vulnerability in this incident has become clear. The Ronin Bridge project did not correctly initialize the newly added data when upgrading the contract, causing the corresponding key data "_totalOperatorWeight" to always be 0, allowing any user's withdrawal request to pass the review.
Before the release of this article, the project owner has confirmed the issue and published a statement saying that the attack was carried out by a white hat, and that a refund has been made without causing excessive losses. This is good news, but it also exposes the error-prone point of contract upgrades.
Upgradable contracts
Upgradable contracts are a Solidity smart contract design that allows deployed contracts to be upgraded or modified in the future without having to be fully redeployed. The core of this concept is to separate the logic and data of the contract and use "delegatecall" to call the logical contract.
Although this model provides flexible upgrade capabilities, we must also pay great attention to its security. Since the proxy contract is responsible for forwarding all user requests, it actually becomes the entry point of the contract system. Any attack on the proxy contract may affect the security of the entire contract. Therefore, when designing and deploying upgradeable contracts, it is crucial to ensure the security of the proxy model. At present, the following points should be noted for contracts in the proxy model:
Function selector conflict
In the Ethereum Virtual Machine (EVM), each smart contract function has a unique identifier called a function selector. This selector is the hash value of the first 4 bytes of the function signature. The function selector is used to determine the specific function in the contract and ensure that the call request is correctly routed to the corresponding function implementation. When calling a function in proxy mode, it will first check whether the function interface in the proxy contract can match the calling function. If it does not match, the delegatecall in the fallback will be used to call the logic contract.
Therefore, if there are functions with the same function selector in the proxy contract and the logic contract, when the proxy contract receives the call, it directly calls the function in the proxy contract instead of the logic contract, which may lead to unexpected behavior or security vulnerabilities.
Storage Conflicts
In the Ethereum Virtual Machine (EVM), the state data of a contract is stored in specific storage slots, and the address of each storage slot is determined by its index (starting from 0). Each state variable in the contract corresponds to a storage slot, and the data is persisted in these slots.
In the proxy mode, storage slots are usually managed by the proxy contract, and the logic contract accesses these slots through the proxy contract. Storage conflicts may occur when newly added state variables in the logic contract conflict with existing state variable slots in the proxy contract. This may cause data in the proxy contract to be overwritten or inconsistent.
Contract initialization problem
In the proxy mode, due to the separation of the proxy contract and the logic contract, each upgrade may involve the change and addition of variables, so it is necessary to ensure that these key variables are set correctly during the upgrade. The ronin bridge incident was caused by this problem.
In addition, it is necessary to ensure that the initialization function initialize can only be called once to prevent malicious attackers from calling it repeatedly after initialization and modifying key variables.
ldelegatecall calling mechanism
Delegatecall is a low-level calling mechanism that allows one contract to execute the code of another contract in its context. This means that the storage, address, and message sender of the calling contract remain unchanged, but the executed logic comes from the called target contract. Although delegatecall provides powerful functionality, it also needs to be used with caution.
If the target contract address does not exist, the execution of delegatecall will fail and return a failure code, but this failure may not be discovered immediately. As a result, the call in the proxy contract may appear to be successful, but the actual operation is not effective, resulting in system inconsistencies or errors.
Proxy contract authority management
In the proxy model, permission management is another key security issue. The proxy model separates the responsibilities of the proxy contract and the logic contract, allowing the contract to be upgraded without changing the data storage, but it also introduces complex permission management issues. Proper permission management is crucial to ensure the security and stability of the contract system.
As one of the earliest blockchain security companies in the world engaged in formal verification, Beosin focuses on the "security + compliance" full-ecological business and has established branches in more than 10 countries and regions around the world. Its business covers code security audits before project launch, security risk monitoring and blocking during project operation, stolen recovery, virtual asset anti-money laundering (AML), and compliance assessments that meet local regulatory requirements, etc. "One-stop" blockchain compliance products + security services