Cross-chain bridges enable token transfers between different layers (e.g., L1 and L2). However, when these bridges support ERC-777 tokens, their advanced callback hooks can introduce reentrancy vulnerabilities—allowing attackers to manipulate token accounting before the original transaction completes.
Fee-on-transfer tokens are tokens that automatically deduct a fee whenever they are transferred. As a result:
- The sender specifies an
amount
to send. - The receiver ends up with a lesser amount (the difference is taken as a fee).
- The bridging contract cannot simply trust the
amount
passed to thetransfer
function.
Hence, bridges often rely on computing balanceAfter - balanceBefore
to determine the exact amount that actually arrived in the contract. This practice accommodates fee-on-transfer tokens, but also opens up potential reentrancy risks when dealing with ERC-777 tokens if the contract is not safeguarded.
-
ERC-777 Callback Hooks
- Unlike standard ERC-20 tokens, ERC-777 supports hooks via the ERC-1820 registry.
- Attackers can register a contract to be notified (
tokensToSend
) whenever a transfer occurs, creating an opening for reentrancy.
-
Balance-Based Bridging Logic
- Bridges measure
balanceBefore
andbalanceAfter
to handle fee-on-transfer tokens. - If a malicious contract re-enters during the transfer, the contract may incorrectly calculate how many tokens were deposited.
- Bridges measure
-
Lack of Reentrancy Protection
- Without proper guards or a safe pattern, the bridge function can be invoked twice (or more) within a single flow, causing inaccurate state updates.
-
Attacker Registers a Malicious Sender Contract
- The attacker sets their contract as an
ERC777TokensSender
in the ERC-1820 registry.
- The attacker sets their contract as an
-
Initial Bridge Call
- The attacker calls
bridgeToken
with 500 tokens. - The bridge records
balanceBefore = 0
, then initiatessafeTransferFrom
.
- The attacker calls
-
Reentrancy During Callback
tokensToSend
is triggered in the attacker’s contract, which re-entersbridgeToken
to transfer another 500 tokens.- Since the first call hasn’t updated state yet, the second call still sees
balanceBefore = 0
.
-
Combined Transfers
- By the time the original call finishes,
balanceAfter
is 1000 in the original call, but the logic credits 1500 tokens on L2 (500 from the reentrant call plus the erroneously calculated amount from the first call).
- By the time the original call finishes,
function bridgeToken(address token, uint256 amount) external {
uint256 balanceBefore = IERC20(token).balanceOf(address(this));
// Transfer tokens from msg.sender to the bridge
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
uint256 balanceAfter = IERC20(token).balanceOf(address(this));
uint256 bridgedAmount = balanceAfter - balanceBefore;
}
Leverage mechanisms like ReentrancyGuard or similar logic that disallows nested calls to vulnerable functions.