mirror of
https://github.com/luxfi/audits.git
synced 2026-11-03 06:36:02 +00:00
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.
1004 lines
31 KiB
TeX
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}
|