Ronin Bridge
Summary
ExpandThe Ronin Bridge mainchain gateway contains a critical reentrancy vulnerability in its withdrawal flow. The `withdrawERC20For` and `withdrawERC721For` functions perform external token transfers before recording the withdrawal in state storage. An attacker with valid validator signatures can re-enter the withdrawal function through token callback hooks (e.g., ERC777 `tokensReceived`) and drain the gateway of funds by executing the same withdrawal multiple times.
Additionally, the signature verification scheme omits the gateway contract address from the signed hash, enabling cross-deployment replay of withdrawal signatures. The custom WETH implementation also fails to track `totalSupply`, breaking ERC20 compliance.
The most urgent recommendation is to reorder state updates before external calls in all withdrawal functions to eliminate the reentrancy attack vector. Signature hashes should be extended to include the contract address to prevent replay attacks across deployments.
Findings
7 issues identified
Reentrancy in withdrawal functions allows draining gateway funds
MainchainGatewayManager.sol:135
View Details
Description
In withdrawERC20For and withdrawERC721For, the withdrawal state (_insertWithdrawalEntry) is recorded AFTER external token transfers. An attacker receiving tokens via a callback hook (ERC777 tokensReceived) can re-enter the same withdrawal function with identical parameters. Since the onlyNewWithdrawal check passes (state not yet updated), the withdrawal executes again, draining the gateway.
Impact
Complete loss of deposited funds. An attacker with valid validator signatures for a single withdrawal can re-enter repeatedly to drain the entire gateway balance of the target token.
Recommendation
Move _insertWithdrawalEntry before all external calls in both withdrawERC20For and withdrawERC721For to follow checks-effects-interactions pattern.
Fix
By moving _insertWithdrawalEntry before the external token transfer, the withdrawal ID is marked as used in storage before any external call. Any re-entrant attempt with the same withdrawal ID will fail the onlyNewWithdrawal check.
contracts/v0.5/chain/mainchain/MainchainGatewayManager.sol
Reentrancy in Withdrawal Functions Allows Double-Withdrawal
MainchainGatewayManager.sol:134
View Details
Description
Both withdrawERC20For and withdrawERC721For record the withdrawal entry (the replay-prevention mechanism) AFTER transferring tokens to the user via external calls. An attacker receiving a callback-capable token (ERC777 tokensReceived hook, or ERC721 onERC721Received) can re-enter the withdrawal function with the same withdrawalId and signatures before the entry is written. The onlyNewWithdrawal check passes on re-entry because the entry does not yet exist, allowing multiple withdrawals from a single valid signature set.
Impact
Complete drainage of bridge funds for the targeted token. An attacker with one valid set of withdrawal signatures can withdraw the same amount repeatedly until the gateway is empty. Given bridge gateways typically hold all deposited mainchain assets, this represents catastrophic loss of funds.
Recommendation
Move _insertWithdrawalEntry before all external calls in both withdrawERC20For and withdrawERC721For to follow the checks-effects-interactions pattern.
Fix
Moving _insertWithdrawalEntry before external calls ensures the onlyNewWithdrawal modifier will reject any re-entrant call with the same withdrawalId, since the entry is already recorded before tokens are transferred.
contracts/v0.5/chain/mainchain/MainchainGatewayManager.sol
Reentrancy in withdrawal functions allows draining gateway
MainchainGatewayManager.sol:138
View Details
Description
In withdrawERC20For and withdrawERC721For, the withdrawal replay protection (_insertWithdrawalEntry) is performed after the external token transfer. Since onlyNewWithdrawal checks owner == address(0), a reentrant call with the same withdrawalId passes the check before the entry is written. Any mapped ERC20 token with transfer hooks (ERC777) allows the recipient to re-enter and execute the same withdrawal repeatedly, draining the gateway.
Impact
Complete drainage of any mapped ERC20 token balance from the gateway. All deposited user funds for the affected token are at risk. Attacker only needs one valid set of validator signatures.
Recommendation
Move _insertWithdrawalEntry before all external calls in both withdrawERC20For and withdrawERC721For to follow the checks-effects-interactions pattern.
Fix
Moves the withdrawal entry insertion before external calls so that the onlyNewWithdrawal check prevents reentrant calls from replaying the same withdrawal.
contracts/v0.5/chain/mainchain/MainchainGatewayManager.sol
Withdrawal signature hash missing contract address enables cross-deployment replay
MainchainGatewayManager.sol:138
View Details
Description
The withdrawal signature hashes in withdrawERC20For and withdrawERC721For do not include address(this). Signatures are computed as keccak256(abi.encodePacked(‘withdrawERC20’, _withdrawalId, _user, _token, _amount)) without binding to the specific gateway deployment. If the gateway is redeployed or if the same validator set is used on another deployment, all historical withdrawal signatures can be replayed against the new contract.
Impact
All past withdrawal signatures become valid against any new gateway deployment sharing the same validator set. An attacker can extract signatures from on-chain transaction data and replay them to drain a redeployed gateway.
Recommendation
Include address(this) in both ERC20 and ERC721 withdrawal signature hashes.
Fix
Adding address(this) to the signed hash binds each signature to a specific gateway deployment. Signatures produced for one gateway address cannot be replayed against a different deployment. Validators must also update their signing logic to include the gateway address.
contracts/v0.5/chain/mainchain/MainchainGatewayManager.sol
Withdrawal Signatures Lack Chain ID and Contract Address Binding
MainchainGatewayManager.sol:140
View Details
Description
The withdrawal signature hash is computed as keccak256(abi.encodePacked(‘withdrawERC20’, _withdrawalId, _user, _token, _amount)) without including block.chainid or address(this). This means valid withdrawal signatures are not bound to a specific chain or contract deployment. After an Ethereum hard fork, or if the gateway is redeployed, the same signatures can be replayed to execute unauthorized withdrawals.
Impact
Cross-chain signature replay after forks (e.g., ETH/ETC split) could drain the gateway on the forked chain. Cross-deployment replay could drain a redeployed gateway if the validator set remains the same.
Recommendation
Include chainId and address(this) in the signature hash for both withdrawERC20For and withdrawERC721For. Add a getChainId helper function.
Fix
Adding chainId and address(this) to the hash ensures signatures are only valid for the specific chain and contract deployment they were issued for. Validators must also include these fields when signing. The getChainId helper retrieves the chain ID via assembly since Solidity 0.5 does not expose block.chainid natively.
contracts/v0.5/chain/mainchain/MainchainGatewayManager.sol
Cross-chain signature replay due to missing chainId/address binding
MainchainGatewayManager.sol:140
View Details
Description
Withdrawal signature hashes use keccak256(abi.encodePacked(‘withdrawERC20’, _withdrawalId, _user, _token, _amount)) without including block.chainid or address(this). For a cross-chain bridge deployed on multiple EVM chains with overlapping validator sets, valid withdrawal signatures from one chain can be replayed on another chain to double-withdraw funds.
Impact
Double-withdrawal across chains. Attacker who obtains a valid withdrawal signature on Chain A can replay it on Chain B, draining bridge funds on the target chain.
Recommendation
Include address(this) and the chain ID in all withdrawal signature hashes. Since block.chainid is not available in Solidity 0.5.x, store the chain ID at construction time using assembly.
Fix
Adds chain ID and contract address to the signed hash, preventing cross-chain and cross-contract signature replay. A getChainId() helper using inline assembly is needed since block.chainid is unavailable in Solidity 0.5.x.
contracts/v0.5/chain/mainchain/MainchainGatewayManager.sol
Fee-on-transfer tokens cause deposit accounting mismatch
MainchainGatewayManager.sol:62
View Details
Description
depositERC20For records the caller-specified _amount as the deposited value without verifying the actual tokens received. If a mapped token charges a fee on transfer, the gateway receives fewer tokens than recorded, creating a growing insolvency that prevents later withdrawers from receiving their full amounts.
Impact
Gateway token balance becomes less than the sum of recorded deposits. Cumulative deficit grows with each deposit. Last users to withdraw cannot get their funds.
Recommendation
Measure the actual balance change by reading balanceOf before and after the transferFrom call.
Fix
Measures the actual token balance change rather than trusting the input amount, correctly accounting for fee-on-transfer tokens.
contracts/v0.5/chain/mainchain/MainchainGatewayManager.sol
Conclusion
ExpandThe Ronin Bridge mainchain gateway contains a critical reentrancy vulnerability in its withdrawal flow that could allow an attacker to drain deposited funds by re-entering during token transfer callbacks. This is the highest-priority fix required before any deployment or continued operation.
The signature scheme also lacks deployment-specific binding (address(this)), creating a replay vector across gateway redeployments. Both issues stem from the withdrawal functions violating the checks-effects-interactions pattern and omitting standard signature domain separation.
The core deposit flow and validator management logic are soundly structured, with proper use of SafeMath, sorted-signer deduplication, and threshold verification. With the reentrancy fix and signature hardening applied, the gateway’s security posture would be substantially improved.
The codebase should undergo a follow-up audit after remediations are applied, with particular attention to the interaction between the proxy pattern and storage layout, and integration testing of the full deposit-withdrawal cycle with callback-enabled tokens.
Legal Disclaimer: This report covers the code submitted for analysis. It does not account for infrastructure, deployment configuration, third-party dependencies, or changes made after the audit date. Automated analysis may produce false positives or miss context-dependent vulnerabilities. audited.xyz provides this report “as is” without warranty of any kind.