Appearance
L3-4: Shorting(做空市场)
1. 问题
做空是金融市场表达看跌观点的基础操作,但链上实现比传统金融更复杂。传统做空流程:借入股票 → 卖出 → 等价格下跌 → 回购 → 归还,利润来自价差。在 DeFi 中,这个过程需要完全在链上执行:用户存入抵押品(如 USDC)、借入目标代币(如 ETH)、在 DEX 上卖出,当价格下跌后回购并归还。
核心设计挑战在于:(1) 如何确保借出的代币能够偿还(超额抵押);(2) 当价格反向波动时如何触发清算;(3) 清算机制如何设计以防止坏账。这些问题在永续合约协议(如 GMX、dYdX)中同样存在,是做空机制的核心。
2. 原因
做空不仅是投机工具,更是市场效率的必要组件。做空者提供流动性、加速价格发现、抑制泡沫。在 DeFi 中,理解做空机制使你能够构建对冲策略、参与永续合约市场、甚至设计合成资产。Aave 上的杠杆做空、GMX 的永续合约,以及 Ethena 的 delta 中性策略,都建立在做空原语之上。
从合约设计角度看,做空市场是一个优秀的仓位管理系统练习:它涉及抵押品管理、价格预言机集成、清算阈值计算、以及盈亏实时结算。这些模式是所有保证金交易和永续合约协议的基础。
3. 方案
ShortingMarket.sol 实现了完整的做空市场,核心流程为:
开仓(Open Short):openShort() 接收两个参数——抵押品数量(_collateral)和借入数量(_borrowAmount)。然后验证抵押率是否满足最低要求(MIN_COLLATERAL_RATIO = 150%)。计算公式:
borrowValue = borrowAmount * currentPrice / 1e18
minCollateral = borrowValue * 150 / 100
验证通过后,用户存入抵押品并获得借出的代币(这些代币将在外部 DEX 上被卖出)。
平仓(Close Short):closeShort() 计算盈亏。如果价格下跌,做空者获利:
borrowValueAtOpen = borrowAmount * openPrice / 1e18
borrowValueNow = borrowAmount * currentPrice / 1e18
profit = borrowValueAtOpen - borrowValueNow // 价格下跌时为正
用户需要归还借出的代币,然后取回抵押品 + 利润(或抵押品 - 亏损)。
清算(Liquidate):liquidate() 在抵押率低于 LIQUIDATION_COLLATERAL_RATIO = 120% 时被触发。清算人需要提供借出的代币来覆盖仓位,并获得抵押品 + 10% 的清算奖励。
预言机:currentPrice 由合约 owner 手动设置(setPrice()),作为简化设计。生产环境中应使用 Chainlink 等去中心化预言机。
参考源文件:src/level3/ShortingMarket.sol
4. 遭遇的陷阱
- 价格操纵与预言机选择:
currentPrice是手动设置的单一价格,没有 TWAP 或去中心化验证,攻击者可能在设置价格的瞬间利用窗口进行攻击 - 清算时抵押品不足:当价格剧烈波动(如闪电崩盘)时,抵押品可能不足以支付清算奖励,导致坏账
- 借入的代币无法获取:
openShort()中合约直接将borrowToken转给用户,但合约地址需要持有足够的借入代币——这在设计上意味着需要预先注资,或者依赖外部流动性池 - 仓位 ID 被操控:使用
positions.length作为 ID,在极端情况下可能出现 ID 冲突(如通过 delegatecall) - 盈亏计算中的精度丢失:在
closeShort()中,多次除以1e18可能导致小额取整误差积累
5. 陷阱的原因
价格预言机是做空市场最脆弱的一环。在 ShortingMarket.sol 的当前设计中,currentPrice 由 owner 调用 setPrice() 来更新。这引入了中心化信任——如果 owner 恶意设置错误的价格(如将暴跌中的资产价格设置得过高),他们可以阻止合法清算,或让不健康的仓位被人为地显示为健康。
更深层的问题在于流动性:当市场剧烈波动时,链上 DEX 的滑点可能极大。即使合约正确计算了清算价格,清算人在执行清算时可能因为 DEX 上缺乏流动性而无法真正实现套利。生产级的做空市场通常使用预言机 + 链下做市商的组合来缓解这个问题。
借入代币的来源问题也值得注意:合约在 openShort() 中直接 transfer(borrowToken) 给用户,这意味着合约地址必须预先持有这些代币。在实际应用中,这需要一个流动性池(类似于 Aave 的 aToken 池)。做空的"借入"通常有两种实现方式:(1) 从借贷池中借出(如 Aave 的闪电贷模式);(2) 合成资产模式(如 GMX 的 GLP 池),用户不是在真实市场上卖出,而是与协议对赌。
6. 如何解决陷阱
预言机安全:替换手动 setPrice() 为 Chainlink 价格源:
solidity
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
AggregatorV3Interface public priceFeed;
function getCurrentPrice() public view returns (uint256) {
(, int256 price,,,) = priceFeed.latestRoundData();
return uint256(price);
}
防清算中的坏账:在清算中增加 "cover remains" 逻辑——如果清算后的抵押品不足以完全覆盖债务,差额仍记录为坏账。在实际设计中,可以使用保险基金(Insurance Fund)来吸收坏账,或引入逐步清算(partial liquidation)机制,在仓位接近清算线时就允许部分清算。
流动性保障:对于借入代币的来源,推荐使用资产池模式——用户存入的抵押品形成池子,做空者从池子中借出代币。这创造了一个自然的借贷市场,借入的利息由市场供需决定。
精度一致性:统一所有价格计算中的精度单位——当前使用 1e18 缩放,确保在乘除过程中使用相同的缩放因子,避免 (a * b / c) * d / e 这种多次除法导致精度损失。推荐先乘后除:(a * b * d) / (c * e)。
7. 技术要点
| 技术点 | 说明 |
|---|---|
| 做空本质 | 借入 → 卖出 → 价格下跌 → 回购 → 归还 |
| 最低抵押率 | 150%(MIN_COLLATERAL_RATIO) |
| 清算抵押率 | 120%(LIQUIDATION_COLLATERAL_RATIO) |
| 清算奖励 | 10%(LIQUIDATION_BONUS) |
| 盈亏计算 | borrowValueAtOpen - borrowValueNow |
| 预言机 | 手动设置(简化),生产用 Chainlink |
| 仓位追踪 | userPositions 映射 + positions 数组 |
| 状态管理 | active 标志位 + PositionNotActive 错误 |