Files
zeekay ff13fb1d15 audits: convert 33 .md content files to .tex
LaTeX is the canonical format for audit reports per workspace policy.
Conversion via pandoc --from=gfm --to=latex --standalone preserves
content verbatim. Meta files (README/INDEX/LLM/CHANGELOG) remain .md.
2026-04-28 14:15:57 -07:00

1004 lines
31 KiB
TeX

% Options for packages loaded elsewhere
\PassOptionsToPackage{unicode}{hyperref}
\PassOptionsToPackage{hyphens}{url}
\documentclass[
]{article}
\usepackage{xcolor}
\usepackage{amsmath,amssymb}
\setcounter{secnumdepth}{-\maxdimen} % remove section numbering
\usepackage{iftex}
\ifPDFTeX
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{textcomp} % provide euro and other symbols
\else % if luatex or xetex
\usepackage{unicode-math} % this also loads fontspec
\defaultfontfeatures{Scale=MatchLowercase}
\defaultfontfeatures[\rmfamily]{Ligatures=TeX,Scale=1}
\fi
\usepackage{lmodern}
\ifPDFTeX\else
% xetex/luatex font selection
\fi
% Use upquote if available, for straight quotes in verbatim environments
\IfFileExists{upquote.sty}{\usepackage{upquote}}{}
\IfFileExists{microtype.sty}{% use microtype if available
\usepackage[]{microtype}
\UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts
}{}
\makeatletter
\@ifundefined{KOMAClassName}{% if non-KOMA class
\IfFileExists{parskip.sty}{%
\usepackage{parskip}
}{% else
\setlength{\parindent}{0pt}
\setlength{\parskip}{6pt plus 2pt minus 1pt}}
}{% if KOMA class
\KOMAoptions{parskip=half}}
\makeatother
\usepackage{color}
\usepackage{fancyvrb}
\newcommand{\VerbBar}{|}
\newcommand{\VERB}{\Verb[commandchars=\\\{\}]}
\DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\{\}}
% Add ',fontsize=\small' for more characters per line
\newenvironment{Shaded}{}{}
\newcommand{\AlertTok}[1]{\textcolor[rgb]{1.00,0.00,0.00}{\textbf{#1}}}
\newcommand{\AnnotationTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textbf{\textit{#1}}}}
\newcommand{\AttributeTok}[1]{\textcolor[rgb]{0.49,0.56,0.16}{#1}}
\newcommand{\BaseNTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{#1}}
\newcommand{\BuiltInTok}[1]{\textcolor[rgb]{0.00,0.50,0.00}{#1}}
\newcommand{\CharTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{#1}}
\newcommand{\CommentTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textit{#1}}}
\newcommand{\CommentVarTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textbf{\textit{#1}}}}
\newcommand{\ConstantTok}[1]{\textcolor[rgb]{0.53,0.00,0.00}{#1}}
\newcommand{\ControlFlowTok}[1]{\textcolor[rgb]{0.00,0.44,0.13}{\textbf{#1}}}
\newcommand{\DataTypeTok}[1]{\textcolor[rgb]{0.56,0.13,0.00}{#1}}
\newcommand{\DecValTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{#1}}
\newcommand{\DocumentationTok}[1]{\textcolor[rgb]{0.73,0.13,0.13}{\textit{#1}}}
\newcommand{\ErrorTok}[1]{\textcolor[rgb]{1.00,0.00,0.00}{\textbf{#1}}}
\newcommand{\ExtensionTok}[1]{#1}
\newcommand{\FloatTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{#1}}
\newcommand{\FunctionTok}[1]{\textcolor[rgb]{0.02,0.16,0.49}{#1}}
\newcommand{\ImportTok}[1]{\textcolor[rgb]{0.00,0.50,0.00}{\textbf{#1}}}
\newcommand{\InformationTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textbf{\textit{#1}}}}
\newcommand{\KeywordTok}[1]{\textcolor[rgb]{0.00,0.44,0.13}{\textbf{#1}}}
\newcommand{\NormalTok}[1]{#1}
\newcommand{\OperatorTok}[1]{\textcolor[rgb]{0.40,0.40,0.40}{#1}}
\newcommand{\OtherTok}[1]{\textcolor[rgb]{0.00,0.44,0.13}{#1}}
\newcommand{\PreprocessorTok}[1]{\textcolor[rgb]{0.74,0.48,0.00}{#1}}
\newcommand{\RegionMarkerTok}[1]{#1}
\newcommand{\SpecialCharTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{#1}}
\newcommand{\SpecialStringTok}[1]{\textcolor[rgb]{0.73,0.40,0.53}{#1}}
\newcommand{\StringTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{#1}}
\newcommand{\VariableTok}[1]{\textcolor[rgb]{0.10,0.09,0.49}{#1}}
\newcommand{\VerbatimStringTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{#1}}
\newcommand{\WarningTok}[1]{\textcolor[rgb]{0.38,0.63,0.69}{\textbf{\textit{#1}}}}
\usepackage{longtable,booktabs,array}
\newcounter{none} % for unnumbered tables
\usepackage{calc} % for calculating minipage widths
% Correct order of tables after \paragraph or \subparagraph
\usepackage{etoolbox}
\makeatletter
\patchcmd\longtable{\par}{\if@noskipsec\mbox{}\fi\par}{}{}
\makeatother
% Allow footnotes in longtable head/foot
\IfFileExists{footnotehyper.sty}{\usepackage{footnotehyper}}{\usepackage{footnote}}
\makesavenoteenv{longtable}
\setlength{\emergencystretch}{3em} % prevent overfull lines
\providecommand{\tightlist}{%
\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
\usepackage{bookmark}
\IfFileExists{xurl.sty}{\usepackage{xurl}}{} % add URL line breaks if available
\urlstyle{same}
\hypersetup{
hidelinks,
pdfcreator={LaTeX via pandoc}}
\author{}
\date{}
\begin{document}
\section{Security Audit: Lux Liquid Staking
Contracts}\label{security-audit-lux-liquid-staking-contracts}
\textbf{Auditor}: Trail of Bits-level Smart Contract Security Review
\textbf{Date}: 2025-01-30 \textbf{Scope}: \texttt{/contracts/liquid/} -
LiquidToken, Bridge Tokens, Teleport System \textbf{Severity Levels}:
CRITICAL, HIGH, MEDIUM, LOW, INFORMATIONAL
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsection{Executive Summary}\label{executive-summary}
This audit examines the Lux liquid staking protocol comprising:
\begin{itemize}
\tightlist
\item
\textbf{LiquidToken.sol}: Base mintable token with ERC-3156 flash
loans
\item
\textbf{LRC20B.sol}: Bridge token base with admin-controlled mint/burn
\item
\textbf{32 Token Contracts}: LBTC, LETH, LCYRUS, LMIGA, LPARS, LUSD,
etc.
\item
\textbf{LiquidLUX.sol}: Master yield vault (xLUX) with fee aggregation
\item
\textbf{Teleport System}: TeleportVault, LiquidVault, Teleporter,
LiquidYield, LiquidETH
\end{itemize}
\subsubsection{Risk Summary}\label{risk-summary}
{\def\LTcaptype{none} % do not increment counter
\begin{longtable}[]{@{}ll@{}}
\toprule\noalign{}
Severity & Count \\
\midrule\noalign{}
\endhead
\bottomrule\noalign{}
\endlastfoot
CRITICAL & 3 \\
HIGH & 6 \\
MEDIUM & 8 \\
LOW & 5 \\
INFORMATIONAL & 4 \\
\end{longtable}
}
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsection{CRITICAL FINDINGS}\label{critical-findings}
\subsubsection{{[}C-01{]} Flash Loan Fee Accounting Error - Infinite
Mint Attack
Vector}\label{c-01-flash-loan-fee-accounting-error---infinite-mint-attack-vector}
\textbf{File}: \texttt{LiquidToken.sol:176-202}
\textbf{Description}: The flash loan implementation burns
\texttt{amount\ +\ fee} from the borrower, then mints \texttt{fee} to
the fee recipient. This creates a supply increase of \texttt{fee} tokens
per flash loan while only the fee is "paid". However, the critical issue
is that if \texttt{flashMintFee} is 0 (or admin sets it to 0), the
entire flash loan becomes free with zero accountability.
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{function flashLoan(...) external override nonReentrant returns (bool) \{}
\NormalTok{ // ...}
\NormalTok{ \_mint(address(receiver), amount);}
\NormalTok{ // callback happens here}
\NormalTok{ \_burn(address(receiver), amount + fee); // Burns principal + fee}
\NormalTok{ if (fee \textgreater{} 0) \{}
\NormalTok{ \_mint(feeRecipient, fee); // Mints fee to recipient}
\NormalTok{ \}}
\NormalTok{ return true;}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
\textbf{Attack Vector}:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\tightlist
\item
Admin (or compromised admin) sets \texttt{flashMintFee\ =\ 0}
\item
Attacker takes flash loan of any amount
\item
Loan is free with zero fee
\item
Can be combined with other DeFi protocols for arbitrage with no cost
\end{enumerate}
\textbf{Impact}: Economic attacks become free; governance manipulation
via zero-cost flash loans.
\textbf{Recommendation}: Add minimum flash fee floor (e.g., 1 bps) that
cannot be set to zero.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}C-02{]} Cross-Chain Mint Replay Attack via Stale
Backing
Attestation}\label{c-02-cross-chain-mint-replay-attack-via-stale-backing-attestation}
\textbf{File}: \texttt{Teleporter.sol:493-507}
\textbf{Description}: The \texttt{\_checkBackingRatio} function allows
minting even with stale attestations (\textgreater24 hours old) by
simply returning without validation:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{function \_checkBackingRatio(uint256 srcChainId, uint256 additionalMint) internal view \{}
\NormalTok{ BackingAttestation memory attestation = backingAttestations[srcChainId];}
\NormalTok{ // VULNERABILITY: Stale attestation allows unlimited minting}
\NormalTok{ if (block.timestamp {-} attestation.timestamp \textgreater{} 24 hours) \{}
\NormalTok{ return; // No check performed!}
\NormalTok{ \}}
\NormalTok{ // ...}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
\textbf{Attack Vector}:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\tightlist
\item
MPC oracle goes offline or attestation becomes stale
\item
Attacker crafts deposit proofs for amounts exceeding actual backing
\item
Mints unbacked collateral tokens
\end{enumerate}
\textbf{Impact}: Complete protocol insolvency through unbacked token
minting.
\textbf{Recommendation}: Revert on stale attestations instead of
skipping the check. Implement a grace period with reduced minting caps.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}C-03{]} Admin Key Compromise Enables Unlimited Token
Minting}\label{c-03-admin-key-compromise-enables-unlimited-token-minting}
\textbf{File}: \texttt{LRC20B.sol:51-63} and all token contracts
\textbf{Description}: All 32 bridge tokens (LBTC, LETH, LUSD, etc.) use
\texttt{DEFAULT\_ADMIN\_ROLE} for mint/burn with no timelocks, no
multisig requirements, and no rate limits. Any admin can:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{function mint(address account, uint256 amount) public onlyAdmin \{}
\NormalTok{ \_mint(account, amount);}
\NormalTok{\}}
\NormalTok{function grantAdmin(address to) public onlyAdmin \{}
\NormalTok{ grantRole(DEFAULT\_ADMIN\_ROLE, to); // Can add arbitrary admins}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
\textbf{Attack Scenario}:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\tightlist
\item
Single admin key compromised (phishing, key extraction, insider)
\item
Attacker mints unlimited tokens across all 32 contracts
\item
Dumps on DEXes, draining liquidity
\end{enumerate}
\textbf{Impact}: Complete protocol collapse; all user funds at risk.
\textbf{Recommendation}:
\begin{itemize}
\tightlist
\item
Implement timelock for admin operations
\item
Require multisig (2-of-3 minimum) for minting
\item
Add per-block/daily mint caps
\item
Consider using role hierarchy with separate MINTER\_ROLE
\end{itemize}
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsection{HIGH FINDINGS}\label{high-findings}
\subsubsection{{[}H-01{]} Whitelist Bypass via Paused State Race
Condition}\label{h-01-whitelist-bypass-via-paused-state-race-condition}
\textbf{File}: \texttt{LiquidToken.sol:139-142}
\textbf{Description}: The \texttt{mint} function checks
\texttt{paused{[}msg.sender{]}} after verifying whitelist. A sentinel
can pause an address, but there\textquotesingle s no atomicity
guarantee:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{function mint(address recipient, uint256 amount) external onlyWhitelisted \{}
\NormalTok{ if (paused[msg.sender]) revert IllegalState(); // Check after whitelist}
\NormalTok{ \_mint(recipient, amount);}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
\textbf{Attack Vector}:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\tightlist
\item
Sentinel observes malicious whitelisted address attempting to mint
\item
Sentinel calls \texttt{setPaused(minter,\ true)}
\item
Race condition: minter\textquotesingle s tx can be mined before pause
tx
\item
Minter front-runs pause transaction
\end{enumerate}
\textbf{Impact}: Malicious whitelisted addresses can front-run pause
attempts.
\textbf{Recommendation}: Implement a whitelist removal function with
immediate effect, not just pause.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}H-02{]} No Burn Authorization Check in LRC20B
Tokens}\label{h-02-no-burn-authorization-check-in-lrc20b-tokens}
\textbf{File}: \texttt{LRC20B.sol:82-89} and all token contracts
\textbf{Description}: The \texttt{burn} function in token contracts
burns from an arbitrary account without checking if msg.sender is
authorized:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{// In each token contract:}
\NormalTok{function burn(address account, uint256 amount) public onlyAdmin \{}
\NormalTok{ \_burn(account, amount); // Burns from ANY account}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
\textbf{Attack Vector}:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\tightlist
\item
Compromised or malicious admin
\item
Burns tokens from any user\textquotesingle s wallet without approval
\item
Funds effectively stolen/destroyed
\end{enumerate}
\textbf{Impact}: Admin can destroy any user\textquotesingle s tokens at
will.
\textbf{Recommendation}: Remove arbitrary account burning. Admin should
only burn from contract-controlled addresses or require explicit
allowance.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}H-03{]} LiquidVault Strategy Allocation Signature
Replay}\label{h-03-liquidvault-strategy-allocation-signature-replay}
\textbf{File}: \texttt{LiquidVault.sol:189-216}
\textbf{Description}: The \texttt{allocateToStrategy} signature includes
\texttt{block.timestamp} which is too coarse-grained:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{bytes32 messageHash = keccak256(abi.encodePacked(}
\NormalTok{ "ALLOCATE",}
\NormalTok{ strategyIndex,}
\NormalTok{ amount,}
\NormalTok{ block.timestamp // Only timestamp, no nonce!}
\NormalTok{));}
\end{Highlighting}
\end{Shaded}
\textbf{Attack Vector}:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\tightlist
\item
MPC signs allocation for 100 ETH at timestamp T
\item
Within same block (same timestamp), attacker replays signature
\item
Multiple allocations with same signature in same block
\end{enumerate}
\textbf{Impact}: Strategy funds can be over-allocated, potentially to
malicious contracts.
\textbf{Recommendation}: Add a unique nonce to all MPC-signed
operations, increment per operation.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}H-04{]} LiquidETH Yield Index
Manipulation}\label{h-04-liquideth-yield-index-manipulation}
\textbf{File}: \texttt{LiquidETH.sol:303-315}
\textbf{Description}: The yield index calculation can be manipulated by
an attacker who controls timing:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{function onYieldReceived(uint256 amount, uint256 srcChainId) external onlyRole(LIQUID\_YIELD\_ROLE) \{}
\NormalTok{ // ...}
\NormalTok{ uint256 yieldPerDebt = amount * 1e18 / totalDebt; // Integer division}
\NormalTok{ yieldIndex += yieldPerDebt;}
\end{Highlighting}
\end{Shaded}
\textbf{Attack Vector}:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\tightlist
\item
Attacker borrows maximum debt just before yield distribution
\item
Receives disproportionate yield allocation due to timing
\item
Immediately repays and exits with profit
\end{enumerate}
\textbf{Impact}: Sophisticated attackers can extract excess yield at
expense of long-term holders.
\textbf{Recommendation}: Implement yield snapshots or time-weighted
averaging for debt positions.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}H-05{]} Missing reentrancy protection in TeleportVault
markBridged}\label{h-05-missing-reentrancy-protection-in-teleportvault-markbridged}
\textbf{File}: \texttt{TeleportVault.sol:230-237}
\textbf{Description}: The \texttt{markBridged} function lacks
\texttt{nonReentrant} modifier:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{function markBridged(uint256 nonce) external onlyRole(MPC\_ROLE) \{}
\NormalTok{ // No nonReentrant modifier}
\NormalTok{ DepositProof storage proof = deposits[nonce];}
\NormalTok{ if (proof.nonce == 0) revert DepositNotFound();}
\NormalTok{ if (proof.bridged) revert AlreadyBridged();}
\NormalTok{ proof.bridged = true;}
\NormalTok{ emit DepositBridged(nonce);}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
While currently not directly exploitable, if MPC role is granted to a
contract, reentrancy could occur.
\textbf{Impact}: Potential for future reentrancy if MPC becomes a
contract.
\textbf{Recommendation}: Add \texttt{nonReentrant} modifier to all
state-modifying functions.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}H-06{]} LiquidYield LETH Burn Without Balance
Check}\label{h-06-liquidyield-leth-burn-without-balance-check}
\textbf{File}: \texttt{LiquidYield.sol:199-200}
\textbf{Description}: The contract attempts to burn LETH without first
checking its balance:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{// In processYieldEvent:}
\NormalTok{leth.burn(amount); // Assumes contract has balance}
\end{Highlighting}
\end{Shaded}
If \texttt{leth.burn()} is called on this contract\textquotesingle s
behalf but tokens weren\textquotesingle t actually transferred here,
this will fail or burn from wrong source depending on burn
implementation.
\textbf{Impact}: Yield processing could fail unexpectedly or burn wrong
tokens.
\textbf{Recommendation}: Add explicit balance check before burning:
\texttt{require(leth.balanceOf(address(this))\ \textgreater{}=\ amount)}.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsection{MEDIUM FINDINGS}\label{medium-findings}
\subsubsection{{[}M-01{]} ERC-3156 Deviation - Fee Calculation May
Overflow}\label{m-01-erc-3156-deviation---fee-calculation-may-overflow}
\textbf{File}: \texttt{LiquidToken.sol:170-173}
\textbf{Description}: The flash fee calculation can theoretically
overflow for very large amounts:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{function flashFee(address token\_, uint256 amount) public view override returns (uint256) \{}
\NormalTok{ if (token\_ != address(this)) revert IllegalArgument();}
\NormalTok{ return (amount * flashMintFee) / BPS; // Potential overflow pre{-}0.8.0}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
With Solidity 0.8.31, this will revert on overflow, but not gracefully.
\textbf{Impact}: Very large flash loan requests revert unexpectedly.
\textbf{Recommendation}: Add explicit overflow check with clear error
message.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}M-02{]} No Maximum Supply
Cap}\label{m-02-no-maximum-supply-cap}
\textbf{File}: \texttt{LiquidToken.sol}, all token contracts
\textbf{Description}: Neither LiquidToken nor any LRC20B token
implements a maximum supply cap. Combined with admin minting, this
allows unlimited inflation.
\textbf{Impact}: Token supply can grow infinitely, diluting existing
holders.
\textbf{Recommendation}: Implement \texttt{maxSupply} constant and check
in \texttt{\_mint()}.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}M-03{]} Pause Mechanism Incomplete - No Global
Pause}\label{m-03-pause-mechanism-incomplete---no-global-pause}
\textbf{File}: \texttt{LiquidToken.sol:129-132}
\textbf{Description}: Pause is per-minter address, not global.
There\textquotesingle s no way to pause all minting simultaneously in an
emergency:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{mapping(address =\textgreater{} bool) public paused;}
\NormalTok{function setPaused(address minter, bool state) external onlySentinel \{}
\NormalTok{ paused[minter] = state; // Per{-}address only}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
\textbf{Attack Vector}: During an exploit, sentinel must pause each
whitelisted address individually, allowing time for more damage.
\textbf{Impact}: Slow emergency response.
\textbf{Recommendation}: Add \texttt{bool\ public\ globalPaused} with
sentinel control.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}M-04{]} LiquidLUX First Depositor Inflation
Attack}\label{m-04-liquidlux-first-depositor-inflation-attack}
\textbf{File}: \texttt{LiquidLUX.sol:459-465}
\textbf{Description}: Classic ERC4626 inflation attack vector exists:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{function \_convertToShares(uint256 assets) internal view returns (uint256) \{}
\NormalTok{ uint256 supply = totalSupply();}
\NormalTok{ if (supply == 0) \{}
\NormalTok{ return assets; // 1:1 for first deposit}
\NormalTok{ \}}
\NormalTok{ return (assets * supply) / totalAssets();}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
\textbf{Attack Vector}:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\tightlist
\item
Attacker is first depositor with 1 wei
\item
Attacker donates large amount directly to vault
\item
Next depositor gets 0 shares due to rounding
\item
Attacker redeems for donated amount
\end{enumerate}
\textbf{Impact}: First depositors can steal from subsequent depositors.
\textbf{Recommendation}: Implement virtual offset (dead shares) or
minimum deposit requirement.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}M-05{]} Teleporter withdraw nonce is
predictable}\label{m-05-teleporter-withdraw-nonce-is-predictable}
\textbf{File}: \texttt{Teleporter.sol:337-344}
\textbf{Description}: Withdraw nonce generation uses predictable inputs:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{withdrawNonce = uint256(keccak256(abi.encodePacked(}
\NormalTok{ block.timestamp,}
\NormalTok{ msg.sender,}
\NormalTok{ amount,}
\NormalTok{ srcChainId,}
\NormalTok{ block.number}
\NormalTok{)));}
\end{Highlighting}
\end{Shaded}
All inputs are known to miners/validators who can manipulate for
collision.
\textbf{Impact}: Potential nonce collision in adversarial conditions.
\textbf{Recommendation}: Include a monotonic counter or use
block.prevrandao.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}M-06{]} Missing zero address check for feeRecipient
setter}\label{m-06-missing-zero-address-check-for-feerecipient-setter}
\textbf{File}: \texttt{LiquidToken.sol:117-120}
\textbf{Description}: While constructor sets feeRecipient to msg.sender,
the setter checks for zero:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{function setFeeRecipient(address recipient) external onlyAdmin \{}
\NormalTok{ if (recipient == address(0)) revert IllegalArgument();}
\NormalTok{ feeRecipient = recipient;}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
This is correct, but the initial value from constructor could
theoretically be zero if deployer is a self-destructed contract.
\textbf{Impact}: Low - edge case only.
\textbf{Recommendation}: Check constructor parameters as well.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}M-07{]} LiquidVault Strategy Array Growth
Unbounded}\label{m-07-liquidvault-strategy-array-growth-unbounded}
\textbf{File}: \texttt{LiquidVault.sol:286-298}
\textbf{Description}: Strategies are added to an array but only
deactivated, never removed:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{function removeStrategy(uint256 index) external onlyRole(DEFAULT\_ADMIN\_ROLE) \{}
\NormalTok{ Strategy storage strategy = strategies[index];}
\NormalTok{ if (strategy.allocated != 0) revert StrategyHasFunds();}
\NormalTok{ strategy.active = false; // Never actually removed from array}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
\textbf{Impact}: Gas costs grow over time for harvest operations
iterating all strategies.
\textbf{Recommendation}: Implement proper array compaction or use
mappings.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}M-08{]} Double Accounting in LiquidLUX
reconcile()}\label{m-08-double-accounting-in-liquidlux-reconcile}
\textbf{File}: \texttt{LiquidLUX.sol:419-430}
\textbf{Description}: The \texttt{reconcile()} function
doesn\textquotesingle t account for user deposits/withdrawals properly:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{function reconcile() external view returns (...) \{}
\NormalTok{ expectedBalance = totalProtocolFeesIn + totalValidatorRewardsIn}
\NormalTok{ {-} totalPerfFeesTaken {-} totalSlashingLosses;}
\NormalTok{ // Missing: user deposits and withdrawals!}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
\textbf{Impact}: Reconciliation view returns incorrect values,
misleading auditors.
\textbf{Recommendation}: Track user deposit/withdraw totals separately
for accurate reconciliation.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsection{LOW FINDINGS}\label{low-findings}
\subsubsection{{[}L-01{]} Missing events for critical parameter
changes}\label{l-01-missing-events-for-critical-parameter-changes}
\textbf{File}: \texttt{LiquidToken.sol:117-120}
The \texttt{setFeeRecipient} function doesn\textquotesingle t emit an
event:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{function setFeeRecipient(address recipient) external onlyAdmin \{}
\NormalTok{ if (recipient == address(0)) revert IllegalArgument();}
\NormalTok{ feeRecipient = recipient;}
\NormalTok{ // Missing event!}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
\textbf{Recommendation}: Add
\texttt{event\ FeeRecipientUpdated(address\ indexed\ newRecipient)}.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}L-02{]} Inconsistent naming between LiquidToken and
LRC20B
tokens}\label{l-02-inconsistent-naming-between-liquidtoken-and-lrc20b-tokens}
\textbf{File}: Various
LiquidToken uses \texttt{whitelisted} mapping while LRC20B uses
\texttt{DEFAULT\_ADMIN\_ROLE}. This inconsistency can lead to
integration confusion.
\textbf{Recommendation}: Standardize access control patterns across all
token contracts.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}L-03{]} LiquidETH MIN\_POSITION\_SIZE too
low}\label{l-03-liquideth-min_position_size-too-low}
\textbf{File}: \texttt{LiquidETH.sol:76}
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{uint256 public constant MIN\_POSITION\_SIZE = 0.001 ether;}
\end{Highlighting}
\end{Shaded}
At current ETH prices, 0.001 ETH (\$2-3) positions create dust
that\textquotesingle s uneconomical to liquidate.
\textbf{Recommendation}: Increase to at least 0.01 ETH or implement
dynamic minimum based on gas costs.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}L-04{]} No deadline in LiquidVault strategy
operations}\label{l-04-no-deadline-in-liquidvault-strategy-operations}
\textbf{File}: \texttt{LiquidVault.sol}
MPC signatures include \texttt{block.timestamp} but no explicit
deadline:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{bytes32 messageHash = keccak256(abi.encodePacked(}
\NormalTok{ "ALLOCATE",}
\NormalTok{ strategyIndex,}
\NormalTok{ amount,}
\NormalTok{ block.timestamp // Current timestamp, not deadline}
\NormalTok{));}
\end{Highlighting}
\end{Shaded}
\textbf{Impact}: Stale signatures could be valid indefinitely if
replayed at exact timestamp match.
\textbf{Recommendation}: Add explicit \texttt{deadline} parameter.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}L-05{]} LiquidYield unbounded loop in
getUnprocessedCount}\label{l-05-liquidyield-unbounded-loop-in-getunprocessedcount}
\textbf{File}: \texttt{LiquidYield.sol:252-258}
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{function getUnprocessedCount() external view returns (uint256 count) \{}
\NormalTok{ for (uint256 i = 0; i \textless{} yieldEvents.length; i++) \{ // Unbounded loop}
\NormalTok{ if (!yieldEvents[i].processed) \{}
\NormalTok{ count++;}
\NormalTok{ \}}
\NormalTok{ \}}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
\textbf{Impact}: View function can run out of gas for large event
counts.
\textbf{Recommendation}: Track unprocessed count in state variable.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsection{INFORMATIONAL}\label{informational}
\subsubsection{{[}I-01{]} Solidity version 0.8.31 is very
recent}\label{i-01-solidity-version-0831-is-very-recent}
All contracts use \texttt{pragma\ solidity\ \^{}0.8.31}. This version is
recent and may have undiscovered bugs.
\textbf{Recommendation}: Consider using a more battle-tested version
like 0.8.24.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}I-02{]} Missing NatSpec documentation in several
functions}\label{i-02-missing-natspec-documentation-in-several-functions}
Several public functions lack complete NatSpec documentation, making
integration more difficult.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}I-03{]} Consider using OpenZeppelin\textquotesingle s
Pausable}\label{i-03-consider-using-openzeppelins-pausable}
Instead of custom pause logic, using OpenZeppelin\textquotesingle s
battle-tested Pausable would reduce attack surface.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsubsection{{[}I-04{]} Token symbol/name
shadowing}\label{i-04-token-symbolname-shadowing}
In token contracts:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{string public constant \_name = "Liquid BTC"; // underscore prefix}
\NormalTok{string public constant \_symbol = "LBTC";}
\end{Highlighting}
\end{Shaded}
The underscore prefix suggests internal variables but
they\textquotesingle re public constants. This could cause confusion.
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsection{Recommendations Summary}\label{recommendations-summary}
\subsubsection{Immediate Actions
(CRITICAL)}\label{immediate-actions-critical}
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\tightlist
\item
Add minimum flash fee floor that cannot be zero
\item
Revert on stale backing attestations instead of skipping
\item
Implement timelock and multisig for admin operations
\end{enumerate}
\subsubsection{Short-term Actions (HIGH)}\label{short-term-actions-high}
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\setcounter{enumi}{3}
\tightlist
\item
Add unique nonces to all MPC-signed operations
\item
Implement proper authorization for burn functions
\item
Add balance checks before burns in LiquidYield
\item
Add nonReentrant to all state-modifying functions
\end{enumerate}
\subsubsection{Medium-term Actions}\label{medium-term-actions}
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\setcounter{enumi}{7}
\tightlist
\item
Implement global pause functionality
\item
Add maximum supply caps
\item
Fix first-depositor attack in LiquidLUX
\item
Implement proper strategy array management
\item
Fix reconciliation accounting
\end{enumerate}
\subsubsection{Best Practices}\label{best-practices}
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\setcounter{enumi}{12}
\tightlist
\item
Add comprehensive event emission
\item
Standardize access control patterns
\item
Add deadline parameters to time-sensitive operations
\item
Complete NatSpec documentation
\end{enumerate}
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\subsection{Conclusion}\label{conclusion}
The Lux liquid staking contracts have significant security concerns that
require immediate attention. The three critical findings (flash loan
fee, stale attestation, admin key) represent existential risks to the
protocol. The high-severity findings, while not immediately exploitable
in all cases, present material risk.
Before mainnet deployment, we strongly recommend:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\tightlist
\item
Addressing all CRITICAL and HIGH findings
\item
Implementing comprehensive test coverage for attack vectors identified
\item
Conducting a follow-up audit after fixes
\item
Implementing a bug bounty program
\item
Establishing clear incident response procedures
\end{enumerate}
\begin{center}\rule{0.5\linewidth}{0.5pt}\end{center}
\textbf{Audit Hash}: \texttt{keccak256("AUDIT\_LIQUID\_20250130")}
\textbf{Contracts Reviewed}: 42 files, \textasciitilde3,500 lines of
Solidity \textbf{Time Spent}: Comprehensive review
\end{document}