Skip to content
Quantix Security Labs

Risk summary

State at remediation commit d04a8f2c6b19

Findings
8
Critical and high
2
Open or partial
1
Remediation
94%

Findings by severity

  • Critical0
  • High2
  • Medium3
  • Low2
  • Informational1

Remediation statusAnalysis

  • Resolved6
  • Partially resolved1
  • Acknowledged1
  • Open0

Veyra Finance

Lending

Document
QX-TSA-2026-007 · v1.1
Type
Smart Contract Audit
Review
9 Feb 2026 – 27 Feb 2026
Published
24 Mar 2026
Ecosystems
Base
Languages
Solidity
Risk Index
BBB77.9
Assessment contents · 12 sections

01

Assessment overview

Veyra Finance is a pooled money market on Base. Depositors supply assets to shared pools and receive interest-bearing vTokens; borrowers draw against collateral priced by an aggregated oracle that checks the Base sequencer-uptime feed. The pool also offers single-transaction flash loans. Quantix reviewed the pool, token, oracle, liquidation and treasury contracts at the commit listed in this report.

The review identified two high-severity issues. The first is a read-only reentrancy: while a flash loan is outstanding the vToken exchange rate is temporarily depressed, and a separate liquidation contract that values vToken collateral could be invoked from inside the flash-loan callback to liquidate healthy accounts at a discount. The second is an oracle fallback that switched to an AMM spot price whenever the primary feed was older than a fixed one-hour threshold, which for slow-moving assets made a flash-loan manipulable price the effective source most of the time.

Three medium-severity findings concerned the vToken permit domain, the absence of a grace period after the sequencer recovers, and a reserve-management role left on a deployment key. All high and medium findings were fixed in the remediation commit and the fixes were verified, including new invariant tests that reproduce both high-severity issues against the original code.

Residual risk is concentrated in collateral listings: any new asset whose primary feed has a long heartbeat should be listed with an explicit staleness configuration, and markets that accept vTokens as collateral depend on the reentrancy check added for VF-01 remaining in place.

02

Technical analysis: protocol architecture

The LendingPool holds every market's cash, tracks borrows with a per-market borrow index, and mints and burns VToken receipts. Interest accrues lazily on every state-changing call through accrueInterest(market).

Collateral value comes from PriceOracle, which reads a primary feed per asset, rejects prices older than a configured staleness window, and refuses all prices while the Base sequencer is reported down. The Liquidator is a separate contract that checks account health through the pool and runs liquidation auctions; it has its own reentrancy guard.

vTokens of one market may be used as collateral in another, valued at exchangeRateStored() × underlying price. This composability is the root of the read-only reentrancy described in VF-01.

Trusted· 5

Assumed to behave as specified; compromise is out of the threat model.

  • LendingPoolMarket accounting, flash loans, interest accrual
  • VTokenDeposit receipts with permit
  • PriceOracleCollateral and debt pricing
  • LiquidatorLiquidations and auctions
  • TreasuryReserve accrual and withdrawals
trust boundary

Semi-trusted· 1

Trusted for liveness or within bounds; misbehaviour is partly modelled.

  • Primary price feedsExternal price source
trust boundary

Untrusted· 1

May act adversarially; every input is validated.

  • Flash-loan receivers, liquidators, depositorsAny external account
Components grouped by trust level. Calls that cross a trust boundary are where the review concentrated input validation and authorisation checks.

Trust assumptions

  • Primary price feeds report honest prices within their documented heartbeat and deviation thresholds.
  • The governance multisig and timelock act in the interest of users; parameter changes pass through a 48-hour timelock.
  • The Base sequencer-uptime feed reflects sequencer status accurately.
  • Listed assets are standard ERC-20 tokens without transfer fees or rebasing.

03

Scope

Lending pool, vTokens with permit, price oracle, liquidator and treasury. 1,927 nSLOC of Solidity on Base.

Repository
github.com/example-org/veyra-protocol
Review effort
Three reviewers over fifteen working days, followed by a five-day remediation review
Reviewed commit
7c1e94b0a3d25f86e0b4c97a12d8f63e5b0a9c41
Remediation commit
d04a8f2c6b19e73a5f0c8d2e41b67a9c3e5f1082
Files and components in scope
PathDescriptionnSLOC
src/LendingPool.solDeposits, borrows, repayments, interest accrual and flash loans812
src/tokens/VToken.solInterest-bearing deposit receipts with EIP-2612 permit286
src/oracle/PriceOracle.solAggregated price feeds, staleness rules and sequencer-uptime check214
src/liquidation/Liquidator.solHealth checks and liquidation auctions398
src/Treasury.solReserve-factor accrual and reserve withdrawals121
src/libraries/WadRayMath.solFixed-point arithmetic helpers96
Total6 items1,927

Out of scope

  • Web front end, keeper and liquidation bots
  • The Base sequencer, bridge and L1 contracts
  • External price-feed providers, which were treated as behaving as documented
  • Deployment of the governance multisig and its signer set

04

Threat model

Each threat lists the actor, the vector and the mitigation, and links to the findings that realised it in the reviewed code.

Assets at risk

  • Pooled deposits in every market
  • Collateral of borrowers
  • Protocol reserves in the Treasury
  • Integrity of vToken exchange rates used by integrators

Actors

  • Flash-loan attackerUntrustedBorrows large amounts within one transaction and calls any external function
  • LiquidatorUntrustedCalls liquidation functions on any unhealthy account
  • Permit relayerUntrustedObserves and submits signed permits from the mempool
  • Governance multisigTrustedLists markets and changes risk parameters through the timelock
  • Deployer keySemi-trustedDeploys contracts and initial roles
Threats, vectors, mitigations and related findings
ThreatActorVectorMitigationFindings
Liquidate healthy accounts using a transiently wrong priceFlash-loan attackerRe-enter a separate contract during the flash-loan callbackRevert views while the pool is entered; account for outstanding flash loansVF-01
Borrow against inflated collateralFlash-loan attackerSkew an AMM pool used as a fallback price sourceNo spot-price fallback; per-feed staleness matching heartbeatVF-02
Replay a permit signature on another deploymentPermit relayerDomain separator without chain-specific valuesEIP-712 domain with chain ID and verifying contractVF-03
Block a user depositPermit relayerFront-run the permit embedded in a depositTolerate an already-consumed permitVF-04
Liquidations at stale prices after sequencer recoveryLiquidatorAct before users can respond to price moves that happened during downtimeGrace period after the sequencer comes back upVF-05
Withdraw protocol reservesDeployer keyUse a role that was never revokedRoles held only by the timelockVF-06

05

Findings

Severity

8 findings

VF-01HighResolvedRead-only reentrancy

Read-only reentrancy lets flash-loan callbacks liquidate vToken collateral at a depressed exchange rate

src/LendingPool.sol:L402–L447 (flashLoan), src/tokens/VToken.sol:L118 (exchangeRateStored), src/liquidation/Liquidator.sol:L86

Likelihood
Medium
Impact
High
Weakness
CWE-1265
Description

The vToken exchange rate is computed as (cash + totalBorrows − reserves) / totalSupply, where cash is the pool's token balance for the market. flashLoan() transfers the requested amount to the receiver and then calls onFlashLoan() before the funds come back. For the duration of the callback cash is lower by the loan amount while nothing else changes, so exchangeRateStored() reports a value that can be far below the true rate.

All state-changing pool functions share a nonReentrant guard, which correctly prevents a deposit or redemption at the wrong rate. The view function is not guarded, however, and the Liquidator is a separate contract with its own guard. When an account uses vTokens as collateral, Liquidator.liquidate() values that collateral with exchangeRateStored(), so a liquidation triggered from inside the callback sees the depressed value.

This is a read-only reentrancy: the pool never re-enters itself, but another contract reads the pool's state while it is temporarily inconsistent and acts on it.

Preconditions
  • At least one market accepts vTokens as collateral (enabled for vUSDC and vWETH at the reviewed commit).
  • Accounts holding vToken collateral are within the reach of the price drop the attacker can induce with available flash-loan liquidity.
  • The flash-loaned market holds enough cash for the loan to move the exchange rate materially.
Exploit scenario
  1. 1.The attacker identifies accounts whose vUSDC collateral would fall below the liquidation threshold if the vUSDC exchange rate dropped by 15%.
  2. 2.The attacker flash-borrows most of the USDC market's cash, which lowers cash and therefore exchangeRateStored() for vUSDC.
  3. 3.Inside onFlashLoan(), the attacker calls Liquidator.liquidate() on each target. The liquidator reads the depressed rate, judges the accounts unhealthy and sells their collateral to the attacker at the liquidation discount.
  4. 4.The attacker repays the flash loan plus fee. The exchange rate returns to normal and the attacker now holds collateral bought at a discount from accounts that were never unhealthy.
Impact

Healthy borrowers lose the liquidation bonus on collateral that should never have been liquidated. Profit scales with the vToken collateral near the threshold and is limited only by flash-loan liquidity. Integrators outside Veyra that price vTokens with the same view function are exposed to the same manipulation.

Recommendation

Make the pool's accounting consistent during flash loans by adding outstanding flash-loan principal back into the asset total, and make vToken valuation views revert while the pool is entered. The Liquidator should call a pool function that asserts the guard is not entered before valuing any collateral.

Fix
src/LendingPool.sol and src/tokens/VToken.sol (remediation commit)diff
   function flashLoan(address receiver, address asset, uint256 amount, bytes calldata data) external nonReentrant {       Market storage m = markets[asset];+      m.flashOutstanding += amount;       IERC20(asset).safeTransfer(receiver, amount);       uint256 fee = _flashFee(amount);       require(IFlashBorrower(receiver).onFlashLoan(msg.sender, asset, amount, fee, data) == CALLBACK_SUCCESS, "callback");       IERC20(asset).safeTransferFrom(receiver, address(this), amount + fee);+      m.flashOutstanding -= amount;       m.reserves += fee;   }    function totalAssets(address asset) public view returns (uint256) {       Market storage m = markets[asset];-      return m.cash() + m.totalBorrows - m.reserves;+      return m.cash() + m.flashOutstanding + m.totalBorrows - m.reserves;   } +  /// Reverts if called while a state-changing pool function is executing.+  function assertNotEntered() public view {+      if (_reentrancyGuardEntered()) revert ReentrantRead();+  }
Resolution

Fixed. Outstanding flash-loan principal is included in totalAssets, exchangeRateStored() calls assertNotEntered(), and the Liquidator calls it before valuing collateral. A regression test reproduces the original attack and now reverts.

VF-02HighResolvedOracle manipulation

Oracle falls back to a manipulable AMM spot price whenever the primary feed exceeds a one-hour staleness window

src/oracle/PriceOracle.sol:L71–L118 (getPrice, _fallbackPrice)

Likelihood
Medium
Impact
High
Weakness
CWE-807
Description

getPrice(asset) reads the primary feed and, if block.timestamp − updatedAt > STALENESS (a global constant of 3,600 seconds), calls _fallbackPrice(asset), which derives the price from the reserves of a configured AMM pool: reserveQuote * 1e18 / reserveBase.

Primary feeds update on a deviation threshold or on a heartbeat. For assets that move slowly, the heartbeat is commonly 24 hours, so the feed is routinely more than an hour old while still correct. For those assets the fallback is active most of the day, and the fallback is a spot price that can be moved arbitrarily within a single transaction.

The flaw is the combination of a staleness rule that does not match the feed's heartbeat and a fallback source with no manipulation resistance. Either alone would be a lower-severity configuration issue.

Preconditions
  • A listed collateral asset whose primary feed heartbeat is longer than one hour (two such assets at the reviewed commit).
  • An AMM pool configured as the fallback source with liquidity small enough to be moved with flash-loan capital.
Exploit scenario
  1. 1.The attacker waits until the primary feed for the collateral asset is more than an hour old, which happens daily for a 24-hour heartbeat feed.
  2. 2.The attacker flash-borrows the quote asset and buys the collateral asset in the fallback pool, inflating its spot price several times.
  3. 3.The attacker deposits a small amount of the collateral asset into Veyra, which now values it at the inflated fallback price, and borrows other assets against it.
  4. 4.The attacker sells back into the pool, repays the flash loan and keeps the borrowed assets. The position is left with bad debt.
Impact

Depositors in the markets the attacker borrows from absorb the bad debt. The loss is bounded by the borrowable liquidity across markets and the collateral factor of the manipulated asset.

Recommendation

Remove the spot-price fallback. Configure staleness per feed, slightly above the feed's documented heartbeat, and when the primary price is stale stop new borrows and withdrawals of collateral priced by that feed rather than substituting another source. If a secondary source is required, use a time-weighted average over a window long enough to make manipulation uneconomic and bound the deviation between sources.

Fix
src/oracle/PriceOracle.sol (remediation commit)diff
-  uint256 public constant STALENESS = 1 hours;+  mapping(address => uint256) public maxStaleness; // set per feed: heartbeat + buffer    function getPrice(address asset) public view returns (uint256) {       (, int256 answer,, uint256 updatedAt,) = feeds[asset].latestRoundData();       if (answer <= 0) revert InvalidPrice(asset);-      if (block.timestamp - updatedAt > STALENESS) return _fallbackPrice(asset);+      if (block.timestamp - updatedAt > maxStaleness[asset]) revert StalePrice(asset);       return _scale(uint256(answer), feeds[asset].decimals());   }--  function _fallbackPrice(address asset) internal view returns (uint256) {-      (uint112 r0, uint112 r1,) = fallbackPools[asset].getReserves();-      return uint256(r1) * 1e18 / r0;-  }
Resolution

Fixed. The fallback was removed and staleness is configured per feed. Borrowing against an asset whose price is stale now reverts; repayments and deposits remain possible.

VF-03MediumResolvedSignature replay

vToken permit domain separator uses a hard-coded chain ID and omits the verifying contract

src/tokens/VToken.sol:L44–L61 (DOMAIN_SEPARATOR, permit)

Likelihood
Low
Impact
High
Weakness
CWE-294
Description

The vToken implements EIP-2612 permit with its own domain separator: keccak256(abi.encode(TYPEHASH, keccak256(bytes(name())), keccak256("1"), 8453)). The chain ID is a literal and the verifyingContract field required by the EIP-712 domain recommendations is missing.

The Veyra team plans to deploy the same contracts to a second chain using deterministic deployment, so vTokens will have the same name and address there. Without a chain-specific value in the domain, a permit signed for a vToken on Base is equally valid for the vToken with the same name on the second deployment. Omitting the verifying contract also means two vTokens that ever share a name would accept each other's permits.

The nonce does not prevent this: nonces are tracked per contract, so the same signature with nonce n is fresh on each deployment.

Preconditions
  • A second deployment of vTokens with the same name, which is on the Veyra roadmap.
  • A holder who has signed a permit on one deployment and holds vTokens on the other.
Exploit scenario
  1. 1.A user signs a permit on Base allowing a router to spend 1,000 vUSDC.
  2. 2.An observer copies the signature from the Base transaction and submits it to the vUSDC contract on the second chain.
  3. 3.The router (or anyone it lets act) can now move up to 1,000 vUSDC of the same user on the second chain without their consent there.
Impact

Allowances the user never granted on one chain become usable by the spender named in the permit. The spender is chosen by the signer, which limits the attack to signatures naming a spender that can be abused, but routers and aggregators commonly expose such paths.

Recommendation

Use an EIP-712 implementation that builds the domain from block.chainid and address(this), caches it, and rebuilds it if the chain ID changes after a fork.

Fix
src/tokens/VToken.sol (remediation commit)diff
-contract VToken is ERC20 {-    bytes32 public immutable DOMAIN_SEPARATOR =-        keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(_name)), keccak256("1"), 8453));+contract VToken is ERC20, ERC20Permit {+    constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) ERC20Permit(name_) {}+    // ERC20Permit uses OpenZeppelin EIP712: domain includes block.chainid and address(this)+    // and is recomputed if block.chainid differs from the cached value.
Resolution

Fixed by adopting ERC20Permit from OpenZeppelin Contracts 5.x. Signatures from the original domain are no longer accepted.

VF-04LowResolvedDenial of service

Front-running the permit inside depositWithPermit blocks the deposit

src/LendingPool.sol:L188–L203 (depositWithPermit)

Likelihood
Medium
Impact
Low
Weakness
CWE-400
Description

depositWithPermit() calls IERC20Permit(asset).permit(...) and then pulls the tokens. The permit signature is visible in the public mempool before inclusion. Anyone can copy it and call permit() on the token directly first. The signature is then consumed, the nonce has advanced, and the pool's own permit() call reverts, failing the whole deposit.

No funds are at risk: the front-runner only sets the allowance the user intended to set. The effect is a griefing denial of service that costs the attacker gas and the user a failed transaction.

Preconditions
  • The user deposits through depositWithPermit rather than a prior approve.
Exploit scenario
  1. 1.A user submits depositWithPermit with a valid permit signature.
  2. 2.A griefer copies the signature and submits permit() directly to the token with a higher priority fee.
  3. 3.The user's transaction reverts on the already-used nonce.
Impact

Deposits can be blocked repeatedly for users relying on permits. No loss of funds.

Recommendation

Wrap the permit() call in try/catch and proceed if the allowance is already sufficient.

Fix
src/LendingPool.sol (remediation commit)diff
-      IERC20Permit(asset).permit(msg.sender, address(this), amount, deadline, v, r, s);+      try IERC20Permit(asset).permit(msg.sender, address(this), amount, deadline, v, r, s) {} catch {+          if (IERC20(asset).allowance(msg.sender, address(this)) < amount) revert PermitFailed();+      }       _deposit(asset, msg.sender, amount);
Resolution

Fixed as recommended.

VF-05MediumResolvedOracle manipulation

No grace period after the Base sequencer recovers before prices are used for liquidations

src/oracle/PriceOracle.sol:L52–L66 (_checkSequencer)

Likelihood
Low
Impact
High
Weakness
CWE-754
Description

The oracle reads the L2 sequencer-uptime feed and rejects prices while answer == 1 (sequencer down). As soon as the feed reports the sequencer up again, prices are accepted.

While the sequencer is down, users cannot top up collateral or repay debt on L2, although prices can move on other venues. When the sequencer restarts, the first transactions to land can liquidate positions that became unhealthy during the outage before their owners had any chance to react. The feed exposes startedAt for the current status precisely so that integrators can wait a grace period.

Preconditions
  • A sequencer outage during which collateral prices move against borrowers.
Exploit scenario
  1. 1.The sequencer goes down for 40 minutes while ETH falls 9%.
  2. 2.When it restarts, a liquidator with transactions queued through the forced-inclusion path or submitted first liquidates accounts that would have been topped up.
Impact

Borrowers lose liquidation bonuses they could have avoided. The impact is concentrated around outages, which are rare but correlated with volatile markets.

Recommendation

Reject prices until block.timestamp − startedAt exceeds a grace period (for example one hour) after the sequencer reports up, while still allowing repayments and deposits.

Fix
src/oracle/PriceOracle.sol (remediation commit)diff
   function _checkSequencer() internal view {-      (, int256 answer,,,) = sequencerFeed.latestRoundData();+      (, int256 answer, uint256 startedAt,,) = sequencerFeed.latestRoundData();       if (answer != 0) revert SequencerDown();+      if (block.timestamp - startedAt <= GRACE_PERIOD) revert GracePeriodNotOver();   }
Resolution

Fixed with a one-hour grace period. Liquidations and new borrows revert during the grace period; repayments are allowed.

VF-06MediumResolvedAccess control

Reserve-manager role remains with the deployment key

script/Deploy.s.sol:L77, src/Treasury.sol:L33 (withdrawReserves)

Likelihood
Low
Impact
High
Weakness
CWE-284
Description

Treasury.withdrawReserves() is restricted to RESERVE_MANAGER_ROLE. The deployment script grants this role to the timelock and to msg.sender (the deployer) to configure initial reserve factors, and never revokes the deployer's copy. The deployment key is a hot key used by CI.

The contract logic is correct; the issue is the deployed role assignment, which the review covered because the deployment script was in scope. A compromise of the CI key would allow immediate withdrawal of all accrued reserves without passing through the timelock.

Preconditions
  • Compromise of the deployment key.
Exploit scenario
  1. 1.An attacker who obtains the CI deployment key calls withdrawReserves(asset, attacker, amount) for each market.
Impact

Loss of all protocol reserves, which also serve as the first loss buffer for bad debt.

Recommendation

Revoke every role from the deployer at the end of the deployment script and add a post-deployment check that only the timelock holds privileged roles.

Fix
script/Deploy.s.sol (remediation commit)diff
   treasury.grantRole(RESERVE_MANAGER_ROLE, address(timelock));-  treasury.grantRole(RESERVE_MANAGER_ROLE, msg.sender);+  // initial configuration performed through the timelock proposal batch+  treasury.renounceRole(DEFAULT_ADMIN_ROLE, msg.sender);+  require(!treasury.hasRole(RESERVE_MANAGER_ROLE, msg.sender), "deployer role");
Resolution

Fixed. The role was revoked on-chain and the deployment script now asserts the final role set.

VF-07LowPartially resolvedPrecision and rounding

Share conversion rounds in the depositor's favour on mint and redeem

src/libraries/WadRayMath.sol:L41 (rayDiv), src/tokens/VToken.sol:L131–L150

Likelihood
Medium
Impact
Low
Weakness
CWE-682
Description

Both mint and redeem convert between underlying amounts and vToken shares with rayDiv, which rounds half up. Rounding should always favour the pool: shares minted on deposit should round down, and shares burned for a given withdrawal should round up.

Each operation leaks at most one unit of the smallest denomination, which is negligible for 18-decimal assets but adds up for 6-decimal assets across many small operations, and it breaks invariant tests that compare total assets with redeemable value.

Preconditions
  • Any deposit or withdrawal whose conversion is not exact.
Exploit scenario
  1. 1.A user repeatedly deposits and withdraws amounts chosen so that each conversion rounds up in their favour by one unit.
Impact

Value leaks from the pool at most one unit per operation; gas costs make deliberate extraction unprofitable.

Recommendation

Use explicit rounding direction: mulDiv with floor on mint and ceil on the shares burned for a withdrawal.

Fix
src/tokens/VToken.sol (remediation commit)diff
-      uint256 shares = amount.rayDiv(rate);+      uint256 shares = Math.mulDiv(amount, RAY, rate, Math.Rounding.Floor);
Resolution

Mint rounding fixed. Redeem still rounds half up; the team accepted the residual one-unit leakage pending a broader math refactor.

VF-08InformationalAcknowledgedCode quality

Liquidation events are emitted before state changes and lack indexed fields

src/liquidation/Liquidator.sol:L141, L188

Likelihood
Low
Impact
Low
Description

Liquidated and AuctionSettled are emitted before the collateral transfer and debt update, and neither indexes the borrower or the liquidator. Off-chain monitoring has to decode every event to find liquidations of a specific account, and a revert after the emit in a future refactor could mislead indexers that read pending transactions.

Preconditions
  • None; affects observability only.
Exploit scenario
  1. 1.Not exploitable.
Impact

Slower and less reliable monitoring of liquidations.

Recommendation

Emit after state changes and index borrower, liquidator and market.

Resolution

Acknowledged. Planned for the next release because indexers already depend on the current event signature.

06

Severity

8findings
  • Critical0 · 0%
  • High2 · 25%
  • Medium3 · 38%
  • Low2 · 25%
  • Informational1 · 13%

By vulnerability class

  • Oracle manipulation2
  • Access control1
  • Code quality1
  • Denial of service1
  • Precision and rounding1
  • Read-only reentrancy1
  • Signature replay1

Likelihood and impact

Each finding placed by the likelihood of its preconditions and the impact of exploitation.

Findings by likelihood (columns) and impact (rows)
Impact ↓ / Likelihood →LowMediumHigh
High
0
Medium
0
0
0
Low
0

07

Remediation

6 of 8 findings are resolved, 1 acknowledged, 1 partially resolved and 0 open at remediation commit d04a8f2c6b19.

Remediation progress94%

Resolved and acknowledged findings count fully; partially resolved findings count half.

94%remediated
  • Resolved6 · 75%
  • Partially resolved1 · 13%
  • Acknowledged1 · 13%
  • Open0 · 0%
Findings by severity and remediation status
SeverityResolvedPartially resolvedAcknowledgedOpenTotal
Critical00000
High20002
Medium30003
Low11002
Informational00101
All findings61108

Residual items

  • VF-07LowPartially resolvedMint rounding fixed. Redeem still rounds half up; the team accepted the residual one-unit leakage pending a broader math refactor.
  • VF-08InformationalAcknowledgedAcknowledged. Planned for the next release because indexers already depend on the current event signature.
  • ResolvedFixed and verified in the remediation review.
  • Partially resolvedMitigated; the residual risk is documented.
  • AcknowledgedAccepted by the protocol team with a stated rationale.
  • OpenNot yet addressed at the reviewed remediation commit.

08

Formal verification

Four accounting properties were expressed as bounded symbolic tests and run against the remediation commit. The asset-conservation property failed on the original commit while a flash loan was outstanding, which is how VF-01 was first confirmed; it holds after the fix. Monotonicity of the exchange rate could not be completed within the solver time budget.

Tool: Halmos (bounded symbolic execution) and Foundry invariant tests

  • P1ProvedPool assets used for the exchange rate are never less than cash + borrows − reserves, including while a flash loan is outstanding.Violated on the original commit (VF-01); holds on the remediation commit up to depth 6.
  • P2ProvedA successful borrow never leaves the borrower with a health factor below 1.Bounded to three markets and two collateral assets.
  • P3ProvedA flash loan either returns principal plus fee to the pool or reverts.
  • P4InconclusiveThe vToken exchange rate never decreases except through bad-debt realisation in a liquidation.Solver timeout at depth 5; no counterexample found by 50 million fuzzing runs.

A proved property holds for every execution of the model under its stated assumptions. Fuzzing and property-based results are evidence, not proofs, and are recorded as inconclusive.

09

Recommendations

  1. 01

    Keep vToken valuation behind the reentrancy check

    Any new contract or integration that values vTokens must call `assertNotEntered()` or read the guarded view. Add this to the integration guide.

  2. 02

    Document feed heartbeats with each listing

    Record the primary feed heartbeat and deviation threshold for every listed asset and set `maxStaleness` from it in the same governance proposal.

  3. 03

    Run the invariant suite in CI

    The invariant and symbolic tests written during the review reproduce both high-severity findings. Running them on every change protects the fixes.

  4. 04

    Assert final role assignments after every deployment

    Extend the deployment script checks to every privileged role on every contract, including future chains.

  5. 05

    Monitor oracle and sequencer status

    Alert on stale-price reverts and on sequencer status changes so that operators know when borrowing is paused.

10

Methodology and limitations

  1. 01Line-by-line manual review of every in-scope contract by at least two reviewers
  2. 02Specification walk-through with the Veyra team covering interest accrual, liquidation incentives and oracle configuration
  3. 03Static analysis with triage of all detector output
  4. 04Stateful invariant fuzzing of pool accounting, including flash loans and cross-market vToken collateral
  5. 05Economic analysis of oracle fallbacks and liquidation incentives under flash-loan capital
  6. 06Bounded symbolic tests for flash-loan repayment and borrow health properties
  7. 07Remediation review of the fix commit with regression tests for every high and medium finding

Severity classification

Critical
Direct loss of funds or permanent loss of control is achievable with little or no precondition.
High
Material loss or protocol disruption is achievable under realistic conditions.
Medium
Limited loss, degraded functionality, or impact that depends on specific conditions.
Low
Minor impact, defence-in-depth gaps, or issues with unlikely preconditions.
Informational
Code quality, documentation or best-practice observations without direct security impact.

Limitations

  • Only the files and commit listed in scope were reviewed; later changes are not covered.
  • Price-feed providers and the Base sequencer were treated as behaving as documented.
  • Economic analysis assumed liquidity conditions at the time of review; thinner liquidity increases manipulation risk.
  • Bounded symbolic tests prove properties only up to the stated depth and market count.

No assessment can show that a system is free of vulnerabilities. This research assessment reflects the code at the commits listed. Quantix methodology

11

Technical appendix

Tools

  • FoundryUnit, fork and invariant tests; proof-of-concept exploits
  • SlitherStatic analysis
  • HalmosBounded symbolic tests for accounting properties
  • Custom scriptsRole-assignment extraction from deployment broadcasts
Reviewed files (SHA-256) · 6 files
SHA-256 hashes of reviewed files
FileSHA-256
src/LendingPool.soladeccdfa16289b6c40e8513c2a36d16449eb14c46b0fd465c1db7bdba7f6096d
src/tokens/VToken.solf658c9fb3c4b4f0109267c68b00a7551cdee7e8553150b1c51cf413554f8f1f4
src/oracle/PriceOracle.sol56f616f1a90f2f59f81b55cb9117fdd1afac8757e036a95baa63cd6967a7c28a
src/liquidation/Liquidator.sol0d4d579accd5fe21164d6a553c3fd0e418bf66d162c3b01411afffd724f57cfc
src/Treasury.sol8ad9f6df56917d2239f25e1fcbd1046d176d146f2b3c591f808f7e8d6778d1a8
src/libraries/WadRayMath.sol9019bc6a83f3f32542a04575d1b99acdf786ced721ddbf2dfbd29c8bb7351db1

Notes

  • File hashes are SHA-256 digests of the reviewed sources at the audited commit.
  • Severity follows the Quantix impact × likelihood matrix described in the methodology.

Technical references

  1. 1.SWC-107: Reentrancy (opens in a new tab)
  2. 2.OpenZeppelin Contracts: ReentrancyGuard (utilities) (opens in a new tab)
  3. 3.ERC-3156: Flash Loans (opens in a new tab)
  4. 4.Chainlink Data Feeds documentation (opens in a new tab)
  5. 5.Uniswap v3 price oracles (opens in a new tab)
  6. 6.EIP-712: Typed structured data hashing and signing (opens in a new tab)
  7. 7.EIP-2612: Permit extension for EIP-20 signed approvals (opens in a new tab)
  8. 8.SWC-121: Missing protection against signature replay attacks (opens in a new tab)
  9. 9.Chainlink: L2 sequencer uptime feeds (opens in a new tab)
  10. 10.OpenZeppelin Contracts: Access control (opens in a new tab)
  11. 11.EIP-4626: Tokenized Vaults (rounding direction) (opens in a new tab)
  12. 12.CWE-682: Incorrect Calculation (opens in a new tab)
  13. 13.Solidity documentation: Events (opens in a new tab)
  14. 14.Solidity documentation: Security considerations (opens in a new tab)

Quantix Technical Security Assessment QX-TSA-2026-007

Version 1.1, published 24 Mar 2026. A4 PDF generated from this assessment record.

Download Report

12

Report history

  1. 9 Feb 2026Review openedReviewed commit 7c1e94b0a3d2 in github.com/example-org/veyra-protocol
  2. 27 Feb 2026Review closedThree reviewers over fifteen working days, followed by a five-day remediation review
  3. After reviewRemediation reviewFixes checked at commit d04a8f2c6b19
  4. 24 Mar 2026Version 1.1 publishedQX-TSA-2026-007, A4 PDF generated from this record

Commission an assessment written to be verified.

Share the repository and the commit you plan to deploy. We reply with scoping questions and an estimate.