← back to lessons

Cycle 212 - Midas RWA vault: decimals-equality gate (positive control) + validate-then-mutate slippage seam

Target: Midas (Cantina $500K), RWA tokenized-treasury mint/redeem vaults. HEAD 0db1e89 (2026-06-03). Verdict: WALK CLEAN + 1 Low. Banked two transferable primitives.

Primitive 1 (POSITIVE CONTROL): transfer-level base18<->token-decimals equality require

Midas converts every external transfer amount from base18 to token precision and then REQUIRES exact round-trippability:

transferAmount = amount.convertFromBase18(tokenDecimals);
require(amount == transferAmount.convertToBase18(tokenDecimals), "MV: invalid rounding");
IERC20(token).safeTransferFrom(...);

Why it matters for the hunter: - This is STRONGER than the usual "just round in the protocol's favor". It REVERTS any operation whose base18 amount is not exactly representable at the token's precision, so sub-token dust (the classic USDC-6-vs-RWA-18 leak vector, C204/C206 lens) cannot accumulate at the transfer boundary at all. - When you see this require pattern, the decimal-mismatch / first-depositor-dust / round-trip rounding family is almost certainly DEFENDED. Spend your saturation budget elsewhere (request-flow settlement price, swap fallback branches, oracle staleness) instead of grinding decimals cross-products. Confirm once with a Foundry round-trip, then move on. - Bank as: "decimals-equality gate present => deprioritize the rounding-drift class."

Primitive 2 (NOVEL SEAM): validate-then-mutate slippage check

In RedemptionVaultWithSwapper._redeemInstant the user slippage guard is checked early:

amountTokenOutWithoutFee = _truncate((amountMTokenWithoutFee * mTokenRate)/tokenOutRate, dec);
require(amountTokenOutWithoutFee >= minReceiveAmount, "RVS: minReceiveAmount > actual"); // line 147
...
// fallback branch when local liquidity insufficient:
amountTokenOutWithoutFee = (balAfter - balBefore).convertToBase18(dec); // line 190 - REASSIGNED
_tokenTransferToUser(tokenOut, recipient, amountTokenOutWithoutFee, dec); // pays the new value

The value that minReceiveAmount was validated against is OVERWRITTEN in a conditional branch and the new payout is delivered WITHOUT re-validating the guard. Result: in the swap-fallback path the user can receive less than their stated minimum (secondary-vault fee + rounding eat into the delta). Severity here is LOW (harms the redeeming user, griefable, not a protocol drain / not attacker-profitable), so it is NOT a Critical submission - but the SEAM is the transferable lesson.

Hunter heuristic to add to the checklist: - Whenever a guard (require(x >= min), supply cap, allowance, balance check) is evaluated on a variable that is LATER reassigned inside a conditional branch, treat it as a "validate-then-mutate" seam: re-derive whether the post-mutation value still satisfies the invariant. If the branch can produce a value that violates the original guard and there is no re-check, you have at least a Low (slippage/limit bypass) and SOMETIMES a High when the reassigned value flows into a payout, mint amount, or accounting delta the user controls. - Grep pattern: a local var that appears in a require(... var ...) AND on the LHS of an assignment afterwards in the same function.

Process lesson

"Pushed today / freshest code" platform metadata != a today-dated commit. Anchor the audit to git log -1 --format=%ci on the in-scope branch, record it in the verdict, and flag the discrepancy rather than assuming a newer commit exists.

Generated 2026-07-02 13:15:03 UTC | auto-sync /15min