xanoutas/solidity-security-findings huggingface dataset:
bridge fee without any checks may result in insufficient rewards
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#L207-L250
https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/tokenvault/ERC721Vault.sol#L26-L74
https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/tokenvault/ERC1155Vault.sol#L39-L90
https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/bridge/Bridge.sol#L217-L307


# Vulnerability details

## Impact
We can see that if the bridge fee is equal to 0, the person who sends the message needs to go to the target chain to trigger the processMessage. bridge fee not equal to 0 can be triggered by the relayer or the message sender. If the message sender sets the bridge fee to 1wei, it is obvious that it is not enough to pay the gas fee. Although the bridge fee will be used to reward the relayer after the execution, the relayer is in a loss state.

## Proof of Concept
The whole process of sending the message is:
srchain
sendToken(ERC20Vault/ERC721Vault/ERC1155Vault)->bridge._handleMessage
destchain
relayer(msg's owner)->bridge.processMessage->onMessageInvocation
We can see that the code to send a token across the chain is as follows:
https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/tokenvault/ERC20Vault.sol#L207-L235
```solidity
    function sendToken(BridgeTransferOp calldata _op)
        external
        payable
        nonReentrant
        whenNotPaused
        returns (IBridge.Message memory message_)
    {
        if (_op.amount == 0) revert VAULT_INVALID_AMOUNT();
        if (_op.token == address(0)) revert VAULT_INVALID_TOKEN();
        if (btokenBlacklist[_op.token]) revert VAULT_BTOKEN_BLACKLISTED();

        (bytes memory data, CanonicalERC20 memory ctoken, uint256 balanceChange) =
            _handleMessage(msg.sender, _op.token, _op.to, _op.amount);

        IBridge.Message memory message = IBridge.Message({
            id: 0, // will receive a new value
            from: address(0), // will receive a new value
            srcChainId: 0, // will receive a new value
            destChainId: _op.destChainId,
            srcOwner: msg.sender,
            destOwner: _op.destOwner != address(0) ? _op.destOwner : msg.sender,
            to: resolve(_op.destChainId, name(), false),
            refundTo: _op.refundTo,
            value: msg.value - _op.fee,
            fee: _op.fee,// @audit The fee is completely defined by the sender and can be set to 1wei.
            gasLimit: _op.gasLimit,
            data: data,
            memo: _op.memo
        });
```
As you can see, there is no checking of the fee anywhere.
In the code, we can see the following comment：
https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a4
Operator can steal funds by crafting message directly to the bridge 
Audit: 2024-03-taiko-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/tokenvault/ERC20Vault.sol#L253
https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/tokenvault/ERC721Vault.sol#L110
https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/tokenvault/ERC1155Vault.sol#L93


# Vulnerability details

## Impact
A customer can use a Vault to send tokens cross-chain by calling `sendToken()` on a given vault which transfers the token(s) from the caller then creates a message which is sent to the bridge. 

```solidity
function sendToken(BridgeTransferOp calldata _op)
        external
        payable
        nonReentrant
        whenNotPaused
        returns (IBridge.Message memory message_)
    {
        if (_op.amount == 0) revert VAULT_INVALID_AMOUNT();
        if (_op.token == address(0)) revert VAULT_INVALID_TOKEN();
        if (btokenBlacklist[_op.token]) revert VAULT_BTOKEN_BLACKLISTED();

        (bytes memory data, CanonicalERC20 memory ctoken, uint256 balanceChange) =
            _handleMessage(msg.sender, _op.token, _op.to, _op.amount);

        IBridge.Message memory message = IBridge.Message({
            id: 0, // will receive a new value
            from: address(0), // will receive a new value
            srcChainId: 0, // will receive a new value
            destChainId: _op.destChainId,
            srcOwner: msg.sender,
            destOwner: _op.destOwner != address(0) ? _op.destOwner : msg.sender,
            to: resolve(_op.destChainId, name(), false),
            refundTo: _op.refundTo,
            value: msg.value - _op.fee,
            fee: _op.fee,
            gasLimit: _op.gasLimit,
            data: data,
            memo: _op.memo
        });

        bytes32 msgHash;
        (msgHash, message_) =
            IBridge(resolve("bridge", false)).sendMessage{ value: msg.value }(message);

        emit TokenSent({
            msgHash: msgHash,
            from: message_.srcOwner,
            to: _op.to,
            destChainId: _op.destChainId,
            ctoken: ctoken.addr,
            token: _op.token,
            amount: balanceChange
        });
    }
```

From the Bridge `processMessage()` is called on the destination bridge to finalize the customer's action by eventually calling `onMessageInvocation()` on the Vault. 

```solidity
function onMessageInvocation(bytes calldata _data)
        external
        payable
        nonReentrant
        whenNotPaused
    {
        (CanonicalERC20 memory ctoken, address from, address to, uint256 amount) =
            abi.decode(_data, (CanonicalERC20, address, address, uint256));

        // `onlyFromBridge` checked in checkProcessMessageContext
        IBridge.Context memory ctx = checkProcessMessageContext();

        // Don't allow sending to disallowed addresses.
        // Don't send the tokens back to `from` because `from` is on the source chain.
        if (to == address(0) || to == address(this))
Protocol does not check inside GuardianProver::approve() if all the guardians are approving the same proof
Audit: 2024-03-taiko-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/L1/provers/GuardianProver.sol#L33


# Vulnerability details

## Summary
The [natspec for GuardianProver::approve() says the following on L33](https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/L1/provers/GuardianProver.sol#L33) but the **_"sent the same proof"_** clause is never really checked inside the function:
```js
  File: contracts/L1/provers/GuardianProver.sol

  29:               /// @dev Called by guardians to approve a guardian proof
  30:               /// @param _meta The block's metadata.
  31:               /// @param _tran The valid transition.
  32:               /// @param _proof The tier proof.
  33:  @--->        /// @return approved_ If the minimum number of participants sent the same proof, and proving
  34:               /// transaction is fired away returns true, false otherwise.
  35:               function approve(
  36:                   TaikoData.BlockMetadata calldata _meta,
  37:                   TaikoData.Transition calldata _tran,
  38:                   TaikoData.TierProof calldata _proof
  39:               )
  40:                   external
  41:                   whenNotPaused
  42:                   nonReentrant
  43:                   returns (bool approved_)
  44:               {
  45:                   if (_proof.tier != LibTiers.TIER_GUARDIAN) revert INVALID_PROOF();
  46:                   bytes32 hash = keccak256(abi.encode(_meta, _tran));
  47:                   approved_ = approve(_meta.id, hash);
  48:           
  49:                   if (approved_) {
  50:                       deleteApproval(hash);
  51:  @--->                ITaikoL1(resolve("taiko", false)).proveBlock(_meta.id, abi.encode(_meta, _tran, _proof));
  52:                   }
  53:           
  54:                   emit GuardianApproval(msg.sender, _meta.id, _tran.blockHash, approved_);
  55:               }
```

## Description & Impact
- Assume there are a total of 9 guardians. Hence approvals needed for `approved_ = true` is `(9 + 1) / 2 = 5`.
- Guardian1 to Guardian3 pass `proofX` while calling `approve()`.
- Guardian4 passes `proofY`.
- Guardian5 passes `proofX`. Since the `hash` on L46 is calculated without involving the proof, L47 will return `true` at the 5th guardian's call even though Guardian4 had used `proofY`.
- `proveBlock()` is called with `_proof` equalling `proofX`.
- Note that whatever `_proof` Guradian5 uses while calling `approve()` will be used to call `proveBlock()` on L51, irrespective of whatever the previous guardians passed.

Thus the guardian proof is approved even when in reality the required votes have not been achieved.

## Tools Used
Manual review

## Recommended Mitigation Steps
Compare the `_proof.data` to make sure approval for the same proof is being provided by all the guardians.





## Assessed type

Invalid Validation
Grant could be changed into a invalid one after voidGrant
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#L225-L233


# Vulnerability details

## Impact
Tokens already granted to the recipient will NOT be voided, but after voidGrant, will be changed to be an invalid one.

## Proof of Concept
    function _voidGrant(Grant storage _grant) private returns (uint128 amountVoided) {
        uint128 amountOwned = _getAmountOwned(_grant);

        amountVoided = _grant.amount - amountOwned;
        _grant.amount = amountOwned;

        _grant.grantStart = 0;
        _grant.grantPeriod = 0;
    }

  When _voidGrant(), both _grant.grantStart and _grant.grantPeriod will be cleared to 0, but _grant.grantCliff is not cleared, which could be a value > 0.

  function _validateGrant(Grant memory _grant) private pure {
        if (_grant.amount == 0) revert INVALID_GRANT();
        _validateCliff(_grant.grantStart, _grant.grantCliff, _grant.grantPeriod);
        _validateCliff(_grant.unlockStart, _grant.unlockCliff, _grant.unlockPeriod);
    }

    function _validateCliff(uint64 _start, uint64 _cliff, uint32 _period) private pure {
        if (_start == 0 || _period == 0) {
            if (_cliff > 0) revert INVALID_GRANT();
        } else {
            if (_cliff > 0 && _cliff <= _start) revert INVALID_GRANT();
            if (_cliff >= _start + _period) revert INVALID_GRANT();
        }
    }

   Then in function _validateGrant(),  _validateCliff() will check _grant.grantStart(value=0),  _grant.grantPeriod(value=0) and _grant.grantCliff(value > 0), _grant will be judged as an invalid grant, although  _grant.amount  > 0.

## Tools Used
manually reviewed

## Recommended Mitigation Steps
In function _voidGrant, set _grant.grantCliff= 0;


## Assessed type

Other
Banned addresses can be invoked when `retryMessage` is called
Audit: 2024-03-taiko-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Issue Description

When processing a message on the destination chain using the `processMessage` function, there are certain addresses upon which messages cannot be invoked. These addresses are checked in the code snippet below:

```solidity
function processMessage(
    Message calldata _message,
    bytes calldata _proof
)
    external
    nonReentrant
    whenNotPaused
    sameChain(_message.destChainId)
{
    ...

    // Process message differently based on the target address
    if (
        _message.to == address(0) || _message.to == address(this)
            || _message.to == signalService || addressBanned[_message.to]
    ) {
        // Handle special addresses that don't require actual invocation but
        // mark message as DONE
        refundAmount = _message.value;
        _updateMessageStatus(msgHash, Status.DONE);
    } else {
        // Use the specified message gas limit if called by the owner, else
        // use remaining gas
        uint256 gasLimit = msg.sender == _message.destOwner ? gasleft() : _message.gasLimit;

        if (_invokeMessageCall(_message, msgHash, gasLimit)) {
            _updateMessageStatus(msgHash, Status.DONE);
        } else {
            _updateMessageStatus(msgHash, Status.RETRIABLE);
        }
    }
    ...
}
```

For this issue, we will consider that `_message.to` is different from (`address(0)`, `address(this)`, `signalService`) addresses, and we will only be interested in the banned state `addressBanned[_message.to]`, which is set by the `banAddress` function.

From the code snippet above, we see that when `_message.to` is not banned, the `else` block is executed. It will call `_invokeMessageCall`, and if the call is successful, the message status is updated to `Status.DONE`, or to `Status.RETRIABLE` otherwise.

When a message is marked as `Status.RETRIABLE` (when `_invokeMessageCall` returns false), the user is allowed to call it again (later) using the `retryMessage` function:

```solidity
function retryMessage(
    Message calldata _message,
    bool _isLastAttempt
) external nonReentrant whenNotPaused sameChain(_message.destChainId) {
    // If the gasLimit is set to 0 or isLastAttempt is true, the caller must
    // be the message.destOwner.
    if (_message.gasLimit == 0 || _isLastAttempt) {
        if (msg.sender != _message.destOwner) revert B_PERMISSION_DENIED();
    }

    bytes32 msgHash = hashMessage(_message);
    if (messageStatus[msgHash] != Status.RETRIABLE) {
        revert B_NON_RETRIABLE();
    }

    //@audit doesn't check if _message.to is banned or not

    // Attempt to invoke the messageCall.
    if (_invokeMessageCall(_message, msgHash, gasleft())) {
        _updateMessageStatus(msgHash, Status.DONE);
    } els
Size of `__gap` Was Not Updated After States Addition in `TimelockTokenPool`
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#L82


# Vulnerability details

## Issue Description

In the `TimelockTokenPool` contract, two states, `costToken` and `totalCostPaid`, were added as seen [here](https://github.com/taikoxyz/taiko-mono/pull/15522/files#diff-9c0f84ccdd63980995664a1e65b4ba83e175d880c9fc17df1207ffda953b42f8). These two states occupy 2 more storage slots. Therefore, the contract's `__gap` (as it's an upgradable contract) was supposed to decrease by two from `uint128[44]` to `uint128[42]`. However, this was not implemented, and the current implementation still maintains the same `__gap` size:

```solidity
uint128[44] private __gap;
```

This oversight can lead to issues in future upgrades since the current gap size doesn't accurately reflect the real storage space available, potentially resulting in storage collisions.

## Impact

The `__gap` size of the `TimelockTokenPool` contract was not updated after the addition of two new states. This could lead to potential storage collisions in future upgrades.

## Tools Used

Manual review, VS Code

## Recommended Mitigation

To mitigate this issue, update the size of `__gap` in the `TimelockTokenPool` contract:

```diff
--uint128[44] private __gap;

++uint128[42] private __gap;
```



## Assessed type

Context
No return value check on "ECDSA.recover"
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#L171


# Vulnerability details

## Impact
Detailed description of the impact of this finding.
There is no return value check on ECDSA.recover.This can return 0 address value.
## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
 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);
    }
## Tools Used

## Recommended Mitigation Steps
check for return value of ECDSA.recover.


## Assessed type

Context
Signature Malleability
Audit: 2024-03-taiko-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/automata-attestation/utils/RsaVerify.sol#L93


# Vulnerability details

## Impact
Detailed description of the impact of this finding.
The code does not account for signature malleability, where different signatures could produce the same result due to padding or other manipulations.

## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

Here input is concat of (bytes32(_s.length), bytes32(_e.length), bytes32(_m.length), _s, _e, _m).

    @>    bytes memory input =
            bytes.concat(bytes32(_s.length), bytes32(_e.length), bytes32(_m.length), _s, _e, _m);
        uint256 inputlen = input.length;

        uint256 decipherlen = _m.length;
        bytes memory decipher = new bytes(decipherlen);
        assembly {
            pop(
                staticcall(
                    sub(gas(), 2000),
                    5,
                    add(input, 0x20),
                    inputlen,
                    add(decipher, 0x20),
                    decipherlen
                )
            )
        }

## Tools Used

## Recommended Mitigation Steps
use some other way to do the input verification.


## Assessed type

Context
Unchecked Return Values
Audit: 2024-03-taiko-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/automata-attestation/utils/RsaVerify.sol#L248


# Vulnerability details

## Impact
Detailed description of the impact of this finding.
Unchecked Return Values: The pop(staticcall(...)) pattern is used without checking the return value, which could lead to ignoring failed calls.

## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
  assembly {
            pop(
                staticcall(
                    sub(gas(), 2000),
                    5,
                    add(input, 0x20),
                    inputlen,
                    add(decipher, 0x20),
                    decipherlen
                )
            )
        }
## Tools Used

## Recommended Mitigation Steps

check the return value for pop.


## Assessed type

Context
Loss of message fee even when the message is never executed
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#L269


# Vulnerability details

# Loss of message fee even when the message is never executed

### Line of code
1. https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/bridge/Bridge.sol#L269
### Proof of concept
[Line of code](https://github.com/code-423n4/2024-03-taiko/blob/f58384f44dbf4c6535264a472322322705133b11/packages/protocol/contracts/bridge/Bridge.sol#L269)
```solidity
            // Process message differently based on the target address
            if (
                _message.to == address(0) || _message.to == address(this)
                    || _message.to == signalService || addressBanned[_message.to]
            ) {
                // Handle special addresses that don't require actual invocation but
                // mark message as DONE
                refundAmount = _message.value;
                _updateMessageStatus(msgHash, Status.DONE);
            } else {
                // Use the specified message gas limit if called by the owner, else
                // use remaining gas
                uint256 gasLimit = msg.sender == _message.destOwner ? gasleft() : _message.gasLimit;

                if (_invokeMessageCall(_message, msgHash, gasLimit)) {
                    _updateMessageStatus(msgHash, Status.DONE);
                } else {
                    _updateMessageStatus(msgHash, Status.RETRIABLE);
                }
            }

            // Determine the refund recipient
            address refundTo =
                _message.refundTo == address(0) ? _message.destOwner : _message.refundTo;

            // Refund the processing fee
            if (msg.sender == refundTo) {
                refundTo.sendEther(_message.fee + refundAmount);
            } else {
                // If sender is another address, reward it and refund the rest
                msg.sender.sendEther(_message.fee);
                refundTo.sendEther(refundAmount);
            }
```
### Impact
This is the logic when process a message, not when the target address is banned, 

the action is marked as DONE,

but even the message is not executed, 

the caller still get paid for the message.fee

```
// Refund the processing fee
if (msg.sender == refundTo) {
    refundTo.sendEther(_message.fee + refundAmount);
} else {
   // If sender is another address, reward it and refund the rest
    msg.sender.sendEther(_message.fee);
    refundTo.sendEther(refundAmount);
}
```

### Recommendation
refund the message fee is the message is never executed and marked as DONE directly


## Assessed type

Token-Transfer
Pending owner can be the wrong recipient of ownership 
Audit: 2023-06-lukso-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-06-lukso/blob/9dbc96410b3052fc0fd9d423249d1fa42958cae8/contracts/LSP0ERC725Account/LSP0ERC725AccountCore.sol#L624-L640


# Vulnerability details

## Impact
An attacker can call the acceptOwnership function with their address as the pending owner before the legitimate pending owner has a chance to call the function

## Proof of Concept
The transferOwnership() function allows the current owner to set a pending owner.
The acceptOwnership() function is supposed to be called by the pending owner to accept ownership.
However, there are no verification in the acceptOwnership() function to verify that the caller is indeed the pending owner. This means that any address can call this function and become the owner of the contract.
An attacker can exploit this vulnerability by performing the following steps:
- Call the transferOwnership() function with their desired address as the pending owner.
- Call the acceptOwnership() function from any address, not necessarily the pending owner's address.
- The contract will transfer ownership to the caller of the acceptOwnership() function, regardless of whether they are the intended pending owner or not.

## Tools Used
Manual
## Recommended Mitigation Steps 
Finetune the acceptOwnership() function to verify that the caller is the pending owner before transferring ownership. This can be done by comparing the caller's address with the pending owner's address before executing the ownership transfer


## Assessed type

Token-Transfer