Farseen0/scar-pairs huggingface dataset:
HIGH severity: Incorrect allowance check in transfer functions prevents users from transferring their own tokens. **Description:** The protocol implements transfer functions in both `RWASegWrap` and `SecuritizeRWASegWrap` contracts that violate standard token transfer practices by always checking allowance, even when users are transferring their own tokens.

In `RWASegWrap::safeTransferFrom()`, lines 448-450...

Vulnerable code:
uint256 currentAllowance = allowance(from, _msgSender(), id);
if (currentAllowance < value) {
    revert ERC1155MissingApprovalForAll(_msgSender(), from);
}

// OpenZeppelin's ERC1155Upgradeable
address sender = _msgSender();
if (from != sender && !isApprovedForAll(from, sender)) {
    revert ERC1155MissingApprovalForAll(sender, from);
}

function safeTransferFrom(
    address from,
    address to,
    uint256 id,
    uint256 value,
    bytes memory data
) public virtual override whenNotPaused idNotZero(id) recognizedVault(id) {
    if (to == address(0)) {
        revert ERC1155InvalidReceiver(address(0));
    }
    if (from == address(0)) {
        revert ERC1155InvalidSender(address(0));
    }
    uint256 vaultId = getVaultId(from);
    if (id != vaultId) {
        revert InvestorVaultMismatch(id, from);
    }

-   uint256 currentAllowance = allowance(from, _msgSender(), id);
-   if (currentAllowance < value) {
-       revert ERC1155MissingApprovalForAll(_msgSender(), from);
-   }
+   address sender = _msgSender();
+   if (from != sender) {
+       uint256 currentAllowance = allowance(from, sender, id);
+       if (currentAllowance < value) {
+           revert ERC1155MissingApprovalForAll(sender, from);
+       }
+   }

    uint256 currentBalance = balanceOf(from, id);
    if (currentBalance < value) {
        revert ERC1155InsufficientBalance(from, currentBalance, value, id);
    }

    emit TransferSingle(_msgSender(), from, to, id, value);
    ISegregatedVault(vaults[id]).internalTransferFrom(from, to, _msgSender(), value);
    ERC1155Utils.checkOnERC1155Received(_msgSender(), from, to, id, value, data);
}
LOW severity: Missing `notEmptyURI` modifier during initialization. **Description:** Currently, there is no `notEmptyURI` modifier present in the `initialize()` function that checks for the empty URI and, if it's empty, reverts the transaction:

**Impact:** Insufficient validation, `projectURI` may not be set during the initialization process.

**Recommended...

Vulnerable code:
//RWASegWrap.sol#L98-103
    modifier notEmptyUri(string memory newUri) {
        if (bytes(newUri).length == 0) {
            revert EmptyUriInvalid();
        }
        _;
    }

//RWASegWrap.sol#L131-147
   function initialize(
        string memory baseNameArg,
        string memory baseSymbolArg,
        string memory uriArg,
        address liquidationAssetArg,
        address assetArg,
        address vaultDeployerArg
    )
    public
    virtual
    override
    onlyProxy
    initializer
    addressNotZero(liquidationAssetArg)
    addressNotZero(assetArg)
    addressNotZero(vaultDeployerArg)
    {
HIGH severity: Redundant balance check in safeTransferFrom before calling underlying transfer function. **Description:** The `RWASegWrap::safeTransferFrom()` function performs an unnecessary balance check on lines 452-454 before calling the underlying transfer function:

This balance check is redundant because the subsequent call to `ISegregatedVault(vaults[id]).internalTransferFrom(from, to,...

Vulnerable code:
uint256 currentBalance = balanceOf(from, id);
if (currentBalance < value) {
    revert ERC1155InsufficientBalance(from, currentBalance, value, id);
}

uint256 fromBalance = $._balances[from];
if (fromBalance < value) {
    revert ERC20InsufficientBalance(from, fromBalance, value);
}

function safeTransferFrom(
    address from,
    address to,
    uint256 id,
    uint256 value,
    bytes memory data
) public virtual override whenNotPaused idNotZero(id) recognizedVault(id) {
    if (to == address(0)) {
        revert ERC1155InvalidReceiver(address(0));
    }
    if (from == address(0)) {
        revert ERC1155InvalidSender(address(0));
    }
    uint256 vaultId = getVaultId(from);
    if (id != vaultId) {
        revert InvestorVaultMismatch(id, from);
    }

    uint256 currentAllowance = allowance(from, _msgSender(), id);
    if (currentAllowance < value) {
        revert ERC1155MissingApprovalForAll(_msgSender(), from);
    }

-   uint256 currentBalance = balanceOf(from, id);
-   if (currentBalance < value) {
-       revert ERC1155InsufficientBalance(from, currentBalance, value, id);
-   }

    emit TransferSingle(_msgSender(), from, to, id, value);
    ISegregatedVault(vaults[id]).internalTransferFrom(from, to, _msgSender(), value);
    ERC1155Utils.checkOnERC1155Received(_msgSender(), from, to, id, value, data);
}
HIGH severity: `setApprovalForAll()` function is double initialized in the child contract. **Description:** At the moment, there is a double function initialization of the ERC1155 `setApprovalForAll()` function both in the parent `RWASegWrap` and the child `SecuritizeRWASegWrap` contracts:

**Impact:** Increased deployment costs.

**Recommended Mitigation:** Remove `setApprovalForAll()`.....

Vulnerable code:
// @inheritdoc IERC1155
    function setApprovalForAll(address, bool) public virtual pure override {
        revert FeatureNotSupported();
    }

// @inheritdoc IERC1155
    function setApprovalForAll(address, bool) public virtual pure override {
        revert FeatureNotSupported();
    }
HIGH severity: Unnecessary gas consumption in deposit function due to redundant maximum deposit check. **Description:** The `SecuritizeVaultV2::_depositWithFee` function performs an unnecessary check against `maxDeposit(from)` to validate that the deposit amount doesn't exceed the maximum allowed deposit limit. However, `SecuritizeVaultV2` does not override the `maxDeposit` function from its parent.....

Vulnerable code:
// In SecuritizeVaultV2::_depositWithFee
function _depositWithFee(uint256 assets, address from) private returns (uint256) {
    address caller = _msgSender();
    uint256 maxAssets = maxDeposit(from); // Returns type(uint256).max
    if (assets > maxAssets) { // This condition will never be true
        revert ERC4626ExceededMaxDeposit(from, assets, maxAssets);
    }
    // ... rest of function
}

// In ERC4626Upgradeable (not overridden by SecuritizeVaultV2)
function maxDeposit(address) public view virtual returns (uint256) {
    return type(uint256).max; // Always returns maximum uint256 value
}

function _depositWithFee(uint256 assets, address from) private returns (uint256) {
    address caller = _msgSender();
-   uint256 maxAssets = maxDeposit(from);
-   if (assets > maxAssets) {
-       revert ERC4626ExceededMaxDeposit(from, assets, maxAssets);
-   }

    uint256 fee = 0;
    if (address(feeManager) != address(0)) {
        fee = IFeeManager(feeManager).computeFee(IFeeManager.FeeApplicableOperation.Deposit, assets);
    }

    uint256 shares = previewDeposit(assets - fee);
    _depositAndSendFees(caller, from, assets, fee, shares);
    return shares;
}
HIGH severity: Gas inefficient approval pattern in liquidation function leads to unnecessary gas consumption. **Description:** The `SecuritizeVaultV2::_liquidateTo` function performs an approval operation to the redemption contract on every liquidation call when a redemption contract is configured. On line 489, the function calls `IERC20Metadata(asset()).approve(address(redemption), assets)` for each...

Vulnerable code:
function _initialize(
    string memory _name,
    string memory _symbol,
    address _securitizeToken,
    address _redemptionAddress,
    address _liquidationToken,
    address _navProvider
) private {
    // ... existing initialization logic ...

    redemption = ISecuritizeOffRamp(_redemptionAddress);
    liquidationToken = IERC20Metadata(_liquidationToken);
    navProvider = ISecuritizeNavProvider(_navProvider);

+   // Grant maximum approval to redemption contract if it exists
+   if (address(redemption) != address(0)) {
+       bool success = IERC20Metadata(_securitizeToken).approve(address(redemption), type(uint256).max);
+       if (!success) {
+           revert AssetApprovalFailed();
+       }
+   }

    _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
}

function _liquidateTo(address to, uint256 shares) internal {
    if (balanceOf(to) < shares) {
        revert NotEnoughShares();
    }
    uint256 assets = convertToAssets(shares);
    _burn(to, shares);

    emit Liquidate(to, assets, shares);

    if (address(0) != address(redemption)) {
-       bool success = IERC20Metadata(asset()).approve(address(redemption), assets);
-       if (!success) {
-           revert AssetApprovalFailed();
-       }
        uint256 balanceBefore = liquidationToken.balanceOf(address(this));
        redemption.redeem(assets, 0);
        uint256 receivedAmount = liquidationToken.balanceOf(address(this)) - balanceBefore;
        liquidationToken.safeTransfer(to, receivedAmount);
    } else {
        liquidationToken.safeTransfer(to, assets);
    }
}
LOW severity: `PriceStorage::setPrice` maximum lower and upper bounds can be easily bypassed by repeatedly calling the function in the same block. **Description:** `PriceStorage::setPrice` limits the price movement to a maximum lower/upper range delta based on the current price, to prevent sudden extreme changes in price:

But this can be easily bypassed by repeatedly calling the `setPrice` function multiple times in the same block, each...

Vulnerable code:
uint256 lastPriceValue = lastPrice.price;
if (lastPriceValue != 0) {
  uint256 upperBound = lastPriceValue + ((lastPriceValue * upperBoundPercentage) / BOUND_PERCENTAGE_DENOMINATOR);
  uint256 lowerBound = lastPriceValue - ((lastPriceValue * lowerBoundPercentage) / BOUND_PERCENTAGE_DENOMINATOR);
  if (_price > upperBound || _price < lowerBound) {
    revert InvalidPriceRange(_price, lowerBound, upperBound);
  }
}

error PriceUpdateTooSoon(uint256 lastUpdate, uint256 minWaitTime);

// In setPrice:
if(lastPrice.timestamp != 0) {
    if(block.timestamp < lastPrice.timestamp + minPriceUpdateDelay) {
        revert PriceUpdateTooSoon(lastPrice.timestamp, minPriceUpdateDelay);
    }
}
HIGH severity: Use multi-sig wallet for contract admin and owner. **Description:** As part of the audit we were asked to investigate the on-chain state of the deployed contracts. Using Ethereum as the example:
* the primary deployer appears to be [0xA5Ab0683d4f4AD107766a9fc4dDd49B5a960e661](https://etherscan.io/address/0xA5Ab0683d4f4AD107766a9fc4dDd49B5a960e661)
*...

Vulnerable code:
cast code 0xA5Ab0683d4f4AD107766a9fc4dDd49B5a960e661 --rpc-url $ETH_RPC_URL
cast code 0xD47777Cf34305Dec4F1095F164792C1A4AFB327e --rpc-url $ETH_RPC_URL
0x
0x
HIGH severity: In Solidity don't initialize to default values. **Description:** In Solidity don't initialize to default values:

**Avant:**
Fixed in commit [0b590fa](https://github.com/Avant-Protocol/Avant-Contracts-Max/commit/0b590fa60fa75d73396b9bb48543b52396c204ca).

**Cyfrin:** Verified.

Vulnerable code:
RequestsManager.sol
72:    for (uint256 i = 0; i < _allowedTokenAddresses.length; i++) {
HIGH severity: Use named mapping parameters to make explicit the purpose of keys and values. **Description:** Named mapping parameters are already being used in almost all of the codebase, the one exception is:

**Avant:**
Fixed in commit [1cc43e3](https://github.com/Avant-Protocol/Avant-Contracts-Max/commit/1cc43e3c59baa16b5b529ad06fee637bd6131ec1).

**Cyfrin:** Verified.

Vulnerable code:
SimpleToken.sol
13:  mapping(bytes32 => bool) private mintIds;
14:  mapping(bytes32 => bool) private burnIds;
HIGH severity: Modifier `mintRequestExist` can be safely removed from `RequestsManager::cancelMint, cancelBurn` saving 1 identical storage read. **Description:** `RequestsManager::cancelMint` validates that `request.provider == msg.sender`:

Hence the modifier `mintRequestExist(_id)` can be safely removed to save 1 identical storage read of `mintRequests[_id].provider`.

The same applies to `RequestsManager::cancelBurn`.

**Avant:**
Fixed...

Vulnerable code:
L152:    _assertAddress(request.provider, msg.sender);
HIGH severity: Broken identity-to-wallet binding in redeem allows country restriction bypass. **Description:** The `redeem` instruction accepts three identity objects that should all refer to the same person and wallet: `IdentityRegistryAccount`, `IdentityAccount`, and `WalletIdentity`. The account constraints only ensure:
- `IdentityRegistryAccount` matches the `asset_mint`.
-...

Vulnerable code:
/// Identity registry for asset mint compliance
#[account(
    has_one = asset_mint,
    seeds = [asset_mint.key().as_ref()],
    seeds::program = ::identity_registry::ID,
    bump = identity_registry.bump,
)]
pub identity_registry: Box<Account<'info, IdentityRegistryAccount>>,

/// User's identity account with country information
#[account(
    has_one = identity_registry,
    seeds = [identity_registry.key().as_ref(), identity_account.owner.as_ref()],
    seeds::program = ::identity_registry::ID,
    bump
)]
pub identity_account: Box<Account<'info, IdentityAccount>>,

/// Links wallet to identity account
#[account(
    seeds = [redeemer.key().as_ref(), asset_mint.key().as_ref()],
    seeds::program = ::identity_registry::ID,
    bump,
)]
pub wallet_identity: Box<Account<'info, WalletIdentity>>,
MEDIUM severity: DoS in initialize via pre-created ATA for `off_ramp_authority`. **Description:** The `initialize` instruction creates the liquidity vault as an **associated token account** for the program PDA `off_ramp_authority`:

Associated token accounts are globally derivable and can be created by **anyone** for any owner without the owner’s signature. Because both...

Vulnerable code:
#[account(
    init,
    payer = admin,
    associated_token::mint = liquidity_token_mint,
    associated_token::authority = off_ramp_authority,
    associated_token::token_program = liquidity_token_program,
)]
pub liquidity_token_vault: Box<InterfaceAccount<'info, TokenAccount>>;

#[account(
    init_if_needed,
    payer = admin,
    associated_token::mint = liquidity_token_mint,
    associated_token::authority = off_ramp_authority,
    associated_token::token_program = liquidity_token_program,
)]
pub liquidity_token_vault: Box<InterfaceAccount<'info, TokenAccount>>;
MEDIUM severity: Permissionless OffRampState initialization under official program ID enables spoofed “official” instances. **Description:** The `initialize` instruction lets any signer create a new `OffRampState` and become its `admin`. The global `OffRampCounter` is `init_if_needed` and unguarded, and the new state PDA is derived from `[OFF_RAMP_STATE_SEED, off_ramp_counter.counter.to_le_bytes()]`. There is no...

Vulnerable code:
/// Global counter for generating unique off-ramp IDs
#[account(
    init_if_needed,
    payer = admin,
    space = 8 + OffRampCounter::INIT_SPACE,
    seeds = [OFF_RAMP_COUNTER_SEED],
    bump,
)]
pub off_ramp_counter: Box<Account<'info, OffRampCounter>>,

/// Off-ramp state containing configuration and settings
#[account(
    init,
    payer = admin,
    space = 8 + OffRampState::INIT_SPACE,
    seeds = [OFF_RAMP_STATE_SEED, off_ramp_counter.counter.to_le_bytes().as_ref()],
    bump,
)]
pub off_ramp_state: Box<Account<'info, OffRampState>>,
LOW severity: Silent truncation on u128 to u64 cast in liquidity token amount calculator. **Description:** The `utils::token_calculator::calculate_liquidity_token_amount` function computes the output in `u128` and returns it with a plain `as u64` cast:

If `result` fits in `u128` but exceeds `u64::MAX`, the cast truncates the high bits without error. The function then reports a much...

Vulnerable code:
use core::convert::TryFrom;

let result_u64 = u64::try_from(result)
    .map_err(|_| SecuritizeOffRampError::Overflow)?;
Ok(result_u64)
LOW severity: `Licredity::decreaseDebtShare` bypasses interest accrual. **Description:** A borrower can call [`Licredity::decreaseDebtShare`](https://github.com/Licredity/licredity-v1-core/blob/e8ae10a7d9f27529e39ca277bf56cef01a807817/src/Licredity.sol#L472-L536) to repay their loan. Because this call can only reduce debt, it’s treated as “safe” and allowed outside...

Vulnerable code:
function test_decraseDebtBalance_doesntAccrueInterest() public {
    uint256 positionId = licredityRouter.open();
    licredityRouter.depositFungible{value: 10e8}(positionId, Fungible.wrap(ChainInfo.NATIVE), 10e8);

    uint256 delta = 1e8 * 1e6;

    licredityRouterHelper.addDebt(positionId, delta, address(this));
    uint256 amountBorrowed = licredity.balanceOf(address(this));

    // should cause interest accrual
    oracleMock.setQuotePrice(2 ether);
    vm.warp(block.timestamp + 180 days);

    // decrease debt share call is done directly to licredity
    // avoiding the `unlock` context that accrues interest
    uint256 amountRepaid = licredity.decreaseDebtShare(positionId, delta, false);

    // no interest has been accrued
    assertEq(amountRepaid, amountBorrowed);
}
MEDIUM severity: Missing Chainlink oracle staleness check. **Description:** The `getPrice` function in `ChainlinkFeedLibrary` fetches the latest price from a Chainlink feed using `latestRoundData`, but does not check if the returned data is stale. Chainlink feeds can become stale if no new price updates have occurred for a significant period, which may...

Vulnerable code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import {ChainlinkFeedLibrary} from "./../src/libraries/ChainlinkFeedLibrary.sol";
import {AggregatorV3Interface} from "./../src/interfaces/external/AggregatorV3Interface.sol";

contract ChainlinkFeedLibraryTest is Test {
    address internal constant MOCK_FEED = address(0xFEED);

    function test_getPrice_acceptsStaleData() public {
        vm.warp(block.timestamp + 10 days); // Set the block timestamp to 10 day in the future

        // Mock the Chainlink feed to return a price but with stale timestamp (e.g., updated 1 day ago)
        // In a secure implementation, this should revert due to staleness, but here it does not
        vm.mockCall(
            MOCK_FEED,
            abi.encodeWithSelector(AggregatorV3Interface.latestRoundData.selector),
            abi.encode(
                uint80(1),                  // roundId
                int256(100e8),              // answer
                uint256(0),                 // startedAt (unused)
                block.timestamp - 1 days,   // updatedAt (stale)
                uint80(1)                   // answeredInRound
            )
        );

        // Call getPrice; it should return the stale price without reverting
        uint256 price = ChainlinkFeedLibrary.getPrice(AggregatorV3Interface(MOCK_FEED));

        // Assert that the stale price is returned
        assertEq(price, 100e8);
    }
}

(uint80 roundId, int256 answer,, uint256 updatedAt, uint80 answeredInRound) = feed.latestRoundData();
require(updatedAt >= block.timestamp - MAX_STALENESS, "Chainlink: stale price");
require(answeredInRound >= roundId, "Chainlink: incomplete round");
LOW severity: Deployment script requires unencrypted private key. **Description:** The deployment script [`deploy.sh`](https://github.com/Licredity/licredity-v1-core/blob/e8ae10a7d9f27529e39ca277bf56cef01a807817/deploy.sh#L29-L44) requires a private key to be stored in clear text inside the `.env` file:

Storing private keys in plain text represents an...

Vulnerable code:
# Load environment variables
source .env

...

if [ -z "$PRIVATE_KEY" ]; then
    echo "Error: PRIVATE_KEY not set in .env file"
    exit 1
fi

forge script script/Deploy.s.sol:DeployScript \
    --rpc-url "$RPC_URL" \
    --broadcast \
    --account deployerKey \
    --sender <address associated with deployerKey> \
    -vvv
HIGH severity: Reentrancy via `stagedFungible` not cleared before external call. **Description:** In [`Licredity::exchangeFungible`](https://github.com/Licredity/licredity-v1-core/blob/e8ae10a7d9f27529e39ca277bf56cef01a807817/src/Licredity.sol#L217-L222), the transient `stagedFungible` flag is cleared only after the external call `baseFungible.transfer(recipient, amountOut)`....

Vulnerable code:
(Fungible fungibleIn, uint256 amountIn) = _getStagedFungibleAndAmount();
    uint256 amountOut;

+   assembly ("memory-safe") {
+       // clear staged fungible
+       tstore(stagedFungible.slot, 0)
+   }

    // ...

    assembly ("memory-safe") {
        // clear staged fungible
-       tstore(stagedFungible.slot, 0)
LOW severity: Erroneous packing in `Licredity::_calculateLiquidityKey`. **Description.**
The contract creates a “liquidity key” (the ID used to track when an LP added liquidity) with custom bit-twiddling instead of the usual `keccak256(abi.encodePacked(provider, tickLower, tickUpper, salt))` in...

Vulnerable code:
assembly ("memory-safe") {
    // key = keccak256(abi.encodePacked(provider, tickLower, tickUpper, salt));
    mstore(0x00, or(or(shl(0x06, provider), shl(0x03, tickLower)), tickUpper))
    mstore(0x20, salt)
    key := keccak256(0x06, 0x3a)
}

function test_calculateLiquidityKey() public view {
    address ad = address(0x1234);
    int24 tL = -1;
    int24 tH = 1;
    bytes32 salt = "";
    bytes32 key1 = licredity._calculateLiquidityKey(ad,tL,tH,salt);
    bytes32 key2 = keccak256(abi.encodePacked(ad, tL, tH, salt));
    assertEq(key1, key2);
}

assembly ("memory-safe") {
    // key = keccak256(abi.encodePacked(provider, tickLower, tickUpper, salt));
    // Byte-aligned shifts and masks.
    let addr := and(provider, 0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff) // 20B
    let tl   := and(tickLower,  0xFFFFFF) // 3B two's complement
    let tu   := and(tickUpper,  0xFFFFFF) // 3B

    // [ 20B addr | 3B tl | 3B tu | (6B zero padding) ] in the 32B word,
    // so the slice from 0x06 of length 58 is exactly 20+3+3 + 32 salt bytes.
    let word := or(or(shl(48, addr), shl(24, tl)), tu) // 48 bits = 6 bytes, 24 bits = 3 bytes
    mstore(0x00, word)
    mstore(0x20, salt)
    key := keccak256(0x06, 0x3a)
}
HIGH severity: EIP-20 transfer functions accept zero address causing unintended token burns. **Description:** EIP-20 compliance revealed that transfer() and transferFrom() accept the zero address as a recipient, which triggers token burning and causes unexpected totalSupply changes.

**Impact:** EIP-20 best practices.

**Proof of Concept:** :x:Violated:...

Vulnerable code:
// EIP20-05: Verify transfer() reverts in invalid conditions
// EIP-20: "The function SHOULD throw if the message caller's account balance does not have enough tokens to spend."
rule eip20_transferMustRevert(env e, address to, uint256 amount) {

    setup(e);

    // Snapshot the 'from' balance
    mathint fromBalancePrev = ghostERC20Balances128[_Licredity][e.msg.sender];

    // Attempt transfer with revert path
    transfer@withrevert(e, to, amount);
    bool reverted = lastReverted;

    assert(e.msg.sender == 0 => reverted,
           "[SAFETY] Transfer from zero address must revert");

    assert(to == 0 => reverted,
           "[SAFETY] Transfer to zero address must revert");

    assert(fromBalancePrev < amount => reverted,
           "[EIP-20] Transfer must revert if sender has insufficient balance");
}

diff --git a/core/src/BaseERC20.sol b/core/src/BaseERC20.sol
index bed3880..5903415 100644
--- a/core/src/BaseERC20.sol
+++ b/core/src/BaseERC20.sol
@@ -56,6 +56,7 @@ abstract contract BaseERC20 is IERC20 {

     /// @inheritdoc IERC20
     function transfer(address to, uint256 amount) public returns (bool) {
+        require(to != address(0)); // @certora fix for eip20_transferMustRevert
         _transfer(msg.sender, to, amount);

         return true;
@@ -63,6 +64,7 @@ abstract contract BaseERC20 is IERC20 {

     /// @inheritdoc IERC20
     function transferFrom(address from, address to, uint256 amount) public returns (bool) {
+        require(to != address(0)); // @certora fix for eip20_transferFromMustRevert
         assembly ("memory-safe") {
             from := and(from, 0xffffffffffffffffffffffffffffffffffffffff)