xanoutas/solidity-security-findings huggingface dataset:
Calls to the RIPEMD160 precompile can result in RPC-level reverts, bricking contracts
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/precompiles/ripemd160.cairo#L470


# Vulnerability details

The `RIPEMD160` precompile makes use of several Cairo `default_dict` primitives to keep track of the evolving state of the hash function.

In its function `finish`, it uses two different dictionaries, reusing the `x` variable, therefore shadowing the previous declaration:

```cairo
File: ripemd160.cairo
431:     let (x) = default_dict_new(0);
432:     tempvar start = x;
---
469:     // reset dict to all 0.
470:     let (x) = default_dict_new(0);
471: 
472:     dict_write{dict_ptr=x}(14, val);
473:     dict_write{dict_ptr=x}(15, val_15);
474: 
475:     let (local arr_x: felt*) = alloc();
476:     dict_to_array{dict_ptr=x}(arr_x, 16);
477:     default_dict_finalize(start, x, 0);
```

The problem with this implementation is that while `x` was re-allocated at L470, `start` wasn't, and at the time `default_dict_finalize()` is called (L477), `start` points to the starting state of the `x` defined at L431 instead of L470.

This creates a problem because, if the two dictionaries (L431 and L470) aren't allocated in the same memory segment, finalization will throw a Cairo exception. If they are, it is to be expected that the attempt to finalize the malformed dictionary will also fail.

Any input that enters this condition will be impossible to hash using the precompile, and once such inputs are known, malicious EVM contracts can cause transactions that call them to revert at the RPC level, regardless of how defensively they were called. Protective measures generally considered safe in the EVM space like [ExcessivelySafeCall](https://github.com/LayerZero-Labs/endpoint-v1-solidity-examples/blob/main/contracts/libraries/ExcessivelySafeCall.sol) (which is used in LayerZero cross-chain applications to prevent channel DoSes that can permanently freeze in-flight tokens), can be bypassed by causing a RPC-level cairo revert.

## Proof of Concept
The following test case can be added to the existing `test_ripemd160.py` test suite:

```python
    async def test_ripemd160_should_not_crash(self, cairo_run):
        msg_bytes = bytes("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", "ascii")
        precompile_hash = cairo_run("test__ripemd160", msg=list(msg_bytes))

        # Hash with RIPEMD-160 to compare with precompile result
        ripemd160_crypto = RIPEMD160.new()
        ripemd160_crypto.update(msg_bytes)
        expected_hash = ripemd160_crypto.hexdigest()

        assert expected_hash.rjust(64, "0") == bytes(precompile_hash).hex()
```

Running this test yields the following error:
```
E           Exception: 2024-09-kakarot/kakarot/.venv/lib/python3.10/site-packages/starkware/cairo/common/squash_dict.cairo:29:5: Error at pc=0:162:
E           Can only subtract two relocatable values of the same segment (16 != 13).
E               local ptr_diff = dict_accesses_end - dict_accesses;
In edge cases, user might lose funds when sending l1 to l2 messages due to flaw in L1KakarotMessaging.sol
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/solidity_contracts/src/L1L2Messaging/L1KakarotMessaging.sol#L8-L9


# Vulnerability details

## Proof of Concept
Kakarot’s L1-L2 messaging is based on starknet’s L1-l2 mechanism using the starknet core contract(`starknetMessaging`).

Based on starknet [doc](https://docs.starknet.io/architecture-and-concepts/network-architecture/messaging-mechanism/#l2-l1_message_cancellation), starknet allows a user to cancel a l1→l2 message to recover funds in case of a failed l2 execution through StarknetMessaging::[startL1ToL2MessageCancellation](https://github.com/starkware-libs/cairo-lang/blob/4e233516f52477ad158bc81a86ec2760471c1b65/src/starkware/starknet/eth/StarknetMessaging.sol#L134).

The problem is kakarot’s L1KakrotMessaging.sol doesn’t provide any mechanism to invoke `startL1ToL2MessageCancellation` on starknet core contract. Because kakarot’s users interact with L1KakarotMessaging.sol, this means if l2 tx fails, no cancellation can be initiated on-behalf of the user, and user’s funds will be lost.
```solidity
//solidity_contracts/src/L1L2Messaging/L1KakarotMessaging.sol
//@audit No method to cancel a l1->l2message (calling StarknetMessaging::startL1ToL2MessageCancellation)
interface IL1KakarotMessaging {
    function sendMessageToL2(address to, uint248 value, bytes memory data) external payable;
    function consumeMessageFromL2(address fromAddress, bytes calldata payload) external;
}
```
(https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/solidity_contracts/src/L1L2Messaging/L1KakarotMessaging.sol#L8-L9)

L1 sender is allowed to call any L2 evm target contract through L1KakarotMessaging::sendMessageToL2, so the need for L1 user to cancel L1 to L2 message is justified. 

POC:
A special case is when kakarot core contract(kakarot.cairo) is [paused by admin](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/kakarot.cairo#L61) after L1 user initiated sendMessageToL2 call. L2 tx(`handle_l1_message`) will fail due to [not pausing assertion](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/kakarot.cairo#L374). L1 user will lose funds and such cases might not be easily prevented.

Impacts:

L1 senders lose funds when Kakarot core is paused after L1-L2 message initiation, or due to other L2 tx failures.

## Recommended Mitigation Steps
In L1KakarotMessaging.sol, provide an accounting of user message requests, and add functionality to cancel L1tol2 messages on starknet core contract on behalf of the user.






## Assessed type

Other
Transaction revert handling diverges from EVM spec, whenever EVM to Starknet contract call is involved.
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/accounts/account_contract.cairo#L347


# Vulnerability details

## Proof of Concept
`kakarot_precompiles` allows EVM contracts to call a starknet contract in a transaction through invoking `call_contract` system call. According to starknet, if `call_contract` reverts either due to target contract revert logic or failure, the entire cairo transaction will throw without propagating a failure status.

Flows: a user solidity contract (uses Cairolib.sol) → CAIRO_PRECOMPILE_ADDRESS.delegatecall → … → kakarot_precompiles::cairo_precompile → account_contract::execute_starknet_call → call_contract 

**`call_contract` revert behavior in Starknet:**
Based on [starknet doc](https://docs.starknet.io/architecture-and-concepts/smart-contracts/system-calls-cairo1/#call_contract), when `call_contract` revert or fail, the entire cairo transaction will throw.
> An internal call can’t return **`Err(_)`** as this is not handled by the sequencer and the Starknet OS.
  If **`call_contract_syscall`** fails, this can’t be caught and will therefore result in the entire transaction being reverted.

**kakarot’s handling of `call_contract`:**
We see that in kakarot’s account_contract::execute_starknet_call, call_contract is invoked to make the starknet call. But if the starknet call reverts for any reason,  no failure status will be passed, `redata_len` `retdata` will not be propagated, and the entire execution of the tx will immediately throw.

In the kakarot EVM’s call flow of `eth_send_raw_unsigned_tx`, this means `evm.gas_left` and `evm.gas_refund` propagated in the cached state will be disregarded and EVM gas will not be charged. This deviates from EVM spec.
```rust
//src/kakarot/accounts/account_contract.cairo
@external
func execute_starknet_call{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
    to: felt, function_selector: felt, calldata_len: felt, calldata: felt*
) -> (retdata_len: felt, retdata: felt*, success: felt) {
    Ownable.assert_only_owner();
    let (kakarot_address) = Ownable.owner();
    let is_get_starknet_address = Helpers.is_zero(
        GET_STARKNET_ADDRESS_SELECTOR - function_selector
    );
    let is_kakarot = Helpers.is_zero(kakarot_address - to);
    tempvar is_forbidden = is_kakarot * (1 - is_get_starknet_address);
    if (is_forbidden != FALSE) {
        let (error_len, error) = Errors.kakarotReentrancy();
        return (error_len, error, FALSE);
    }
|>  let (retdata_len, retdata) = call_contract(to, function_selector, calldata_len, calldata);
    return (retdata_len, retdata, TRUE);
}
```
(https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/accounts/account_contract.cairo#L347)

Impact:

Because EVM contract to starknet contract call can be a very common use case (e.g. existing starknet erc20/erc721 token transfer), this means any tx that involves a starknet contract
DualVMToken::approve/transfer will always revert when trying to approve amount greater than type(uint128).max due to vulnerable implementation
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/solidity_contracts/src/CairoPrecompiles/DualVmToken.sol#L221
https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/solidity_contracts/src/CairoPrecompiles/DualVmToken.sol#L261


# Vulnerability details

## Proof of Concept
In DualVMToken.sol, when converting `uint256 amount` into two uint128 amounts, `uint128(amount)` conversion is directly invoked. 

The problem is DualVMToken.sol uses 0.8.27 compiler. Since solidity 0.8.0, built-in overflow check is included, which will revert when `uint256 amount` exceeds `type(uint128).max` during conversion.
 
(1) approve reverts
```solidity
//solidity_contracts/src/CairoPrecompiles/DualVmToken.sol

    function _approve(uint256 spender, uint256 amount) private {
        if (spender >= STARKNET_FIELD_PRIME) {
            revert InvalidStarknetAddress();
        }
        // Split amount in [low, high]
 |>     uint128 amountLow = uint128(amount);
        uint128 amountHigh = uint128(amount >> 128);
        uint256[] memory approveCallData = new uint256[](3);
        approveCallData[0] = spender;
        approveCallData[1] = uint256(amountLow);
        approveCallData[2] = uint256(amountHigh);

        starknetToken.delegatecallCairo("approve", approveCallData);
    }
```
(https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/solidity_contracts/src/CairoPrecompiles/DualVmToken.sol#L221)

(2) transfer reverts:
```solidity
//solidity_contracts/src/CairoPrecompiles/DualVmToken.sol

    function _transfer(uint256 to, uint256 amount) private {
        if (to >= STARKNET_FIELD_PRIME) {
            revert InvalidStarknetAddress();
        }
        // Split amount in [low, high]
 |>     uint128 amountLow = uint128(amount);
        uint128 amountHigh = uint128(amount >> 128);

        uint256[] memory transferCallData = new uint256[](3);
        transferCallData[0] = to;
        transferCallData[1] = uint256(amountLow);
        transferCallData[2] = uint256(amountHigh);

        starknetToken.delegatecallCairo("transfer", transferCallData);
    }
```
(https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/solidity_contracts/src/CairoPrecompiles/DualVmToken.sol#L261)

Impacts:
approve or transfer a value exceeding type(uint128).max will always revert. This causes DOS of normal functionalities and causes unexpected errors or reversions in applications that call DualVmToken.sol.

## Recommended Mitigation Steps
Consider masking the higher 128 bits first before using `uint128()` conversion.





## Assessed type

Error
Incorrect totalsupply value will be returned due to erroneous return data decode implementation
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/solidity_contracts/src/CairoPrecompiles/DualVmToken.sol#L82


# Vulnerability details

## Proof of Concept
The returned value of `totalSupply()` in a starknet ERC20 contract is expected to fit in uint256, which is expressed in `(uint128, uint128)` with the first uint128 representing the lower 128bits. 

The issue is current implementation of DualVMToken::totalSupply incorrectly decode `returnData` as uint256 instead of `(uint128, uint128)`. Because the first uint128 is the lower 128bits of a uint256 number. This means, `totalSupply` will return an incorrect value because it only reads the lower 128bits. 
```solidity
//kakarot/solidity_contracts/lib/kakarot-lib/src/CairoLib.sol
    function totalSupply() external view returns (uint256) {
        bytes memory returnData = starknetToken.staticcallCairo("total_supply");
|>      return abi.decode(returnData, (uint256));
    }
```
(https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/solidity_contracts/src/CairoPrecompiles/DualVmToken.sol#L82)

Impacts: 
`totalSupply` will return incorrect value. Due to `totalSupply` is critical in many defi or accounting based logic, this potentially leads to fund loss and errorneas accounting in any user application that uses DualVMToken.sol. Depending on the context of the application that calls DualVMToken.sol, the fund loss could be critical.

## Recommended Mitigation Steps
In totalSupply(), change the decode following _balanceOf’s implementation:
```solidity
...
        (uint128 valueLow, uint128 valueHigh) = abi.decode(
            returnData,
            (uint128, uint128)
        );
        return uint256(valueLow) + (uint256(valueHigh) << 128);

```








## Assessed type

Error
Upgraded Q -> 3 from #79 [1724248085458]
Audit: 2024-07-basin-findings
Severity: HIGH

Judge has assessed an item in Issue #79 as 3 risk. The relevant finding follows:

 ## Title:
 1. Lack of Explicit Access Control on Upgrade Functions

## Description:
The `upgradeTo` and `upgradeToAndCall` functions are public, meaning it can be called by anyone. 

The `_authorizeUpgrade` function implements strong checks that significantly mitigate the risk of unauthorized upgrades. 

However, adding an explicit access control (like onlyOwner) to these functions would provide an additional layer of security.

## Recommendation:
Add an explicit access control (like onlyOwner) to the upgrade functions
Upgraded Q -> 3 from #84 [1724248036568]
Audit: 2024-07-basin-findings
Severity: HIGH

Judge has assessed an item in Issue #84 as 3 risk. The relevant finding follows:

 ## 3. decimal1 validation is missing.
https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/functions/Stable2.sol#L317

#### Recommended Mitigation Steps
```diff
        // if well data returns 0, assume 18 decimals.
        if (decimal0 == 0) {
            decimal0 = 18;
        }
-       if (decimal0 == 0) {
+       if (decimal1 == 0) {
            decimal1 = 18;
        }
```

## Tools Used
Manual review, Foundry
Incorrect Decoding Logic in `decodeWellData`function
Audit: 2024-07-basin-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-basin/blob/main/src/functions/Stable2.sol#L314-L318


# Vulnerability details

## Description
In the `decodeWellData` function, there is a logic error where `decimal1` is assigned to 18 based on the condition of `decimal0` being zero.

```soldity
if (decimal0 == 0) {
    decimal0 = 18;
}
if (decimal0 == 0) {
    decimal1 = 18;
}
```


## Impact
This can lead to incorrect handling of token decimals, causing miscalculations in scaling reserves and potentially affecting the accuracy of price and liquidity calculations.

## Proof of Concept
To demonstrate the issue with the incorrect decoding logic in the `decodeWellData` function of the `Stable2` contract, use the following test case:

1.Declare and Initialize the Test Data:
```solidity
uint256 decimal0 = 0;  // Set decimal0 to 0 to trigger faulty logic
uint256 decimal1 = 5;  // Set decimal1 to a non-zero value
bytes memory data = abi.encode(decimal0, decimal1);
```
2.Invoke the Vulnerable Function and Observe Behavior:
```solidity
try stable2.decodeWellData(data) returns (uint256[] memory decimals) {
    // If the function executes, log the resulting decimals
    emit log_uint(decimals[0]);
    emit log_uint(decimals[1]);
} catch {
    // If an error occurs, log an error message
    emit log("Error: Decoding failed");
}
```
Expected Outcome:
The function should incorrectly decode `decimal0 as 18, but it should correctly decode `decimal`1 as 5. The output should be [18, 18] instead of [18, 5], demonstrating the flawed logic in setting the decimal values.

## Tools Used
Manual Review 

## Recommended Mitigation Steps
Correct the condition to check `decimal1`:
```solidity
if (decimal0 == 0) {
    decimal0 = 18;
}
if (decimal1 == 0) {
    decimal1 = 18;
}
``





## Assessed type

Decimal
`WellUpgradeable::upgradeTo/upgradeToAndCall` lack access control
Audit: 2024-07-basin-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/WellUpgradeable.sol#L93
https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/WellUpgradeable.sol#L104


# Vulnerability details

## Impact
Anyone can change implementation of a Well, which could result in funds theft of all liquidity providers. 
![Diagram](https://ibb.co/8zkZHcj)

Currently the original flow, which is demostrated by the team to deploy a `WellUpgradeable` is to pass a `WellUpgradeable` implementation. to `Aquifer#boreWell` function, which will create new `WellUpgradeable` with set immutable data. After that owner of the well should create a `ERC1967Proxy` and as implementation set the newly cloned contract. 

The result will be that users will call functions on the `ERC1967Proxy`, which forwards to creators `WellUpgradeable` for the logic and write to `ERC1967Proxy` storage.

The problem is that the team has forgotten to put `onlyOwner` modifier to `WellUpgradeable#upgradeTo` and `WellUpgradeable#upgradeToAndCall` functions. Actually currently owner role is useless.

This is a critical bug, because proxy contract will be holding token liquidity and any malicious party can change the implementation, if he has first registered the malicious implementation in `Aquifer`, which is easily done.  Hacker can update to implementation, which has function `transferAllTokensToMe()`. Because implementation is the address that `ERC1967Proxy` delegatecall to,  `transferAllTokensToMe` will be executed in the context of the `ERC1967Proxy`, which is the address holding all the liquidity. `transferAllTokensToMe` function will then call `ERC20#transfer({hackerAddress}, {allBalance})` and drain all user's funds.

## Proof of Concept
I have commented `vm.startPrank(initialOwner);` in `/test/WellUpgradeable.t.soltestUpgradeToNewImplementation` test to demonstrate that anyone can upgrade the implementation: 
```
    function testUpgradeToNewImplementation() public {
        IERC20[] memory tokens = new IERC20[](2);
        tokens[0] = new MockToken("BEAN", "BEAN", 6);
        tokens[1] = new MockToken("WETH", "WETH", 18);
        Call memory wellFunction = Call(wellFunctionAddress, abi.encode("2"));
        Call[] memory pumps = new Call[](1);
        pumps[0] = Call(mockPumpAddress, abi.encode("2"));
        // create new mock Well Implementation:
        address wellImpl = address(new MockWellUpgradeable());
        WellUpgradeable well2 =
            encodeAndBoreWellUpgradeable(aquifer, wellImpl, tokens, wellFunction, pumps, bytes32(abi.encode("2")));
        vm.label(address(well2), "upgradeableWell2");

       // vm.startPrank(initialOwner);
        WellUpgradeable proxy = WellUpgradeable(payable(proxyAddress));
        proxy.upgradeTo(address(well2));
        assertEq(initialOwner, MockWellUpgradeable(proxyAddress).owner());
        // verify proxy was upgraded.
        assertEq(address(well2), Moc
`decodeWellData` may return wrong token decimals
Audit: 2024-07-basin-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/functions/Stable2.sol#L317


# Vulnerability details

## Impact
In the `decodeWellData` function of `Stable2.sol` wrong decimal configuration is performed, which may cause the decimals of one of the tokens to be wrong. As a result, `calcLpTokenSupply`, `calcReserve`, `calcRate`, `calcReserveAtRatioSwap` and `calcReserveAtRatioLiquidity` will return critically wrong data.

## Proof of Concept
The following validation is performed in `decodeWellData`:
```solidity
if (decimal0 == 0) {
    decimal0 = 18;
}
if (decimal0 == 0) {
    decimal1 = 18;
}
```

The issue is that in the second `if` statement `decimal0` is used instead of `decimal1`. As a result, if `decimal0` is 0, `decimal1` will also always be equal to 18, even if it's actual value is different, for example:
1/ `decimal0` is 0, therefore its value should be 18
2/ `decimal1` is 6, but it is overridden to 18

Thus, `calcLpTokenSupply`, `calcReserve`, `calcRate`, `calcReserveAtRatioSwap` and `calcReserveAtRatioLiquidity` may return wrong values, which may be much larger than the intended results.

## Tools Used
Manual review

## Recommended Mitigation Steps
Update to the following:
```solidity
if (decimal0 == 0) {
    decimal0 = 18;
}
if (decimal1 == 0) {
    decimal1 = 18;
}
```


## Assessed type

Invalid Validation