xanoutas/solidity-security-findings huggingface dataset:
A recalled ERC20 bridge transfer can lock tokens in the bridge
Audit: 2024-03-taiko-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/tokenvault/ERC20Vault.sol#L320-L335
https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/tokenvault/adapters/USDCAdapter.sol#L43-L45
https://github.com/circlefin/stablecoin-evm/blob/0828084aec860712531e8d79fde478927a34b3f4/contracts/v1/FiatTokenV1.sol#L133-L136


# Vulnerability details

## Impact

A recalling ERC20 bridge transfer can lock funds in the bridge if the call to mint tokens fail on the source chain. Depending on the Native token logic this could either be a permanent lock or a lock of an unknown period of time.

Example of how this can happen with the provided USDCAdapter:

USDC limits the amount of USDC that can be minted on each chain by giving each minter a minting allowance. If the minting allowance is reach minting will revert. If this happens in a recalled message the tokens together with the ETH value is locked.
## Proof of Concept

USDC limits the amount of USDC that can be minted on each chain by giving each minter a minting allowance.

If `_amount <= mintingAllowedAmount` is reached for the `USDCAdapter` tokens can not be minted but since this is a recalled message the funds are stuck.

Both `onMessageIncovation()` and `onMessageRecalled()` call `_transferToken()` to either mint or release tokens

https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/tokenvault/ERC20Vault.sol#L320-L335

```solidity
    function _transferTokens(
        CanonicalERC20 memory _ctoken,
        address _to,
        uint256 _amount
    )
        private
        returns (address token_)
    {
        if (_ctoken.chainId == block.chainid) {
            token_ = _ctoken.addr;
            IERC20(token_).safeTransfer(_to, _amount);
        } else {
            token_ = _getOrDeployBridgedToken(_ctoken);
            IBridgedERC20(token_).mint(_to, _amount);
        }
    }
```

A recalled message to bridge USDC L1->L2 will revert when we attempt to mint through the `USDCAdapter`

https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/tokenvault/adapters/USDCAdapter.sol#L43-L45

```solidity
    function _mintToken(address _account, uint256 _amount) internal override {
        usdc.mint(_account, _amount);
    
```

On the following condition in the native USDC contract

https://github.com/circlefin/stablecoin-evm/blob/0828084aec860712531e8d79fde478927a34b3f4/contracts/v1/FiatTokenV1.sol#L133-L136

```solidity
        require(
            _amount <= mintingAllowedAmount,
            "FiatToken: mint amount exceeds minterAllowance"
        );
```


Course of events that ends in locked funds:

1. User bridges USDC from L2->L1
2. The message is recalled from L1
3. The USDCAdapter has reached the `mintingAllowedAmount`
4. The recalled m
Bridge watcher can forge arbitrary message and drain bridge
Audit: 2024-03-taiko-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

The `bridge_watchdog` role can forge arbitrary messages and drain the bridge of all ETH and tokens. 
## Proof of Concept

`bridge_watchdog` can call `suspendMessasges()` to suspend and un-suspend a message

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

```solidity
    function suspendMessages(
        bytes32[] calldata _msgHashes,
        bool _suspend
    )
        external
        onlyFromOwnerOrNamed("bridge_watchdog")
    {
        uint64 _timestamp = _suspend ? type(uint64).max : uint64(block.timestamp);
        for (uint256 i; i < _msgHashes.length; ++i) {
            bytes32 msgHash = _msgHashes[i];
            proofReceipt[msgHash].receivedAt = _timestamp;
            emit MessageSuspended(msgHash, _suspend);
        }
    }
```

When this function is called to un-suspend a message we set `proofReceipt[msgHash] = _timestamp`. If the msgHash was not proven before it will now be treated as proven since any `msgHash` with a `timestamp != 0` is treated as proven

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

```solidity
        uint64 receivedAt = proofReceipt[msgHash].receivedAt;
        bool isMessageProven = receivedAt != 0
```

`bridge_watchdog` can therefore forge arbitrary messages and have them treated as proven by first suspending them and then un-suspending them. 

`bride_watchdog` is supposed to only be able to ban and suspend messages, in the expected worst case `bridge_watchdog` is limited to DDOSing messages and bans until governance removes the the `bridge_watchdog`.

With the privilege escalation shown here the role can instead drain the bridge of all ETH and tokens. 
### POC

Here is a POC showing that we can forge an arbitrary message by suspending and un-suspending a message 

To run this POC first change the following code in Bridge.t.sol so that we use a real signalService

```diff
register(
+address(addressManager), "signal_service", address(signalService), destChainId 
-address(addressManager), "signal_service", address(mockProofSignalService), destChainId 
);
```

Paste the below code and run into Bridge.t.sol and run `forge test --match-test testWatchdogDrain -vvv`

```solidity
    function testWatchdogDrain() public {
        uint256 balanceBefore = Bob.balance;
        IBridge.Message memory message = IBridge.Message({
            id: 0,
            from: address(bridge),
            srcChainId: uint64(block.chainid),
            destChainId: destChainId,
            srcOwner: Alic
First block proposer check in the `LibProposing._isProposerPermitted` function is errorneous
Audit: 2024-03-taiko-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/L1/libs/LibProposing.sol#L93-L94
https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/L1/libs/LibProposing.sol#L299-L317


# Vulnerability details

## Impact

The `LibProposing.proposeBlock` function calls the `_isProposerPermitted` private function, to ensure if the `proposer is set`. Only that specific address has the permission to propose the block.

In the `_isProposerPermitted` function, for the first block after the genesis block only the `proposerOne` is allowed to propose the first block as shown below:

```solidity
            address proposerOne = _resolver.resolve("proposer_one", true);
            if (proposerOne != address(0) && msg.sender != proposerOne) {
                return false;
            }
```

But the issue here is that when the `msg.sender == proposerOne` the function `does not return true` if the following conditions occur.

If the `proposer != address(0) && msg.sender != proposer`. In which case even though the `msg.sender == proposerOne` is `true` for the first block the `_isProposerPermitted` will still return `false` thus reverting the block proposer for the first block.

Hence even though the `proposer_one` is the proposer of the first block the transaction will still revert if the above mentioned conditions occur and the `_isProposerPermitted` returns `false` for the first block after the genesis block.

Hence this will break the block proposing logic since the proposal of the first block after the genesis block reverts thus not allowing subsequent blocks to be proposed.

## Proof of Concept

```solidity
        TaikoData.SlotB memory b = _state.slotB;
        if (!_isProposerPermitted(b, _resolver)) revert L1_UNAUTHORIZED();
```

https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/L1/libs/LibProposing.sol#L93-L94

```solidity
    function _isProposerPermitted(
        TaikoData.SlotB memory _slotB,
        IAddressResolver _resolver
    )
        private
        view
        returns (bool)
    {
        if (_slotB.numBlocks == 1) {
            // Only proposer_one can propose the first block after genesis
            address proposerOne = _resolver.resolve("proposer_one", true);
            if (proposerOne != address(0) && msg.sender != proposerOne) {
                return false;
            }
        }

        address proposer = _resolver.resolve("proposer", true);
        return proposer == address(0) || msg.sender == proposer;
    }
```

https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/L1/libs/LibProposing.sol#L299-L317

## Tools Used
Manual Review and VSCode

## Recommended Mitigation Steps

It is recommended to add logic in the `LibProposing._isProposerPermitted` function to `return true` when the `msg.sender == proposerOne`, for proposing the first block after genesis block.


## Assessed type

DoS
User can invoke/execute suspended messages through function retryMessage()
Audit: 2024-03-taiko-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
When messages are suspended, the `_message.data` is not allowed to be invoked upon the `_message.to` contract. 

The issue is that if a message is suspended while the STATUS is RETRIABLE, the [retryMessage()](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/bridge/Bridge.sol#L310) does not stop the call from executing. This breaks the suspension and allows the `_message.data` to be called on the target contract.

## Proof of Concept

1. Let's say the bridge_watchdog suspends a message through function [suspendMessages()](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/bridge/Bridge.sol#L82).
 - On Line 91, since `_suspend` is true, `_timestamp` is set to type(uint64).max
 - On Line 96, the `receivedAt` field in Receipt struct of the message is set to `_timestamp` i.e. type(uint64).max
```solidity
File: Bridge.sol
87:     function suspendMessages(
88:         bytes32[] calldata _msgHashes,
89:         bool _suspend
90:     ) external onlyFromOwnerOrNamed("bridge_watchdog") {
91:         uint64 _timestamp = _suspend
92:             ? type(uint64).max
93:             : uint64(block.timestamp);
94:         for (uint256 i; i < _msgHashes.length; ++i) {
95:             bytes32 msgHash = _msgHashes[i];
96:             proofReceipt[msgHash].receivedAt = _timestamp;
97:             emit MessageSuspended(msgHash, _suspend);
98:         }
99:     }
```

2. In case of function [processMessage()](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/bridge/Bridge.sol#L217), a suspended message cannot be executed through the call to [_invokeMessageCall()](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/bridge/Bridge.sol#L477) on Line 320 due to the check on Line 291. The check would evaluate as `block.timestamp >= invocationDelay + type(uint64).max`. This implies that the block.timestamp needs to be extremely far away in the future to enter the if block and continue execution.
```solidity
File: Bridge.sol
291:         if (block.timestamp >= invocationDelay + receivedAt) {
292:             // If the gas limit is set to zero, only the owner can process the message.
293:             if (_message.gasLimit == 0 && msg.sender != _message.destOwner) {
294:                 revert B_PERMISSION_DENIED();
295:             }


320:                 if (_invokeMessageCall(_message, msgHash, gasLimit)) {
321:                     _updateMessageStatus(msgHash, Status.DONE);
322:                 } else {
323:                     _updateMessageStatus(msgHash, Status.RETRIABLE);
324:                 }
```

3. But if the
It can be impossible to migrate to the Native token in some circumstances
Audit: 2024-03-taiko-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/tokenvault/BridgedERC20Base.sol#L75-L89
https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/tokenvault/adapters/USDCAdapter.sol#L43-L45
https://github.com/circlefin/stablecoin-evm/blob/0828084aec860712531e8d79fde478927a34b3f4/contracts/v1/FiatTokenV1.sol#L133-L136


# Vulnerability details

## Impact

The old bridge token could be stuck if the minting function reverts. These tokens can neither be bridged back nor be migrated to the new native token. For the `USDCAdapter` this  would happen if the max minting allowance is reached for the `USDCAdapter`. 

These tokens would most likely lose significant value since they are not redeemable for the actual canonical token.
## Proof of Concept

Each minter has a maximum allowance in USDC, if the limit is reached minting can no happen. 

To migrate a bridge token to a new native token, a user burns their token and mints native tokens through `USDCAdapter` 

https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/tokenvault/BridgedERC20Base.sol#L75-L89

```solidity
    function burn(address _account, uint256 _amount) public nonReentrant whenNotPaused {
        if (_isMigratingOut()) {
            // Only the owner of the tokens himself can migrate out
            if (msg.sender != _account) revert BB_PERMISSION_DENIED();
            // Outbound migration
            emit MigratedTo(migratingAddress, _account, _amount);
            // Ask the new bridged token to mint token for the user.
            IBridgedERC20(migratingAddress).mint(_account, _amount);
        } else if (msg.sender != resolve("erc20_vault", true)) {
            // Only the vault can burn tokens when not migrating out
            revert RESOLVER_DENIED();
        }

        _burnToken(_account, _amount);
    
```


The `USDCAdapter` mints native tokens

https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/tokenvault/adapters/USDCAdapter.sol#L43-L45

```solidity
    function _mintToken(address _account, uint256 _amount) internal override {
        usdc.mint(_account, _amount);
    }
```

but will revert if the allowance is reached

https://github.com/circlefin/stablecoin-evm/blob/0828084aec860712531e8d79fde478927a34b3f4/contracts/v1/FiatTokenV1.sol#L133-L136

```solidity
        require(
            _amount <= mintingAllowedAmount,
            "FiatToken: mint amount exceeds minterAllowance"
        );
```

## Tools Used
foundry, vscode
## Recommended Mitigation Steps

Implement functionality to allow old bridge tokens to be bridged back to the canonical token. If the max limit is reached on L2 user should be able to bridge back to L1 and claim the canonical tokens.


## Assessed type

Other
Reverted recalled messages can permanentley lock funds
Audit: 2024-03-taiko-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

Recalled messages can permanentley lock funds if `_from` implements the `RecallableSender` interface but reverts on the `onMesssageRecalled()` call.
## Proof of Concept

When a message is recalled the bridge will call `onMessageRecalled()` if the interface is supported

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

```solidity
            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);
            
```

But if the `onMEssageRecall()` reverts the funds are stuck. 

## Tools Used
foundry, vscode
## Recommended Mitigation Steps

The original sender of the message should be able to call `recallMessage()` with a `recoverEth` bool to skip the `onMessageRecall()` call and recover the ETH.

```diff
    function recallMessage(
        Message calldata _message,
		bytes calldata _proof,
+       bool recoverEth
    )
```




## Assessed type

Other
Incorrect logic inside `_calcAmount()` ignores `_cliff` when `_period == 0`
Audit: 2024-03-taiko-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Description
As can be seen in the [following comments](https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/team/TimelockTokenPool.sol#L34-L40) and more importantly in the [code logic](https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/team/TimelockTokenPool.sol#L264), a non-zero `grantPeriod` should come into play after the `grantCliff` duration:
```js
  File: contracts/team/TimelockTokenPool.sol

  28:               struct Grant {
  29:                   uint128 amount;
  30:                   // If non-zero, each TKO (1E18) will need some USD stable to purchase.
  31:                   uint128 costPerToken;
  32:                   // If non-zero, indicates the start time for the recipient to receive
  33:                   // tokens, subject to an unlocking schedule.
  34:  @--->            uint64 grantStart;
  35:                   // If non-zero, indicates the time after which the token to be received
  36:                   // will be actually non-zero
  37:  @--->            uint64 grantCliff;
  38:                   // If non-zero, specifies the total seconds required for the recipient
  39:                   // to fully own all granted tokens.
  40:  @--->            uint32 grantPeriod;
```

Same for the 'unlock` variables:
```js
  File: contracts/team/TimelockTokenPool.sol

  41:                   // If non-zero, indicates the start time for the recipient to unlock
  42:                   // tokens.
  43:  @--->            uint64 unlockStart;
  44:                   // If non-zero, indicates the time after which the unlock will be
  45:                   // actually non-zero
  46:  @--->            uint64 unlockCliff;
  47:                   // If non-zero, specifies the total seconds required for the recipient
  48:                   // to fully unlock all owned tokens.
  49:  @--->            uint32 unlockPeriod;
```

Consider the scenario:
- `grantStart` is at timestamp `t`.
- `grantCliff` is at `t + 25`.
- `grantPeriod` is `100`.
- As per the [code logic](https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/team/TimelockTokenPool.sol#L264), if total `amount` is `1000` then at `t + 50`, the return value calculated by `_calcAmount` should ideally be:
    - `return _amount * uint64(block.timestamp - _start) / _period;` OR
    - `1000 * ( ( (t + 50) - t ) / 100 ) = 500` i.e. half-way through the `grantPeriod`, half of the amount should be owned/unlocked **IF** [block.timestamp is greater than _cliff](https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/team/TimelockTokenPool.sol#L262) : `if (block.timestamp <= _cliff) return 0;`

- Similarly if `_period` is `0`, one would reasonably expect that at _a timestamp greater than `_cliff`_, entire amount would be o
Incorrect __Essential_init() function is used in TaikoToken making snapshooter devoid of calling snapshot()
Audit: 2024-03-taiko-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/TaikoToken.sol#L34
https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/TaikoToken.sol#L52


# Vulnerability details

## Impact
The EssentialContract.sol contract is inherited by the TaikoToken contract. This essential contract contains two __Essential_init() functions, one with an owner parameter only (see [here](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/common/EssentialContract.sol#L109)) and the other with owner and address manager parameters (see [here](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/common/EssentialContract.sol#L95)).

The issue with the current code is that it uses the [__Essential_init()](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/common/EssentialContract.sol#L109) function with the owner parameter only. This would cause the [onlyFromOwnerOrNamed("snapshooter")](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/TaikoToken.sol#L52) modifier on the [snapshot](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/TaikoToken.sol#L52) function to not be able to resolve the snapshooter role since the address manager contract was never set during initialization, thus causing a revert. 

Due to this:
1. Snapshooter role is denied from taking snapshots.
2. Timely snapshots for certain periods could have failed by the snapshooter since they would have required the owner to jump in by the time the issue was realized.
3. Correct/Intended functionality of the protocol is affected i.e. the snapshooter role assigned to an address cannot ever perform its tasks validly.

## Proof of Concept
Here is the whole process:

1. Snapshooter address calls the [snapshot()](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/TaikoToken.sol#L52) function. The [onlyFromOwnerOrNamed("snapshooter")](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/L1/TaikoToken.sol#L52) modifier is encountered first.

```solidity
File: TaikoToken.sol
57:     function snapshot() public onlyFromOwnerOrNamed("snapshooter") {
58:         _snapshot();
59:     }
```

2. In the second condition, the [modifier](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/common/EssentialContract.sol#L41) calls the [resolve()](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/c