慢雾:Opyn 合约被黑详细分析
By : Kong @慢雾安全团队
2020 年 8 月 5 日,Opyn 合约遭遇黑客攻击。慢雾安全团队在收到情报后对本次攻击事件进行了全面的分析,下面为大家就这次攻击事件展开具体的技术分析。
攻击细节
逻辑分析
看其中一笔攻击交易:
https://etherscan.io/tx/0xa858463f30a08c6f3410ed456e59277fbe62ff14225754d2bb0b4f6a75fdc8ad
通过查看内联交易可以看到攻击者仅使用 272ETH 最终得到 467ETH
使用 OKO 合约浏览器对具体的攻击细节进行分析
https://oko.palkeo.com/0xa858463f30a08c6f3410ed456e59277fbe62ff14225754d2bb0b4f6a75fdc8ad/
关键点在于 oToken 合约的 exercise 函数,从上图中可以看出在 exercise 函数中通过调用两次 transfer 将 USDC 发送给攻击者合约,接下来我们切入 exercise 函数进行具体的分析
function exercise(
uint256 oTokensToExercise,
address payable[] memory vaultsToExerciseFrom
) public payable {
for (uint256 i = 0; i < vaultsToExerciseFrom.length; i++) {
address payable vaultOwner = vaultsToExerciseFrom[i];
require(
hasVault(vaultOwner),
"Cannot exercise from a vault that doesn't exist"
);
Vault storage vault = vaults[vaultOwner];
if (oTokensToExercise == 0) {
return;
} else if (vault.oTokensIssued >= oTokensToExercise) {
_exercise(oTokensToExercise, vaultOwner);
return;
} else {
oTokensToExercise = oTokensToExercise.sub(vault.oTokensIssued);
_exercise(vault.oTokensIssued, vaultOwner);
}
}
require(
oTokensToExercise == 0,
"Specified vaults have insufficient collateral"
);
}
function _exercise(
uint256 oTokensToExercise,
address payable vaultToExerciseFrom
) internal {
// 1. before exercise window: revert
require(
isExerciseWindow(),
"Can't exercise outside of the exercise window"
);
require(hasVault(vaultToExerciseFrom), "Vault does not exist");
Vault storage vault = vaults[vaultToExerciseFrom];
require(oTokensToExercise > 0, "Can't exercise 0 oTokens");
// Check correct amount of oTokens passed in)
require(
oTokensToExercise <= vault.oTokensIssued,
"Can't exercise more oTokens than the owner has"
);
// Ensure person calling has enough oTokens
require(
balanceOf(msg.sender) >= oTokensToExercise,
"Not enough oTokens"
);
// 1. Check sufficient underlying
// 1.1 update underlying balances
uint256 amtUnderlyingToPay = underlyingRequiredToExercise(
oTokensToExercise
);
vault.underlying = vault.underlying.add(amtUnderlyingToPay);
// 2. Calculate Collateral to pay
// 2.1 Payout enough collateral to get (strikePrice * oTokens) amount of collateral
uint256 amtCollateralToPay = calculateCollateralToPay(
oTokensToExercise,
Number(1, 0)
);
// 2.2 Take a small fee on every exercise
uint256 amtFee = calculateCollateralToPay(
oTokensToExercise,
transactionFee
);
totalFee = totalFee.add(amtFee);
uint256 totalCollateralToPay = amtCollateralToPay.add(amtFee);
require(
totalCollateralToPay <= vault.collateral,
"Vault underwater, can't exercise"
);
// 3. Update collateral + oToken balances
vault.collateral = vault.collateral.sub(totalCollateralToPay);
vault.oTokensIssued = vault.oTokensIssued.sub(oTokensToExercise);
// 4. Transfer in underlying, burn oTokens + pay out collateral
// 4.1 Transfer in underlying
if (isETH(underlying)) {
require(msg.value == amtUnderlyingToPay, "Incorrect msg.value");
} else {
require(
underlying.transferFrom(
msg.sender,
address(this),
amtUnderlyingToPay
),
"Could not transfer in tokens"
);
}
// 4.2 burn oTokens
_burn(msg.sender, oTokensToExercise);
// 4.3 Pay out collateral
transferCollateral(msg.sender, amtCollateralToPay);
emit Exercise(
amtUnderlyingToPay,
amtCollateralToPay,
msg.sender,
vaultToExerciseFrom
);
}
思路验证
让我们通过攻击者的操作来验证此过程是否如我们所想:
1、首先在保险期限内是肯定的
2、攻击者传入的 vaultToExerciseFrom 分别为:
0xe7870231992ab4b1a01814fa0a599115fe94203f、0x076c95c6cd2eb823acc6347fdf5b3dd9b83511e4
经验证,这两个地址都创建了 vault
3、攻击者调用 exercise 传入 oTokensToExercise 为 0x1443fd000 (5440000000),msg.value 为 272ETH,vaultsToExerciseFrom 分别为以上两个地址
4、此时由于此前攻击者创建的 oToken 为 0xa21fe800 (2720000000),及 vault.oTokensIssued 为 2720000000 小于 5440000000,所以将走 exercise 函数中的 else 逻辑,此时 oTokensToExercise 为 0xa21fe800 (2720000000),则以上代码第 60 行 msg.value == amtUnderlyingToPay 是肯定成立的
5、由于 vaultsToExerciseFrom 传入两个地址,所以 for 循环将执行两次 _exercise 函数,因此将 transfer 两次把 USDC 转给攻击者合约
完整的攻击流程如下
1、攻击者使用合约先调用 Opyn 合约的 createERC20CollateralOption 函数创建 oToken
2、攻击合约调用 exercise 函数,传入已创建 vault 的地址
3、通过 exercise 函数中 for 循环逻辑执行调用两次 _exercise 函数
4、exercise 函数调用 transferCollateral 函数将 USDC 转给函数调用者(由于 for 循环调用两次 _exercise 函数,transferCollateral 函数也将执行两次)
5、攻击合约调用 removeUnderlying 函数将此前传入的 ETH 转出
6、最终攻击者拿回了此前投入的 ETH 以及额外的 USDC
攻击合约地址
0xe7870231992Ab4b1A01814FA0A599115FE94203f
Opyn 合约地址
0x951D51bAeFb72319d9FBE941E1615938d89ABfe2
攻击交易(其一)
0xa858463f30a08c6f3410ed456e59277fbe62ff14225754d2bb0b4f6a75fdc8ad
修复建议
此次攻击主要是利用了 _exercise 函数中对 vaultToExerciseFrom 是否创建 vault 的检查缺陷。此检查未校验 vaultToExerciseFrom 是否是调用者自己,而只是简单的检查是否创建了 vault,导致攻击者可以任意传入已创建 vault 的地址来通过检查。
建议如下:
1、在处理用户可控的参数时应做好权限判断,限制 vaultToExerciseFrom 需为调用者本人。
2、项目方可以在项目初期或未完成多次严谨安全审计之前添加合约暂停功能与可升级模型,避免在发生黑天鹅事件时无法有效的保证剩余资金安全。
往期回顾
慢雾导航
慢雾科技官网
https://www.slowmist.com/
慢雾区官网
https://slowmist.io/
慢雾 GitHub
https://github.com/slowmist
Telegram
https://t.me/slowmistteam
https://twitter.com/@slowmist_team
Medium
https://medium.com/@slowmist
币乎
https://bihu.com/people/586104
知识星球
https://t.zsxq.com/Q3zNvvF
火星号
http://t.cn/AiRkv4Gz
链闻号
https://www.chainnews.com/u/958260692213.htm