引言
去中心化永续合约通过“共享流动性”和“预言机定价”机制,在链上复刻了高杠杆衍生品交易。与 AMM 现货交易不同,永续合约系统涉及复杂的保证金核算、盈亏动态调整及清算博弈。微小的逻辑偏差——无论是价格精度的舍入,还是预言机更新的延迟——都可能导致协议资不抵债或用户资产归零。
本手册旨在解构此类系统的核心架构,剖析风险场景,并为智能合约安全审计师或区块链安全研究员提供实战审计检查清单。
基础技术架构
去中心化永续合约的核心在于将传统 CEX 的高性能撮合引擎逻辑,转化为链上的“异步执行”与“资金池博弈”模型。其架构不仅要管理资产,还需处理复杂的订单状态与预言机延迟带来的挑战。
核心功能模块
1. 订单管理
负责处理用户的交易意图,将请求参数化并存储,等待之后的执行撮合。
- 加仓订单(Increase Order):包含开仓和追加保证金。
- 减仓订单(Decrease Order):包含平仓和提取保证金。
- 市价单(Market):即时请求,允许一定的滑点,通常通过“两步执行”机制防止预言机抢跑。
- 限价单(Limit):当预言机价格达到指定的触发价格阈值时触发的加仓请求。
- 止盈/止损订单(TP/SL):特殊的减仓限价单。当标记价格触及触发价格时,执行全平或部分平仓。
- 互换订单(Swap Order):在不同抵押品之间进行转换(如用 USDT 开 BTC 本位合约),通常作为开仓的前置步骤。
2. 仓位管理
这是系统的核心账本,记录每个用户的持仓状态。包括 size(头寸价值 USD)、collateral(抵押品数量)、averagePrice(平均开仓价格)、entryFundingIndex(开仓时的资金费率指数)等。
仓位的 PnL 计算:实时计算未实现盈亏。
- 多单 PnL = size * (currentPrice - averagePrice) / averagePrice
- 空单 PnL = size * (averagePrice - currentPrice) / averagePrice
在减仓或平仓时需要计算用户这部分仓位的实际盈亏,并在仓位保证金数据中进行结算:
Position.collateral += PnL(仓位产生的盈亏)+ FundingFee(仓位产生的资金费率)- OrderExecuteFee(减仓时收取的手续费)
3. 交易执行与撮合
由于链上计算昂贵且需依赖预言机,DEX 通常不采用订单簿撮合(CLOB),而是采用“Keeper 触发 + 预言机定价”的模式。
- 请求队列(Request Queue):用户提交订单后,交易并不立即执行,而是进入队列(或存储为 pending 状态)。
- Keepers(执行订单的角色):链下机器人监听队列,当条件满足(如由时间锁过期、价格触发)时,发送交易并调用合约执行订单来撮合交易。
4. 资金池(Liquidity Pool)
在去中心化永续合约项目中,资金池不仅仅是用户提供流动性的容器,它还是市场上所有交易者的全局对手方。
4.1 虚拟流动性与资产支撑:
- LP 铸造/销毁:流动性提供者存入资产(如 USDC, ETH 等)铸造 LP Token。LP Token 的价格(兑换率)由公式 P_LP = (TotalAssets + UnrealizedPnL) / TotalSupply 决定。
- 零和博弈:交易者的亏损直接成为 LP 的盈利,交易者的盈利则从 LP 池中支付。
- 风险隔离:通常将稳定币池与波动资产池在逻辑上隔离,或通过权重控制特定资产的敞口上限,避免某个资产的价格剧烈波动影响产生流动性风险。
4.2 动态费用(Dynamic Fees / Rebalancing):
- 为了维持池内资产比例平衡,系统根据当前资产权重与目标权重的偏差调整 Swap 费用。
- 机制:若 ETH 占比过高,用 ETH 换 USDC 会收取高额费用(惩罚),反之则由低费用甚至零费用激励。
4.3 全局债务追踪(Global Debt):
- 系统不直接持有空单的抵押品(如 USDC),而是记录“全局空单总额”。
- Guaranteed USD:这是一个关键的记账变量,表示“被多单锁定”的资产价值 + “空单的抵押品”价值。用于确保资金池具备偿付能力。
5. Oracle 预言机模块
预言机模块负责提供“防操纵”的定价服务,是系统的风控核心。
5.1 价格聚合策略(Price Aggregation):
- 双源验证:通常结合 Chainlink/Pyth 等链上预言机的价格(参考价格)和 Keeper 提交的 CEX 价格(快速价格)。
- 价差保护:当 |FastPrice - ChainlinkPrice| > SpreadThreshold 时,强制回退使用 Chainlink 价格或暂停交易,防止 Keeper 恶意报价。
5.2 极值保护策略(Min/Max Price Logic):
为防止预言机短期波动导致用户套利,系统维护两个价格变量:MaxPrice 和 MinPrice。
- 做多(Long):开仓/加仓使用 MaxPrice(高成本入场),平仓/减仓使用 MinPrice(低收益离场)。
- 做空(Short):开仓/加仓使用 MinPrice,平仓/减仓使用 MaxPrice。
5.3 置信区间与陈旧检查:
- 预言机需要检查 updatedAt 时间戳,拒绝接受超过 heartbeat 的陈旧价格(最好不超过 30 秒)。
6. 资金费与借贷费
由于链上无法每秒循环所有用户扣费,系统采用“累计指数”算法进行惰性结算,并以此激励用户往驱使流动性平衡的方向进行交易。
6.1 借贷费:
- 定义:占用资金池流动性所需支付的利息。
- 计算:BorrowRate = UtilizationRate * RateFactor。
- 累计逻辑:系统维护一个全局变量 cumulativeBorrowFeePerToken。每当有人操作仓位时,系统计算 deltaTime * borrowRate 并累加到全局变量。
6.2 资金费:
- 定义:平衡多空力量的费用。多头多于空头时,多头付给空头(或协议),反之亦然。
- 计算:FundingRate = (Longs - Shorts) / PoolSize * Factor。
6.3 用户结算流程:
- 入场记录:用户开仓时,记录当前的 entryFundingIndex = globalCumulativeIndex。
- 持有期间:globalCumulativeIndex 随时间递增。
- 离场结算:用户平仓或调整仓位时,需支付的费用 = PositionSize * (currentGlobalIndex - entryFundingIndex)。
- 扣费顺序:费用直接从用户的抵押品中扣除。如果抵押品不足以支付费用,则触发清算。
关键机制深度解析
A. 创建订单与触发方式
两步执行(Two-Step Execution):
- Commit:用户发送交易,扣除抵押品,记录 Request key。此时暂不确定成交价格,仅锁定预期操作意愿。
- Execute:Keeper 获取最新预言机价格,调用执行函数。系统对比 minOut 或 triggerPrice,若满足则更新到用户的仓位中,否则取消订单。
由 Keeper 执行可以防止用户看到预言机更新交易后,在同一区块内抢跑在低价时创建订单再高价时平仓来套利(但是仍要注意 Keeper 作恶或者被黑的风险)。
B. 止盈止损(TP/SL) 逻辑
止盈止损本质上是“指定条件触发的减仓单”。
- 逻辑:用户预设指定的止盈止损触发价格(triggerPrice) 和触发方向(triggerAboveThreshold)。
- 多单止盈:triggerPrice > averagePrice,triggerAboveThreshold = true。
- 多单止损:triggerPrice < averagePrice,triggerAboveThreshold = false。
- 执行:Keeper 轮询链上订单,一旦预言机喂价满足条件(如 markPrice >= triggerPrice),即触发减仓逻辑。
C. 爆仓清算
当用户保证金率低于维持保证金率(Maintenance Margin) 时触发。
- 触发条件:(Collateral - Losses) / PositionSize < MaintenanceMarginRatio。
- 过程:
- Liquidator(帮助清算用户仓位的角色)调用清算函数例如 liquidatePosition。
- 合约验证标记价格下用户当前仓位的保证金率。
- 若验证通过,仓位被强制关闭。
- 剩余抵押品扣除 Liquidation Fee(支付给清算人)后,剩余部分归入资金池(作为 LP 的收益)。
D. 自动减仓(ADL)
在极端行情下,若资金池发生穿仓(即用户亏损超过其抵押品,且资金池无法覆盖),系统可能触发 ADL。
- 机制:根据盈利百分比和杠杆倍数排名,强制平仓盈利最高的对手方仓位,以释放流动性弥补亏损。
交互流程
以下通过一个“市价开多”的例子,展示一个完整的创建订单到执行的交互流程:
- 用户发起创建订单操作:
- 调用 Router.createIncreasePosition(...)。
- 传入创建订单所需的参数,例如 amountIn(抵押品数额)、sizeDelta(杠杆后头寸大小)、acceptablePrice(预期接受最大滑点的触发价格)等。
- 结果:保证金代币被转入指定合约中(例如 Vault 或 OrderBook),根据用户参数生成订单数据并进行存储,与对应生成的 OrderKey 或 OrderId 映射关联,事件 CreateIncreasePosition 被抛出。
- Keeper 监听与执行订单:
- 链下脚本监听到 CreateIncreasePosition 事件。
- 等待预言机价格更新(或自行上传价格数据更新价格)。
- 执行订单进行交易撮合(例如调用 PositionManager.executeIncreasePosition)。
- 合约验证:
- 价格检查:获取当前 Mark Price。验证 Mark Price <= acceptablePrice(防止价差滑点过大超出用户的预期)。
- 杠杆检查:计算新状态下的杠杆率,确保不超过 MaxLeverage。
- 资金池检查:确保多头总持仓量以及可用流动性等未超过上限。
- 结算并更新状态:
- 对用户的仓位数据进行更新,例如增加 position.size 和 position.collateral,记录开仓方向。
- 扣除订单执行时的费用,例如开仓费、借贷费等。
- 增加资金池的流动性和储备状态(用于计算 AUM)。
- 结果:交易成功,Keeper 获得执行费(Execution Fee)。若失败,订单被取消,资金退回用户。
审计要点 Checklist
预言机与定价审计
- 喂价时效性验证:检查是否使用了 latestRoundData 而非过时的 latestAnswer。必须校验 updatedAt 时间戳,或检查预言机的价格更新心跳阈值,防止可以使用陈旧的价格来执行订单。
- 价格极值选取验证:系统应根据用户持仓方向选择对系统“最有利”的价格(例如用户做多时,系统应计算较低的标记价格来判断清算)。若逻辑反转,用户可利用价差无风险套利。
- 多源校验:确认标记价格计算是否包含 CEX 价格与 AMM 价格的对比逻辑,如 abs(cexPrice - ammPrice) > threshold 时启用保护模式。此外确保预言机使用多个可信源(如 Chainlink 和 Pyth),避免单一价格来源失效或被操纵。
- 精度处理:检查所有涉及 Token Decimals(如 USDC 为 6 位,ETH 为 18 位)与 USD 精度(通常 30 位)转换的乘除法,是否存在精度丢失。
- Decimal 动态获取和转换:检查是否动态获取并转换 Oracle decimals,而非硬编码(如 decimals-8);若使用 Pyth 预言机,还需额外动态获取置信度参数(conf) 并进行检查。
- Pyth Exponent 处理:对于 Pyth 预言机,确保正确处理负数 exponent(如 price * 10^abs(exp))。
- L2 Sequencer 适配:如果在 L2 部署,是否有针对指定链的 Sequencer Uptime 进行检查来防止宕机风险;并且需要检查 Sequencer 地址的正确性。
- TWAP 与瞬时价格检查:清算逻辑建议使用 TWAP 或中位数价格,防止闪电贷操纵瞬时价格触发恶意清算。
- 抵押品价格正常获取检查:在去中心化永续合约中,可能会允许多种稳定币当作抵押品,并默认这些抵押品的价格等于 1 美元,对交易品种的价格获取可能是用的美元计价。而实际上稳定币在极端行情的情况下可能会脱锚,其价格并不恒等于 1 美元,这就导致了协议可能会被利用这个价格差进行恶意清算或套利。因此在以美元计价的前提下,需要检查是否正确从预言机中获取到了抵押品的价格并与交易品种的价格进行转换。
...
订单管理审计
- 执行费覆盖检查:验证用户发送的 msg.value 是否足以支付 Keeper 的 Gas 费。特别注意 L2 上对 L1 数据费用的估算。
- ETH/Native Token 退款逻辑:若订单执行失败或取消,确保绑定的 ETH 能够全额退还给用户,防止资金滞留在合约中。
- 订单更新时的即时触发检查:当用户设置订单的止盈/止损(TP/SL) 价格时,需要检查止盈止损订单的价格以及方向,例如当试图为多单设置止盈时需要确保止盈价格超过当前市价和原订单触发价,当设置止损时需要确保止损价格低于当前市价和原订单触发价(空单则相反),否则订单会在创建后被立即平仓。
- 订单存在性校验:在执行或取消订单前,必须校验订单是否存在,防止重复执行或空指针操作。
- 订单所有权校验:修改或取消订单时,必须严格校验 msg.sender 是否为订单拥有者,防止恶意取消他人订单。
- 最小订单规模限制:设置 minOrderSize,防止攻击者通过大量微小订单(Dust Orders) 发起 DoS 攻击或耗尽 Keeper 资源。
- 订单参数边界检查:创建订单时检查滑点是否合理,triggerPrice 是否非零,防止无效订单阻塞队列。
- 杠杆计算中的溢出与精度丢失检查:在计算 (size price) / collateral 时,若 size price 超过 uint256 上限(虽然少见但可能通过构造恶意参数触发),或除法舍入导致的精度截断可能绕过最大杠杆限制。
...
仓位管理审计
- 杠杆限制:系统必须在开仓和加仓时强制检查 size/margin 不超过最大杠杆限制(MaxLeverage),防止极端行情造成穿仓风险。
- 检查仓位存在性和所有权验证:关键操作(如平仓)前需验证仓位存在且调用者为所有者或者 Keeper 等角色,防止操作无效仓位。
- PnL 计算:检查多空双方的 PnL 计算公式是否对称。注意:未实现盈亏(Unrealized PnL) 是否正确计入 AUM。
- 保证金扣除:检查 decreasePosition 时,扣除的保证金是否优先用于支付 Borrow Fee 和 Funding Fee,而非先返还用户;以及在清算或减仓逻辑中,需要检查扣除保证金后资金池中的账目与实际资产是否相符,防止资不抵债。
- 清算奖励激励不足:若清算费用(Liquidation Fee) 完全归协议所有或无法覆盖 Gas 费,Keepers 将拒绝执行清算交易,导致坏账累积。
- 市场平衡机制检查:如果进行大额的开仓或平仓交易会对盘口流动性造成冲击时或者出现开仓和平仓后会加剧当前多空失衡的情况,则应该在开仓或平仓时收取更多的费用来刺激市场偏向平衡稳定。
- 资金费累计检查:检查资金费的累计指数(Cumulative Index) 更新频率和精度,并确保在任何仓位变动前先更新全局指数。
- 借贷费扣除逻辑检查:在增加保证金时,不应错误地对原有仓位重复扣除已结算的借贷费。
- 维持保证金率检查:在提现保证金或进行加仓时需要仓位的维持保证金率是否健康,即确保 remainingCollateral >= size * MM_Ratio。
- 全局持仓上限检查:必须检查多空双方的全局持仓量(Open Interest) 是否超过流动性池的承载能力,超过则拒绝开仓。
...
交易逻辑审计
- PnL 计算一致性:检查做多和做空、开仓和平仓时的 PnL 计算公式是否在所有场景下(包括 ADL、清算)保持一致,避免双重扣除。
- 两步执行机制(Commit-Reveal):用户提交请求与 Keeper 执行请求必须分在不同区块(或有最小时间间隔),防止原子级抢跑进行套利(Front-running)。
- Gas 消耗风险检查:如果平仓时是由 Keeper 转账原生代币给用户,则需要对消耗的 Gas 进行验证,防止被恶意用户耗尽 Keeper 的 Gas 而阻塞其他交易活动进行。
- 签名重放检查:若使用签名进行链下撮合链上结算,必须检查 chainId、可自增的 nonce 或 deadline,防止同一签名被多次使用。
- 交易回调重入保护:若系统包含回调函数(如 executeOrderCallback),必须检查是否有防重入检查,避免在回调中再次操作仓位。
- ADL 逻辑独立性:如果 ADL 逻辑复用了常规的减仓函数,而该函数包含仓位健康度检查,而被 ADL 的仓位往往处于高风险边缘,所以会导致 ADL 交易失败,系统坏账无法消除。需要确保 ADL 执行路径应跳过常规的清算检查,并使用独立的减仓函数,确保在极端行情下能强制减仓。
- 订单修改即时性:修改订单(如价格、数量)时,必须立即用当前价格判定是否满足触发条件。
- 滑点执行保护:在 executeOrder 中,必须对比当前价格与用户设置的 acceptablePrice,若超出滑点范围应取消订单而非强行执行。
- 自动减仓(ADL) 机制的正常触发:ADL 应在流动性不足时触发,且需正确检查对手仓位状态,防止错误匹配。
...
资金池与 LP 审计
- AUM 计算:审查 getAum 函数。是否区分了 maxPrice(用于铸造 LP)和 minPrice(用于销毁 LP)以防止套利。计算 LP 价格时,需包含“未实现盈亏”。
- 直接转账干扰记账:如果合约依赖 token.balanceOf(address(this)) 来判断用户实际存入金额,而非使用内部记账数据,攻击者可能通过直接向合约转账干扰记账逻辑(例如导致 feeReserves 计算错误)。
- 闪电贷操纵防御:禁止在同一笔交易中铸造并销毁 LP Token(添加冷却期 Cooldown 或区块限制),防止利用预言机更新间隙套利。
- 资金隔离:确保费用储备金(Fee Reserves) 与交易对的流动性池资金在逻辑上严格分离,防止用户提取了本应属于协议收入的资金。
- 重入保护:所有涉及资金转出的函数(如 removeLiquidity, decreasePosition)是否使用了 nonReentrant 修饰符。
- ERC4626 利率通胀攻击:检查是否有“最小流动性锁定”(mint 1000 wei burn) 或“虚拟偏移量”机制,防止首笔存款通胀攻击。
- USDT 兼容性:使用 SafeERC20 库处理转账,确保兼容无返回值的代币。
- 代币白名单:检查是否允许 Rebase 或通缩代币进入。若允许,检查系统是否基于 balanceOf 差值记账而非参数金额。
- 入口唯一性:检查底层合约(Vault/PositionManager) 的关键写操作是否只允许 Router 或 Executor 调用,防止绕过费率/价格检查。
...
治理升级与权限审计
- Timelock 完备性:关键参数(如 setGov, withdrawFees)修改是否强制经过 Timelock 且有合理延迟(如 24 小时)。
- 存储槽冲突检查:如果合约是可升级的(Proxy 模式),检查存储槽(Storage Layout)是否冲突。
- 验证治理提案的阈值:治理决策需满足法定人数和延迟期,避免闪电投票操纵。报告发现参数设置不当可能使治理无效。
- 验证是否可以重复投票:检查投票函数中是否限制一个成员只能进行一次投票或投票用的代币可以进行转移后再次进行投票。
- 初始化函数保护:确保 initialize 函数有 initializer 修饰符,且不能被多次调用。对于实现合约(Implementation),应在构造函数中禁用初始化。
- 权限转移的两步确认:setOwner 或 setGov 应采用 pendingOwner -> acceptOwner 的两步机制,防止将权限误转给错误地址。
- Copy Trading 风险:若支持跟单功能,检查是否验证了“操作者”拥有“被操作账户”的授权,防止未授权的跟单操作。
- 资金提取权限:IDO 或众筹模块是否遗漏了资金提取(Withdraw) 函数,或者提取逻辑被永久锁定。
- 委托-转发机制逻辑检查:如果协议具有委托给其他角色并由其转发交易的机制,则需要检查被委托者与转发者的权限,以及需要检查转发的数据。例如 from 地址是否可以被外部调用恶意构造,导致非法对其他用户的账户进行交易。
- 角色权责分离:Keeper(执行者)、Liquidator(清算人)、Admin(管理员)权限应严格区分。例如,Keeper 不应有权修改预言机地址。
- 敏感参数范围限制:setMaxLeverage,setFees,setFundingRateFactor 等函数必须有硬编码的上下限(如费用上限不超过 10%),防止管理员恶意或误操作。
- 关键地址零值检查:setGov,setVault 等函数必须检查输入的系统合约地址参数设置正确而非 address(0) 或其他错误的地址,防止权限丢失。
- 铸币/销毁权限审计:检查衍生代币的 mint/burn 函数,确保仅允许特定的合约调用。
- 紧急暂停功能:协议应具备暂停功能,且暂停不应阻止用户提取抵押品或追加保证金(防止爆仓)。
- 核心功能权限检查:对于价格更新、订单更新、仓位更新、提取流动性代币等核心函数需要严格检查权限,禁止外部可以随意更改影响其他用户的数据。
...
其他
- Solidity 版本锁定:使用锁定的 Solidity 版本(如 0.8.19 而非 ^0.8.0),避免编译器版本差异引入 Bug。
- 数学溢出检查:即使 Solidity 0.8+ 自带溢出检查,在涉及 unchecked 块或类型转换(如 int256 转 uint256)时需格外注意,防止溢出。
- 循环 gas 限制(DoS):例如在批量清算或结算奖励时,避免对无上限的数组进行遍历,防止 Gas Limit 耗尽导致功能永久失效。
- 事件日志正确性检查:所有状态变更(特别是涉及资金和参数)必须释放 Event,且包含正确的参数,便于链下索引。
- 外部调用返回值检查:对任何 address.call{value: ...} 的低级调用,必须检查返回值 success。
- 冗余代码清理:移除未使用的变量、函数和引入的第三方合约,减少攻击面和部署成本。
- 奖励分发计算:在奖励分发中,检查 rewardPerToken 是否在 totalSupply == 0 时正确处理,防止除零错误。
- 推荐人机制(Referral) 循环:防止用户将推荐人设置为自己(Self-referral) 或建立循环推荐关系(A->B->A) 进行刷量。
...
结论
去中心化永续合约的安全性不仅依赖于 Solidity 代码的健壮性,更取决于金融逻辑的闭环设计。从预言机喂价的毫秒级延迟,到资金池 AUM 计算的微小偏差,任何环节的疏漏都可能被“闪电贷”放大为致命的经济攻击。
审计师应采取对抗式思维,不仅仅验证代码是否符合预期设计,更要假设自己是真实的攻击者,试图通过操纵价格、利用舍入误差或抢跑交易等方式来从协议中套利,从而发现更深层的风险并协助项目方进行修复。
持续监控建议:鉴于 DeFi 乐高属性,建议项目方在主网部署后,建立自动化的链上监控系统或者使用 SlowMist 推出的 MistEye 产品,作为静态代码审计的必要补充。此外,对于正在研究或部署此类复杂协议的开发者来说,深入阅读和参与云栈社区的相关技术文档讨论,或者借鉴优秀的开源实战项目,都是构建安全防线、提升认知的重要途径。