# Basket Mint Smart Contract Review

**Scope reviewed:** the Solidity contracts in `src/`, supplied tests, configuration, and deployment scripts in `basket-mint-contracts-audit.zip`.

**Method:** manual review of privileged, oracle, collateral-accounting, mint/burn, liquidation, and reward paths; compilation and execution of the supplied Foundry tests.

## Summary

| Severity | Count |
| --- | ---: |
| High | 1 |
| Medium | 2 |
| Low | 1 |
| Informational | 0 |

The CDP’s standard $1-collateral path has reasonable basic guards, and all 26 supplied unit and invariant tests passed. However, the optional collateral-price feed is unsafe to enable: it has no freshness validation, and its price is not used when converting USD-denominated fees or liquidation proceeds into collateral units. The deployed configuration must not enable it until both issues are fixed.

## Findings

### H-01 — Collateral oracle accepts stale prices indefinitely

**Affected:** `BasketMarket.sol:173-180`

`collateralPrice()` only checks that `updatedAt` is nonzero; it never compares it to a configured heartbeat. The value is then used to calculate a position’s collateral value and collateral ratio.

If a stablecoin depegs after its feed stops updating, the protocol continues to value collateral at the obsolete price. A borrower can mint or withdraw against this inflated value, leaving the basket token materially undercollateralized in real USD terms. This defeats the documented depeg-protection feature.

**Recommendation:** add a collateral-feed heartbeat to `InitParams`/factory configuration and reject feeds whose `updatedAt` exceeds it. Also reject an invalid round (where applicable) and future timestamps. Reuse a single hardened oracle adapter/registry path rather than maintaining a separate, weaker feed reader.

### M-01 — Fee and liquidation conversions ignore the configured collateral price

**Affected:** `BasketMarket.sol:238-243`, `328-347`, `406-416`

The contract correctly marks collateral to `collateralPrice()` in `collateralValueUSD()`, but converts USD amounts back to collateral units by dividing only by `collateralScale`:

```solidity
feeCollateral = feeUSD / collateralScale;
seizeFull = seizeUSD / collateralScale;
principal = repayUSD / collateralScale;
```

Those conversions are correct only when one collateral token is exactly $1. With a configured $0.50 collateral price, a $100 mint fee collects 100 tokens ($50), and a $110 liquidation seizure pays 110 tokens ($55) for a $100 debt repayment. The liquidator therefore bears the loss and liquidation incentives can fail precisely during a collateral depeg. Conversely, an appreciating collateral token causes systematic overcharging/over-seizure.

**Recommendation:** centralize conversion helpers. For collateral native units to USD use `amount * collateralScale * price / WAD`; for USD to collateral native units use rounded-up division by `collateralScale * price / WAD`. Apply the latter consistently to mint fees, stability fees, liquidation seizure and principal/penalty splitting. Include tests at prices below and above $1.

### M-02 — Reward emissions become permanently stranded when the final staker exits

**Affected:** `VeStaking.sol:98-100`, `115-121`, `144-157`

When the last staker calls `unstake()` during an active reward period, `totalVe` becomes zero while `rewardRate` and `periodFinish` remain active. A later staker’s `updateReward` call invokes `rewardPerVe()`, which returns the old accumulator when `totalVe == 0`, then advances `lastUpdateTime`. The rewards emitted while there were no stakers are never assigned to anyone and remain locked in the contract; no recovery function exists.

This can strand the unvested portion of routed protocol fees whenever all stakers leave before a seven-day stream finishes.

**Recommendation:** when `totalVe` transitions to zero, preserve the remaining undistributed balance and roll it into the next `notifyReward`, or return it to the FeeRouter/treasury through an explicitly governed sweep. Do not advance the emission clock through intervals with zero staking weight.

### L-01 — Overnight market schedules apply the wrong day’s permission

**Affected:** `ScheduledMarketStatus.sol:65-70`

For an overnight schedule (`closeMin <= openMin`), the check first requires the *current* day’s weekday bit. The early-morning portion of an overnight window belongs to the prior day’s session, not the current one. For example, a Mon–Fri `20:00–04:00` schedule is incorrectly closed at Saturday 01:00 (Friday’s session) and incorrectly open at Monday 01:00 (which belongs to an unconfigured Sunday session).

The latter case can allow minting while the intended market is closed, using an old price.

**Recommendation:** branch before the weekday check. For `minute < closeMin` in an overnight schedule, test the previous weekday’s bit; otherwise test the current weekday. Add tests covering both sides of midnight and a week boundary.

## Verification

After installing the dependencies declared by the project, I ran:

- The three non-invariant suites: **24 passed, 0 failed**.
- `InvariantSolvencyTest`: **2 passed, 0 failed** (64 runs and 2,048 calls per invariant).

The current invariants verify supply/debt equality and accounting balance only for the mock $1 collateral path. They do not cover a stale collateral feed, non-$1 conversion math, a zero-staker reward interval, or overnight weekday boundaries.

## Notes

This is a time-boxed source review, not a formal assurance or deployment approval. Privileged owner/guardian control, feed selection, and production-market parameters remain material trust assumptions.
