xanoutas/solidity-security-findings huggingface dataset:
Invalid Signal used to Prove Signal Received
Audit: 2024-03-taiko-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/bridge/Bridge.sol#L384
https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/bridge/Bridge.sol#L364
https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/bridge/Bridge.sol#L579


# Vulnerability details

## Impact
Invalid Signal used to Prove Signal Received in the Bridge contract
## Proof of Concept
```solidity
    function _proveSignalReceived(
        address _signalService,
 >>>       bytes32 _signal,
        uint64 _chainId,
        bytes calldata _proof
    )
        private
        view
        returns (bool success_)
    {
        bytes memory data = abi.encodeCall(
            ISignalService.proveSignalReceived,
            (_chainId, resolve(_chainId, "bridge", false), _signal, _proof)
        );
        (success_,) = _signalService.staticcall(data);
    }
```
The function above shows how Received signals are proven by the _proveSignalReceived(...) function, it can be noted that the second parameter takes in the signal to be proven. A look at the code below also shows where this function is called as noted in the pointer, it can be noted that the hashed Message was converted to signal before it was proved with the _proveSignalReceived(...) function.
```solidity
    function proveMessageFailed(
        Message calldata _message,
        bytes calldata _proof
    )
        public
        view
        returns (bool)
    {
        if (_message.srcChainId != block.chainid) return false;

        return _proveSignalReceived(
            resolve("signal_service", false),
>>>            signalForFailedMessage(hashMessage(_message)),
            _message.destChainId,
            _proof
        );
    }
```
The problem is in the code below in other instance where this signal prove function was called, instead of converting the hashMessage to a provable signal, the hash message was used directly which would means proving an invalid signal
```solidity
 function proveMessageReceived(
        Message calldata _message,
        bytes calldata _proof
    )
        public
        view
        returns (bool)
    {
        if (_message.destChainId != block.chainid) return false;
        return _proveSignalReceived(
>>>            resolve("signal_service", false), hashMessage(_message), _message.srcChainId, _proof
        );
    }
```
## Tools Used
Manual Reveiw
## Recommended Mitigation Steps
The protocol should make necessary adjustment to ensure the hashMessage is first converted to a provable signal to avoid Denial of service or loss of fund from Wrong Signal


## Assessed type

en/de-code
The signature used in the `TimelockTokenPool.withdraw` function might be reused since the signed message doesn't include `nonce` and `deadline`
Audit: 2024-03-taiko-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/team/TimelockTokenPool.sol#L170


# Vulnerability details

The `TimelockTokenPool` contract allows for token withdrawals on behalf of users by utilizing a permit-like signature:
```solidity
function withdraw(address _to, bytes memory _sig) external {
    if (_to == address(0)) revert INVALID_PARAM();
    bytes32 hash = keccak256(abi.encodePacked("Withdraw unlocked Taiko token to: ", _to));
    address recipient = ECDSA.recover(hash, _sig);
    _withdraw(recipient, _to);
}
```
This function might be called multiple times as the tokens are unlocked gradually. The issue arises from the fact that the signed message doesn't include `deadline` and `nonce`. It means that anyone can reuse this signature and replay a withdrawal transaction. 

It's not much of an issue if the user manages approvals meticulously, but users often opt for unlimited approvals to save on gas. In this case, an attacker is still unable to steal anything, but they can replay a withdrawal transaction without the user's consent. This is quite concerning, as this transaction might essentially involve swapping `costToken` for `taikoToken` and the user might have some other use for `costToken` in mind. There also might be extreme cases where the user attempts to prevent liquidation using `costToken`, only to be front-run by the attacker.

This issue becomes even more severe if the `_to` address is compromised. In such a scenario, an attacker can get `taikoToken` at the expense of the user. The issue becomes more likely because of the following:
* Users might not think that the signature produced some time ago can be reused since such kinds of permits usually include `nonce` and `deadline` and can't be reused;
* Users have no means to revoke the signature, and the only way to protect themselves from this attack is to execute both `costToken` approval and withdrawal within a single transaction which is possible but isn't simple and obvious for the common user.

## Impact
An attacker can reuse a signature to execute an action without the user's consent. In some cases it allows the attacker to steal user's funds.

## Proof of Concept
\-

## Tools Used
Manual Review

## Recommended Mitigation Steps
Consider adding a `nonce` and `deadline` to the signed message. Additionally, consider exposing a function that marks the current `nonce` as used, providing a means for the user to revoke signatures.


## Assessed type

Invalid Validation
Users can't withdraw funds from the `ERC20Airdrop2` contract after the withdrawal window
Audit: 2024-03-taiko-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol#L40


# Vulnerability details

The current implementation of the `ERC20Airdrop2` contract allows a user to claim funds between `claimStart` and `claimEnd` and to withdraw funds between `claimEnd` and `claimEnd + withdrawalWindow`. The withdrawable amount also grows linearly and becomes equal to the full `claimAmount` at the end of the withdrawal window. Presumably, it was implemented this way to deal with the tokens that might be never claimed.

However, several issues arise with the current implementation:
1. The only way to withdraw the full amount is to send a transaction that will be mined in the block with `timestamp == claimEnd + withdrawalWindow`. This block might not exist at all so the users might not be able to withdraw the full airdrop amount.
2. To minimize gas costs users are incentivized to send only one transaction as close to the end of the withdrawal window as possible. The issue here is that their transaction might be mined too late (because of the sudden gas spike or if the network is too busy) and they will not be able to withdraw anything.
3. From the UX perspective, some users might be forced to perform some actions or at least monitor the process (if the transaction will be executed on their behalf) at an inconvenient time (4 AM, for example).

## Impact
The users might not be able to withdraw the full airdrop amount or could potentially lose it entirely.

## Proof of Concept
\-

## Tools Used
Manual Review

## Recommended Mitigation Steps
Consider introducing a new storage variable `withdrawalGrowPeriod` that will be used in calculating the withdrawable amount. This value should be less than `withdrawalWindow` and provide a reasonable amount of time for users to execute the full withdrawal.


## Assessed type

Timing
lack of input validation when proposing block which can make the assigned prover loss money
Audit: 2024-03-taiko-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/libs/LibProposing.sol#L68
https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/hooks/AssignmentHook.sol#L100
https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/hooks/AssignmentHook.sol#L18


# Vulnerability details

# lack of input validation when proposing block which can make the assigned prover loss money

### Line of code
1. https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/libs/LibProposing.sol#L68

2. https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/hooks/AssignmentHook.sol#L100

3. https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/hooks/AssignmentHook.sol#L18

### Proof of concept
in function proposeBlock,

[Line of code](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/libs/LibProposing.sol#L68)
```solidity
 function proposeBlock(
        TaikoData.State storage _state,
        TaikoData.Config memory _config,
        IAddressResolver _resolver,
        bytes calldata _data,
        bytes calldata _txList
    )
```
in the end of proposeBlock, the assigned prover needs to transfer 250 token as bond via the assignment hook
```solidity
// Run all hooks.
            // Note that address(this).balance has been updated with msg.value,
            // prior to any code in this function has been executed.
            address prevHook;
            for (uint256 i; i < params.hookCalls.length; ++i) {
                if (uint160(prevHook) >= uint160(params.hookCalls[i].hook)) {
                    revert L1_INVALID_HOOK();
                }

                // When a hook is called, all ether in this contract will be send to the hook.
                // If the ether sent to the hook is not used entirely, the hook shall send the Ether
                // back to this contract for the next hook to use.
                // Proposers shall choose use extra hooks wisely.
                IHook(params.hookCalls[i].hook).onBlockProposed{ value: address(this).balance }(
                    blk, meta_, params.hookCalls[i].data
                );

                prevHook = params.hookCalls[i].hook;
            }
            // Refund Ether
            if (address(this).balance != 0) {
                msg.sender.sendEther(address(this).balance);
            }

            if (tko.balanceOf(address(this)) != tkoBalance + _config.livenessBond) {
                revert L1_LIVENESS_BOND_NOT_RECEIVED();
            }
```
the expect hook and the expected called function is:
```solidity
IHook(params.hookCalls[i].hook).o
lack of input validation when proposing block make the assigned prover loss money
Audit: 2024-03-taiko-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/libs/LibProposing.sol#L68
https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/hooks/AssignmentHook.sol#L100


# Vulnerability details

# lack of input validation when proposing block make the assigned prover loss money

### Line of code
1. https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/libs/LibProposing.sol#L68
2. https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/hooks/AssignmentHook.sol#L100
### Proof of concept
[Line of code](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/libs/LibProposing.sol#L68)
```solidity
function proposeBlock(
        TaikoData.State storage _state,
        TaikoData.Config memory _config,
        IAddressResolver _resolver,
        bytes calldata _data,
        bytes calldata _txList
    )
```
in the end of `proposeBlock`, the assigned prover needs to transfer 250 token as bond via the assignment hook
```solidity
            // Run all hooks.
            // Note that address(this).balance has been updated with msg.value,
            // prior to any code in this function has been executed.
            address prevHook;
            for (uint256 i; i < params.hookCalls.length; ++i) {
                if (uint160(prevHook) >= uint160(params.hookCalls[i].hook)) {
                    revert L1_INVALID_HOOK();
                }

                // When a hook is called, all ether in this contract will be send to the hook.
                // If the ether sent to the hook is not used entirely, the hook shall send the Ether
                // back to this contract for the next hook to use.
                // Proposers shall choose use extra hooks wisely.
                IHook(params.hookCalls[i].hook).onBlockProposed{ value: address(this).balance }(
                    blk, meta_, params.hookCalls[i].data
                );

                prevHook = params.hookCalls[i].hook;
            }
            // Refund Ether
            if (address(this).balance != 0) {
                msg.sender.sendEther(address(this).balance);
            }

            // Check that after hooks, the Taiko Token balance of this contract
            // have increased by the same amount as _config.livenessBond (to prevent)
            // multiple draining payments by a malicious proposer nesting the same
            // hook.
            if (tko.balanceOf(address(this)) != tkoBalance + _config.livenessBond) {
                revert L1_LIVENESS_BOND_NOT_RECEIVED();
            }
        }

```
the expect hook and the expected called function is:
```solidity
IHook(params.hookCalls[i].hook).onBlockProposed{ value: address(this).balance }(
Malicious users can send messages with arbitrary data through the bridge allowing them to steal funds
Audit: 2024-03-taiko-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/bridge/IBridge.sol#L42-L43
https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/bridge/Bridge.sol#L115


# Vulnerability details

## Impact
Users are allowed to pass arbitrary data through Taiko's bridge via `sendMessage`. Malicious users can construct data that will for example contain an address of an ERC20 token, amount and a `to` address allowing them to bridge a canonical token that they've never owned and/or refund/recall tokens that they've never owned thus draining vault(s). 

## Proof of Concept
When a user bridges tokens via an ERC20 vault (for e.g.) the data parameter that is passed into the bridge when calling `sendMessage()` is characterized as the following: 

```
 msgData_ = abi.encodeCall(
            this.onMessageInvocation, abi.encode(ctoken_, _user, _to, balanceChange_)
```

The token and the amount is based on the tokens that the user has sent to the contract, but if a malicious user calls `sendMessage` with constructed malicious data, it allows them to circumvent the process of going through an ERC20 vault and directly input the amount/token that they want without actually having to own it.

There are multiple ways in which this can be exploited, below we'll take a look at a user whose message was intentionally failed and he is using the source ERC20 vault to mint themselves an arbitrary amount of tokens. 

- Bob sends a message with arbitrary data which contains an amount of Bridged tokens that he wants to convert to canonical ones, without him actually owning the tokens or sending them anywhere.
```
msgHash_ = hashMessage(message_);

        ISignalService(resolve("signal_service", false)).sendSignal(msgHash_);
        emit MessageSent(msgHash_, message_);
```
- Bob intentionally sets a low gas limit allowing for the message to fail.
- After the message fails, it is recalled using `recallMessage`, the function logic will take the arbitrary data we've provided when we actually sent the message and call `onMessageRecalled` on the source chain with our data:
```
   // Perform recall
                IRecallableSender(_message.from).onMessageRecalled{ value: _message.value }(
                    _message, msgHash
                );

                // Must reset the context after the message call
                _resetContext();
```

`onMessageRecalled` will execute the arbitrary logic:

```
  (bytes memory data) = abi.decode(_message.data[4:], (bytes));
        (CanonicalERC20 memory ctoken,,, uint256 amount) =
            abi.decode(data, (CanonicalERC20, address, address, uint256));

        // Transfer the ETH and tokens back to the owner
        address token = _transferTokens(ctoken, _message.srcOwner, amount);
        _message.srcOwner.sendEther(_message.value);
```

And perform the transfer to the `srcOwner`, in our case Bo
RecallMessage in Bridge can revert and make user lose refund
Audit: 2024-03-taiko-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/bridge/Bridge.sol#L155


# Vulnerability details

# RecallMessage in Bridge can revert and make user lose refund

### Line of code
1. https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/bridge/Bridge.sol#L155
2. https://docs.soliditylang.org/en/latest/control-structures.html#try-catch
3. https://docs.openzeppelin.com/contracts/2.x/api/utils#Address-isContract-address-
### Proof of concept
Suppose user send a message from source chain to target chain 

and the message cannot be executed in target chain, there is an option to recover the fund

[Line of code](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/bridge/Bridge.sol#L155)
```solidity
  /// @inheritdoc IBridge
  function recallMessage(
      Message calldata _message,
      bytes calldata _proof
  )
      external
      nonReentrant
      whenNotPaused
      sameChain(_message.srcChainId)
  {
```

After the transaction is proved as failed, the function `recallMessage` is triggered.

Inside `recallMessage` function, note these few lines of code
```solidity
if (block.timestamp >= invocationDelay + receivedAt) {
            delete proofReceipt[msgHash];
            messageStatus[msgHash] = Status.RECALLED;

            // IRecallableSender interface
            if (_message.from.supportsInterface(type(IRecallableSender).interfaceId)) {
                _storeContext(msgHash, address(this), _message.srcChainId);

                // Perform recall
                IRecallableSender(_message.from).onMessageRecalled{ value: _message.value }(
                    _message, msgHash
                );

                // Must reset the context after the message call
                _resetContext();
            } else {
                _message.srcOwner.sendEther(_message.value);
            }
            emit MessageRecalled(msgHash);
        }

```
Note the check here:
```solidity
  if (_message.from.supportsInterface(type(IRecallableSender).interfaceId)) { 
  // to xxx
  } else {
     _message.srcOwner.sendEther(_message.value);
  }

```
### Impact

But when sending a message,
```solidity
  message_.from = msg.sender;
```
the `message_.from` is `msg.sender`, if a EOA account trigger `sendMessage` directly, 

a EOA address is not a smart contract and has byte code

so calling the `_message.from.supportsInterface(type(IRecallableSender).interfaceId)` 

when _message.from is a EOA account revert the transaction first before sending refund 

because the code is missing the contract existence check

[Doc Link to refer](https://docs.soliditylang.org/en/latest/control-structures.html#try-catch)
```
Due to the fact that the EVM considers a call to a non-existing contract to always succeed, 

Solidity uses the extcodesize opcode to check that the
Lack of signature replay protection in TimelockTimePool.sol withdraw function
Audit: 2024-03-taiko-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/team/TimelockTokenPool.sol#L168
https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/team/TimelockTokenPool.sol#L208


# Vulnerability details

# Lack of signature replay protection in TimelockTimePool.sol withdraw function

### Line of code
1. https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/team/TimelockTokenPool.sol#L168
2. https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/team/TimelockTokenPool.sol#L208
### Proof of concept
In `TimelockTokenPool.sol`,

[Line of code](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/team/TimelockTokenPool.sol#L168)
```solidity
  /// @notice Withdraws all withdrawable tokens.
  /// @param _to The address where the granted and unlocked tokens shall be sent to.
  /// @param _sig Signature provided by the grant recipient.
  // @audit
  // replay protection is missing
  function withdraw(address _to, bytes memory _sig) external {
      if (_to == address(0)) revert INVALID_PARAM();
      bytes32 hash = keccak256(abi.encodePacked("Withdraw unlocked Taiko token to: ", _to));
      address recipient = ECDSA.recover(hash, _sig);
      _withdraw(recipient, _to);
  }
```
### Impact
there is lack of validation for signature replay,

then once the recipient sign a signature, anyone can replay the signature to trigger withdraw repeatedly

[Line of code](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/team/TimelockTokenPool.sol#L208)
```solidity
  function _withdraw(address _recipient, address _to) private {
        Recipient storage r = recipients[_recipient];

        (,,, uint128 amountToWithdraw, uint128 costToWithdraw) = getMyGrantSummary(_recipient);

        r.amountWithdrawn += amountToWithdraw;
        r.costPaid += costToWithdraw;

        totalAmountWithdrawn += amountToWithdraw;
        totalCostPaid += costToWithdraw;

        IERC20(taikoToken).transferFrom(sharedVault, _to, amountToWithdraw);
        IERC20(costToken).safeTransferFrom(_recipient, sharedVault, costToWithdraw);
```
And this will let the sharedVault recipient replay the amountToWithdraw several times,

and force the _recipient to pay the costToWithdraw several times to drain the fund 

that remain static in the _recipient wallet
### Recommendation
mark the hash as used and do not allow signature reuse.


## Assessed type

Token-Transfer