L1BossBridge contract has only one purpose : bridging an ERC20 token from the current layer (L1) to a L2. To achieve this, the contract can be called to deposit tokens through depositTokensToL2
. This function will ultimately send tokens to L1Vault contract, and emit an event. This event is then listened by some L2 node and new tokens are minted on L2.
In order to withdraw tokens from L2 to L1, L1 bridge operator will listen to events from L2 and will generate a signature each time they receive a withdraw event from L2. They will sign a message with their private key, including the address of the ERC20 token, the value of eth to send with the call, and some data including events data (containing the actual function call to execute, i.e. token.transferFrom(address(vault), to, amount);
.
The vulnerability resides in the fact that the signed message doesn't contain any data uniquely matched to each call, such as a nonce
variable. The signed message is :
This means if the same user wants to withdraw again the same amount, the signature will be the same. This is problematic.
With the current implementation a valid signature could be used again and again by anyone, as blockchain transactions data are public.
This impact of this issue is HIGH as it could lead to the bridge being entirely drained. Anyone who executed one withdrawal from L2 to L1 could use a block explorer to retrieve the transaction's input data, and therefore the signature, and use it again and again, until L1Vault is empty.
Manual
I suggest to add a nonce
variable into the signature. This nonce will be incremented after each withdrawal. This way, any previously valid signature will not be valid anymore because the nonce will be bigger, ultimately modifying the signature of the message.
The contract could declare a new storage variable :
and increase its value by one each time a withdrawal is executed.
withdrawTokensToL1
function could be modified as follows :
and sendToL1
function will be (except other issues declared in other findings):
after declaring a new custom error:
This will ensure each successful withdrawal with sendToL1
function needs a new signature that includes a nonce
value, incremented after each withdrawal. This will ultimately protect the bridge from signature replay attacks.
Nevertheless, this method is not perfect as it is now necessary to strictly respect order with :
Withdraw request on L2
Withdraw execution on L1 through withdrawTokensToL1
function
If there are many requests on L2 at the same time, users may encounter revert on their transaction if someone who requested before them didn't call withdrawTokensToL1
yet.
We could also imagine that L2 contract holds the nonce
variable, and emits through the event this nonce
value, incremented by one after each withdraw request. L1BossBridge contract could declare a mapping (uint256 -> bool)
and check if the nonce used in the signature has already been used (i.e if the mapping value is set to true for this nonce value), updating the mapping after each successful withdrawal.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.