// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity =0.7.6; pragma abicoder v2; import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import '@uniswap/v3-periphery/contracts/base/PeripheryImmutableState.sol'; import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Callee.sol'; import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol'; import '../libraries/UniswapV2Library.sol'; import '../interfaces/ISwapRouter02.sol'; import '../interfaces/ITokenValidator.sol'; import '../base/ImmutableState.sol'; /// @notice Validates tokens by flash borrowing from the token/ pool on V2. /// @notice Returns /// Status.FOT if we detected a fee is taken on transfer. /// Status.STF if transfer failed for the token. /// Status.UNKN if we did not detect any issues with the token. /// @notice A return value of Status.UNKN does not mean the token is definitely not a fee on transfer token /// or definitely has no problems with its transfer. It just means we cant say for sure that it has any /// issues. /// @dev We can not guarantee the result of this lens is correct for a few reasons: /// @dev 1/ Some tokens take fees or allow transfers under specific conditions, for example some have an allowlist /// @dev of addresses that do/dont require fees. Therefore the result is not guaranteed to be correct /// @dev in all circumstances. /// @dev 2/ It is possible that the token does not have any pools on V2 therefore we are not able to perform /// @dev a flashloan to test the token. contract TokenValidator is ITokenValidator, IUniswapV2Callee, ImmutableState { string internal constant FOT_REVERT_STRING = 'FOT'; // https://github.com/Uniswap/v2-core/blob/1136544ac842ff48ae0b1b939701436598d74075/contracts/UniswapV2Pair.sol#L46 string internal constant STF_REVERT_STRING_SUFFIX = 'TRANSFER_FAILED'; constructor(address _factoryV2, address _positionManager) ImmutableState(_factoryV2, _positionManager) {} function batchValidate( address[] calldata tokens, address[] calldata baseTokens, uint256 amountToBorrow ) public override returns (Status[] memory isFotResults) { isFotResults = new Status[](tokens.length); for (uint256 i = 0; i < tokens.length; i++) { isFotResults[i] = validate(tokens[i], baseTokens, amountToBorrow); } } function validate( address token, address[] calldata baseTokens, uint256 amountToBorrow ) public override returns (Status) { for (uint256 i = 0; i < baseTokens.length; i++) { Status result = _validate(token, baseTokens[i], amountToBorrow); if (result == Status.FOT || result == Status.STF) { return result; } } return Status.UNKN; } function _validate( address token, address baseToken, uint256 amountToBorrow ) internal returns (Status) { if (token == baseToken) { return Status.UNKN; } address pairAddress = UniswapV2Library.pairFor(this.factoryV2(), token, baseToken); // If the token/baseToken pair exists, get token0. // Must do low level call as try/catch does not support case where contract does not exist. (, bytes memory returnData) = address(pairAddress).call(abi.encodeWithSelector(IUniswapV2Pair.token0.selector)); if (returnData.length == 0) { return Status.UNKN; } address token0Address = abi.decode(returnData, (address)); // Flash loan {amountToBorrow} (uint256 amount0Out, uint256 amount1Out) = token == token0Address ? (amountToBorrow, uint256(0)) : (uint256(0), amountToBorrow); uint256 balanceBeforeLoan = IERC20(token).balanceOf(address(this)); IUniswapV2Pair pair = IUniswapV2Pair(pairAddress); try pair.swap(amount0Out, amount1Out, address(this), abi.encode(balanceBeforeLoan, amountToBorrow)) {} catch Error(string memory reason) { if (isFotFailed(reason)) { return Status.FOT; } if (isTransferFailed(reason)) { return Status.STF; } return Status.UNKN; } // Swap always reverts so should never reach. revert('Unexpected error'); } function isFotFailed(string memory reason) internal pure returns (bool) { return keccak256(bytes(reason)) == keccak256(bytes(FOT_REVERT_STRING)); } function isTransferFailed(string memory reason) internal pure returns (bool) { // We check the suffix of the revert string so we can support forks that // may have modified the prefix. string memory stf = STF_REVERT_STRING_SUFFIX; uint256 reasonLength = bytes(reason).length; uint256 suffixLength = bytes(stf).length; if (reasonLength < suffixLength) { return false; } uint256 ptr; uint256 offset = 32 + reasonLength - suffixLength; bool transferFailed; assembly { ptr := add(reason, offset) let suffixPtr := add(stf, 32) transferFailed := eq(keccak256(ptr, suffixLength), keccak256(suffixPtr, suffixLength)) } return transferFailed; } function uniswapV2Call( address, uint256 amount0, uint256, bytes calldata data ) external view override { IUniswapV2Pair pair = IUniswapV2Pair(msg.sender); (address token0, address token1) = (pair.token0(), pair.token1()); IERC20 tokenBorrowed = IERC20(amount0 > 0 ? token0 : token1); (uint256 balanceBeforeLoan, uint256 amountRequestedToBorrow) = abi.decode(data, (uint256, uint256)); uint256 amountBorrowed = tokenBorrowed.balanceOf(address(this)) - balanceBeforeLoan; // If we received less token than we requested when we called swap, then a fee must have been taken // by the token during transfer. if (amountBorrowed != amountRequestedToBorrow) { revert(FOT_REVERT_STRING); } // Note: If we do not revert here, we would end up reverting in the pair's swap method anyway // since for a flash borrow we need to transfer back the amount we borrowed + 0.3% fee, and we don't // have funds to cover the fee. Revert early here to save gas/time. revert('Unknown'); } }