Covenant
Findings & Analysis Report
2025-11-17
Table of contents
- Summary
- Scope
- Severity Criteria
-
Low Risk and Non-Critical Issues
- 01 Previews misquote protocol fees by returning cumulative accrual in
LatentSwapLEX - 02 Multicall DoS from strict ETH-invariance check in
Covenant.multicall() - 03 Unsupported exact-out BASE swaps are not rejected early, causing wasted gas (
ValidationLogic,LatentSwapLogic,Covenant) - 04 Default
noCapLimitkeyed by base token leads to misapplied mint/redeem caps inLatentSwapLEX - 05 Preview quotes may return up to 15-minute stale prices in
PythOracle, diverging from live quotes and risking stale-price decisions
- 01 Previews misquote protocol fees by returning cumulative accrual in
- Disclosures
Overview
About C4
Code4rena (C4) is a competitive audit platform where security researchers, referred to as Wardens, review, audit, and analyze codebases for security vulnerabilities in exchange for bounties provided by sponsoring projects.
During the audit outlined in this document, C4 conducted an analysis of the Covenant smart contract system. The audit took place from October 22 to November 03, 2025.
Final report assembled by Code4rena.
Summary
The C4 analysis yielded an aggregated total of 0 unique vulnerabilities with a risk rating in the categories of HIGH and MEDIUM severity.
Additionally, C4 analysis included 65 reports detailing issues with a risk rating of LOW severity or non-critical.
All of the issues presented here are linked back to their original finding, which may include relevant context from the judge and Covenant team.
Scope
The code under review can be found within the C4 Covenant repository, and is composed of 26 smart contracts written in the Solidity programming language and includes 2281 lines of Solidity code.
The code in C4’s Covenant repository was pulled from:
- Repository: https://github.com/covenant-labs/covenant-core
- Commit hash:
7952058c4424ee8c7be11106f6f9f6a96f921cf8
Severity Criteria
C4 assesses the severity of disclosed vulnerabilities based on three primary risk categories: high, medium, and low/non-critical.
High-level considerations for vulnerabilities span the following key areas when conducting assessments:
- Malicious Input Handling
- Escalation of privileges
- Arithmetic
- Gas use
For more information regarding the severity criteria referenced throughout the submission review process, please refer to the documentation provided on the C4 website, specifically our section on Severity Categorization.
Low Risk and Non-Critical Issues
For this audit, 65 reports were submitted by wardens detailing low risk and non-critical issues. The report highlighted below by Agontuk received the top score from the judge.
The following wardens also submitted reports: 0x_DyDx, 0xbrett8571, 0xFBI, 0xki, 0xMilenov, 0xnija, 0xshdax, 30_bugs, Abdulyb, ADAMZ, alicrali, aman234, ARMoh, Astroboy, billyrg131, bravon254, caglankaan, ChainSentry, codexNature, cosin3, deebug, deeney, Diavolo, Eniwealth, Eurovickk, home1344, jerry0422, K42, KamiDancho, KineticsOfWeb3, KKKKK, kmkm, luckygru, Maketer7, mbuba666, Meks079, minos, NeuroSpire, niffylord, pyman, rayss, richa, Sai5417, SarveshLimaye, Sathish9098, sl4x0, spectator, taylorhaun, The_Amazing_One, TOSHI, TrillionaireEmpressClub, v12, valarislife, Warwick, whiterabbit, Willy_Petrus, winnerz, Wojack, Wsecure, X-Tray03, Xmannuel, yixuan, zcai, and zubyoz.
[01] Previews misquote protocol fees by returning cumulative accrual in LatentSwapLEX
quoteMint() and quoteRedeem() return protocolFees as currentState.accruedProtocolFee, which is the total time-based fee accrued since the last update, not the incremental fee attributable to the single previewed action.
In LatentSwapLogic._calculateMarketState(), marketState.accruedProtocolFee is computed from elapsed time, and marketState.baseTokenSupply is reduced by this amount; LatentSwapLEX.quoteMint()/quoteRedeem() then forward this cumulative value as protocolFees. Off-chain integrators that interpret the returned protocolFees as an action fee may double-count, since the output amounts are already computed using the fee-adjusted baseTokenSupply. Relevant references include LatentSwapLEX.quoteMint()/quoteRedeem() returning currentState.accruedProtocolFee and LatentSwapLogic._calculateMarketState() setting marketState.accruedProtocolFee and subtracting it from marketState.baseTokenSupply.
Impact
Previews can mislead wallets/routers about per-action fees, degrading UX and slippage protection (view-path only; no on-chain solvency risk).
Recommended mitigation steps
Return a per-action incremental fee (delta vs a no-op within the same simulated state) and/or rename/document the preview field as cumulative (e.g., accruedProtocolFeeSinceLastUpdate). Alternatively, include both cumulative and per-action fields to avoid ambiguity.
[02] Multicall DoS from strict ETH-invariance check in Covenant.multicall()
Covenant.multicall() computes balanceBeforeCall = address(this).balance - msg.value, then executes all subcalls via MulticallLib.multicall() (which delegatecalls into address(this)), and finally enforces address(this).balance == balanceBeforeCall, reverting with Errors.E_IncorrectPayment() on any deviation. During batched executions, LEX functions forward msg.value to the curator’s updatePriceFeeds() when oracle update data is provided (via _updateOraclePrice()), which in turn forwards ETH to the resolved oracle. Any callee (e.g., a configured oracle) can force-send ETH back to Covenant mid-batch (refunds or selfdestruct), making the post-batch balance differ from balanceBeforeCall and causing the entire multicall() to revert. Because the equality check disallows even benign positive balance deltas, an authorized oracle/callee can grief multicall-based workflows.
Relevant code:
multicall()usesbalanceBeforeCall = address(this).balance - msg.valueand reverts ifaddress(this).balance != balanceBeforeCall.MulticallLib.multicall()performsAddress.functionDelegateCall(address(this), data[i]), so all subcalls share the outermsg.value.- LEX
_updateOraclePrice()forwardsmsg.valuetoCovenantCurator.updatePriceFeeds(); the curator forwards it to the resolved oracle.
Impact
Multicall batches that touch such callees always revert, disabling batching/UX on affected markets (DoS without direct fund loss).
Recommended mitigation steps
Relax the invariant to allow benign positive deltas (e.g., require(address(this).balance >= balanceBeforeCall)), or implement per-subcall ETH accounting to assert only that no net underpayment occurred; alternatively, track and net out intentional refunds in multicall().
[03] Unsupported exact-out BASE swaps are not rejected early, causing wasted gas (ValidationLogic, LatentSwapLogic, Covenant)
Covenant.swap() validates inputs via ValidationLogic.checkSwapParams() but does not proactively forbid unsupported swap modes where assetOut == BASE with isExactIn == false (exact-out of BASE). The LEX implementation (LatentSwapLogic._calcSwap()) only supports:
synth↔synthswaps,assetIn == BASE && isExactIn == true,assetOut == BASE && isExactIn == true.
Any other BASE-involved exact-out combination falls through to a late revert (E_LEX_OperationNotAllowed) inside _calcSwap(), because checkSwapParams() only caps amountSpecified against baseSupply and allows this combination. Users discover the failure deep into execution (after Covenant.swap() dispatches to LEX, and potentially after an oracle update via LatentSwapLEX._updateOraclePrice() when swapParams.data is supplied), wasting gas.
Relevant locations:
- Early validation:
ValidationLogic.checkSwapParams() - Swap flow:
Covenant.swap()→ILiquidExchangeModel.swap()→LatentSwapLogic.swapLogic()→_calcSwap() - Unsupported branch:
_calcSwap()finalelsereverts for(assetOut == BASE && !isExactIn).
Impact
Unnecessary gas waste for users due to late rejection of an unsupported swap mode; no funds at risk.
Recommended mitigation steps
Reject unsupported modes early in ValidationLogic.checkSwapParams():
- Revert when
swapParams.assetOut == AssetType.BASE && !swapParams.isExactIn. - Also revert when
swapParams.assetIn == AssetType.BASE && !swapParams.isExactIn(symmetrical unsupported exact-out case). - Use a clear error to surface intent.
[04] Default noCapLimit keyed by base token leads to misapplied mint/redeem caps in LatentSwapLEX
setDefaultNoCapLimit() is documented to set defaults “for markets with this quote token” (see ILatentSwapLEX.setDefaultNoCapLimit() and SetDefaultNoCapLimit event docstrings). However, the implementation applies the default by base token:
LatentSwapLEXstores defaults intokenNoCapLimitand, duringinitMarket(), readstokenNoCapLimit[marketParams.baseToken].LatentSwapLogic.getInitMarketInfo()receivesuint8 baseTokenNoCapLimit, reinforcing base-token semantics.
Relevant code:
mapping(address token => uint8) internal tokenNoCapLimit;- In
initMarket():tokenNoCapLimit[marketParams.baseToken] - In
getInitMarketInfo():uint8 baseTokenNoCapLimit
As a result, governance that sets defaults keyed by quote tokens (per docs) will not affect new markets; those markets will use a base-token keyed default (or a base-decimal derived fallback), weakening intended issuance throttles until a per-market override is set.
Impact
Governance defaults set by quote token are ignored on new markets, causing caps to be misapplied and enabling unexpectedly large mints/redeems until corrected.
Recommended mitigation steps
Align code and documentation:
- If intent is quote-keyed: index defaults by
marketParams.quoteTokenininitMarket()and passtokenNoCapLimit[marketParams.quoteToken]togetInitMarketInfo(). Rename variables to reflect quote-token semantics and migrate existing configs. - If intent is base-keyed: update interface/event docstrings and admin tooling to state “for markets with this base token,” and guide operators accordingly.
[05] Preview quotes may return up to 15-minute stale prices in PythOracle, diverging from live quotes and risking stale-price decisions
The Covenant PythOracle adapter relaxes staleness checks for preview paths, allowing prices up to a constant 15 minutes old. Specifically, previewGetQuote()/previewGetQuotes() call _previewFetchPriceStruct(), which checks staleness against MAX_STALENESS_UPPER_BOUND (15 minutes), while live quoting checks use the per-instance maxStaleness. As a result, when maxStaleness < staleness ≤ 15 minutes, preview quotes succeed, but live quotes revert on the same feed.
Relevant code:
-
PythOracle.sol:_previewFetchPriceStruct()usesMAX_STALENESS_UPPER_BOUNDpreviewGetQuote()/previewGetQuotes()route through_previewFetchPriceStruct()
- Upstream live path (
_fetchPriceStruct()) enforcesmaxStaleness
This divergence can mislead integrators that rely on preview outputs for decision-making (e.g., sizing min-out or health checks), producing mis-sized parameters or subsequent on-chain reverts when the live path is executed.
Impact
Preview quoting can surface stale prices (up to 15 minutes) that diverge from live behavior, leading to misconfiguration or failed follow-up transactions.
Recommended mitigation steps
Align preview staleness to the instance-configured maxStaleness in _previewFetchPriceStruct(), or introduce a configurable previewMaxStaleness (defaulting to maxStaleness) to avoid stale-price divergence.
Disclosures
C4 audits incentivize the discovery of exploits, vulnerabilities, and bugs in smart contracts. Security researchers are rewarded at an increasing rate for finding higher-risk issues. Audit submissions are judged by a knowledgeable security researcher and disclosed to sponsoring developers. C4 does not conduct formal verification regarding the provided code but instead provides final verification.
C4 does not provide any guarantee or warranty regarding the security of this project. All smart contract software should be used at the sole risk and responsibility of users.