home
✍🏻

Team Finance Incident Analysis

Article realeased on Nov 14, 2022

Introduction

Offer a detailed analysis of the causes of Team Finance’s hacking incident.
In this post, contains the detailed structure of Solidity-based code for Uniswap.
Analyze the root causes of funds hacking incident.

Table of contents

Summary
Transaction Process Analysis
Vulnerability Analysis
How to prevent it

Summary

Recently, it was mentioned on Team Finance’s official Twitter that they lost $14.5M USD of funds.
In addition, transaction about this attack process was also discovered. The transaction is as below.
The various tokens were stolen such as USDC, CAW, TSUKA, and KNDA were leaked and it is mentioned on twitter and many blogs’ posts.

Transaction Process Analysis

The snippet for the token transfer transaction flow of the stolen token is as follows:
From TrustSwap: Team Finance LockTo Uniswap V2: FEG For 15,000 ($1,359,510.29)Uniswap V2 (UNI-V2) From Uniswap V2: FEGTo Null Address: 0x000...000 For 15,000 ($1,359,510.29)Uniswap V2 (UNI-V2) From Uniswap V2: FEGTo Uniswap V3: Migrator For 1,206,669,906,499,720.059815651 ($688,692.37)FEGtoken (FEG) From Uniswap V2: FEGTo Uniswap V3: Migrator For 608.062138500464909927 ($891,832.58)Wrapped Ethe... (WETH) From Uniswap V3: MigratorTo 0xeb2423fbeb5d94cd83136a74341a39a2487fb3cb For 6.63680495038575069 () From Uniswap V3: MigratorTo 0xeb2423fbeb5d94cd83136a74341a39a2487fb3cb For 6.080621385004649099 ($8,918.33)Wrapped Ethe... (WETH) From Uniswap V3: MigratorTo TrustSwap: Team Finance Lock For 1,231,289.186153951716102015 () From TrustSwap: Team Finance LockTo 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4 For 1,231,289.186153951716102015 () From 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4To Team Finance Exploiter 2 For 0 ($0.00)Wrapped Ethe... (WETH) From TrustSwap: Team Finance LockTo Uniswap V2: USDC-CAW For 17,000.042522005059438879 Uniswap V2 (UNI-V2) From Uniswap V2: USDC-CAWTo Null Address: 0x000...000 For 17,000.042522005059438879 Uniswap V2 (UNI-V2) From Uniswap V2: USDC-CAWTo Uniswap V3: Migrator For 5,638,111.353901 ($5,660,663.80)USD Coin (USDC) From Uniswap V2: USDC-CAWTo Uniswap V3: Migrator For 74,613,657,577,043.894100265734907803 ($4,891,748.53)A Hunters Dr... (CAW) From Uniswap V3: MigratorTo Uniswap V3: USDC-CAW 3 For 56,381.113539 ($56,606.64)USD Coin (USDC) From Uniswap V3: MigratorTo Uniswap V3: USDC-CAW 3 For 0.000000051656212178 ($0.00)A Hunters Dr... (CAW) From Uniswap V3: MigratorTo TrustSwap: Team Finance Lock For 5,581,730.240362 ($5,604,057.16)USD Coin (USDC) From Uniswap V3: MigratorTo TrustSwap: Team Finance Lock For 74,613,657,577,043.894100214078695625 ($4,891,748.53)A Hunters Dr... (CAW) From TrustSwap: Team Finance LockTo 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4 For 5,581,730.240362 ($5,604,057.16)USD Coin (USDC) From TrustSwap: Team Finance LockTo 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4 For 74,613,657,577,043.894100214078695625 ($4,891,748.53)A Hunters Dr... (CAW) From 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4To Curve.fi: DAI/USDC/USDT Pool For 5,581,730.240362 ($5,604,057.16)USD Coin (USDC) From Curve.fi: DAI/USDC/USDT PoolTo 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4 For 5,581,220.040550673739846727 ($5,603,544.92)Dai Stableco... (DAI) From 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4To Team Finance Exploiter 2 For 5,581,220.040550673739846727 ($5,603,544.92)Dai Stableco... (DAI) From 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4To Team Finance Exploiter 2 For 0 ($0.00)USD Coin (USDC) From 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4To Team Finance Exploiter 2 For 74,613,657,577,043.894100214078695625 ($4,891,748.53)A Hunters Dr... (CAW) From TrustSwap: Team Finance LockTo Uniswap V2: USDC-TSUKA For 0.000053597710631078 ($1,167,480.92)Uniswap V2 (UNI-V2) From Uniswap V2: USDC-TSUKATo Null Address: 0x000...000 For 0.000053597710631078 ($1,167,480.92)Uniswap V2 (UNI-V2) From Uniswap V2: USDC-TSUKATo Uniswap V3: Migrator For 848,194.266023 ($851,587.04)USD Coin (USDC) From Uniswap V2: USDC-TSUKATo Uniswap V3: Migrator For 11,957,149.213434629 ($610,233.11)Dejitaru Tsu... (TSUKA) From Uniswap V3: MigratorTo Uniswap V3: USDC-TSUKA 3 For 119,571.492134346 ($6,102.33)Dejitaru Tsu... (TSUKA) From Uniswap V3: MigratorTo TrustSwap: Team Finance Lock For 848,194.266023 ($851,587.04)USD Coin (USDC) From Uniswap V3: MigratorTo TrustSwap: Team Finance Lock For 11,837,577.721300283 ($604,130.78)Dejitaru Tsu... (TSUKA) From TrustSwap: Team Finance LockTo 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4 For 848,194.266023 ($851,587.04)USD Coin (USDC) From TrustSwap: Team Finance LockTo 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4 For 11,837,577.721300283 ($604,130.78)Dejitaru Tsu... (TSUKA) From 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4To Curve.fi: DAI/USDC/USDT Pool For 848,194.266023 ($851,587.04)USD Coin (USDC) From Curve.fi: DAI/USDC/USDT PoolTo 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4 For 848,107.618317317621825664 ($851,500.05)Dai Stableco... (DAI) From 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4To Team Finance Exploiter 2 For 848,107.618317317621825664 ($851,500.05)Dai Stableco... (DAI) From 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4To Team Finance Exploiter 2 For 0 ($0.00)USD Coin (USDC) From 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4To Team Finance Exploiter 2 For 11,837,577.721300283 ($604,130.78)Dejitaru Tsu... (TSUKA) From TrustSwap: Team Finance LockTo Uniswap V2: KNDX For 253.409 Uniswap V2 (UNI-V2) From Uniswap V2: KNDXTo Null Address: 0x000...000 For 253.409 Uniswap V2 (UNI-V2) From Uniswap V2: KNDXTo Uniswap V3: Migrator For 301,333,662,914.629319205 ($334,480.37)Kondux (KNDX) From Uniswap V2: KNDXTo Uniswap V3: Migrator For 220.883485968767849303 ($323,965.39)Wrapped Ethe... (WETH) From Uniswap V3: MigratorTo 0xeb2423fbeb5d94cd83136a74341a39a2487fb3cb For 2.410873034705250155 () From Uniswap V3: MigratorTo 0xeb2423fbeb5d94cd83136a74341a39a2487fb3cb For 2.208834859687678493 ($3,239.65)Wrapped Ethe... (WETH) From Uniswap V3: MigratorTo TrustSwap: Team Finance Lock For 298.92278987992406905 () From TrustSwap: Team Finance LockTo 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4 For 298.92278987992406905 () From 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4To Team Finance Exploiter 2 For 0 ($0.00)Wrapped Ethe... (WETH)
Rust
The flow of tokens is a bit complicated, but if you cut a bunch of the flow of transaction and check it, it can be split as follows and can be analyze easily.
From TrustSwap: Team Finance LockTo Uniswap V2: FEG For 15,000 ($1,359,510.29)Uniswap V2 (UNI-V2) From Uniswap V2: FEGTo Null Address: 0x000...000 For 15,000 ($1,359,510.29)Uniswap V2 (UNI-V2) From Uniswap V2: FEGTo Uniswap V3: Migrator For 1,206,669,906,499,720.059815651 ($688,692.37)FEGtoken (FEG) From Uniswap V2: FEGTo Uniswap V3: Migrator For 608.062138500464909927 ($891,832.58)Wrapped Ethe... (WETH) From Uniswap V3: MigratorTo 0xeb2423fbeb5d94cd83136a74341a39a2487fb3cb For 6.63680495038575069 () From Uniswap V3: MigratorTo 0xeb2423fbeb5d94cd83136a74341a39a2487fb3cb For 6.080621385004649099 ($8,918.33)Wrapped Ethe... (WETH) From Uniswap V3: MigratorTo TrustSwap: Team Finance Lock For 1,231,289.186153951716102015 () From TrustSwap: Team Finance LockTo 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4 For 1,231,289.186153951716102015 () From 0xcff07c4e6aa9e2fec04daaf5f41d1b10f3adadf4To Team Finance Exploiter 2 For 0 ($0.00)Wrapped Ethe... (WETH)
Rust
TrustSwap transfers tokens to Uniswap V2, and Uniswap migrates to V3 and transfers tokens through V3. After going through Uniswap, the result token is returned to the user again, and this process is a bit strange.
The token returned is WETH and a token whose format is unknown, even through the deposited pair of tokens transferred is an FEG/WETH pair.
Focusing on this will give us good clues.
Further tracking the flow of funds, the attacker’s initial funds were withdrawn from FixedFloat, and 1.76 Ether was deposited as the initial funds for exploit.
The transaction can be checked at the following link.
And then it can be summarized as below.
1. TrustSwap locks by passing tokens (FEG, USDC-CAW, etc..) towards Uniswap. 2. Uniswap performs token processing while migrating from V2 to V3. 3. The result token is returned to the attacker contract. 4. The returned funds are transferred from the attacker's contract to the attacker's second account.
Markdown
And then, we can summarize attacker’s information for analyze as below.

Vulnerability Analysis

Now, we understand the flow of transaction, so let’s take a look at how the vulnerability actually happened.
First of all, the calling process that occurred in the process of each funds leak is summarized in detail as below.
1. balanceOf 2. migrate 3. [CALL] 4. approve 5. balanceOf 6. transfer
Markdown
The most important part of each step is migrate, and the structure in which the leaked funds after the corresponding migrate function is executed is transferred to the attacker through the transfer.
The migrate function receives a parameter called MigrateParams, and the variables actually passed in the transaction is as below.
"input":{ "_id":"15324" "params":{ "amount0Min":"0" "amount1Min":"0" "deadline":"1666859863" "fee":"500" "liquidityToMigrate":"15000000000000000000000" "pair":"0x854373387e41371ac6e307a1f29603c6fa10d872" "percentageToMigrate":1 "recipient":"0xba399a2580785a2ded740f5e30ec89fb3e617e6e" "refundAsETH":true "tickLower":"-100" "tickUpper":"100" "token0":"0x2d4abfdcd1385951df4317f9f3463fb11b9a31df" "token1":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" } "noLiquidity":true "sqrtPriceX96":"79210883607084793911461085816" "_mintNFT":false }
Markdown
The parts to pay attention to are pair, token0, token1, percentageToMigrate, and recipient.
function migrate( uint256 _id, IV3Migrator.MigrateParams calldata params, bool noLiquidity, uint160 sqrtPriceX96, bool _mintNFT ) external payable whenNotPaused nonReentrant { require(address(nonfungiblePositionManager) != address(0), "NFT manager not set"); require(address(v3Migrator) != address(0), "v3 migrator not set"); Items memory lockedERC20 = lockedToken[_id]; require(block.timestamp < lockedERC20.unlockTime, "Unlock time already reached"); require(_msgSender() == lockedERC20.withdrawalAddress, "Unauthorised sender"); require(!lockedERC20.withdrawn, "Already withdrawn");
Solidity
The migrate function is implemented in the following form.
The important point is that the migrate function is an external function and can be used externally by everybody, so the variables in params and sqrtPriceX96 can be changed and controlled as desired by the attacker.
Initially, some conditions are checked through the require conditional statement, and you can see that _msgSender() checks whether withdrawalAddress is valid.
It can be confirmed that the lock process for the token occurs in the transaction, and if you check the parameter variables performed at the time of lock, you can check the following variables.
The withdrawalAddress passed at this time is an attacker’s contract, and the amount of funds to be locked is specified as _amount.
After that, several major processes are performed, and functionally summarized as below.
1. If the uniswap fund pool corresponding to the delivered token0, token1, and fee variable does not exist Create and initialize a pool through the createAndInitializePoolIfNecessary() function. 2. Execute the main logic of the migrate process through Uniswap V3 Migrator. 3. If there is an extra fund that needs to be refunded, it is returned to the user. 4. NFT is issued to prove that deposit is carried out
Markdown
As a result, the main vulnerability is the migrate function of V3 Migrator.
However, first of all, let’s check how the createAndInitializePoolIfNecessary() function is also called and how the fund pool is created.
function createAndInitializePoolIfNecessary( address token0, address token1, uint24 fee, uint160 sqrtPriceX96 ) external payable override returns (address pool) { require(token0 < token1); pool = IUniswapV3Factory(factory).getPool(token0, token1, fee); if (pool == address(0)) { pool = IUniswapV3Factory(factory).createPool(token0, token1, fee); IUniswapV3Pool(pool).initialize(sqrtPriceX96); } else { (uint160 sqrtPriceX96Existing, , , , , , ) = IUniswapV3Pool(pool).slot0(); if (sqrtPriceX96Existing == 0) { IUniswapV3Pool(pool).initialize(sqrtPriceX96); } } }
Solidity
The createAndInitializePoolIfNecessary() function is implemented as above.
If a pool based on the corresponding token variables and fee variable does not exist, a new pool is created with the createPool() function and pool is initialized with the received sqrtPriceX96 variable.
function createPool( address tokenA, address tokenB, uint24 fee ) external override noDelegateCall returns (address pool) { require(tokenA != tokenB); (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); require(token0 != address(0)); int24 tickSpacing = feeAmountTickSpacing[fee]; require(tickSpacing != 0); require(getPool[token0][token1][fee] == address(0)); pool = deploy(address(this), token0, token1, fee, tickSpacing); getPool[token0][token1][fee] = pool; // populate mapping in the reverse direction, deliberate choice to avoid the cost of comparing addresses getPool[token1][token0][fee] = pool; emit PoolCreated(token0, token1, fee, tickSpacing, pool); }
Solidity
createPool() is implemented as above, and is responsible for creating a pool. In this case, token variables are used in ascending order.
In fact, deployment to the pool is done through the deploy() function, which is not very important so far.
As a result, the important fact is that if there is no pool suitable for the received token0 and token1, a new pool is created. In fact, if you look closely at the transaction process, you can find that the corresponding createPool() is called.
However, the strange thing about this focus is that the pair variable is transferred even though there is no pool for tokens which is suitable.
This pair variable is the FEG/WETH pool, and it transfers the already existing pool information.
function migrate(MigrateParams calldata params) external override { require(params.percentageToMigrate > 0, 'Percentage too small'); require(params.percentageToMigrate <= 100, 'Percentage too large'); // burn v2 liquidity to this address IUniswapV2Pair(params.pair).transferFrom(msg.sender, params.pair, params.liquidityToMigrate); (uint256 amount0V2, uint256 amount1V2) = IUniswapV2Pair(params.pair).burn(address(this)); // calculate the amounts to migrate to v3 uint256 amount0V2ToMigrate = amount0V2.mul(params.percentageToMigrate) / 100; uint256 amount1V2ToMigrate = amount1V2.mul(params.percentageToMigrate) / 100; // approve the position manager up to the maximum token amounts TransferHelper.safeApprove(params.token0, nonfungiblePositionManager, amount0V2ToMigrate); TransferHelper.safeApprove(params.token1, nonfungiblePositionManager, amount1V2ToMigrate); // mint v3 position (, , uint256 amount0V3, uint256 amount1V3) = INonfungiblePositionManager(nonfungiblePositionManager).mint( INonfungiblePositionManager.MintParams({ token0: params.token0, token1: params.token1, fee: params.fee, tickLower: params.tickLower, tickUpper: params.tickUpper, amount0Desired: amount0V2ToMigrate, amount1Desired: amount1V2ToMigrate, amount0Min: params.amount0Min, amount1Min: params.amount1Min, recipient: params.recipient, deadline: params.deadline }) );
Solidity
The main implementation of V3 Migrator’s migrate function is as above.
The migrate function receives MigrateParams parameters, and the same parameters as previously received is submitted for parent migrate function.
As percentageToMigrate is 1%, the require statement is passed.
The root cause of the vulnerability is that there is a problem in the process of calculating amount0V2 and amount1V2 and the process of calculating amount0V3 and amount1V3.
As you can see, amount0V2 and amount1V2 are created based on pair variable, but amount0V3 and amount1V3 are created based on the received token0 and token1 variables.
Because of this, after an attacker arbitrarily distributes a fake token contract on blockchain network, a vulnerability or situation occurs that can receive an amount of LP tokens that should not be actually received.
Consequently, there is no guarantee that the components of the pair are token0 and token1.
// if necessary, clear allowance and refund dust if (amount0V3 < amount0V2) { if (amount0V3 < amount0V2ToMigrate) { TransferHelper.safeApprove(params.token0, nonfungiblePositionManager, 0); } uint256 refund0 = amount0V2 - amount0V3; if (params.refundAsETH && params.token0 == WETH9) { IWETH9(WETH9).withdraw(refund0); TransferHelper.safeTransferETH(msg.sender, refund0); } else { TransferHelper.safeTransfer(params.token0, msg.sender, refund0); } } if (amount1V3 < amount1V2) { if (amount1V3 < amount1V2ToMigrate) { TransferHelper.safeApprove(params.token1, nonfungiblePositionManager, 0); } uint256 refund1 = amount1V2 - amount1V3; if (params.refundAsETH && params.token1 == WETH9) { IWETH9(WETH9).withdraw(refund1); TransferHelper.safeTransferETH(msg.sender, refund1); } else { TransferHelper.safeTransfer(params.token1, msg.sender, refund1); } } }
Solidity
If you look at the lower part of implementation, if there is a difference between amount0V3 and amount0V2, this is determines whether the amount of tokens normally requested or not.
If there is a difference in the each part, it means that there are tokens that are not properly burned or minted properly. Thus, Uniswap has a logic to refund these tokens back to the user.
This causes a leak of funds, because the ratio and amount of tokens between the tokens arbitrarily distributed by the attacker and the actual FEG/WETH pair pool are different.
Because of this, a vulnerability causes in that users can actually get back a larger amount of LP tokens than they have to get back.
Anyway, the main reason of the vulnerability was occured by sufficient verification of the user’s parameter variable was not performed because the code was executed assuming that the received pair parameter was a pair made up of the received token0 and token1.

How to prevent it

Basically, need to thinking about view point of Uniswap.
It is necessary to verify that the pool for the pair created with the received token0 and token1 parameters is valid.
Uniswap V3 has a source code called PoolAddress.sol and a computeAddress() function from it.
Developer can prevent this vulnerability with pre-verification for valid token informations before executing main logics.
Need sufficient verification of parameter informations and token informations before executing.
I don’t know there is simple way than which i wrote, but this is just one way to prevent this vulnerability.
About KALOS
KALOS is a flagship service of HAECHI LABS, the leader of the global blockchain industry. We bring together the best Web2 and Web3 experts. Security Researchers with expertise in cryptography, leaders of the global best hacker team, and blockchain/smart contract experts are responsible for securing your Web3 service.
We have secured over $60b worth of crypto assets across 400+ global crypto projects — L1/L2 projects, defi protocols, P2E games, and bridges — notably 1inch, SushiSwap, Badger DAO, SuperRare, Klaytn and Chainsafe. KALOS is the only blockchain technology company selected for the Samsung Electronics Startup Incubation Program in recognition of our expertise. We have also received technology grants from the Ethereum Foundation and Ethereum Community Fund.
Secure your smart contracts with KALOS.
Email: audit@kalos.xyz
Official website: https://kalos.xyz