xanoutas/solidity-security-findings huggingface dataset:
Barichek - Incorrect withdrawal finalization due to EIP-150 logic
Audit: 2023-01-optimism-judging
Severity: HIGH

Barichek

medium

# Incorrect withdrawal finalization due to EIP-150 logic

## Summary

It is possible to pass incorrect gas to the withdrawal transaction execution and force fail it with no possibility of a second attempt of execution (although in fact, the transaction with the specified amount of gas will not revert).

## Vulnerability Detail

The `finalizeWithdrawalTransaction` function of `OptimismPortal` makes an external call to the withdrawal target contract and passes the required amount of gas in the following manner:

```solidity=
...

// We want to maintain the property that the amount of gas supplied to the call to the
// target contract is at least the gas limit specified by the user. We can do this by
// enforcing that, at this point in time, we still have gaslimit + buffer gas available.
require(
    gasleft() >= _tx.gasLimit + FINALIZE_GAS_BUFFER,
    "OptimismPortal: insufficient gas to finalize withdrawal"
);

...

// Trigger the call to the target contract. We use SafeCall because we don't
// care about the returndata and we don't want target contracts to be able to force this
// call to run out of gas via a returndata bomb.
bool success = SafeCall.call(
    _tx.target,
    gasleft() - FINALIZE_GAS_BUFFER,
    _tx.value,
    _tx.data
);

...
```

According to the [EIP-150](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md) `call` can consume at most `63/64` of parent calls' gas. That means that in case `gasleft() * 63 / 64` in the moment of making the call using the `SafeCall` library is smaller than `gasleft() - FINALIZE_GAS_BUFFER` the amount of gas that will be passed to the external call can be incorrect. This can lead to failing the withdrawal call and marking such withdrawal tx as processed (with no possibility of reexecution).

To better understand the scenario please check the `PoC` subsection. There is used a simplified version of the `OptimismPortal` contract (but with no modifications that have any significant effect on the described problem). As the target of the withdrawal call there used a `CustomBridge` contract, which is also simplified -- instead of processing real logic it just burns some amount of gas that can be practically used during a such call (specifically in the PoC it burns `GAS_TO_BE_USED_FOR_WITHDRAWALS = 3000000` gas). For the simplification and visibility of the withdrawal call result, there is used `stateMarker` public variable.

The `PoC` contract shows how exactly the described scenario can happen (here it is the description of the logic of the `PoC` contract constructor):
- the first try to process the withdrawal transaction with `TO_USE_AS_SAFE_GAS_LIMIT` `gasLimit` reverts (which is checked in the `require(customBridge.stateMarker() == 0);` statement);
- during the such call of the `finalizeWithdrawalTransaction` function withdrawal transaction was marked as processed (which is enforced in the `require(optimismPortalSimplified.finalizedWithdrawals(Hashing.hashWithdrawal(tx)));` st
ladboy233 - Gas left check estimation is not accurate
Audit: 2023-01-optimism-judging
Severity: HIGH

ladboy233

medium

# Gas left check estimation is not accurate

## Summary

Gas left check estimation is not accurate

## Vulnerability Detail

In the current implementation, gasleft() check is used to ensure that function call has enough gas left to finish the call:

In OptimismPortal finalizeWithdrawalTransaction function

```solidity
// We want to maintain the property that the amount of gas supplied to the call to the
// target contract is at least the gas limit specified by the user. We can do this by
// enforcing that, at this point in time, we still have gaslimit + buffer gas available.
require(
	gasleft() >= _tx.gasLimit + FINALIZE_GAS_BUFFER,
	"OptimismPortal: insufficient gas to finalize withdrawal"
);

// Set the l2Sender so contracts know who triggered this withdrawal on L2.
l2Sender = _tx.sender;

// Trigger the call to the target contract. We use SafeCall because we don't
// care about the returndata and we don't want target contracts to be able to force this
// call to run out of gas via a returndata bomb.
bool success = SafeCall.call(
	_tx.target,
	gasleft() - FINALIZE_GAS_BUFFER,
	_tx.value,
	_tx.data
);
```

and in CrossDomainMessenger.sol

```solidity
	require(
		gasleft() >= _minGasLimit + RELAY_GAS_REQUIRED,
		"CrossDomainMessenger: insufficient gas to relay message"
	);

	xDomainMsgSender = _sender;
	bool success = SafeCall.call(_target, gasleft() - RELAY_GAS_BUFFER, _value, _message);
	xDomainMsgSender = Constants.DEFAULT_L2_SENDER;

	if (success == true) {
		successfulMessages[versionedHash] = true;
		emit RelayedMessage(versionedHash);
	} else {
		failedMessages[versionedHash] = true;
		emit FailedRelayedMessage(versionedHash);

		// Revert in this case if the transaction was triggered by the estimation address. This
		// should only be possible during gas estimation or we have bigger problems. Reverting
		// here will make the behavior of gas estimation change such that the gas limit
		// computed will be the amount required to relay the message, even if that amount is
		// greater than the minimum gas limit specified by the user.
		if (tx.origin == Constants.ESTIMATION_ADDRESS) {
			revert("CrossDomainMessenger: failed to relay message");
		}
	}
```

In both code above, 

first 

```solidity
gasleft() >= _minGasLimit + THRESHOLD,
```

is called, then external call is performed.

The RELAY_GAS_REQUIRED and FINALIZE_GAS_BUFFER is used to reserve that function call is completed.

However, the RELAY_GAS_REQUIRED and FINALIZE_GAS_BUFFER is hardcoded 

```solidity
/**
 * @notice Minimum amount of gas required to relay a message.
 */
uint256 internal constant RELAY_GAS_REQUIRED = 45_000;
```

and

```solidity
/**
 * @notice Additional gas reserved for clean up after finalizing a transaction withdrawal.
 */
uint256 internal constant FINALIZE_GAS_BUFFER = 20_000;
```

However, hardcode the gas cost is always risky because the ethereum opcode gas change can change.

Just like the change of SLOAD operation breaks smart contract

>
obront - Malicious user can finalize other’s withdrawal with less than specified gas limit, leading to loss of funds
Audit: 2023-01-optimism-judging
Severity: HIGH

obront

high

# Malicious user can finalize other’s withdrawal with less than specified gas limit, leading to loss of funds

## Summary

Transactions to execute a withdrawal from the Optimism Portal can be sent with 5122 less gas than specified by the user, because the check is performed a few operations prior to the call. Because there are no replays on this contract, the result is that a separate malicious user can call `finalizeWithdrawalTransaction()` with a precise amount of gas, cause the withdrawer’s withdrawal to fail, and permanently lock their funds.

## Vulnerability Detail

Withdrawals can be initiated directly from the `L2ToL1MessagePasser` contract on L2. These withdrawals can be withdrawn directly from the `OptimismPortal` on L1. This path is intended to be used only by users who know what they are doing, presumably to save the gas of going through the additional more “user-friendly” contracts.

One of the quirks of the `OptimismPortal` is that there is no replaying of transactions. If a transaction fails, it will simply fail, and all ETH associated with it will remain in the `OptimismPortal` contract. Users have been warned of this and understand the risks, so Optimism takes no responsibility for user error.

However, there is an issue in the implementation of `OptimismPortal` that a withdrawal transaction can be executed with 5122 gas less than the user specified. In many cases, this could cause their transaction to revert, without any user error involved. Optimism is aware of the importance of this property being correct when they write in the comments:

> We want to maintain the property that the amount of gas supplied to the call to the target contract is at least the gas limit specified by the user. We can do this by enforcing that, at this point in time, we still have gaslimit + buffer gas available.

This property is not maintained because of the gap between the check and the execution.

The check is as follows, where `FINALIZE_GAS_BUFFER == 20_000`:
```solidity
require(
    gasleft() >= _tx.gasLimit + FINALIZE_GAS_BUFFER,
    "OptimismPortal: insufficient gas to finalize withdrawal"
);
```
After this check, we know that the current execution context has at least 20,000 more gas than the gas limit. However, we then proceed to spend gas by (a) assigning the `l2Sender` storage variable, which uses 2900 gas because it’s assigning from a non-zero value, and (b) perform some additional operations to prepare the contract for the external call.

The result is that, by the time the call is sent with `gasleft() - FINALIZE_GAS_BUFFER` gas, `gasleft()` is 5122 lower than it was in the initial check. 

Mathematically, this can be expressed as:

- `gasAtCheck >= gasLimit + 20000`
- `gasSent == gasAtCall - 20000`
- `gasAtCall == gasAtCheck - 5122`

Rearranging, we get `gasSent >= gasLimit + 20000 - 5122 - 20000`, which simplifies to `gasSent >= gasLimit - 5122`.

## Impact

For any withdrawal where a user sets their gas limit within 5122
obront - Withdrawals with high gas limits can be bricked by a malicious user, permanently locking funds
Audit: 2023-01-optimism-judging
Severity: HIGH

obront

high

# Withdrawals with high gas limits can be bricked by a malicious user, permanently locking funds

## Summary

Transactions to execute a withdrawal from the Optimism Portal require the caller to send enough gas to cover `gasLimit` specified by the withdrawer. 

Because the EVM limits the total gas forwarded on to 63/64ths of the total `gasleft()` (and silently reduces it to this value if we try to send more) there are situations where transactions with high gas limits will be vulnerable to being reverted.

Because there are no replays on this contract, the result is that a malicious user can call `finalizeWithdrawalTransaction()` with a precise amount of gas, cause the withdrawer’s withdrawal to fail, and permanently lock their funds.

## Vulnerability Detail

Withdrawals can be withdrawn from L2's `L2ToL1MessagePasser` contract to L1's `OptimismPortal` contract. This is a less "user-friendly" withdrawal path, presumably for users who know what they are doing. 

One of the quirks of the `OptimismPortal` is that there is no replaying of transactions. If a transaction fails, it will simply fail, and all ETH associated with it will remain in the `OptimismPortal` contract. Users have been warned of this and understand the risks, so Optimism takes no responsibility for user error.

In order to ensure that failed transactions can only happen at the fault of the user, the contract implements a check to ensure that the gasLimit is sufficient:

```solidity
require(
    gasleft() >= _tx.gasLimit + FINALIZE_GAS_BUFFER,
    "OptimismPortal: insufficient gas to finalize withdrawal"
);
```

When the transaction is executed, the contract requests to send along all the remaining gas, minus the hardcoded `FINALIZE_GAS_BUFFER` for actions after the call. The goal is that this will ensure that the amount of gas forwarded on is at least the gas limit specified by the user.

Optimism is aware of the importance of this property being correct when they write in the comments:

> “We want to maintain the property that the amount of gas supplied to the call to the target contract is at least the gas limit specified by the user. We can do this by enforcing that, at this point in time, we still have gaslimit + buffer gas available.”


The issue is that the EVM specifies the maximum gas that can be sent to an external call as 63/64ths of the `gasleft()`. For very large gas limits, this 1/64th that remains could be greater than the hardcoded FINALIZE_GAS_BUFFER value. In this case, less gas would be forwarded along than was directed by the contract.

Here is a quick overview of the math:

- We need X gas to be sent as a part of the call.
- This means we need `X * 64 / 63` gas to be available at the time the function is called.
- However, the only check is that we have `X + 20_000` gas a few operations prior to the call (which guarantees that we have `X + 14878` at the time of the call).
- For any situation where `X / 64 > 14878` (in other words, when the amount o
HE1M - Causing users lose their fund during finalizing withdrawal transaction
Audit: 2023-01-optimism-judging
Severity: HIGH

HE1M

high

# Causing users lose their fund during finalizing withdrawal transaction

## Summary
A malicious user can make users lose their fund during finalizing their withdrawal. This is possible due to presence of reentrancy guard on the function `relayMessage`.
## Vulnerability Detail

 - Bob (a malicious user) creates a contract (called `AttackContract`) on L1. 
 ```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;

struct WithdrawalTransaction {
    uint256 nonce;
    address sender;
    address target;
    uint256 value;
    uint256 gasLimit;
    bytes data;
}

interface IOptimismPortal {
    function finalizeWithdrawalTransaction(WithdrawalTransaction memory _tx)
        external;
}

contract AttackContract {
    bool public donotRevert;
    bytes metaData;
    address optimismPortalAddress;

    constructor(address _optimismPortal) {
        optimismPortalAddress = _optimismPortal;
    }

    function enableRevert() public {
        donotRevert = true;
    }

    function setMetaData(WithdrawalTransaction memory _tx) public {
        metaData = abi.encodeWithSelector(
            IOptimismPortal.finalizeWithdrawalTransaction.selector,
            _tx
        );
    }

    function attack() public {
        if (!donotRevert) {
            revert();
        } else {
            optimismPortalAddress.call(metaData);
        }
    }
}

```
 - Bob sends a message from L2 to L1 by calling the function `sendMessage` with the following parameters. He intends to call the function `attack()` through relaying the message from L2 to L1.
   - `_target` = address of `AttackContract` on L1 
   - `_message` = abi.encodeWithSignature("attack()")
   - `_minGasLimit` = just big enough so that the transaction can be executed
https://github.com/sherlock-audit/2023-01-optimism/blob/main/optimism/packages/contracts-bedrock/contracts/universal/CrossDomainMessenger.sol#L212
 - On the L1 side, after challenge period and validation elapsed, the function `attack()` on contract `AttackContract` will be called during relaying the message.
https://github.com/sherlock-audit/2023-01-optimism/blob/main/optimism/packages/contracts-bedrock/contracts/universal/CrossDomainMessenger.sol#L324
 -  But, since `donotRevert` is `false` in the contract `AttackContract`, the relayed message will be unsuccessful. So, we will have `failedMessages[versionedHash] = true`. It means that it is possible again to retry relaying the message later.
https://github.com/sherlock-audit/2023-01-optimism/blob/main/optimism/packages/contracts-bedrock/contracts/universal/CrossDomainMessenger.sol#L331
```solidity
        if (!donotRevert) {
            revert();
        }
```
 - Then, Bob calls the function `enableRevert` to set `donotRevert` to `true`. So that if later the function `attack()` is called again, it will not revert.
```solidity
    function enableRevert() public {
        donotRevert = true;
    }
```
 - Then, Bob notices that Alice is withdrawing large amount of fund from
Allarious - `relayMessage` may call the target functions with less gas than was anticipated by its sender
Audit: 2023-01-optimism-judging
Severity: HIGH

Allarious

low

# `relayMessage` may call the target functions with less gas than was anticipated by its sender

## Summary
The `relayMessage` inside `CrossDomainMessenger` does not check the `minGasLimit` specified by the user correctly and can call the function with less gas than specified by the user.

## Vulnerability Detail
Security check for `_minGasLimit` inside the `relaymessage()` inside the `CrossDomainMessenger` contract is implemented such as:
```solidity
require(
            gasleft() >= _minGasLimit + RELAY_GAS_REQUIRED,
            "CrossDomainMessenger: insufficient gas to relay message"
        );
```
However, while the `baseGas` used by the `sendMessage` function does calculate the 1/64 of the gas that is reserved through internal calls according to [EIP-150](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md), is not considered by the implemented security check by `relayMessage`. It should be mentioned that the user has to pay for it on the other chain, where `baseGas` calculates this amount.

Related LoCs:
https://github.com/sherlock-audit/2023-01-optimism/blob/main/optimism/packages/contracts-bedrock/contracts/universal/CrossDomainMessenger.sol#L318-L321
https://github.com/sherlock-audit/2023-01-optimism/blob/main/optimism/packages/contracts-bedrock/contracts/L1/OptimismPortal.sol#L313-L316

## Impact
While a user can provide the correct amount of `_minGasLimit` to the protocol, the target function can be called with less gas and can fail by running out of gas. The transaction should then be replayed later with a greater gas value as it gets stored in `failedMessages`.

This issue is highlighted when the amount of `_minGasLimit` is increased. If it is more than `RELAY_GAS_REQUIRED * 64 = 45000 * 64 = 2880000`, the problem will show itself. In the below PoC, target function is called with more than 6000 gas less than `_minGasLimit`, this problem increases with the amount of `_minGasLimit`.

## Code Snippet
```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import "../contracts/universal/CrossDomainMessenger.sol";

import "forge-std/Test.sol";

contract PoC is Test {

    CrossDomainMessenger cdm;
    TargetContract target;
    uint256 minGas = 3000000;

    function setUp() public {
        cdm = new CDM();
        target = new TargetContract(minGas);
    }

    function testMinGas() public {
        console.log("Starting the test");

        cdm.relayMessage{gas: 3065000}(
            cdm.messageNonce(), // It is ok to use the same messanger nonce here, does not matter for our case
            address(1),
            address(target),
            0,
            minGas,
            abi.encodeWithSignature("targetFunction()")
        );
    }

}

contract CDM is CrossDomainMessenger {
    constructor() CrossDomainMessenger(address(0)) initializer {
        __CrossDomainMessenger_init(); // We do not care if it is behind a proxy for this test!
    }
    function _sendMessage(
        address _to,
vatsal - rounding error due to internal accounting and can steal some portion of the first depositors  funds
Audit: 2024-08-sentiment-v2-judging
Severity: HIGH

vatsal

High

# rounding error due to internal accounting and can steal some portion of the first depositors  funds

## Summary

## Vulnerability Detail

- where: All basepool 
- when: Total Supply of a pool is zero 

- When total supply of pool is zero an attacker goes ahead and executes the following steps

1. mint some asset and deposit some collateral sufficient to borrow those assets. 
2. Borrow a few of those assets and wait for a few block. In 100 second when at least more than 1wei interest has occurred, repay all the borrowed funds.
3. [to match this condition ](https://github.com/sentimentxyz/protocol-v2/blob/master/src/Pool.sol#L361) attacker will directly transfer some funds because[ total balance](https://github.com/sentimentxyz/protocol-v2/blob/master/src/Pool.sol#L360) is calculate like using balanceof(Address(this)) 
4. after this withdraw all but 2 wei of shares. to makes it so that the totalDepositShares = 1 and totalDepositAssets = 2 due to rounding.
- Now attacker takes advantage of rounding down when depositing to inflate the price of a share.


In a loop attacker does the following till they get their desired price of 1 share

- deposit totalDeposits + 1 assets and withdraw  1 shares 
  - according to `convertToShares = assets.mulDiv(totalShares, totalAssets, rounding);`
  - it mints  `shares = (amount * total.shares) / total.amount` of shares.
  - Since the attacker has deposited totalDeposits + 1 assets and totalDepositShares is 1, shares = (totalDeposits  + 1 * 1) / totalDeposits = 1 
  - This should have been 1.9999... but due to rounding down, the attacker gets minted 1 shares is minted
  - 
  - and attacker withdrew in the same 1 wei transactions . 
  - This means at this point `totalDepositShares = 1+1 (minted shares )- 1 (withdrew amount )= 1` and `totalDeposits = totalDeposits + totalDeposits + 1`
  - In this loop the supply stays at 1 and totalDeposits increase exponentially. Take a look at the POC to get a better idea.
 

So when a user comes to the deposit get some shares but they lose of assets which get proportionally divided between existing share holders (including the attacker) due to rounding errors.
- users keep losing up to 33% of their assets. (see [here](https://www.desmos.com/calculator/0hrgaxkdyr))
- This means that for users to not lose value, they have to make sure that they have to deposit exact proportion of the attacker shares is an integer.


## Impact
- Loss of 33% of all pool 1st depositor funds 

## Code Snippet
```solidity
function testInternalDepoisitBug(uint96 assets) public {
        vm.assume(assets > 0);
        
        // address notPositionManager = makeAddr("notPositionManager");
        
        vm.startPrank(user);

        asset1.mint(user, 50_000 ether);
        asset1.approve(address(pool), 50_000 ether);

        pool.deposit(linearRatePool, 1 ether, user);
        
        vm.startPrank(registry.addressFor(SENTIMENT_POSITION_MANAGER_KEY));
        asset1.mint(registry.address
smbv-1923 - Loss of users fund through share inflation attack in `Pool.sol`
Audit: 2024-08-sentiment-v2-judging
Severity: HIGH

smbv-1923

High

# Loss of users fund through share inflation attack in `Pool.sol`

## Summary
Loss of users fund through share inflation attack in `Pool.sol`
## Vulnerability Detail
- When total supply of Pool is zero attacker would do the attack.
  - Attacker would deposit asset in Pool and then attacker borrow the assets from that pool.
  - Then attacker waits for 1 block so the borrowed assets accrue some interest.
  - Then attacker repay the assets to pool .
  - So now at this stage total asset > total share,the scenario which attacker wanted.
  - After this attacker withdraws assets such that there are only 2 assets remaining in pool , this makes such that total assets = 2 , total share =1 due to rounding nature.
  - Now attacker takes benefit of rounding down and inflates the share price of pool.
  - Attacker would keep depositing  2(*total assets) - 1 of assets and withdrawing assets in a multiple of 2(i) in a while loop till the target price is reached and such that no zero shares get minted during withdraw.
  - Now 1 Share would be equal to desired target price which is set by attacker.
  Ex - totalShare = 1 ; 
         Totalssets = 1000 Ether ( desired Target Price = 1000 Ether)
## Impact
-There are 2 impacts in the attack.
  - First - Whenever a user deposits less than target price(totalAssets) achieved by attacker, the transaction would always revert .
  - Second - Whenever a user deposits greater than target price(totalAssets) but not in a multiple of target price(totalAssets) , the user would loose some percentage of supplied assets and the lossed amount gets distributed to share holders.
  - This means that for users to not lose value, they have to make sure that amount deposit is a multiple of target price.
## Code Snippet
https://github.com/sherlock-audit/2024-08-sentiment-v2/blob/main/protocol-v2/src/Pool.sol#L309
https://github.com/sherlock-audit/2024-08-sentiment-v2/blob/main/protocol-v2/src/Pool.sol#L339
## Tool used

Manual Review

## Recommendation
- Mint some shares to zero address after deploying Pool 
- Implement Erc4626 Upgradeable Contract in share mechanism.

Duplicate of #597