#47330 [SC-Low] The fee calculation in `settle_market` is unreasonable.
Submitted on Jun 12th 2025 at 16:57:58 UTC by @shaflow1 for IOP | Paradex
Report ID: #47330
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/tradeparadex/audit-competition-may-2025/tree/main/paraclear
Impacts:
Theft of unclaimed yield
Description
Brief/Intro
In the settle_market
function, only the taker fee from the account is charged, which is unreasonable:
Charging only the taker fee may result in a negative total fee, causing a loss to the fee account.
In a settle trade, the user acts as both the taker and the maker, so both sides' fees should be charged to ensure that the total fee is not negative and to prevent losses to the fee account.
Vulnerability Details
fn _settlement_fee_payments(
ref self: ContractState,
account: ContractAddress,
account_state: @AccountState,
pending_token_balance: i128, // usdc
token_balance_address: ContractAddress,
trade_size: i128,
trade_price: i128,
settlement_token_price: NonZero<i128>,
asset: @PerpetualMarketAsset,
) -> i128 {
let fee_account = self.getFeeAccount();
let base_fee = asset
.calculate_fee(
*account_state.asset_data,
trade_size,
trade_price,
false,
account_state.fee_rates,
);
let (fee, referrer, fee_commission) = account_state
.get_trade_fee_and_referral_commission(base_fee);
let balance_after_fee = pending_token_balance - fee;
self
.token
.write_asset_balance(account, token_balance_address, balance_after_fee.into());
let settlement_token_address = self.getSettlementTokenAsset();
let fee_in_settlement_token = div_128(fee, settlement_token_price);
if fee_commission == 0 {
self
.token
.upsert_asset_balance(
fee_account, settlement_token_address, fee_in_settlement_token.into(),
);
} else {
let fee_commission_in_settlement_token = div_128(
fee_commission, settlement_token_price,
);
self
.token
.upsert_asset_balance(
referrer,
settlement_token_address,
fee_commission_in_settlement_token.into(),
);
self
.token
.upsert_asset_balance(
fee_account,
settlement_token_address,
(fee_in_settlement_token - fee_commission_in_settlement_token).into(),
);
}
self.emit(AccountComponent::Event::Fee(Fee { account: account, fee: fee.into() }));
return balance_after_fee;
}
_settlement_fee
only charges the taker fee, which is inappropriate.
In a settlement trade, the user acts as both the taker and the maker, so charging only the taker fee is unreasonable.
Moreover, if one of the fee rates (taker or maker) is negative, for example, if the taker fee rate is negative, then the settlement operation would result in a negative fee, causing the feeAccount
to lose funds.
Impact Details
The rewards from fee_account
can be stolen under certain conditions
References
https://github.com/tradeparadex/audit-competition-may-2025/blob/0eb81b26a67666c399b4e16b39a96c19848ab7fd/paraclear/src/paraclear/paraclear.cairo#L1981
Proof of Concept
Proof of Concept
Suppose the user's
taker_fee_rate
is -1% and themaker_fee_rate
is 1.2%. A malicious actor can place an order and immediately settle it to steal funds from thefee_account
.Assume that when placing the order, the user acts as the taker — in this case, the taker receives a 1% fee. If the user acts as the maker, they pay a 1.2% fee.
Then the user immediately settles the trade, earning another 1% fee from the settlement operation.
Summary:
If the user is the taker when placing the order:
User profit = 1% (order) + 1% (settlement) = 2%
fee_account
loss = 1.2% (maker fee) - 1% (taker fee rebate) = 0.2%
If the user is the maker when placing the order:
User loss = 1.2% (maker fee) - 1% (settlement rebate) = 0.2%
fee_account
gains 1.2% maker fee
In the first case, the user gains a large profit (2%). If the second case occurs, the loss is limited to only 0.2%. By repeatedly placing orders (e.g., 10 times), the user only needs to be the taker once to offset all losses and still profit, effectively draining value from the fee_account
.
Was this helpful?