Farseen0/scar-pairs huggingface dataset:
LOW severity: Incorrect Amount Logged in `PerpWithdrawReport`. **Description:** The `perp_withdraw` function logs the requested amount (`data.amount`) instead of the actual withdrawn amount (`amount`) in the `PerpWithdrawReport` event. This creates a discrepancy between the logged amount and the actual funds transferred, particularly when `data.amount == 0`...

Vulnerable code:
let amount = if margin_call {
    let margin_call_funds =
        funds.min(engine.get_avail_funds(client_state.temp_client_id, true)?);
    if margin_call_funds <= 0 {
        bail!(ImpossibleToWithdrawFundsDuringMarginCall);
    }
    if data.amount == 0 {
        margin_call_funds  // Actual amount may differ from data.amount
    } else {
        // ... validation logic ...
        data.amount
    }
} else if data.amount == 0 {
    funds  // Actual amount may differ from data.amount
} else {
    // ... validation logic ...
    data.amount
};

client_state.add_crncy_tokens(amount)?;
client_state
    .perp_info()?
    .sub_funds(amount)
    .map_err(|err| drv_err!(err))?;

solana_program::log::sol_log_data(&[bytemuck::bytes_of::<PerpWithdrawReport>(
    &PerpWithdrawReport {
        tag: log_type::PERP_WITHDRAW,
        client_id: client_state.id,
        instr_id: data.instr_id,
        amount: data.amount,  // ❌ Should be: amount
        time: ctx.time,
        ..PerpWithdrawReport::zeroed()
    },
)]);
LOW severity: Silent Failure in `voting_reset`. **Description:** The `voting_reset` function silently ignores failures when attempting to upgrade the community account header for writing. If the `community_acc` account is not marked as writable, the `upgrade()` call fails, but the function still returns `Ok(())`, giving the caller a false...

Vulnerable code:
if let Ok(header) = community_state.header.upgrade() {
        header.spot_fee_rate = START_SPOT_FEE_RATE;
        header.perp_fee_rate = START_PERP_FEE_RATE;
        header.max_discount = START_MAX_DISCOUNT;
        header.margin_call_penalty_rate = START_MARGIN_CALL_PENALTY_RATE;
        header.fees_prepayment_for_max_discount = START_FEES_PREPAYMENT_FOR_MAX_DISCOUNT;
        header.spot_pool_ratio = START_SPOT_POOL_RATIO;
    }

if let Some(ref mut header) = client_state.header {
        header.upgrade()?.points = 0;
        header.upgrade()?.mask &= 0xFFFFFFFFFFFFFF;
    }

if community_state.header.voting_counter == 0 {
            community_state.header.upgrade()?.voting_counter += 1;
        }
HIGH severity: wrong error emitted from `spot-order-cancel`. **Description:** Inside [spot-order-cancel](https://github.com/deriverse/protocol-v1/blob/30b06d2da69e956c000120cdc15907b5f33088d7/src/program/processor/spot_order_cancel.rs#L51), wrong error is emitted when accounts length is more than expected length, which is wrong and inconsistent with rest of.....

Vulnerable code:
if accounts.len() < SpotOrderCancelInstruction::MIN_ACCOUNTS {
        //@audit wrong, should have been InvalidAccountsNumber
        bail!(InvalidDataLength {
            expected: SpotOrderCancelInstruction::MIN_ACCOUNTS,
            actual: accounts.len(),
        });
    }

bail!(InvalidAccountsNumber {
            expected: SpotMassCancelInstruction::MIN_ACCOUNTS,
            actual: accounts.len(),
        });
HIGH severity: `ERAdapter::maxMint` reverts for uncapped target vaults. **Description:** `ERC4626Adapter::maxMint` forwards `TARGET_VAULT.maxDeposit(address(this))` into `_convertToShares`:

The [EIP-4626 standard for `maxMint`/`maxDeposit`](https://eips.ethereum.org/EIPS/eip-4626#maxdeposit) states that:
> MUST return `2 ** 256 - 1` if there is no limit on the...

Vulnerable code:
function maxMint(address /* user */ ) public view override returns (uint256) {
    if (paused() || emergencyMode) return 0;
    uint256 maxAssets = TARGET_VAULT.maxDeposit(address(this));
    return _convertToShares(maxAssets, Math.Rounding.Floor);
}

function test_MaxMintReverts() public {
    vm.expectRevert(stdError.arithmeticError);
    vault.maxMint(alice);
}

function maxMint(address /* user */ ) public view override returns (uint256) {
    if (paused() || emergencyMode) return 0;

    uint256 maxAssets = TARGET_VAULT.maxDeposit(address(this));
    if (maxAssets == type(uint256).max) {
        // Underlying vault is effectively uncapped: propagate this instead of converting
        return type(uint256).max;
    }

    return _convertToShares(maxAssets, Math.Rounding.Floor);
}
HIGH severity: `ERAdapter::maxMint` doesn't consider pending fees to be harvested which leads to under-calculating the real shares that can be minted. **Description:** The ERC4626Adapter::maxMint function computes the maximum quantity of shares that the Vault may mint by converting the maximum depositable assets in the underlying TARGET_VAULT into corresponding vault shares. However, this conversion process does not account for any pending...

Vulnerable code:
function test_PoC_MaxMint_DoesNotConsiderPendingYield() public {
        uint256 yield = 50_00e6;
        targetVault.setLiquidityCap(500_000e6);

        _seedVaults(yield);

        //@audit-info => Vault has pending yield

        uint256 snapshot = vm.snapshot();
            uint256 maxDeposit = vault.maxDeposit(alice);
            vm.prank(alice);
            vault.deposit(maxDeposit, alice);
            assertEq(vault.maxDeposit(alice), 0);
            assertEq(vault.maxMint(alice), 0);
        vm.revertTo(snapshot);

        //@audit-info => Given that Vault has pending yield, maxMint() is not accurate and will mint less shares than the actual maxMint post harvesting fees
        uint256 maxMintShares = vault.maxMint(alice);
        vm.prank(alice);
        vault.mint(maxMintShares, alice);
        assertGt(vault.maxDeposit(alice), 0);
        assertGt(vault.maxMint(alice), 0);

        //@audit-info => After attempting to mint the maxShares reported by the vault (and fees have been harvested during the mint), a second mint is possible when it shouldn't be because the previous mint was supposed to mint the max
        maxMintShares = vault.maxMint(alice);
        vm.prank(alice);
        vault.mint(maxMintShares, alice);
        assertEq(vault.maxDeposit(alice), 0);
        assertEq(vault.maxMint(alice), 0);
    }

    function _seedVaults(uint256 yield) internal {
        vm.prank(alice);
        vault.deposit(100_000e6, alice);

        vm.startPrank(bob);
        usdc.approve(address(targetVault), 100_000e6);
        targetVault.deposit(100_000e6, bob);
        vault.deposit(100_000e6, alice);
        vm.stopPrank();

        // mint yield to targetVault
        usdc.mint(address(targetVault), yield);
    }
HIGH severity: Use named mappings to explicitly denote the purpose of keys and values. **Description:** Use named mappings to explicitly denote the purpose of keys and values:

**Lido:** Fixed in commit [4898c26](https://github.com/lidofinance/defi-interface/commit/4898c26cd0abc8426ad9e2220a8d7cac487ab9b8).

**Cyfrin:** Verified.

Vulnerable code:
RewardDistributor.sol
52:    mapping(address => bool) private recipientExists;
HIGH severity: In Solidity don't initialize to default values. **Description:** In Solidity don't initialize to default values:

**Lido:** Fixed in commit [4898c26](https://github.com/lidofinance/defi-interface/commit/4898c26cd0abc8426ad9e2220a8d7cac487ab9b8) for `totalBps`.

**Cyfrin:** Verified.

Vulnerable code:
RewardDistributor.sol
143:        uint256 totalBps = 0;
145:        for (uint256 i = 0; i < recipients_.length; i++) {
230:        for (uint256 i = 0; i < recipientsLength; i++) {
HIGH severity: `Vault::decimals` does not reflect correct decimals when `OFFSET` is used. **Description:** [`Vault::decimals`](https://github.com/lidofinance/defi-interface/blob/99fd2b2c64c345a3c14b023dca4cb6393ffce5aa/src/Vault.sol#L543-L545) simply returns the asset decimals:

This is different to the OpenZeppelin [ERC4626...

Vulnerable code:
function decimals() public view virtual override(ERC20, ERC4626) returns (uint8) {
    return IERC20Metadata(asset()).decimals();
}

function decimals() public view virtual override(IERC20Metadata, ERC20) returns (uint8) {
    return _underlyingDecimals + _decimalsOffset();
}

- return IERC20Metadata(asset()).decimals();
+ return IERC20Metadata(asset()).decimals(); + OFFSET;
HIGH severity: Not including `_decimalsOffset` when calculating the fee shares. **Description:** Formulas converting assets to shares across the codebase utilize the virtual supply to prevent rate manipulation. However, when calculating the number of shares to be minted for accrued fees, the conversion formula does not call `_calculateFeeShares()` and omits...

Vulnerable code:
function test_PoC_InflateRatioViaFees() public {
        vm.prank(alice);
        uint256 alice_receivedShares = vault.deposit(1, alice);

        emit log_named_uint("totalAssets", vault.totalAssets());
        emit log_named_uint("totalSupply", vault.totalSupply());

        emit log_named_uint("assets per wei of share", vault.convertToAssets(alice_receivedShares));

        usdc.mint(address(targetVault), 1_000e18); //
        vault.harvestFees();

        emit log_named_uint("totalSupply", vault.totalSupply());
        emit log_named_uint("assets per wei of share", vault.convertToAssets(alice_receivedShares));

        vm.prank(bob);
        uint256 bob_receivedShares = vault.deposit(100e18, bob);

        uint256 bobAssetsBeforeRedeem = usdc.balanceOf(bob);

        vm.prank(bob);
        vault.redeem(bob_receivedShares, bob, bob);

        uint256 bobAssetsAfterRedeem = usdc.balanceOf(bob);

        emit log_named_uint("bob assets withdrawn", bobAssetsAfterRedeem
         - bobAssetsBeforeRedeem);
        assertTrue(bobAssetsAfterRedeem - bobAssetsBeforeRedeem > 99e18);

        // @audit //
        // With current formula, bob withdraws: 99999880924647933225 [9.999e19])

        // With formula using _decimalsOffset(): val: 99999803787934739453 [9.999e19])

        // @audit => Difference is neglegible for the required amount to donate to inflate the ratio //

    }
HIGH severity: Use `ReentrancyGuardTransient` for faster `nonReentrant` modifiers. **Description:** Use [ReentrancyGuardTransient](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/ReentrancyGuardTransient.sol) for faster `nonReentrant` modifiers:

**Lido:** Fixed in commit...

Vulnerable code:
Vault.sol
10:import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
32:abstract contract Vault is ERC4626, ERC20Permit, AccessControl, ReentrancyGuard, Pausable {
MEDIUM severity: Incorrect use of `investorExists` modifier in `PublicStockOnRamp::swap`. **Description:** `PublicStockOnRamp::swap` functions incorrectly validates investor registration. These functions use the `investorExists` modifier which checks if `msg.sender` is a registered wallet, but they also have the `onlyRole(OPERATOR_ROLE)` modifier, meaning `msg.sender` is always the...

Vulnerable code:
function swap(
        uint256 _liquidityAmount,
        uint256 _minOutAmount,
        address _investorWallet,
        bytes memory _investorSignature,
        uint8 _marketStatus,
        uint256 _anchorPrice,
        uint256 _anchorPriceExpiresAt
    )
        public
        whenNotPaused
        investorExists <--------
        initializedNavProvider
        validateMinSubscriptionAmount(_liquidityAmount)
        nonZeroAnchorPrice(_anchorPrice)
        onlyRole(OPERATOR_ROLE)
    {...}

modifier investorExists() {
        IDSRegistryService registryService = IDSRegistryService(dsToken.getDSService(dsToken.REGISTRY_SERVICE()));
        if (!registryService.isWallet(_msgSender())) {
            revert InvestorNotRegisteredError();
        }
        _;
    }

modifier investorWalletExists(address _wallet) {
    IDSRegistryService registryService = IDSRegistryService(
        dsToken.getDSService(dsToken.REGISTRY_SERVICE())
    );
    if (!registryService.isWallet(_wallet)) {
        revert InvestorNotRegisteredError();
    }
    _;
}
MEDIUM severity: Zero `curvePriceWad` from rounding causes incorrect pricing or denial of service. **Description:** The curve price calculation in `SecuritizeAmmNavProvider::_curveBuy` can round to zero when reserves become imbalanced:

Mathematically, this simplifies to:

For `curvePriceWad` to round to zero:

This state is reachable through repeated sell operations which increase...

Vulnerable code:
curvePriceWad = WAD * (quoteReserves + amountInQuote) / baseReserves

// Proof of simplification:
// Step 1: Expand `deltaBase`
newQuote = Y + amountInQuote

newBase = k / newQuote
        = (X * Y) / (Y + amountInQuote)

deltaBase = X - newBase
          = X - (X * Y) / (Y + amountInQuote)

// Step 2: Find common denominator
deltaBase = X * (Y + amountInQuote) / (Y + amountInQuote) - (X * Y) / (Y + amountInQuote)
          = (X * Y + X * amountInQuote - X * Y) / (Y + amountInQuote)
          = (X * amountInQuote) / (Y + amountInQuote)

// Step 3: Substitute into curvePriceWad
curvePriceWad = (amountInQuote * WAD) / deltaBase
              = (amountInQuote * WAD) / [(X * amountInQuote) / (Y + amountInQuote)]
              = (amountInQuote * WAD) * (Y + amountInQuote) / (X * amountInQuote)
              = WAD * (Y + amountInQuote) / X

Since X = baseReserves, Y = quoteReserves therefore:
curvePriceWad = WAD * (quoteReserves + amountInQuote) / baseReserves

// WAD = 1e18 gives:
1e18 * (quoteReserves + amountInQuote) < baseReserves

uint256 r0Wad = (quoteBaseline * WAD) / baseBaseline;
uint256 mWad = (curvePriceWad * WAD) / r0Wad;  // @audit 0

uint256 baseExecPriceWad = (anchorPriceWad * mWad) / WAD;  // @audit 0

// @audit Since 0 < anchorPriceWad, enter the `else` statement:
uint256 diff = anchorPriceWad - baseExecPriceWad;  // @audit = anchorPriceWad
execPriceWad = anchorPriceWad - (diff / priceScaleFactor);

// @audit With default `priceScaleFactor = 2`:
execPriceWad = anchorPriceWad - anchorPriceWad/2 = anchorPriceWad/2

// With `priceScaleFactor = 1`:
execPriceWad = anchorPriceWad - anchorPriceWad = 0
baseOut = (amountInQuote * WAD) / execPriceWad;  // Division by zero → REVERT
LOW severity: `RedStoneNavProvider::rate` will return massively inflated value if underlying oracle returns a negative value and lacks common oracle validations. **Description:** `RedStoneNavProvider::rate` has an underlying oracle price feed which returns an `int256`, then performs an unsafe cast to `uint256`:

**Impact:** If the underlying oracle returned a negative number, performing an unsafe cast to `uint256` will return an absurdly high...

Vulnerable code:
int256 rsRate = priceFeed.latestAnswer();
require(rsRate != 0, 'Rate must not be zero');
return Helper.normalizeRate(uint256(rsRate), oracleDecimals, assetDecimals);

function rate() external view override returns (uint256) {
        uint8 oracleDecimals = priceFeed.decimals();
        uint8 assetDecimals = asset.decimals();
        int256 rsRate = priceFeed.latestAnswer();
-       require(rsRate != 0, 'Rate must not be zero');
+       require(rsRate > 0, 'Rate must be greater than zero');
        return Helper.normalizeRate(uint256(rsRate), oracleDecimals, assetDecimals);
    }
HIGH severity: Lack of Price Feed Update Function in `RedStoneNavProvider`. **Description:** The `RedStoneNavProvider` contract sets the RedStone price feed address during initialization but provides no mechanism to update it afterwards. The priceFeed is set once in the `RedStoneNavProvider::initialize` function and becomes immutable for the lifetime of the contract:

The.....

Vulnerable code:
function initialize(address _priceFeed, address _asset) public onlyProxy initializer {
        __BaseDSContract_init();
        priceFeed = IPriceFeed(_priceFeed);
        asset = IERC20Metadata(_asset);
    }
LOW severity: Signatures used in `PublicStockOnRamp` and `PublicStockOffRamp` lack investor-specified deadline and nonce parameters so can be used multiple times by operators. **Description:** Both `PublicStockOnRamp` and `PublicStockOffRamp` contracts use EIP-712 signatures to authorize investor transactions. However, these signatures do not include a deadline/expiration parameter in their signed data structure, making them valid indefinitely until executed.
In ...

Vulnerable code:
bytes32 private constant TXTYPE_HASH = keccak256("Swap(uint256 liquidityAmount,uint256 minOutAmount)");

  function hashTx(uint256 _liquidityAmount, uint256 _minOutAmount) private view returns (bytes32) {
        bytes32 structHash = keccak256(
            abi.encode(TXTYPE_HASH, _liquidityAmount, _minOutAmount)
        );

        return _hashTypedDataV4(structHash);
    }

bytes32 private constant TXTYPE_HASH = keccak256("Redeem(uint256 assetAmount,uint256 minOutputAmount)");

function hashTx(uint256 _assetAmount, uint256 _minOutputAmount) private view returns (bytes32) {
        bytes32 structHash = keccak256(
            abi.encode(TXTYPE_HASH, _assetAmount, _minOutputAmount)
        );

        return _hashTypedDataV4(structHash);
    }
LOW severity: `SecuritizeAmmNavProvider` virtual reserve rounding erosion can lead to denial of service. **Description:** The virtual AMM's constant-product math erodes `k` through integer division truncation on every trade:

Each trade can lose up to `(denominator - 1)` from k. Over many trades, `baseReserves` (or `quoteReserves` for sells) progressively decreases.

The `_checkAndResetBaseline`...

Vulnerable code:
// In `_curveBuy`:
newQuote = Y + amountInQuote;
newBase = kLocal / newQuote;  // Rounds down

// After trade execution:
baseReserves = newBase;
quoteReserves = newQuote;
k = newBase * newQuote;  // New k ≤ old k due to truncation

if (shouldReset) {
    uint256 newBase = baseReserves;  // Uses current depleted value
    uint256 newQuote = (newBase * anchorPriceWad) / WAD;
    _resetBaseline(newBase, newQuote);  // k = newBase * newQuote (tiny)
}

// If k = 1, quoteReserves = 1, and amountInQuote = 2:
newQuote = 1 + 2 = 3;
newBase = 1 / 3 = 0;  // Rounds to zero

// Trade completes (deltaBase = 1 passes the check), then:
baseReserves = 0;
k = 0 * 3 = 0;
LOW severity: `RedStoneNavProvider::rate` can return zero for non-zero oracle input due to rounding in `Helper::normalizeRate`. **Description:** In `RedStoneNavProvider::rate`, the zero check validates the raw oracle value but not the normalized result:

The `Helper::normalizeRate` function divides when `fromDecimals > toDecimals`:

When the raw oracle value is smaller than the divisor, integer division truncates to...

Vulnerable code:
function rate() external view override returns (uint256) {
    uint8 oracleDecimals = priceFeed.decimals();
    uint8 assetDecimals = asset.decimals();
    int256 rsRate = priceFeed.latestAnswer();
    require(rsRate != 0, 'Rate must not be zero');  // @audit Checks raw value only

    // @audit normalized rate never zero checked
    return Helper.normalizeRate(uint256(rsRate), oracleDecimals, assetDecimals);
}

function normalizeRate(uint256 value, uint8 fromDecimals, uint8 toDecimals) internal pure returns (uint256) {
    if (fromDecimals == toDecimals) {
        return value;
    } else if (fromDecimals > toDecimals) {
        return value / (10**(fromDecimals - toDecimals));  // @audit Can round to zero
    } else {
        return value * (10**(toDecimals - fromDecimals));
    }
}

oracleDecimals = 18 (common for RedStone)
assetDecimals = 6 (like USDC)
rsRate = 5e11 (passes the != 0 check)
divisor = 10^(18 - 6) = 1e12
normalizedRate = 5e11 / 1e12 = 0
LOW severity: Incorrect rounding direction in `SecuritizeAmmNavProvider::executeBuyBase` when scaling down `execPrice`. **Description:** In `SecuritizeAmmNavProvider::executeBuyBase`, the execution price is scaled down from WAD precision (18 decimals) to the asset's native decimal precision using integer division. The current implementation uses floor division, which systematically rounds DOWN the execution...

Vulnerable code:
uint256 scaleDown = 10 ** (18 - d);

 execPrice = rawExecPriceWad / scaleDown;

function _pricingFromCurveBuy(
        uint256 amountInQuote,
        uint256 curvePriceWad,
        uint256 anchorPriceWad
    ) internal view returns (uint256 baseOut, uint256 execPriceWad) {
        require(anchorPriceWad > 0, "anchor=0");
        require(priceScaleFactor > 0, "scaleFactor = 0");

        uint256 r0Wad = (quoteBaseline * WAD) / baseBaseline;
        uint256 mWad = (curvePriceWad * WAD) / r0Wad;

        uint256 baseExecPriceWad = (anchorPriceWad * mWad) / WAD;

        // Smooth towards anchor:
        // execPriceWad = anchor + (baseExec - anchor) / scaleFactor
        if (baseExecPriceWad >= anchorPriceWad) {
            uint256 diff = baseExecPriceWad - anchorPriceWad;
            execPriceWad = anchorPriceWad + (diff / priceScaleFactor);
        } else {
            uint256 diff = anchorPriceWad - baseExecPriceWad;
            execPriceWad = anchorPriceWad - (diff / priceScaleFactor);
        }

        baseOut = (amountInQuote * WAD) / execPriceWad;
    }
LOW severity: Incorrect rounding of rate in favor of users when they buy DS Tokens. **Description:** Consider this line in `Helper::normalizeRate` which always rounds down:

`Helper::normalizeRate` is called by `RedStoneNavProvider::rate`, which is called by both `SecuritizeOnRamp.sol` and `SecuritizeOffRamp.sol`.

`SecuritizeOnRamp::calculateDsTokenAmount` is used to buy...

Vulnerable code:
26:            return value / (10**(fromDecimals - toDecimals));

rate = navProvider.rate(); // assumed to be in `assetDecimals`

dsTokenAmount = (liquidityAmountExcludingFee * (10 ** (2 * assetDecimals))) / (rate * (10 ** liquidityTokenDecimals));
LOW severity: Block number deadline is chain dependent and unreliable across s. **Description:** The `SecuritizeOnRamp::subscribe` uses `block.number` as a deadline mechanism to prevent stale transactions:

The protocol is intended to be deployed in this chains as evidenced by the `hardhat.config.ts:

Block production rates differ drastically across this chains:
* Ethereum...

Vulnerable code:
function subscribe(
        string memory _investorId,
        address _investorWallet,
        string memory _investorCountry,
        uint8[] memory _investorAttributeIds,
        uint256[] memory _investorAttributeValues,
        uint256[] memory _investorAttributeExpirations,
        uint256 _minOutAmount,
        uint256 _liquidityAmount,
        uint256 _blockLimit,
        bytes32 _agreementHash
    )
        public
        whenNotPaused
        onlySecuritizeOnRamp
        nonZeroNavRate
        validateMinSubscriptionAmount(_liquidityAmount)
    {
        if (_blockLimit < block.number) {
            revert TransactionTooOldError();
        }//@audit where this is going to be deployed?
        ...

networks: {
        sepolia: {
            chainId: 11155111,
            url: process.env.SEPOLIA_RPC_URL ?? '',
            accounts: [process.env.DEPLOYER_PRIV_KEY!].filter((x) => x),
        },
        arbitrum: {
            chainId: 421614,
            // ...
        },
        optimism: {
            chainId: 11155420,
            // ...
        },
        avaxtest: {
            // ...
        },
    },
LOW severity: `SecuritizeAmmNavProvider` violates core AMM invariant that `k` should never decrease. **Description:** Both `SecuritizeAmmNavProvider::_curveBuy` and `_curveSell` round down when calculating new reserves:

This causes users to receive slightly more output than mathematically correct and also `k` to effectively decrease over time.

Best practice is to have an invariant that `k`...

Vulnerable code:
// `_curveBuy`
newQuote = Y + amountInQuote;
newBase = kLocal / newQuote; // @audit rounds down, k decreases

// `_curveSell`
newBase = X + amountInBase;
newQuote = kLocal / newBase; // @audit rounds down, k decreases