Describe the bug
A PartiallyFilled(n) order is re-executed for its full order.amount when a relayer re-submits the same signed order with SignedOrder.partial_fill = None, because the cumulative-fill cap lives entirely inside the Some(_) branch.
The cap that bounds a fill to the remaining amount only runs when partial_fill is Some:
if let Some(partial_fill) = signed_order.partial_fill {
ensure!(order.relayer.is_some(), Error::<T>::RelayerRequiredForPartialFill);
ensure!(order.partial_fills_enabled, Error::<T>::PartialFillsNotEnabled);
let max_fill = if let Some(OrderStatus::PartiallyFilled(already_filled)) = order_status {
order.amount.saturating_sub(already_filled)
} else { order.amount };
ensure!(partial_fill > 0 && partial_fill <= max_fill, Error::<T>::IncorrectPartialFillAmount);
}
(lib.rs:648.) With partial_fill = None the whole block is skipped. PartiallyFilled(n) is non-terminal, so it passes the Fulfilled / Cancelled checks. The execution path then sizes the swap from the full order amount:
let tao_in = TaoBalance::from(signed_order.partial_fill.unwrap_or(order.amount));
(lib.rs:729.) buy_alpha debits the signer the full order.amount again, and compute_order_status early-returns Fulfilled (lib.rs:682), discarding the recorded n. This breaks the conservation invariant sum(fills) <= order.amount. partial_fill lives on the SignedOrder envelope, not the signed payload, so a relayer flips it to None without invalidating the signature or order_id. The signer cannot cancel a PartiallyFilled order, so there is no recourse. The same applies to sell orders and the batched path.
To Reproduce
- Sign a buy order for 1,000 TAO with
partial_fills_enabled = true and a relayer on the allow-list.
- As the relayer, submit it with
partial_fill = Some(600): the signer is debited 600 TAO and the order becomes PartiallyFilled(600).
- Re-submit the same signed order with
partial_fill = None.
- Observe the cumulative-fill cap is skipped, the swap re-executes for the full 1,000 TAO, the signer is debited another 1,000 TAO, and the order is marked
Fulfilled.
- Total debit is 1,600 TAO for a 1,000-TAO order (with a first fill of
amount - 1, the over-debit approaches 100% / nearly 2x).
Expected behavior
A partial_fill = None submission against a PartiallyFilled(n) order is rejected, or treated as "fill the remaining amount - n" (swap size clamped to amount - n and accumulated into the running total). compute_order_status must account for already_filled on the None path instead of unconditionally returning Fulfilled.
Screenshots
No response
Environment
testnet@0c774f00
Additional context
No response
Describe the bug
A
PartiallyFilled(n)order is re-executed for its fullorder.amountwhen a relayer re-submits the same signed order withSignedOrder.partial_fill = None, because the cumulative-fill cap lives entirely inside theSome(_)branch.The cap that bounds a fill to the remaining amount only runs when
partial_fillisSome:(
lib.rs:648.) Withpartial_fill = Nonethe whole block is skipped.PartiallyFilled(n)is non-terminal, so it passes theFulfilled/Cancelledchecks. The execution path then sizes the swap from the full order amount:(
lib.rs:729.)buy_alphadebits the signer the fullorder.amountagain, andcompute_order_statusearly-returnsFulfilled(lib.rs:682), discarding the recordedn. This breaks the conservation invariantsum(fills) <= order.amount.partial_filllives on theSignedOrderenvelope, not the signed payload, so a relayer flips it toNonewithout invalidating the signature ororder_id. The signer cannot cancel aPartiallyFilledorder, so there is no recourse. The same applies to sell orders and the batched path.To Reproduce
partial_fills_enabled = trueand a relayer on the allow-list.partial_fill = Some(600): the signer is debited 600 TAO and the order becomesPartiallyFilled(600).partial_fill = None.Fulfilled.amount - 1, the over-debit approaches 100% / nearly 2x).Expected behavior
A
partial_fill = Nonesubmission against aPartiallyFilled(n)order is rejected, or treated as "fill the remainingamount - n" (swap size clamped toamount - nand accumulated into the running total).compute_order_statusmust account foralready_filledon theNonepath instead of unconditionally returningFulfilled.Screenshots
No response
Environment
testnet@0c774f00Additional context
No response