作者:Polaris,ScaleBit 研究团队,来源:ScaleBit
TL;DR
本文深入研究了 DeFi 杠杆的多样性和应用场景,详细分析了代码层面的漏洞,同时提出了杠杆协议关键的安全要点。
DeFi 杠杆简介
最近 dYdX V4 的推出引发了大家对永续合约交易所的大量关注和参与。dYdX 成功应用了杠杆交易的案例,而我们不仅应该期待 dYdX V4 的巨大潜力,更要注重杠杆协议的安全性。接下来,我们将会结合具体的代码分析和示例带领大家熟悉不同的杠杆策略和安全考量。
什么是杠杆
在金融领域,杠杆是一种策略,依赖于借入资金以增加投资的潜在回报。简而言之,投资者或交易员借入资金,以放大对特定类型的资产、项目或工具的敞口,远远超过仅依赖自己的资本所能达到的程度。通常情况下,通过使用杠杆,投资者能够在市场上放大其购买力。
DeFi 交易中的杠杆
使用杠杆是加密资产交易中最重要且常见的特性之一。在去中心化交易所成立后不久,尽管加密市场已经表现出高度波动性,但使用杠杆进行交易变得越来越流行。
与传统金融一样,交易员使用杠杆要么仅仅是为了借入资金以增加其购买力,要么是为了利用各种金融衍生品,如期货和期权。
杠杆比例也从 3 倍、5 倍增加到超过 100 倍。更高的杠杆意味着更高的风险,但正如大多数中心化交易所所见,随着杠杆交易量的增长,这是寻求更高回报的激进交易员愿意承担的风险。
杠杆分类详解
就 DeFi 而言,杠杆产品主要分为四种类型,其产生杠杆的机制各不相同:杠杆借贷,保证金交易杠杆,永续合约杠杆,杠杆代币。
杠杆借贷
DeFi 借贷和借出是最早也是最大的 DeFi 应用之一,市场上已经有像 MakerDao、Compound、AAVE、Venus 等巨头在运营。通过借贷加密资产获取杠杆的逻辑很简单。
例如,如果你持有 1 万美元的以太币(ETH)并看涨,你可以将你的 ETH 作为抵押存入 Compound,并借出 5,000 美元的 USDC,然后用这 5,000 美元的 USDC 交易换取另外 5,000 美元的 ETH。这样你将在 ETH 上获得 1.5 倍杠杆,相比于你最初的 1 万美元资本,你将获得 1.5 万美元的 ETH 敞口。
同样,如果你看跌,你可以选择存入稳定币并借出 ETH。如果 ETH 价格下跌,你可以以更低的价格在市场上购买 ETH 并偿还债务。
需要注意的是,由于你将从一个去中心化协议中借款,如果抵押物的价值下降或你所借资产的价值超过一定阈值,你可能会被清算。
保证金交易杠杆
通过 DeFi 借贷,你可以用这些数字资产做你想做的事。DeFi 保证金交易更注重增加头寸规模(增加购买力),被认为是真正的“杠杆头寸”。然而,有一个重要的区别——在保证金头寸仍然开放时,交易者的资产充当了借款资金的抵押品。
dYdX 是知名的去中心化保证金交易平台,允许最高杠杆为 5 倍。在 dYdX 的保证金交易中,交易者使用自己的资金作为担保,将其原始本金放大数倍,并使用这些放大的资金进行更大规模的投资。
交易者需要支付利息费用以及与交易相关的费用。该头寸不是虚拟构造的,它涉及到实际的借款和购买/卖出。
如果市场朝不利的方向发展,交易者的资产可能无法完全偿还借款。为防止这种情况发生,协议将在达到一定的清算比例之前清算你的头寸。
对于保证金交易中杠杆如何变化——
假设你在保证金交易中看涨 ETH 3 倍,但又不愿意时常调整敞口。
你持有 100 美元的 USDC,借入另外 200 美元的 USDC,用于交易 300 美元的 ETH 以建立所需的 ETH 多头头寸。杠杆水平为 300 美元 / 100 美元 = 3 倍。
如果 ETH 的价格上涨 20%,你的利润将为 300(1+20%)-300 = 60 美元。你相对于被清算的风险更低,而实际的杠杆水平降低为 360/(360-200)= 2.25 倍。换句话说,你在 ETH 价格上涨时会自动减杠杆。
如果 ETH 的价格下跌 20%,你的亏损将为 300(1-20%)-300 = -60美元。在涉及清算的方面,你处于更危险的位置,而实际的杠杆水平会自动增加为 240/(240-200)= 6 倍。换句话说,你在 ETH 价格下跌时会重新平衡杠杆,这表明你比之前处于更高的风险位置。
因此,尽管你可能认为通过进行固定的 3 倍保证金交易,可以保持不变的杠杆,但实时的杠杆是不断变化的。请查看下图,了解根据价格变动杠杆将会如何变化。
永续合约杠杆
永续合约类似于传统的期货合约,但没有到期日。永续合约模仿基于保证金的现货市场,因此交易接近基础参考指数价格。
有许多 DeFi 项目为交易者提供永续合约,如 dYdX、MCDEX、Perpetual Protocol、Injective 等。许多交易者可能很难区分在保证金交易和永续合约上的区别 —— 实际上,它们都涉及用户的杠杆。
然而,在杠杆机制、费用和杠杆水平上存在一些差异。
永续合约是一种交易合成资产的衍生产品,具有按保证金进行交易的特性。对基础资产价格的跟踪以一种合成的方式进行,无需交易实际的基础资产。但是,保证金交易涉及实际借款和实际加密资产的交易。
随着永续合约的出现,出现了资金费率的概念,其目的是保持永续合约的交易价格与基础参考价格保持一致。如果合约价格高于现货价格,则多头将支付空头。换句话说,交易者需要不断为借款支付费用。
永续合约中的杠杆通常比保证金交易中的杠杆更高,可以高达 100 倍。清算和实际杠杆机制与保证金交易相同。
杠杆代币
杠杆代币是一种衍生品,为持有者提供对加密货币市场的杠杆敞口,而无需担心积极管理杠杆头寸。虽然它们为持有者提供了杠杆敞口,但不要求他们处理保证金、清算、抵押品或资金费率。
杠杆代币与保证金交易/永续合约的最大区别在于,杠杆代币将定期或在达到一定阈值时重新平衡,以保持特定的杠杆。
这显然与保证金交易和永续合约不同 - 这些产品的实际杠杆根据价格波动不断变化,即使交易者最初可能指定了一个杠杆水平。
让我们看一下上面的 3 倍 ETH 示例中重新平衡的工作方式:
你持有 100 美元的 USDC 并购买一个 ETHBULL(3 倍)杠杆代币。协议将自动借入 200 美元的 USDC 并交易 200 美元的 ETH。
假设 ETH 的价格上涨了 20%,而 ETHBULL(3 倍)代币价格在重新平衡之前上升到 300*(1+20%)-200 = 160 美元。现在,你的实际杠杆变为 2.25(360/160),低于目标杠杆。
在重新平衡过程的一部分,协议将从稳定币池中借入更多的美元,并购买额外的 ETH 代币,以将杠杆调回到 3 倍。在我们的示例中,协议将再借入 120 美元并将其兑换成 ETH。因此,总杠杆再次变为(360+120)/160 = 3 倍。
假设 ETH 的价格下跌了 20%,而 ETHBULL(3 倍)代币价格在重新平衡之前下降到 300*(1-20%)-200 = 40 美元。现在,你的实际杠杆将变为 6(240/40),高于目标杠杆。
在这种情况下,协议将出售 ETH 代币并偿还未偿还的债务以降低杠杆。在这个例子中,协议将出售 120 美元的 ETH 以支付给池。债务将变为 80 美元,总杠杆再次为(240-120)/40 = 3 倍。
换句话说,杠杆代币将在盈利中自动重新杠杆,而在亏损中去杠杆,以恢复其目标杠杆水平。如果这个机制运作良好,即使在不利的市场趋势中,杠杆代币持有者也不会被清算,因为去杠杆机制将不断降低用户的有效杠杆水平。
因此,在杠杆代币模型中的借贷池将免于清算风险,比保证金交易中的借贷池更安全。
杠杆应用案例
我们已经了解了一些杠杆的常见 DeFi 协议类型,接下来我们结合具体的 DeFi 协议详解杠杆的应用。
GMX
GMX 是一个去中心化的现货和永续交易所,为交易者提供高达 50 倍杠杆的资产交易能力。该协议目前在 Arbitrum 和 Avalanche 上运行。在 GMX 上,交易者完全了解对手方的情况,这与在 CEX 上交易完全不同。GMX 与其他永续合约协议如 dYdX 不同,它完全在链上运作,并使用 AMM 功能来实现杠杆交易。
GMX 与其他服务的不同之处在于它是一个提供杠杆交易服务的去中心化交易所。在这方面,它将类似于 Uniswap 等其他 DeFi 交易所的体验与 Binance 等提供的杠杆交易服务相结合。
GMX 有一个流动性池 GLP,这是一个为保证金交易提供流动性的多资产池:用户可以通过铸造和销毁 GLP 代币来做多/ 做空和执行交易。该池从交易和杠杆交易中赚取 LP 费用,这些费用会分配给 GMX 和 GLP 持有人。
为了进行杠杆交易,交易者将抵押品存入协议中。交易者可以选择最高 50 倍的杠杆,杠杆越高,清算价格越高,随着借贷费用的增加,清算价格将逐渐增加。
例如,当做多 ETH 时,交易者正在从 GLP 池中“租出”ETH 的上行空间;当做空 ETH 时,交易者正在从 GLP 池中“租出”稳定币相对于 ETH 的上涨空间。但 GLP 池中的资产实际上并没有被租出。
平仓时,如果交易者押对了,利润将从 GLP 池中以代币做多的形式支付;否则,损失将从抵押品中扣除并支付到池中。GLP 从交易者的损失中获利,并从交易者的利润中获利。
在此过程中,交易者支付交易费、开仓/平仓费和借入费,以换取对美元做多/做空指定代币(BTC、ETH、AVAX、UNI 和 LINK)的上行空间。
Merkle Trade
Merkle Trade 是一个去中心化的交易平台,提供加密货币、外汇和大宗商品交易,杠杆率高达 1,000 倍,并提供以用户为中心的高级交易功能。Merkle Trade 由 Aptos 区块链提供支持,具有一流的性能和可扩展性。相比 Gains Network 在提供同样高杠杆的情况下,具有更低的交易延时和手续费。
与大多数交易所不同,Merkle Trade 上没有订单簿。相反,Merkle LP 充当每笔交易的交易对手,当交易者亏损时,它收取抵押品,并在具有正收益的封闭交易上支付利润。
- 交易加密货币、外汇和大宗商品,杠杆高达 1,000 倍
Merkle Trade 旨在从一开始提供广泛的交易对,包括加密货币、外汇和大宗商品,并提供市场上一些最高的杠杆;在加密货币上最高可达 150 倍,在外汇上最高可达 1,000 倍。
- 公平价格的订单执行,毫秒级延迟,最小滑点
借助迄今为止最低延迟的 Aptos 产生区块链,能够提供最快的链上交易体验。对于交易者来说,这意味着更迅速的交易体验,由于执行延迟而导致的价格滑点更小。
- 去中心化、非托管的交易,无交易对手风险
交易者与流动性池(Merkle LP)进行交易,该流动性池充当协议上每笔交易的交易对手。所有交易和结算都由智能合约执行,任何时候都没有用户资金的托管。
- 最低手续费
Merkle Trade 宣称在市场上拥有迄今为止最低的手续费之一。在推出时,加密货币交易对的手续费低至 0.05%,外汇交易对的手续费低至 0.0075%。
dYdX
dYdX是一个去中心化交易所(DEX),赋予用户在完全掌控资产的同时高效交易永续合约的能力。自 2021 年上线以来,dYdX V3 采用了独特的非托管第二层扩展解决方案来实现其交易所,但其订单簿和撮合引擎仍然由中心化管理。
而现在,通过 dYdX V4,该协议正在发展成为自己的链,并全面重构整个协议以实现完全去中心化,同时提高吞吐量。dYdX 同时包含借贷、杠杆交易与永续合约三种功能。杠杆交易自带借贷功能,用户存入的资金自动组成资金池,交易时若资金不足,则自动借入并支付利息。
杠杆安全分析
我们介绍了杠杆在 DeFi 中常见类型和应用,同样在杠杆的设计中依然存在很多的安全问题值得我们注意,我们将结合具体的审计案例分析 DeFi 杠杆的安全问题和审计点。
区分限价单和市价单
在大多数杠杆的交易所应用中,都存在限价单和市价单,对限价单和市价单的严格区分和校验是很有必要的。接下来,将以我们在 Merkle Trade 审计中发现的问题来做详细分析。
let now = timestamp::now_seconds(); if (now - order.created_timestamp > 30) { cancel_order_internal<PairType, CollateralType>( _order_id, order, T_CANCEL_ORDER_EXPIRED ); return };
该部分代码是执行订单函数中的校验,在该函数中,它会检查订单创建后是否已超过 30 秒。如果满足条件,则调用 cancel_order_internal() 取消订单。但是,如果订单是限价订单,则意味着订单有一个由交易者设定的具体价格,他们愿意在该价格买入或卖出资产。在执行限价单的时候不应有该判断,这可能导致大多数限价单无法得到执行。因此严格区分限价单和市价单的交易逻辑是很重要的。
杠杆计算错误
计算错误一直是 DeFi 中很常见的问题,在杠杆中也尤为常见,我们将以 Unstoppable[6] 协议在第三方审计中发现的问题,来深入研究杠杆的计算问题。
让我们来看 Unstoppable 中计算杠杆的代码:
def _calculate_leverage(
_position_value: uint256, _debt_value: uint256, _margin_value: uint256
) -> uint256:
if _position_value <= _debt_value:
# bad debt
return max_value(uint256)
return (
PRECISION
* (_debt_value + _margin_value)
/ (_position_value - _debt_value)
/ PRECISION
)
_calculate_leverage 函数通过使用 _debt_value + _margin_value 作为分子而不是 _position_value,导致错误地计算了杠杆。该函数的三个输入参数 _position_value、_debt_value 和 _margin_value 都是由 Chainlink 链上预言机提供的价格信息决定的。其中,_debt_value 表示将仓位的负债份额转换为美元债务金额的价值。_margin_value 表示仓位初始保证金金额的当前价值(以美元计)。_position_value 表示仓位初始仓位金额的当前价值(以美元计)。
以上计算的问题在于 _debt_value + _margin_value 不代表仓位的价值。杠杆是当前仓位价值与当前保证金价值之间的比率。_position_value - _debt_value 是正确的,它表示当前的保证金价值,但 _debt_value + _margin_value 并不代表仓位的当前价值,因为不能保证负债代币和仓位代币有相关的价格波动。
举例说明:负债代币为 ETH,仓位代币为 BTC。
Alice 使用 1 个 ETH 作为保证金,借入 14 个 ETH(每个 ETH 2,000 美元)并获得 1 个 BTC(每个 BTC 30,000 美元)的仓位代币。杠杆为 14。
第二天,ETH 的价格仍为 2,000 美元/ETH,但 BTC 的价格从 30,000 美元/BTC 下跌到 29,000 美元/BTC。此时,杠杆应为(_position_value == 29,000)/(_position_value == 29,000 - _debt_value == 28,000)= 29,而不是合约中计算的值:(_debt_value == 28,000 + _margin_value == 2,000)/(_position_value == 29,000 - _debt_value == 28,000)= 30。
因此,为了修复这个问题,应该使用上述提到的正确的公式来计算智能合约中的杠杆。杠杆计算错误可能导致不公平的清算或者在价格波动的情况下出现过度杠杆的仓位。
在智能合约中,确保正确的杠杆计算对于维护系统的稳健性和用户的利益至关重要。正确的杠杆计算应该基于仓位的当前价值和当前保证金价值之间的比率。如果使用了错误的计算公式,可能会导致系统对于价格变动的反应不当,可能会清算不应该被清算的仓位,或者允许过度杠杆的仓位继续存在,从而增加系统和用户的风险。
逻辑错误
逻辑错误在智能合约的审计中尤其需要重视,特别是在 DeFi 杠杆交易等复杂逻辑中。
让我们以 Tigris(Tigris 是一个基于 Arbitrum 和 Polygon 的去中心化合成杠杆交易平台)在第三方审计中发现的问题,来讨论 DeFi 杠杆中需要注意的逻辑问题。
让我们来看 Tigris 中的限价平仓函数逻辑:
function limitClose(
uint _id,
bool _tp,
PriceData calldata _priceData,
bytes calldata _signature
)
external
{
_checkDelay(_id, false);
(uint _limitPrice, address _tigAsset) = tradingExtension._limitClose(_id, _tp, _priceData, _signature);
_closePosition(_id, DIVISION_CONSTANT, _limitPrice, address(0), _tigAsset, true);
}
function _limitClose(
uint _id,
bool _tp,
PriceData calldata _priceData,
bytes calldata _signature
) external view returns(uint _limitPrice, address _tigAsset) {
_checkGas();
IPosition.Trade memory _trade = position.trades(_id);
_tigAsset = _trade.tigAsset;
getVerifiedPrice(_trade.asset, _priceData, _signature, 0);
uint256 _price = _priceData.price;
if (_trade.orderType != 0) revert("4"); //IsLimit
if (_tp) {
if (_trade.tpPrice == 0) revert("7"); //LimitNotSet
if (_trade.direction) {
if (_trade.tpPrice > _price) revert("6"); //LimitNotMet
} else {
if (_trade.tpPrice < _price) revert("6"); //LimitNotMet
}
_limitPrice = _trade.tpPrice;
} else {
if (_trade.slPrice == 0) revert("7"); //LimitNotSet
if (_trade.direction) {
if (_trade.slPrice < _price) revert("6"); //LimitNotMet
} else {
if (_trade.slPrice > _price) revert("6"); //LimitNotMet
}
//@audit stop loss is closed at user specified price NOT market price
_limitPrice = _trade.slPrice;
}
}
在使用止损平仓时,用户的平仓价格是其设定的止损价,而不是资产的当前价格。在方向性市场和高杠杆的情况下,用户可能会滥用这一点,实现几乎无风险的交易。用户可以开设一个做多仓位,并设置一个止损价格,该价格比当前价格低 $0.01。
如果在下一次更新中价格立即下跌,他们将以他们的入场价格平仓,只需支付开仓和平仓手续费。如果价格上涨,他们则有可能获得大额收益。以致于用户可以滥用止损的价格设定方式,以开设高杠杆、上行潜力大、下行风险小的交易。
价格波动
价格波动对 DeFi 杠杆的影响是很重要的,时刻考虑价格波动才能保证杠杆协议的安全。让我们以 DeFiner[8] 协议在第三方审计中发现的问题作为例子深入分析:
DeFiner 协议在处理提款之前进行两次检查。
首先,该方法检查用户请求提取的金额是否超过了该资产的余额:
function withdraw(address _accountAddr, address _token, uint256 _amount) external onlyAuthorized returns(uint256) {
// Check if withdraw amount is less than user's balance
require(_amount <= getDepositBalanceCurrent(_token, _accountAddr), "Insufficient balance.");
uint256 borrowLTV = globalConfig.tokenInfoRegistry().getBorrowLTV(_token);
其次,该方法会检查提款是否会使用户的杠杆率过高。提取的金额会从用户当前价格的 "borrow power" 中减去。如果用户的借贷总值超过了新的 borrow power,则方法失败,因为用户不再有足够的抵押品来支持其借贷头寸。不过,只有在用户尚未过度杠杆化的情况下,才会检查此 require:
if(getBorrowETH(_accountAddr) <= getBorrowPower(_accountAddr)) require( getBorrowETH(_accountAddr) <= getBorrowPower(_accountAddr).sub( _amount.mul(globalConfig.tokenInfoRegistry().priceFromAddress(_token)) .mul(borrowLTV).div(Utils.getDivisor(address(globalConfig), _token)).div(100) ), "Insufficient collateral when withdraw.");
如果用户借入的资金已超过其 "borrow power" 所允许的数额,则无论如何都可以提款。这种情况可能出现在多种情况下,最常见的是价格波动。该协议并没有考虑价格波动对协议的影响,所以造成了这个问题。
其他
除了上面提到的没有区分限价单和市价单、计算错误、逻辑错误、价格波动的影响,还有许多与杠杆协议相关的安全要点需要我们注意。这包括但不限于闪电贷攻击、价格操纵、预言机安全、权限控制、杠杆检查不够严格或缺少检查等问题。在设计和实施杠杆协议时,必须细致审慎地考虑这些因素,以确保协议的健壮性和用户资产的安全。预防措施、实时监控和紧急响应计划也是关键,以降低潜在风险并保障用户利益。
总结
杠杆交易的引入在 DeFi 协议中确实为市场提供了更大的可操作性,同时也带来了更复杂的交易机制。尽管杠杆交易为用户提供了更多的投资机会,但其潜在风险和对协议安全性的挑战也变得更加显著。
随着杠杆的增加,协议的操作变得更为灵活,但也因此变得更加脆弱,更容易受到各种安全威胁的影响。这包括潜在的没有严格区分限价单和市价单、计算错误、逻辑错误,以及对价格波动等因素的极端敏感性。在这种情况下,我们必须更加关注协议的安全性,以确保用户的资产得到有效保护。