Transfer Amount Underflow Can Lead to Loss of Funds
Description. In Renegade’s atomic settlement, the external party deposits tokens, exchanges, and withdraws tokens in one transaction. The withdrawal involves three ERC20 transfers: withdrawing the tokens bought by the receiver, paying the protocol fee, and paying the relayer fee. The protocol fee and relayer fee are deducted from the tokens bought by the receiver. However, due to an oversight, the relayer fee is not properly restricted in the circuit and can be larger than the amount bought by the receiver. As a result, an underflow may occur when deducting the fees.
// The amount received by the external party after deducting the fees
let trader_take = receive_amount - fees.total();
transfers_batch.push(SimpleErc20Transfer::new_withdraw(
tx_sender,
receive_mint,
trader_take,
));
In the code above, receive_amount and the fees are restricted to a 100-bit number. If an underflow occurs, it will result in trader_take being a very large value, close to U256::MAX. An attack is only possible when the total deposit of the protocol is close to U256::MAX.
At first glance, such an attack seems impossible: The protocol always withdraws the total amount of trader_take + protocol_fee + relayer_fee. If an underflow occurs, the total withdrawal amount would exceed U256::MAX, which is the maximum possible amount of ERC20 tokens. Thus, the attack seems doomed to fail.
However, the above statement is not true. Such an attack can occur. The attacker can set their relayer fee address as the protocol contract address. In this case, the relayer_fee will be transferred to the protocol itself. The total withdrawal amount will be trader_take + protocol_fee, which will not exceed U256::MAX.
Recommendations. It is recommended to use safe math in the contract when dealing with token arithmetic. For Rust, we can use checked_sub and checked_add:
// The amount received by the external party after deducting the fees
let trader_take = receive_amount.checked_sub(fees.total());
transfers_batch.push(SimpleErc20Transfer::new_withdraw(
tx_sender,
receive_mint,
trader_take,
));
If there is an upper bound ratio (e.g., 1%) for the relayer fee for the external party, it’s recommended to restrict it directly in the circuit. This also reduces the risk to the external party in case they forget to check the relayer fee.
