Farseen0/scar-pairs huggingface dataset:
HIGH severity: Confusing timestamp naming in `ChainlinkOracle`. **Description:** In `ChainlinkOracle`, the timestamp variables are named in a confusing way. `currentTimeStamp` actually stores the last update time, and `lastUpdateTimeStamp` stores the previous update time....

Vulnerable code:
// if timestamp has changed, update cache
if (block.timestamp != currentTimeStamp) {
    lastPriceX96 = currentPriceX96;
    lastUpdateTimeStamp = currentTimeStamp;
    currentTimeStamp = block.timestamp;
}
LOW severity: Dirty bits in `NonFungible` mid-field can break comparisons. **Description:** [`NonFungible`](https://github.com/Licredity/licredity-v1-core/blob/e8ae10a7d9f27529e39ca277bf56cef01a807817/src/types/NonFungible.sol#L8-L16) is packed as `bytes32` with the layout `160 bits address | 32 bits empty | 64 bits tokenId`. Equality is implemented as a raw `bytes32`...

Vulnerable code:
/// @dev 160 bits token address | 32 bits empty | 64 bits token ID
type NonFungible is bytes32;

...

function equals(NonFungible self, NonFungible other) pure returns (bool) {
    return NonFungible.unwrap(self) == NonFungible.unwrap(other);
}

// 160 bits addr | 96 bits id
  function pack(address token, uint256 id) pure returns (NonFungible nf) {
      uint256 w = (uint256(uint160(token)) << 96) | (id & ((1 << 96) - 1));
      return NonFungible.wrap(bytes32(w));
  }

uint256 constant MASK_ADDR  = uint256(type(uint160).max) << 96;
  uint256 constant MASK_ID64  = uint256(type(uint64).max);
  uint256 constant MASK_CANON = MASK_ADDR | MASK_ID64;

  function pack(address token, uint256 id) pure returns (NonFungible nf) {
      uint256 w = (uint256(uint160(token)) << 96) | (id & MASK_ID64); // gap zeroed
      return NonFungible.wrap(bytes32(w));
  }

  function equals(NonFungible a, NonFungible b) pure returns (bool) {
      return (uint256(NonFungible.unwrap(a)) & MASK_CANON)
           == (uint256(NonFungible.unwrap(b)) & MASK_CANON);
  }
HIGH severity: Fungible array can contain zero-balance entries. **Description:** The `addFungible` function allows adding fungibles with zero amounts, while `removeFungible` correctly removes fungibles when balance reaches zero. This creates an inconsistency where the fungibles array can legitimately contain entries with zero balance in...

Vulnerable code:
// VS-LI-08: Fungibles in array must have corresponding fungibleStates with non-zero balance
// For every fungible in the fungibles array, there must be a corresponding fungibleStates entry with non-zero balance
invariant fungiblesHaveNonZeroBalance(env e)
    forall uint256 positionId. forall uint8 i.
        i < ghostLiPositionFungiblesLength[positionId]
            => ghostLiPositionFungibleStatesBalance112[positionId][ghostLiPositionFungibles[positionId][i]] != 0
filtered { f -> !EXCLUDED_FUNCTION(f) } { preserved with (env eFunc) { SETUP(e, eFunc); } }

diff --git a/core/src/types/Position.sol b/core/src/types/Position.sol
index 929f27e..d346262 100644
--- a/core/src/types/Position.sol
+++ b/core/src/types/Position.sol
@@ -44,6 +44,9 @@ library PositionLibrary {
         FungibleState state = self.fungibleStates[fungible];

         if (state.index() == 0) {
+
+            require(amount != 0); // @certora fix for fungiblesHaveNonZeroBalance
+
             // add a fungible to the fungibles array
             assembly ("memory-safe") {
                 let slot := add(self.slot, FUNGIBLES_OFFSET)
HIGH severity: `TotalBalanceEnforcer` validation bypass when mixed with state modifying enforcers. **Description:** Total Balance Change Enforcers fail to validate the final balance state when mixed with state-modifying enforcers (such as `NativeTokenPaymentEnforcer`) in delegation chains.

The early return bypass in `afterAllHook` causes total balance enforcers positioned after state-modifying.....

Vulnerable code:
function afterAllHook(...) public override {
    // ... setup code ...

    BalanceTracker memory balanceTracker_ = balanceTracker[hashKey_];

    // @audit this returns early
    if (balanceTracker_.expectedIncrease == 0 && balanceTracker_.expectedDecrease == 0) return;

    // ... validation logic that gets bypassed ...

    delete balanceTracker[hashKey_]; // Cleanup after validation
}

Alice: `TotalBalanceChangeEnforcer` (validates net effect)
Bob: `NativeTokenPaymentEnforcer` (modifies balance during afterAllHook)
Dave: `TotalBalanceChangeEnforcer` (should validate final state)

// NativeTokenPaymentEnforcer.sol
function afterAllHook(...) public override {
    // ... setup ...

    uint256 balanceBefore_ = recipient_.balance;

    // @audit Transfers ETH during afterAll ook
    delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_);

    uint256 balanceAfter_ = recipient_.balance;
    require(balanceAfter_ >= balanceBefore_ + amount_, "NativeTokenPaymentEnforcer:payment-not-received");
}
HIGH severity: Overly restrictive balance consistency check in `TotalBalanceChange` class of enforcers can cause potential DoS. **Description:** `TotalBalanceChange` class of enforcers contain a defensive check in `beforeAll` hook that can cause DoS attacks in cross-execution batch scenarios and prevents legitimate balance-modifying enforcers from being used.

The issue stems from overly restrictive validation that assumes.....

Vulnerable code:
function beforeAllHook(...) public override {
    // ... setup code ...

    uint256 currentBalance_ = IERC20(token_).balanceOf(recipient_);
    if (balanceTracker_.expectedDecrease == 0 && balanceTracker_.expectedIncrease == 0) {
        balanceTracker_.balanceBefore = currentBalance_;
        emit TrackedBalance(msg.sender, recipient_, token_, currentBalance_);
    } else {
        // @audit Overly restrictive check can prevent legitimate enforcers
        require(balanceTracker_.balanceBefore == currentBalance_, "ERC20TotalBalanceChangeEnforcer:balance-changed");
    }
    // ...
}

// Execution 1: Bob uses TotalBalanceChangeEnforcer for Alice
// Execution 2: Dave uses PrepaymentEnforcer + TotalBalanceChangeEnforcer for Alice

// Attack flow:
// 1. Exec1 TotalBalanceChangeEnforcer.beforeAllHook: Sets balanceBefore = Alice's current balance
// 2. Exec2 PrepaymentEnforcer.beforeAllHook: Collects 1 ETH from Alice → Alice balance changes
// 3. Exec2 TotalBalanceChangeEnforcer.beforeAllHook: require(old_balance == new_balance) → REVERT
// 4. Entire batch transaction fails

function beforeAllHook(...) public override {
    // ... setup code ...

    uint256 currentBalance_ = IERC20(token_).balanceOf(recipient_);

    if (balanceTracker_.expectedDecrease == 0 && balanceTracker_.expectedIncrease == 0) {
        balanceTracker_.balanceBefore = currentBalance_;
        emit TrackedBalance(msg.sender, recipient_, token_, currentBalance_);
    } else {
        // @audit Update baseline to current balance instead of requiring equality
        balanceTracker_.balanceBefore = currentBalance_;
    }
    // ...
}
MEDIUM severity: Incomplete validation in `OpNetVaultAutoDeploy` allows invalid burner configuration for registering operators with auto vault deployments. **Description:** `OpNetVaultAutoDeploy._validateConfig()` is missing burner address validation that could lead to deployment failures. The function fails to validate a configuration scenario where `isBurnerHook` is enabled with burner as `address(0)`.

This validation gap allows administrators to...

Vulnerable code:
//OpNetVaultAutoDeployLogic

 function _validateConfig(
        IOpNetVaultAutoDeploy.AutoDeployConfig memory config
    ) public view {
        if (config.collateral == address(0)) {
            revert IOpNetVaultAutoDeploy.OpNetVaultAutoDeploy_InvalidCollateral();
        }
        if (config.epochDuration == 0) {
            revert IOpNetVaultAutoDeploy.OpNetVaultAutoDeploy_InvalidEpochDuration();
        }
        uint48 slashingWindow = IVotingPowerProvider(address(this)).getSlashingWindow();
        if (config.epochDuration < slashingWindow) {
            revert IOpNetVaultAutoDeploy.OpNetVaultAutoDeploy_InvalidEpochDuration();
        }
        if (!config.withSlasher && slashingWindow > 0) {
            revert IOpNetVaultAutoDeploy.OpNetVaultAutoDeploy_InvalidWithSlasher();
        }
        if (!config.withSlasher && config.isBurnerHook) {
            revert IOpNetVaultAutoDeploy.OpNetVaultAutoDeploy_InvalidBurnerHook();
        } //@audit missing checks on burner address
    }

//BaseSlasher
  function _initialize(
        bytes calldata data
    ) internal override {
        (address vault_, bytes memory data_) = abi.decode(data, (address, bytes));

        if (!IRegistry(VAULT_FACTORY).isEntity(vault_)) {
            revert NotVault();
        }

        __ReentrancyGuard_init();

        vault = vault_;

        BaseParams memory baseParams = __initialize(vault_, data_);

        if (IVault(vault_).burner() == address(0) && baseParams.isBurnerHook) {
            revert NoBurner();
        } //@audit if no burner, vault deployment reverts

        isBurnerHook = baseParams.isBurnerHook;
    }

AutoDeployConfig memory maliciousConfig = AutoDeployConfig({
    epochDuration: 1000,
    collateral: validToken,
    burner: address(0),     //  Zero address burner
    withSlasher: true,      //  Slasher enabled
    isBurnerHook: true      //  Hook enabled but no burner!
});

// @audit This passes validation incorrectly
deployer.setAutoDeployConfig(maliciousConfig);
LOW severity: Missing bounds check on weight values in `WeightedTokensVPCalc` and `WeightedVaultsVPCalc`. **Description:** The `WeightedTokensVPCalc` and `WeightedVaultsVPCalc` contracts lack proper bounds checking when setting weight values, which could lead to integer overflow during voting power calculations or complete elimination of voting power through zero weights.

The weight-setting functions.....

Vulnerable code:
// WeightedTokensVPCalc.sol
function setTokenWeight(address token, uint208 weight) public virtual checkPermission {
    _setTokenWeight(token, weight);  // No bounds checking
}

// WeightedVaultsVPCalc.sol
function setVaultWeight(address vault, uint208 weight) public virtual checkPermission {
    _setVaultWeight(vault, weight);  // No bounds checking
}

// WeightedTokensVPCalc.sol
function stakeToVotingPower(address vault, uint256 stake, bytes memory extraData)
    public view virtual override returns (uint256) {
    return super.stakeToVotingPower(vault, stake, extraData) * getTokenWeight(_getCollateral(vault)); //@audit could go to 0 or overflow based on weight set
 }

contract WeightedTokensVPCalc is NormalizedTokenDecimalsVPCalc, PermissionManager {
    uint208 public constant MIN_WEIGHT = 1e6;     // Minimum non-zero weight
    uint208 public constant MAX_WEIGHT = 1e18;    // Maximum reasonable weight

    function setTokenWeight(address token, uint208 weight) public virtual checkPermission {
        require(weight <= MAX_WEIGHT, "Weight exceeds maximum");
        require(weight <= MAX_SAFE_WEIGHT, "Weight risks overflow");

        _setTokenWeight(token, weight);
    }
}
LOW severity: `unwhitelistOperator` allows state changes when whitelist Is disabled, causing inconsistent operator state. **Description:** The `unwhitelistOperator` function performs state changes (removing an operator from the whitelist and potentially unregistering them) even when the whitelist feature is **disabled**. This violates the expected behavior that **whitelist enforcement should only be active when...

Vulnerable code:
function unwhitelistOperator(address operator) public virtual checkPermission {
    if (!isWhitelistEnabled()) {
        revert OperatorsWhitelist_WhitelistDisabled( );
    }

    _getOperatorsWhitelistStorage()._whitelisted[operator] = false;

    if (isOperatorRegistered(operator)) {
        _unregisterOperator(operator);
    }

    emit UnwhitelistOperator(operator);
}
MEDIUM severity: Incorrect haircut asset value conversion in `STBL_PT1_Issuer::generateMetaData`. **Description:** `STBL_PT1_Issuer::generateMetaData` (and its `STBL_LT1_Issuer` counterpart) uses the wrong oracle conversion function when calculating `haircutAmountAssetValue`, resulting in mathematically incorrect values being stored in NFT metadata.  The function uses `fetchForwardPrice()`...

Vulnerable code:
function generateMetaData(uint256 assetValue) internal view returns (YLD_Metadata memory MetaData) {
    // ... other calculations ...

    // @audit Step 1: Convert asset to USD using fetchForwardPRice
    MetaData.stableValueGross = iSTBL_PT1_AssetOracle(AssetData.oracle)
        .fetchForwardPrice(MetaData.assetValue);

    // @audit Step 2: Calculate haircut in USD terms
    MetaData = MetaData.calculateDepositFees(); // Sets haircutAmount in USD

    // @audit BUG - Wrong conversion function used
    MetaData.haircutAmountAssetValue = iSTBL_PT1_AssetOracle(AssetData.oracle)
        .fetchForwardPrice(MetaData.haircutAmount);  // @audit inverse of price needs to be used
        //                 ^^^^^^^^^^^^^^^^^^^^
        // @audit This expects ASSET AMOUNT but receives USD AMOUNT
}

Step 1: stableValueGross = fetchForwardPrice(100e18)
       = (110000000 * 100e18) / 1e8 = 110e18 USD

Step 2: haircutAmount = (110e18 * 500) / 10000 = 5.5e18 USD

Step 3 (WRONG): haircutAmountAssetValue = fetchForwardPrice(5.5e18)
       = (110000000 * 5.5e18) / 1e8 = 6.05e18

Step 3 (CORRECT): haircutAmountAssetValue = fetchInversePrice(5.5e18)
       = (5.5e18 * 1e8) / 110000000 = 5e18
MEDIUM severity: High centralization risk in `STBL_USST::bridgeBurn`. **Description:** Current implementation of `STBL_USST::bridgeBurn` allows `BRIDGE_ROLE` to burn tokens from any arbitrary address without user approval. This creates unnecessary centralization risk when a safer approach is already demonstrated in the protocol's own `STBL_Token` contract.

The...

Vulnerable code:
// STBL_USST.solfunction bridgeBurn(
    address _from,      // @audit can be any address
    uint256 _amt,
    bytes memory _data
) external whenNotPaused onlyRole(BRIDGE_ROLE) {
    _burn(_from, _amt);  // @audit Burns from arbitrary address without consent
    emit BridgeBurn(_from, _amt, _data);
}

// STBL_Token.sol
function bridgeBurn(uint256 _amt) external whenNotPaused onlyRole(BRIDGE_ROLE) {
    _burn(_msgSender(), _amt);  // @audit Only burns caller's (bridge's) own tokens
}
LOW severity: Insufficient fee validation in `STBL_Register::setupAsset` can cause underflow. **Description:** The `STBL_Register::setupAsset` and `STBL_Register::setFees` functions validate individual fees but ignore their cumulative impact. Consider the following logic in `STBL_MetadataLib.calculateDepositFees` where the `depositfeeAmount`, `haircutAmount` and `insurancefeeAmount` are...

Vulnerable code:
function calculateDepositFees(YLD_Metadata memory data) internal pure returns (YLD_Metadata memory) {
    // All deposit-time fees calculated on same base (stableValueGross)
    data.depositfeeAmount = (data.stableValueGross * data.Fees.depositFee) / 10000;
    data.haircutAmount = (data.stableValueGross * data.Fees.hairCut) / 10000;
    data.insurancefeeAmount = (data.stableValueGross * data.Fees.insuranceFee) / 10000;
    data.withdrawfeeAmount = (data.stableValueGross * data.Fees.withdrawFee) / 10000;
    return data; //@audit depositfeeAmount, haircutAmount and insurancefeeAmount are calculated on stableValueGross
}

MetaData.stableValueNet = (MetaData.stableValueGross -
            (MetaData.depositfeeAmount +
                MetaData.haircutAmount +
                MetaData.insurancefeeAmount));
LOW severity: Insufficient duration validation in `STBL_Register::setupAsset` can lock user withdrawals. **Description:** Lack of duration validation in `STBL_Register::setupAsset` and `STBL_Register::setDurations` allows asset configurations where `yieldDuration > duration`, creating mathematically impossible withdrawal conditions that  lock user funds.

The withdrawal logic requires users to wait...

Vulnerable code:
function iWithdraw(uint256 _tokenID, address _sender) internal isSetupDone {
    YLD_Metadata memory MetaData = iSTBL_YLD(registry.fetchYLDToken()).getNFTData(_tokenID);
    // code..

    //ensures that users can't withdraw after duration has passed
        if ((MetaData.depositBlock + MetaData.Fees.duration) < block.timestamp)
            revert STBL_Asset_WithdrawDurationNotReached(assetID, _tokenID);

        //ensures that users must wait for yield duration to withdraw assets
        if (
            (MetaData.depositBlock + MetaData.Fees.yieldDuration) >
            block.timestamp
        ) revert STBL_Asset_YieldDurationNotReached(assetID, _tokenID);

    // @audit withdrawal proceeds only if BOTH conditions are false...
   //@audit when yieldDuration > duration, no withdrawal window exists for users
}
LOW severity: Disabling an asset via `STBL_Register::disableAsset` with active deposits can DOS user withdrawals. **Description:** The `STBL_Register::disableAsset` does not check for active deposits before disabling an asset, creating a  Denial of Service (DOS) condition where users cannot withdraw their deposited funds.

`STBL_Register::disableAsset` only checks if the asset is currently enabled but ignores.....

Vulnerable code:
// STBL_Register.sol - disableAsset()
function disableAsset(uint256 _id) external onlyRole(REGISTER_ROLE) {
    if (assetData[_id].status != AssetStatus.ENABLED)
        revert STBL_AssetNotActive();
    assetData[_id].status = AssetStatus.DISABLED;  // @audit does not check for active deposits
    emit AssetStateUpdateEvent(_id, true);
}

// STBL_Core.sol
modifier isValidIssuer(uint256 _assetID) {
    AssetDefinition memory AssetData = registry.fetchAssetData(_assetID);
    if (!AssetData.isIssuer(_msgSender())) revert STBL_UnauthorizedIssuer();
    if (!AssetData.isActive()) revert STBL_AssetDisabled(_assetID);  // @audit blocks exit on disabled assets
    _;
}

function exit(uint256 _assetID, address _from, uint256 _tokenID, uint256 _value)
    external isValidIssuer(_assetID) {  // @audit Modifier prevents exit on disabled assets
    // ... burn tokens and decrement deposits
}
LOW severity: `STBL_Register::addAsset` does not check for non-empty asset name. **Description:** `STBL_Register::setupAsset` allows an assets to be created with empty names. However, elsewhere in the code, the `STBL_AssetDefinitionLib::isValid` function marks an an asset with empty name field as invalid.  This creates an inconsistency where assets can be successfully created...

Vulnerable code:
// STBL_Register.sol - addAsset function
function addAsset(
    string memory _name,    // @audit No validation on name field
    string memory _desc,
    uint8 _type,
    bool _aggType
) external onlyRole(REGISTER_ROLE) returns (uint256) {
    unchecked {
        assetCtr += 1;
    }
    assetData[assetCtr].id = assetCtr;
    assetData[assetCtr].name = _name;        // @audit Can be empty string
    assetData[assetCtr].description = _desc;
    assetData[assetCtr].contractType = _type;
    assetData[assetCtr].isAggreagated = _aggType;
    assetData[assetCtr].status = AssetStatus.INITIALIZED;

    emit AddAssetEvent(assetCtr, assetData[assetCtr]);
    return assetCtr;
}

// STBL_AssetDefinitionLib.sol
function isValid(AssetDefinition memory asset) internal pure returns (bool) {
    return
        asset.id != 0 &&
        bytes(asset.name).length > 0 &&  // ✅ Requires non-empty name
        asset.token != address(0) &&
        asset.issuer != address(0) &&
        asset.rewardDistributor != address(0) &&
        asset.vault != address(0);
}
LOW severity: Race condition at duration expiration boundary during withdrawals. **Description:** The `STBL_LT1_Issuer` contract contains inconsistent boundary condition logic in the withdrawal validation checks that creates a race condition when `block.timestamp == depositBlock + duration`.

In the `iWithdraw()` function (user withdrawal):

In the `withdrawExpired()` function.....

Vulnerable code:
if ((MetaData.depositBlock + MetaData.Fees.duration) < block.timestamp)
    revert STBL_Asset_WithdrawDurationNotReached(assetID, _tokenID);

if ((MetaData.depositBlock + MetaData.Fees.duration) > block.timestamp)
    revert STBL_Asset_WithdrawDurationNotReached(assetID, _tokenID);

// withdrawExpired()
if ((MetaData.depositBlock + MetaData.Fees.duration) >= block.timestamp) //@audit modify > to >=
    revert STBL_Asset_WithdrawDurationNotReached(assetID, _tokenID);
LOW severity: Treasury cannot withdraw expired assets if NFT is disabled. **Description:** The `STBL_LT1_Issuer::withdrawExpired` function attempts to claim rewards for disabled NFTs, but the `STBL_LT1_YieldDistributor::claim` function explicitly reverts when called on disabled NFTs.

In `STBL_LT1_Issuer.withdrawExpired`:

In...

Vulnerable code:
// If NFT is disabled then claim for yield is not done
if (MetaData.isDisabled) {
    iSTBL_LT1_AssetYieldDistributor(AssetData.rewardDistributor).claim(_tokenID);
}

if (MetaData.isDisabled) revert STBL_YLDDisabled(id);

function claim(uint256 id) external returns (uint256) {
  // current logic

   // @audit Allow issuer to claim for disabled NFTs, but block regular users
    if (MetaData.isDisabled && msg.sender != AssetData.issuer) {
        revert STBL_YLDDisabled(id);
    }
}
HIGH severity: Misleading `ContractType` field suggests that assets can be non ERtokens. **Description:** The `AssetDefinition` struct includes a `contractType` field that suggests support for ERC20, ERC721, and Custom asset types, but the entire codebase implicitly assumes all assets are ERC20 tokens. This creates misleading documentation and potential integration issues.

The...

Vulnerable code:
// STBL_Structs.sol
struct AssetDefinition {
    // ...
    /** @notice Type of contract (ERC20 = 0, ERC721 = 1, Custom = 2) */
    uint8 contractType;
    // ...
}
HIGH severity: Unnecessary usage of `_msgSender()` to validate if caller is the `Issuer` on the `STBL_PT1_YieldDistributor`. **Description:** On the `STBL_PT1_YieldDistributor` contract, the functions `enableStaking()` and `disableStaking()` use the modifier `isIssuer()` to validate if the caller is the authorized issuer of the `assetId` configured for the YieldDistributor.

`STBL_PT1_YieldDistributor.isIssuer()` calls...

Vulnerable code:
modifier isIssuer() {
        AssetDefinition memory AssetData = registry.fetchAssetData(assetID);
@>      if (!AssetData.isIssuer(_msgSender()))
            revert STBL_Asset_InvalidIssuer(assetID);
        _;
    }


    function _msgSender()
        internal
        view
        override(ERC2771ContextUpgradeable)
        returns (address)
    {
@>      return ERC2771ContextUpgradeable._msgSender();
    }

function distributeReward(uint256 reward) external {
        ...
@>      if (!AssetData.isVault(_msgSender()))
            revert STBL_Asset_InvalidVault(assetID);
        ...
        IERC20(AssetData.token).safeTransferFrom(
@>          _msgSender(),
            address(this),
            reward
        );
        ...
    }
MEDIUM severity: Unlimited token reallocation power creates centralization risk. **Description:** The `WorldLibertyFinancialV::ownerReallocateFrom` function provides the owner with unrestricted power to burn tokens from any address and mint them to any other address, completely bypassing all security mechanisms implemented throughout the contract.

While this may be intended...

Vulnerable code:
//WorldLibertyFinancialV2.sol
function ownerReallocateFrom(
    address _from,
    address _to,
    uint256 _value
) public onlyOwner {
    _burn(_from, _value);  // No approval, no checks
    _mint(_to, _value);    // No restrictions
}