Farseen0/scar-pairs huggingface dataset:
HIGH severity: Unnecessary native token checks. **Description:** `FullRangeHook::beforeInitialize` attempts to retrieve the symbol of both currencies, with explicit handling of the native token; however, it is not possible for `currency1` to be the native token given ordering rules in Uniswap (`currency0` < `currency1`) since it is represented...

Vulnerable code:
// Prepare the symbol for the LP token to be deployed
string memory symbol0 =
    key.currency0.isAddressZero() ? NATIVE_CURRENCY_SYMBOL : IERC20Metadata(Currency.unwrap(key.currency0)).symbol();
string memory symbol1 =
    key.currency1.isAddressZero() ? NATIVE_CURRENCY_SYMBOL : IERC20Metadata(Currency.unwrap(key.currency1)).symbol();

/// @dev Settles any deltas after adding/removing liquidity
    function _settleDeltas(address sender, PoolKey memory key, BalanceDelta delta) internal {
        key.currency0.settle(poolManager, sender, uint256(int256(-delta.amount0())), false);
        key.currency1.settle(poolManager, sender, uint256(int256(-delta.amount1())), false);
@>      if (key.currency0.isAddressZero() || key.currency1.isAddressZero()) _returnNative(sender);
    }

string memory symbol0 =
       key.currency0.isAddressZero() ? NATIVE_CURRENCY_SYMBOL : IERC20Metadata(Currency.unwrap(key.currency0)).symbol();
-- string memory symbol1 =
--     key.currency1.isAddressZero() ? NATIVE_CURRENCY_SYMBOL : IERC20Metadata(Currency.unwrap(key.currency1)).symbol();
++ string memory symbol1 = IERC20Metadata(Currency.unwrap(key.currency1)).symbol();
LOW severity: `EIP7702StatelessDeleGator` violates `EIP4337` signature validation standards. **Description:** The `EIP7702StatelessDeleGator` implementation doesn't properly adhere to the EIP4337 standard regarding signature validation behavior.

According to EIP4337, the `validateUserOp` method should:

1. Return SIG_VALIDATION_FAILED (without reverting) only for signature mismatch...

Vulnerable code:
// EIP7702StatelessDeleGator.sol
    function _isValidSignature(bytes32 _hash, bytes calldata _signature) internal view override returns (bytes4) {
>>      if (_signature.length != SIGNATURE_LENGTH) return ERC1271Lib.SIG_VALIDATION_FAILED;

        if (ECDSA.recover(_hash, _signature) == address(this)) return ERC1271Lib.EIP1271_MAGIC_VALUE;

        return ERC1271Lib.SIG_VALIDATION_FAILED;
    }
LOW severity: `AllowedCalldataEnforcer` cannot authenticate empty calldata preventing `receive()` function calls. **Description:** The `AllowedCalldataEnforcer` contract has a design flaw that prevents it from authenticating calls with empty calldata. This issue arises from a requirement check in the `getTermsInfo` function that enforces `_terms.length >= 33`:

The first 32 bytes represent the starting offset.....

Vulnerable code:
// AllowedCalldataEnforcer.sol
    function getTermsInfo(bytes calldata _terms) public pure returns (uint256 dataStart_, bytes memory value_) {
>>      require(_terms.length >= 33, "AllowedCalldataEnforcer:invalid-terms-size");
        dataStart_ = uint256(bytes32(_terms[0:32]));
        value_ = _terms[32:];
    }

function getTermsInfo(bytes calldata _terms) public pure returns (uint256 dataStart_, bytes memory value_) {
    require(_terms.length >= 32, "AllowedCalldataEnforcer:invalid-terms-size");
    dataStart_ = uint256(bytes32(_terms[0:32]));
    if (_terms.length == 32) {
        value_ = new bytes(0); // @audit Empty bytes for empty calldata
    } else {
        value_ = _terms[32:];
    }
}
HIGH severity: NFT safe transfers will revert using `ERTransferEnforcer`. **Description:** The `ERC721TransferEnforcer` enforcer is designed to authorize NFT token transfers, but it currently restricts transfers to only use the `transferFrom` selector. The issue lies in this code section:

The ERC721 standard includes multiple transfer methods:

`transferFrom(address...

Vulnerable code:
//ERC721TransferEnforcer.sol
bytes4 selector_ = bytes4(callData_[0:4]);

if (target_ != permittedContract_) {
    revert("ERC721TransferEnforcer:unauthorized-contract-target");
} else if (selector_ != IERC721.transferFrom.selector) {
    revert("ERC721TransferEnforcer:unauthorized-selector");
} else if (transferTokenId_ != permittedTokenId_) {
    revert("ERC721TransferEnforcer:unauthorized-token-id");
}
LOW severity: Parameter mismatch in `IdEnforcer::beforeHook()` event emission. **Description:** There's a parameter ordering mismatch in the IdEnforcer contract when emitting the `UsedId` event in the `beforeHook()` function. The event declaration and the actual emission use different parameter orders for the delegator and redeemer arguments.

The event is declared with...

Vulnerable code:
//IdEnforcer.sol
    event UsedId(address indexed sender, address indexed delegator, address indexed redeemer, uint256 id);
// ---------
    function beforeHook( ... ) ... {
        ...
>>      emit UsedId(msg.sender, _redeemer, _delegator, id_);
    }
LOW severity: Inconsistent timestamp range validation in `TimestampEnforcer`. **Description:** The `TimestampEnforcer` contract allows the creation of delegations with a time-based validity window. However, it lacks validation to ensure logical consistency of the time range.

Specifically, when both the "after" and "before" thresholds are specified, there is no check to...

Vulnerable code:
//TimeStampEnforcer.sol

function getTermsInfo(bytes calldata _terms)
    public
    pure
    returns (uint128 timestampAfterThreshold_, uint128 timestampBeforeThreshold_)
{
    require(_terms.length == 32, "TimestampEnforcer:invalid-terms-length");
    timestampBeforeThreshold_ = uint128(bytes16(_terms[16:]));
    timestampAfterThreshold_ = uint128(bytes16(_terms[:16]));
}

//TimeStampEnforcer.sol

require(block.timestamp > timestampAfterThreshold_, "TimestampEnforcer:early-delegation");
require(block.timestamp < timestampBeforeThreshold_, "TimestampEnforcer:expired-delegation");
//@audit no check on validity of the range
HIGH severity: Native token prizes cannot be funded due to missing `receive()` function. **Description:** SpinGame supports multiple prize types, including ERC721, ERC20, and native tokens, where native tokens are represented as `prize.tokenAddress = address(0)`.

To ensure that prizes can be successfully claimed, the protocol team is responsible for maintaining a sufficient token...

Vulnerable code:
function testTransferNativeToken() public {
    vm.deal(admin,1e18);

    vm.prank(admin);
    (bool success, ) = address(spinGame).call{value: 1e18}("");

    // transfer failed as there is no `receive` or `fallback` function
    assertFalse(success);
}
HIGH severity: Rounding errors in boosted probability calculation can cause guaranteed wins to fail. **Description:** The Linea SpinGame includes a boosting feature that allows the protocol to increase a specific user's chance of winning. However, this mechanism introduces the possibility of a user's total winning probability exceeding 100%, as the boosted probabilities can sum to a value greater.....

Vulnerable code:
// Apply boost on the sum of totalProbabilities.
uint256 boostedTotalProbabilities = totalProbabilities * userBoost / BASE_POINT;

// If boostedTotalProbabilities exceeds 100% we have to increase the winning threshold so it stays in bound.
//
// Example:
//   PrizeA probability: 50%
//   PrizeB probability: 30%
//   User boost: 1.5x
//   boostedPrizeAProbability: 75%
//   boostedPrizeBProbability: 45%
//
//   We now have a total of 120% totalBoostedProbability so we need to increase winning threshold by boostedTotalProbabilities to BASE_POINT ratio.
//
//   winningThreshold = winningThreshold * 12_000 / 10_000
if (boostedTotalProbabilities > BASE_POINT) {
    winningThreshold =
        (winningThreshold * boostedTotalProbabilities) /
        BASE_POINT;
}

// Apply boost on a single prize probability.
uint256 boostedPrizeProbability = prizeProbability * userBoost / BASE_POINT;

unchecked {
    cumulativeProbability += boostedPrizeProbability;
}

if (winningThreshold < cumulativeProbability) {
    selectedPrizeId = localPrizeIds[i];

    // ... win
    break;
}

uint256 boostedPrizeProbability = prize.probability +
    ((prize.probability * userBoost) / BASE_POINT);
HIGH severity: Users can select higher-value NFTs by delaying prize claims. **Description:** When a user wins, the contract only tracks that they have won a specific `prizeID` in [`Spin::_fulfillRandomness`](https://github.com/Consensys/linea-hub/blob/295344925ec4321265f7cbac174fcf903b529a4e/contracts/src/Spin.sol#L576-L592):

However, when a user claims their prize, if...

Vulnerable code:
if (winningThreshold < cumulativeProbability) {
        selectedPrizeId = localPrizeIds[i];

        // ...
        break;
    }
}

userToPrizesWon[user][selectedPrizeId] += 1;

uint256 tokenId = prize.availableERC721Ids[
    prize.availableERC721Ids.length - 1
];
HIGH severity: Probability overflow can bypass `MaxProbabilityExceeded` check. **Description:** When adding new prizes, the contract includes a check to ensure that the total probability does not exceed 100% in [`Spin::_addPrizes#L511-L513`](https://github.com/Consensys/linea-hub/blob/295344925ec4321265f7cbac174fcf903b529a4e/contracts/src/Spin.sol#L511-L513):

However, this...

Vulnerable code:
if (totalProbabilities > BASE_POINT) {
    revert MaxProbabilityExceeded(totalProbabilities);
}

unchecked {
      totalProbIncrease += probability;
  }

unchecked {
      totalProbabilities += totalProbIncrease;
  }
HIGH severity: Scaling `winningThreshold` incorrectly reduces randomness distribution. **Description:** When a user has a boost that results in a >100% probability of winning, the contract adjusts `winningThreshold` to match `boostedTotalProbabilities` in [`Spin::_fulfillRandomness`](https://github.com/Consensys/linea-hub/blob/295344925ec4321265f7cbac174fcf903b529a4e/contracts/src/Spi...

Vulnerable code:
uint256 winningThreshold = _randomness % BASE_POINT;

// ...

if (boostedTotalProbabilities > BASE_POINT) {
    winningThreshold =
        (winningThreshold * boostedTotalProbabilities) /
        BASE_POINT;
}

if (boostedTotalProbabilities > BASE_POINT) {
-     winningThreshold =
-         (winningThreshold * boostedTotalProbabilities) /
-         BASE_POINT;

+     winningThreshold = _randomness % boostedTotalProbabilities;
  }
HIGH severity: Native token transfers lack explicit balance check. **Description:** One possible prize type is native tokens, represented by `tokenAddress = address(0)`. A user who wins native tokens can claim them in [`Spin::_transferPrize`](https://github.com/Consensys/linea-hub/blob/295344925ec4321265f7cbac174fcf903b529a4e/contracts/src/Spin.sol#L347-L352):

For...

Vulnerable code:
if (prize.tokenAddress == address(0)) {
    (bool success, ) = _winner.call{value: prize.amount}("");
    if (!success) {
        revert NativeTokenTransferFailed();
    }
} else {
HIGH severity: Assembly blocks could benefit from `"memory-safe"` annotation. **Description:** When hashing request data in [`Spin::_hashParticipation`](https://github.com/Consensys/linea-hub/blob/295344925ec4321265f7cbac174fcf903b529a4e/contracts/src/Spin.sol#L383-L409) and [`Spin::_hashClaim`](https://github.com/Consensys/linea-hub/blob/295344925ec4321265f7cbac174fcf903b529...

Vulnerable code:
assembly {
    let mPtr := mload(0x40)
    mstore(
        mPtr,
        0x4635ca970da82693e235d3cdaa3678d42c6824330c48b4135f080d655e54da78 // keccak256("ClaimRequest(address user,uint256 expirationTimestamp,uint64 nonce,uint32 prizeId)")
    )
    mstore(add(mPtr, 0x20), _user)
    mstore(add(mPtr, 0x40), _expirationTimestamp)
    mstore(add(mPtr, 0x60), _nonce)
    mstore(add(mPtr, 0x80), _prizeId)
    claimHash := keccak256(mPtr, 0xa0)
}
HIGH severity: Protocol fee should round up in favor of the protocol. **Description:** Protocol fee should round up in favor of the protocol in `onMorphoFlashLoan`:

Consider [using](https://x.com/DevDacian/status/1892529633104396479) OpenZeppelin [`Math::mulDiv`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/fda6b85f2c65d146b86d513a604554d15abd6679/cont...

Vulnerable code:
uint256 rockoFeeBP = ROCKO_FEE_BP;
if (rockoFeeBP > 0) {
    unchecked {
        feeAmount = (flashBorrowAmount * rockoFeeBP) / BASIS_POINTS_DIVISOR;
        borrowAmountWithFee += feeAmount;
    }
}
HIGH severity: Error messages hardcode `USDC` but other debt tokens may be used. **Description:** Error messages hardcode `USDC` but other token may be used, eg:

This code in `onMorphoFlashLoan` also assumes the debt token will be USDC:

**Rocko:** Fixed in commit [ec9f5be](https://github.com/getrocko/onchain/commit/ec9f5be20f9249cd20fcc1e173192361ecd97ef5).

**Cyfrin:**...

Vulnerable code:
function _closeLoanPositionAndReturnCollateralBalance(
    // @audit debt token can be other tokens apart from USDC but error
    // message hardcodes USDC
    require(
        debtBalance <= IERC20(debtTokenAddress).balanceOf(FLASH_LOAN_CONTRACT),
        "Insufficient USDC available in the flash contract"
    );

uint256 usdcBalance = IERC20(ctx.debtTokenAddress).balanceOf(FLASH_LOAN_CONTRACT);
bool feeAmountAvailable = usdcBalance >= feeAmount;
HIGH severity: Remove obsolete check in `updateFee`. **Description:** Remove obsolete check in `updateFee`:

This check is obsolete since `newFee` is declared as `uint256` therefore cannot be negative.

**Rocko:** Fixed in commit [2c50838](https://github.com/getrocko/onchain/commit/2c50838a5cc5e498a14874a5d3348da20087e6bc).

**Cyfrin:** Verified.

Vulnerable code:
-        require(newFee >= 0, "Fee must not be negative");
HIGH severity: Use `msg.sender` instead of `owner()` inside `onlyOwner` functions. **Description:** Using `msg.sender` instead of `owner()` inside `onlyOwner` functions is more efficient as it eliminates reading from storage. It is also safe since the `onlyOwner` modifier ensures that `msg.sender` is the owner:

**Rocko:** Fixed in commit...

Vulnerable code:
757:        IERC20(tokenAddress).safeTransfer(owner(), amount);
766:        (bool success, ) = owner().call{ value: amount }("");
HIGH severity: Don't initialize to default values. **Description:** Don't initialize to default values as Solidity already does this:

**Rocko:** Fixed in commit [751e906](https://github.com/getrocko/onchain/commit/751e906b7c2df6cb587e709b12de25593eb02c75).

**Cyfrin:** Verified.

Vulnerable code:
78:        ROCKO_FEE_BP = 0;
597:        uint256 debtBalance = 0;
598:        uint256 morphoDebtShares = 0;
LOW severity: Use named return variables to eliminate redundant local variables and `return` statements. **Description:** Use named return variables to eliminate redundant local variables and `return` statements:

Same idea can be applied to `_collateralBalanceOfAave`, `_getDebtBalanceOfAave`.

**Rocko:** Fixed in commit...

Vulnerable code:
// _closeLoanPositionAndReturnCollateralBalance L457
-    ) private returns (uint256) {
+    ) private returns (uint256 collateralBalance) {

// L464
-        uint256 collateralBalance;

// L480
-        return collateralBalance;
HIGH severity: Malicious user can create delegations that will spend unlimited amount of gas with `SpecificActionERTransferBatchEnforcer` caveat. **Description:** The `SpecificActionERC20TransferBatchEnforcer` first execution has no limit on the calldata size. This allows a malicious user to create delegations that will waste unlimited amount of gas from the delegates.

In order to compute the `keccak256` hash function, the code needs to...

Vulnerable code:
function beforeHook(
        bytes calldata _terms,
        bytes calldata,
        ModeCode _mode,
        bytes calldata _executionCallData,
        bytes32 _delegationHash,
        address _delegator,
        address
    )
        public
        override
        onlyBatchExecutionMode(_mode)
    {
        ...
        if (
            executions_[0].target != terms_.firstTarget || executions_[0].value != 0
                || keccak256(executions_[0].callData) != keccak256(terms_.firstCalldata)
        ) {
            revert("SpecificActionERC20TransferBatchEnforcer:invalid-first-transaction");
        }
        ...
    }

0x7cf5dab00000000000000000000000000000000000000000000000000000000000000001

0x7cf5dab00000000000000000000000000000000000000000000000000000000000000001fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff...
HIGH severity: Bugged `erimplementation` commit imported. **Description:** Currently, the codebase imports the `erc7579-implementation` external repository at commit `42aa538397138e0858bae09d1bd1a1921aa24b8c `to use the `ExecutionHelper`. This import is used inside the `DelegationMetaSwapAdapter` to implement the `executeFromExecutor` method:

When the...

Vulnerable code:
function executeFromExecutor(
        ModeCode _mode,
        bytes calldata _executionCalldata
    )
        external
        payable
        onlyDelegationManager
        returns (bytes[] memory returnData_)
    {
        (CallType callType_, ExecType execType_,,) = _mode.decode();

        // Check if calltype is batch or single
        if (callType_ == CALLTYPE_BATCH) {
            // Destructure executionCallData according to batched exec
            Execution[] calldata executions_ = _executionCalldata.decodeBatch();
            // check if execType is revert or try
            if (execType_ == EXECTYPE_DEFAULT) returnData_ = _execute(executions_);
            else if (execType_ == EXECTYPE_TRY) returnData_ = _tryExecute(executions_);
            else revert UnsupportedExecType(execType_);
        } else if (callType_ == CALLTYPE_SINGLE) {
            // Destructure executionCallData according to single exec
            (address target_, uint256 value_, bytes calldata callData_) = _executionCalldata.decodeSingle();
            returnData_ = new bytes[](1);
            bool success_;
            // check if execType is revert or try
            if (execType_ == EXECTYPE_DEFAULT) {
                returnData_[0] = _execute(target_, value_, callData_);
            } else if (execType_ == EXECTYPE_TRY) {
                (success_, returnData_[0]) = _tryExecute(target_, value_, callData_);
                if (!success_) emit TryExecuteUnsuccessful(0, returnData_[0]);
            } else {
                revert UnsupportedExecType(execType_);
            }
        } else {
            revert UnsupportedCallType(callType_);
        }
    }

function _tryExecute(
        address target,
        uint256 value,
        bytes calldata callData
    )
        internal
        virtual
        returns (bool success, bytes memory result)
    {
        /// @solidity memory-safe-assembly
        assembly {
            result := mload(0x40)
            calldatacopy(result, callData.offset, callData.length)
@>          success := iszero(call(gas(), target, value, result, callData.length, codesize(), 0x00))
            mstore(result, returndatasize()) // Store the length.
            let o := add(result, 0x20)
            returndatacopy(o, 0x00, returndatasize()) // Copy the returndata.
            mstore(0x40, add(o, returndatasize())) // Allocate the memory.
        }
    }
HIGH severity: Wrong event data field in `ERPeriodTransferEnforcer`. **Description:** In the `ERC20PeriodTransferEnforcer` it emits the following event when this caveat is used:

As we can see, the second field is the token recipient. However, in other token related caveats such as `ERC20StreamingEnforcer` and `NativeTokenStreamingEnforcer` the second field in the...

Vulnerable code:
/**
     * @notice Emitted when a transfer is made, updating the transferred amount in the active period.
     * @param sender The address initiating the transfer.
@>   * @param recipient The address that receives the tokens.
     * @param delegationHash The hash identifying the delegation.
     * @param token The ERC20 token contract address.
     * @param periodAmount The maximum tokens transferable per period.
     * @param periodDuration The duration of each period (in seconds).
     * @param startDate The timestamp when the first period begins.
     * @param transferredInCurrentPeriod The total tokens transferred in the current period after this transfer.
     * @param transferTimestamp The block timestamp at which the transfer was executed.
     */
    event TransferredInPeriod(
        address indexed sender,
@>      address indexed recipient,
        bytes32 indexed delegationHash,
        address token,
        uint256 periodAmount,
        uint256 periodDuration,
        uint256 startDate,
        uint256 transferredInCurrentPeriod,
        uint256 transferTimestamp
    );

function _validateAndConsumeTransfer(
        bytes calldata _terms,
        bytes calldata _executionCallData,
        bytes32 _delegationHash,
        address _redeemer
    )
        private
    {
        ...
        emit TransferredInPeriod(
            msg.sender,
@>          _redeemer,
            _delegationHash,
            token_,
            periodAmount_,
            periodDuration_,
            allowance_.startDate,
            allowance_.transferredInCurrentPeriod,
            block.timestamp
        );
    }

event TransferredInPeriod(
        address indexed sender,
--      address indexed recipient,
++      address indexed redeemer,
        bytes32 indexed delegationHash,
        address token,
        uint256 periodAmount,
        uint256 periodDuration,
        uint256 startDate,
        uint256 transferredInCurrentPeriod,
        uint256 transferTimestamp
    );
HIGH severity: First transaction on `SpecificActionERTransferBatchEnforcer` does not support sending native value. **Description:** The `SpecificActionERC20TransferBatchEnforcer` is an enforcer to execute an arbitrary transaction first that is constrained by the delegator and afterwards an ERC20 transfer. Both executions are constrained to not support sending value:

This check makes sense for the ERC20...

Vulnerable code:
function beforeHook(
        bytes calldata _terms,
        bytes calldata,
        ModeCode _mode,
        bytes calldata _executionCallData,
        bytes32 _delegationHash,
        address _delegator,
        address
    )
        public
        override
        onlyBatchCallTypeMode(_mode)
        onlyDefaultExecutionMode(_mode)
    {
        ...

        // Validate first transaction
        if (
            executions_[0].target != terms_.firstTarget || executions_[0].value != 0
                || keccak256(executions_[0].callData) != keccak256(terms_.firstCalldata)
        ) {
            revert("SpecificActionERC20TransferBatchEnforcer:invalid-first-transaction");
        }

        // Validate second transaction
        if (
            executions_[1].target != terms_.tokenAddress || executions_[1].value != 0 || executions_[1].callData.length != 68
                || bytes4(executions_[1].callData[0:4]) != IERC20.transfer.selector
                || address(uint160(uint256(bytes32(executions_[1].callData[4:36])))) != terms_.recipient
                || uint256(bytes32(executions_[1].callData[36:68])) != terms_.amount
        ) {
            revert("SpecificActionERC20TransferBatchEnforcer:invalid-second-transaction");
        }

        ...
    }

function beforeHook(
        bytes calldata _terms,
        bytes calldata,
        ModeCode _mode,
        bytes calldata _executionCallData,
        bytes32 _delegationHash,
        address _delegator,
        address
    )
        public
        override
        onlyBatchCallTypeMode(_mode)
        onlyDefaultExecutionMode(_mode)
    {
        ...

        // Validate first transaction
        if (
--          executions_[0].target != terms_.firstTarget || executions_[0].value != 0
++          executions_[0].target != terms_.firstTarget
                || keccak256(executions_[0].callData) != keccak256(terms_.firstCalldata)
        ) {
            revert("SpecificActionERC20TransferBatchEnforcer:invalid-first-transaction");
        }
        ...
    }