An attacker can call withdrawTokensToL1()
or sendToL1()
with the same signed message, essentially utilizing a signature replay attack.
The contract makes use of signed messages to withdraw funds. These messages are signed by the Signer
role.
The signed message includes: address
of the token, value
of eth(passed as 0), the address
of the vault and the amount
of token to transfer. Whenever a user wants to withdraws funds, the signer generates a signature and the signed message approving the withdrawal of funds. Then, the user can call withdrawTokensToL1()
using the signature, token amount and receiver address. The problem is that there is nothing to keep track of the uniqueness of a single signed message.
This opens an attack vector for a replay attack. An attacker can, like a normal user, deposit and then request a signed message to withdraw their funds. After that they can use the same signed message to withdraw the rest of funds in the vault.
Let's look at the function withdrawTokensToL1()
:
As we can see the signed message has a receiver address
and amount
to be transfered. The transaction cannot be uniquely identified.
This vulnerability can be used to completely drain the funds of the vault.
Manual review
Along with an address
and amount
, the signed message should also have a nonce
to denote the transactions uniqueness. Additionally, transactions (e.g the transaction hash
) should be marked as fulfilled in the contracts storage. For example using a mapping(bytes32 => bool)
. This way the contract can keep track of which transactions are fulfilled.
The following test shows how an attacker can drain the funds completely from the vault. Paste this in L1TokenBridge.t.sol
.
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.