xanoutas/solidity-security-findings huggingface dataset:
receive_cross_chain_callback in handler_erc20.cairo doesn't handle the case of failed cross chain message
Audit: 2024-08-chakra-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-chakra/blob/d0d45ae1d26ca1b87034e67180fac07ce9642fd9/cairo/handler/src/handler_erc20.cairo#L138-L165


# Vulnerability details

## Impact
If failed cross chain message is sent to `receive_cross_chain_callback` function on Starknet, the status of message will always be successful after callback. `cross_chain_msg_status` in `CrossChainResult` will be emitted wrongly.

## Proof of Concept
https://github.com/code-423n4/2024-08-chakra/blob/d0d45ae1d26ca1b87034e67180fac07ce9642fd9/cairo/handler/src/handler_erc20.cairo#L138-L165
From above code, the `cross_chain_msg_status` is included in the input of `receive_cross_chain_callback` function. However this function doesn't consider the case of failed cross chain message. It assumes all cross chain message is successful which isn't right.
https://github.com/code-423n4/2024-08-chakra/blob/d0d45ae1d26ca1b87034e67180fac07ce9642fd9/cairo/settlement/src/settlement.cairo#L417-L423
From above code, if the call to handler is successful, the status of cross chain message will be successful which didn't consider the case of failed cross chain message.
`receive_cross_chain_callback` function doesn't handle the case of failed cross chain message.

## Tools Used
manual

## Recommended Mitigation Steps
1. handler_erc20.cairo
Add struct:
```
    pub mod CrossChainMsgStatus {
        pub const UNKNOW: u8 = 0;
        pub const PENDING: u8 = 1;
        pub const SUCCESS: u8 = 2;
        pub const FAILED: u8 = 3;
    }
```
Then(`receive_cross_chain_callback`):
```diff
+           let mut status = CrossChainTxStatus::SETTLED;
+           if cross_chain_msg_status == CrossChainMsgStatus::SUCCESS {
            let erc20 = IERC20MintDispatcher{contract_address: self.token_address.read()};
                if self.mode.read() == SettlementMode::MintBurn{
                    erc20.burn_from(get_contract_address(), self.created_tx.read(cross_chain_msg_id).amount);
                }
+           }
+           if cross_chain_msg_status == CrossChainMsgStatus::FAILED {
+               status = CrossChainTxStatus::FAILED; 
+           }
            let created_tx = self.created_tx.read(cross_chain_msg_id);
            self.created_tx.write(cross_chain_msg_id, CreatedCrossChainTx{
                tx_id: created_tx.tx_id,
                from_chain: created_tx.from_chain,
                to_chain: created_tx.to_chain,
                from:created_tx.from,
                to:created_tx.to,
                from_token: created_tx.from_token,
                to_token: created_tx.to_token,
                amount: created_tx.amount,
+               tx_status: CrossChainTxStatus::SETTLED
+               tx_status: status
            });
```
2.settlement.cairo::receive_cross_chain_callback
```diff
            if success{
-               state = CrossChainMsgStatus::SUCCESS;
+               state = cross_chain_msg_status;
Funds can be permanently locked up in ChakraSettlementHandler on failed crosschain transfers
Audit: 2024-08-chakra-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-chakra/blob/abef77d95866f2fec93270491fc5abc9ab14f86d/solidity/handler/contracts/ChakraSettlementHandler.sol#L391


# Vulnerability details

## Description
In ChakraSettlementHandler contract, a user funds can become permanently locked if a cross-chain transfer fails. When a transfer is initiated, tokens are locked in the contract. However, if the transfer fails, these tokens are not automatically returned to the user.

Vulnerable part of the code:
```
 function receive_cross_chain_callback(
        uint256 txid,
        string memory from_chain,
        uint256 from_handler,
        CrossChainMsgStatus status,
        uint8 /* sign_type */, // validators signature type /  multisig or bls sr25519
        bytes calldata /* signatures */
    ) external onlySettlement returns (bool) {
        //  from_handler need in whitelist
        if (is_valid_handler(from_chain, from_handler) == false) {
            return false;
        }

        require(
            create_cross_txs[txid].status == CrossChainTxStatus.Pending,
            "invalid CrossChainTxStatus"
        );

        if (status == CrossChainMsgStatus.Success) {
            if (mode == SettlementMode.MintBurn) {
                _erc20_burn(address(this), create_cross_txs[txid].amount);
            }

            create_cross_txs[txid].status = CrossChainTxStatus.Settled;
        }

        //@audit no refund logic for failed transfer
        if (status == CrossChainMsgStatus.Failed) {
            create_cross_txs[txid].status = CrossChainTxStatus.Failed;
        }

        return true;
    }
```
As you can see, in the receive_cross_chain_callback function, when a transfer fails (status == CrossChainMsgStatus.Failed), the transaction status is updated, but no action is taken to return the locked funds to the user.


## Impact 
This vulnerability can lead to permanent loss of user funds. If a cross-chain transfer fails, users have no built-in mechanism to recover their locked tokens. This could result in significant financial losses for users.
## Proof of Concept
The vulnerability is demonstrated in the testFlaw() function of the POC contract below. The test shows that after a failed cross-chain transfer:

1. The user's balance remains reduced (900 tokens instead of the initial 1000).
2. The contract retains the locked funds (100 tokens).
3. The transaction is marked as failed, but no refund occurs.

Key test output:
User balance after locking: 900.000000000000000000
User balance after failed transfer: 900.000000000000000000
Contract balance: 100.000000000000000000

```
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "forge-std/Test.sol";

// Shared enums and structs
enum ERC20Method { Transfer }
enum PayloadType { ERC20 }
enum SettlementMode { MintBurn, LockUnlock, LockMint, BurnUnlock }
enum CrossChainTxStatus { Pending, Settled, Failed }
enum CrossChainMsgStatus { Success, Failed }

struct ERC20TransferPayload {
    ERC
Unprotected Initialization Function Allows Multiple Calls in BaseSettlement
Audit: 2024-08-chakra-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-chakra/blob/abef77d95866f2fec93270491fc5abc9ab14f86d/solidity/settlement/contracts/BaseSettlement.sol#L54


# Vulnerability details

## Description
The BaseSettlement contract contains an initialization function _Settlement_init that lacks protection against multiple calls/ multiple initialization. The function sets critical contract parameters but can be called repeatedly, allowing an attacker to override essential contract state.
Vulnerable code snippet:
```
function _Settlement_init(
        string memory _chain_name,
        uint256 _chain_id,
        address _owner,
        address[] memory _managers,
        uint256 _required_validators,
        address _signature_verifier
    ) public {
        __Ownable_init(_owner);
        __UUPSUpgradeable_init();
        _grantRole(DEFAULT_ADMIN_ROLE, _owner);

        for (uint256 i = 0; i < _managers.length; i++) {
            _grantRole(MANAGER_ROLE, _managers[i]);
        }

        chain_id = _chain_id;
        contract_chain_name = _chain_name;
        required_validators = _required_validators;
        signature_verifier = ISettlementSignatureVerifier(_signature_verifier);
    }
```


## Impact
An attacker can repeatedly call this function to modify the existing state and reinitialize the contract multiple times.

## POC
The provided PoC demonstrates this vulnerability:

1. It initializes the contract with initial values (chain_id: 1, required_validators: 3).
2. It then reinitializes the contract with different values (chain_id: 999, required_validators: 1).
3. The test confirms that the contract state is successfully altered after reinitialization.


Key output from the PoC:
```
Initial chain_id: 1.0
Initial required_validators: 3.0
Reinitialized chain_id: 999.0
Reinitialized required_validators: 1.0
```

Here is the test funtion
```
    function testReinitializationVulnerability() public {
        // Initial setup
        settlement._Settlement_init("Chain1", 1, owner, managers, 3, address(mockVerifier));
        
        emit log_named_decimal_uint("Initial chain_id", settlement.chain_id(), 0);
        emit log_named_decimal_uint("Initial required_validators", settlement.required_validators(), 0);

        // Attacker reinitializes with different values
        address[] memory attackerManagers;
        settlement._Settlement_init("AttackerChain", 999, attacker, attackerManagers, 1, address(mockVerifier));

        emit log_named_decimal_uint("Reinitialized chain_id", settlement.chain_id(), 0);
        emit log_named_decimal_uint("Reinitialized required_validators", settlement.required_validators(), 0);

        // Assert the changes
        assertEq(settlement.chain_id(), 999, "Chain ID should be changed");
        assertEq(settlement.required_validators(), 1, "Required validators should be changed");
        assertEq(settlement.contract_chain_name(), "AttackerChain", "Chain name should be changed");
    }
}
```

## Recommended Mitigati
Unprotected Initialization Function Allows Multiple Calls in BaseSettlementHandler
Audit: 2024-08-chakra-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-chakra/blob/abef77d95866f2fec93270491fc5abc9ab14f86d/solidity/handler/contracts/BaseSettlementHandler.sol#L113


# Vulnerability details

## Description
The BaseSettlementHandler contract contains an initialization function _Settlement_handler_init that lacks protection against multiple calls/ multiple initialization. The function sets critical contract parameters but can be called repeatedly, allowing an attacker to override essential contract state.
Vulnerable code snippet:
```
function _Settlement_handler_init(
    address _owner,
    SettlementMode _mode,
    address _token,
    address _verifier,
    string memory _chain,
    address _settlement
) public {
    __Ownable_init(_owner);
    __UUPSUpgradeable_init();
    settlement = ISettlement(_settlement);
    verifier = ISettlementSignatureVerifier(_verifier);
    mode = _mode;
    token = _token;
    chain = _chain;
}
```


## Impact
An attacker can repeatedly call this function to modify the existing state and reinitialize the contract multiple times.

## POC
The provided PoC demonstrates this vulnerability:

The provided PoC demonstrates this vulnerability:

1. It initializes the contract with initial values (mode: MintBurn, token: mockToken, chain: "Chain1").
2. It then reinitializes the contract with different values (mode: LockUnlock, token: attackerToken, chain: "AttackerChain").
3. The test confirms that the contract state is successfully altered after reinitialization.



Key output from the PoC:
```
PASS] testReinitializationVulnerability() (gas: 126947)
Logs:
  Initial token: 0x0000000000000000000000000000000000000003
  Initial mode: 0
  Initial chain: Chain1
  Reinitialized token: 0x0000000000000000000000000000000000000006
  Reinitialized mode: 3
  Reinitialized chain: AttackerChain

```

Here is the test function
```
    function testReinitializationVulnerability() public {
        // Initial setup
        handler._Settlement_handler_init(
            owner,
            BaseSettlementHandler.SettlementMode.MintBurn,
            mockToken,
            mockVerifier,
            "Chain1",
            mockSettlement
        );
        
        emit log_named_address("Initial token", handler.token());
        emit log_named_uint("Initial mode", uint(handler.mode()));
        emit log_string(string(abi.encodePacked("Initial chain: ", handler.chain())));

        // Attacker reinitializes with different values
        address attackerToken = address(0x6);
        handler._Settlement_handler_init(
            attacker,
            BaseSettlementHandler.SettlementMode.LockUnlock,
            attackerToken,
            attacker,
            "AttackerChain",
            attacker
        );

        emit log_named_address("Reinitialized token", handler.token());
        emit log_named_uint("Reinitialized mode", uint(handler.mode()));
        emit log_string(string(abi.encodePacked("Reinitialized chain: ", handler.chain())));

        //
Unprotected Initialization Function Allows Unauthorized Ownership Transfer
Audit: 2024-08-chakra-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-chakra/blob/abef77d95866f2fec93270491fc5abc9ab14f86d/solidity/handler/contracts/TokenRoles.sol#L19


# Vulnerability details

## Description
The TokenRoles contract contains an unprotected initialization function __TokenRoles_init that can be called multiple times by any address. This function sets critical contract parameters including the owner and operator roles.

Vulnerable code snippet:
```
function __TokenRoles_init(address owner_, address _operator) public {
    __Ownable_init(owner_);
    _grantRole(DEFAULT_ADMIN_ROLE, owner_);
    _grantRole(OPERATOR_ROLE, _operator);
}
```
## Impact
An attacker can call this function at any time to:

1. Transfer ownership of the contract to themselves.
2. Grant themselves operator privileges.
3. Override existing role assignments.

## Proof of Concept
The provided PoC demonstrates this vulnerability:

1. It initializes the contract with an original owner and operator.
2. An attacker then calls __TokenRoles_init, becoming the new owner.
3. The PoC shows that the attacker gains owner privileges (can add new operators).
It confirms that the original owner loses all control.

Key output from the PoC:
```
Initial owner: 0x0000000000000000000000000000000000000001
Reinitialized owner: 0x0000000000000000000000000000000000000003
Can attacker add operators: 1
```
The test also verifies that the original owner can no longer perform owner actions, receiving a "Ownable: caller is not the owner" error.

Here is the test function
```
 function testReinitializationVulnerability() public {
        // Initial setup
        tokenRoles.__TokenRoles_init(owner, operator);
        
        emit log_named_address("Initial owner", tokenRoles.owner());
        emit log_named_uint("Is initial operator", tokenRoles.is_operator(operator) ? 1 : 0);

        // Attacker reinitializes with different values
        tokenRoles.__TokenRoles_init(attacker, attacker);

        emit log_named_address("Reinitialized owner", tokenRoles.owner());
        emit log_named_uint("Is attacker operator", tokenRoles.is_operator(attacker) ? 1 : 0);
        emit log_named_uint("Is original operator still operator", tokenRoles.is_operator(operator) ? 1 : 0);

        // Verify the attacker can perform owner actions
        vm.prank(attacker);
        tokenRoles.add_operator(address(0x4));
        
        emit log_named_uint("Can attacker add operators", tokenRoles.is_operator(address(0x4)) ? 1 : 0);

        // Verify the original owner cannot perform owner actions
        vm.expectRevert("Ownable: caller is not the owner");
        vm.prank(owner);
        tokenRoles.add_operator(address(0x5));
    }
```

## Recommended Mitigation Steps
Use the initializer modifier from OpenZeppelin's Initializable contract to prevent multiple initializations.
```
function __TokenRoles_init(address _owner, address _operator) public initializer {
        // Added initializer modifier to prevent multiple initializat
Minimum required validators in signatures verification can be bypassed
Audit: 2024-08-chakra-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-chakra/blob/abef77d95866f2fec93270491fc5abc9ab14f86d/solidity/settlement/contracts/SettlementSignatureVerifier.sol#L207


# Vulnerability details

## Impact
The functions [ChakraSettlement::receive_cross_chain_msg](https://github.com/code-423n4/2024-08-chakra/blob/abef77d95866f2fec93270491fc5abc9ab14f86d/solidity/settlement/contracts/ChakraSettlement.sol#L195) and [ChakraSettlement::receive_cross_chain_callback](https://github.com/code-423n4/2024-08-chakra/blob/abef77d95866f2fec93270491fc5abc9ab14f86d/solidity/settlement/contracts/ChakraSettlement.sol#L298) rely on `SettlementSignatureVerifier::verify` to validate the signatures. For the `signatures` to be considered valid, two conditions must be met:
1. The signatures must be from authorized validators.
2. The minimum required number of validators must sign the message.

The issue lies in the fact that `SettlementSignatureVerifier::verify` does not correctly enforce the second requirement:
```solidity
function verify(
    bytes32 msgHash,
    bytes calldata signatures,
    uint8 sign_type
) external view returns (bool) {
    if (sign_type == 0) {
        return verifyECDSA(msgHash, signatures);
    } else {
        return false;
    }
}


function verifyECDSA(
    bytes32 msgHash,
    bytes calldata signatures
) internal view returns (bool) {
    require(
        signatures.length % 65 == 0,
        "Signature length must be a multiple of 65"
    );

    uint256 len = signatures.length;
    uint256 m = 0;
    for (uint256 i = 0; i < len; i += 65) {
        bytes memory sig = signatures[i:i + 65];
        if (
>>>            validators[msgHash.recover(sig)] && ++m >= required_validators
        ) {
             return true;
        }
    }

    return false;
}
```
As shown above, the following logic fails to adequately verify the second requirement:
```solidity
validators[msgHash.recover(sig)] && ++m >= required_validators
```
A **single** validator can create one signature and duplicate it multiple times to meet the `required_validators` threshold, effectively bypassing the minimum validator requirement. As a result, a single validator can temper with cross-chain messages without reaching a consensus on the Chakra network

The same issue exists on Cairo code:
```rs
fn check_chakra_signatures( self: @ContractState, message_hash: felt252, signatures: Array<(felt252, felt252, bool)> ){
    let mut pass_count = 0;
    let mut i = 0;
    loop {
        if i > signatures.len()-1{
            break;
        }
        let (r,s,y) = * signatures.at(i);
        let pub_key: felt252 = recover_public_key(message_hash,r,s,y).unwrap();
>>>        if self.chakra_validators_pubkey.read(pub_key) > 0{
            pass_count += 1;
        }
        i += 1;
    };
    assert(pass_count >= self.required_validators_num.read(), "Not enough validate signatures");
}
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
Modify the loop within `verifyECDSA
Missing `disableInitializers` Call in Proxy Upgradeable Contract Constructor
Audit: 2024-08-chakra-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-chakra/blob/abef77d95866f2fec93270491fc5abc9ab14f86d/solidity/handler/contracts/BaseSettlementHandler.sol#L113


# Vulnerability details

## Description
In the contracts implement Openzeppelin’s UUPS model, uninitialized implementation contract can be taken over by an attacker with initialize function, it’s recommended to invoke the _disableInitializers function in the constructor to prevent the implementation contract from being used by the attacker. However all the contracts which implements OwnablePausableUpgradeable do not call _disableInitializers in the constructors

Examples
in -> handler/contracts/BaseSettlementHandler.sol

```
abstract contract BaseSettlementHandler is
    OwnableUpgradeable,
    UUPSUpgradeable,
    AccessControlUpgradeable
{
```
in -> settlement/contracts/BaseSettlement.sol
```
abstract contract BaseSettlement is
    OwnableUpgradeable,
    UUPSUpgradeable,
    AccessControlUpgradeable
{
```
in -> handler/contracts/TokenRoles.sol
```
abstract contract TokenRoles is
    OwnableUpgradeable,
    UUPSUpgradeable,
    AccessControlUpgradeable
{
```

in -> handler/contracts/ERC20CodecV1.sol
```
contract ERC20CodecV1 is IERC20CodecV1, OwnableUpgradeable, UUPSUpgradeable {
```
etc...

## Recommended Mitigation Steps

Invoke _disableInitializers in the constructors of contracts which implement OwnablePausableUpgradeable including following:
```
BaseSettlement
BaseSettlementHandler
```
Add the implementation
```
constructor() {
    _disableInitializers();
}
```











## Assessed type

Upgradable
`from_chain` exclusion in validator signatures allows malicious actors to block cross-chain callbacks on source chain
Audit: 2024-08-chakra-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-08-chakra/blob/abef77d95866f2fec93270491fc5abc9ab14f86d/solidity/settlement/contracts/ChakraSettlement.sol#L294


# Vulnerability details

## Impact

When a cross-chain ERC20 transfer is initiated, and the destination `ChakraSettlement` contract processes the message via `receive_cross_chain_msg`, it emits a `CrossChainHandleResult` event. Chakra validators then detect this event and invoke `ChakraSettlement::receive_cross_chain_callback` on the source chain to finalize the cross-chain message. The issue is that the signatures used for validation do not include the `from_chain` information. This omission creates a vulnerability where a malicious user can front-run the `receive_cross_chain_callback` and provide a fraudulent `from_chain` value that is not approved for the `from_handler`. This results in the cross-chain message status being set to `Failed`, thereby blocking the transaction from being finalized.
```solidity
function receive_cross_chain_callback(
    uint256 txid,
    string memory from_chain,
    uint256 from_handler,
    address to_handler,
    CrossChainMsgStatus status,
    uint8 sign_type,
    bytes calldata signatures
) external {
>>>    verifySignature(txid, from_handler, to_handler, status, sign_type, signatures);
    processCrossChainCallback(txid, from_chain, from_handler, to_handler, status, sign_type, signatures);
    emitCrossChainResult(txid);
}

function verifySignature(
    uint256 txid,
    uint256 from_handler,
    address to_handler,
    CrossChainMsgStatus status,
    uint8 sign_type,
    bytes calldata signatures
) internal view {
    bytes32 message_hash = keccak256(
>>>        abi.encodePacked(txid, from_handler, to_handler, status)
    );

    require(signature_verifier.verify(message_hash, signatures, sign_type),"Invalid signature");
}
```
In the above code, the `from_chain` is not included in the signatures, which enables an attacker to exploit this vulnerability by front-running the `receive_cross_chain_callback` function with an invalid `from_chain` for the `from_handler`. As a result, the `ChakraSettlementHandler::receive_cross_chain_callback` will return `false`, marking the transaction as `Failed`:
```solidity
function processCrossChainCallback(
    uint256 txid,
    string memory from_chain,
    uint256 from_handler,
    address to_handler,
    CrossChainMsgStatus status,
    uint8 sign_type,
    bytes calldata signatures
) internal {
    // --SNIP

    if (
        ISettlementHandler(to_handler).receive_cross_chain_callback(
>>>            txid, from_chain, from_handler, status, sign_type, signatures)
    ) {
        create_cross_txs[txid].status = status;
    } else {
>>>        create_cross_txs[txid].status = CrossChainMsgStatus.Failed;
    }
}
```
`ISettlementHandler(to_handler).receive_cross_chain_callback` returns `false` if the `from_handler` is not approved for the `from_chain`:
```solidity
function receive_cross_chain_callback(
    uint256 txi