https://github.com/OpenEdenHQ/openeden.usdoexpress.audit/tree/f3f31d2ac15e3253cba342229f9d05495f95d6fd
yo,
in USDO.sol, the total supply cap is only enforced on mint, not when the bonus multiplier is increased (the daily “rebase”), because totalSupply() is computed as convertToTokens(_totalShares) and that conversion multiplies by bonusMultiplier, any positive multiplier bump can push totalSupply() above _totalSupplyCap without a revert, breaking the intuitive invariant “total supply ≤ cap” and potentially blocking subsequent mints until the cap is manually raised, this behavior is not documented as intended in the public docs i reviewed,
USDO rebases daily via a “bonus multiplier”; balances are shares * bonusMultiplier and the multiplier is programmatically updated daily so holders “should expect to see their balances increase daily” (https://docs.openeden.com/usdo/usdo-token/bonus-multiplier)
the USDO docs include instant/general minting flows; if the cap is unintentionally breached by rebases, later mints will revert (availability impact to “instant mint”) (https://docs.openeden.com/usdo/usdo-token/minting-workflow)
main state & formulas
totalSupply() = convertToTokens(_totalShares) = (_totalShares * bonusMultiplier) / _BASE , increasing bonusMultiplier increases every balance and the total supply_totalSupplyCap with totalSupplyCap() getter and updateTotalSupplyCap() (admin)where the cap is enforced (only on mint):
_mint() -> checkNewTotalSupply(amount) -> computes newTotalSupply = convertToTokens(_totalShares + shares) and reverts with USDOExceedsTotalSupplyCap if newTotalSupply > _totalSupplyCap , no other place enforces the capwhere the cap is not enforced (the crux):
_updateBonusMultiplier(uint256 _bonusMultiplier) requires only _bonusMultiplier >= _BASE and updates bonusMultiplier; no cap check here, exposed via:
updateBonusMultiplier (DEFAULT_ADMIN_ROLE)addBonusMultiplier (MULTIPLIER_ROLE), which solely increases the multiplierimplication: if _totalShares > 0 and cap was set near current totalSupply, then any daily positive rebase (expected per docs) can make totalSupply() exceed _totalSupplyCap silently, from then on, any further mint will fail (since checkNewTotalSupply uses the now‑higher multiplier), effectively blocking mints until the admin increases the cap
totalSupply() can exceed _totalSupplyCap after routine rebase updates. Nothing reverts when this happensassume _totalShares = S and bonusMultiplier = 1e18 (_BASE), let _totalSupplyCap = C just above current convertToTokens(S)
after a day, issuer increments multiplier: addBonusMultiplier(delta), so bonusMultiplier’ = 1e18 + delta
Observation:
anyone can read:
USDO.totalSupply() -> now (S * (1e18 + delta)) / 1e18, which may be > CUSDO.totalSupplyCap() -> still Cthis proves the invariant break on‑chain (view calls)
an “instant mint” attempt (by an authorized client) reverts inside USDO.mint() because checkNewTotalSupply compares post‑mint supply vs. cap using the increased multiplier, this contradicts the expectation that instant mint is steadily available (unless the issuer updates the cap first)
goal: show
totalSupply > totalSupplyCapafter a routine multiplier increase
(Admin, one‑time) updateTotalSupplyCap(C) set to a value slightly above current supply
(Admin, optional) mint(alice, X) so that totalSupply is close to C
(Admin, daily routine) addBonusMultiplier(delta) with delta > 0
(anyone, read‑only) check:
USDO.totalSupply() -> now strictly larger (due to rebase)USDO.totalSupplyCap() -> unchanged at CtotalSupply() > C, the cap has been breached without any revert(authorized minter) attempt fresh mint through instant Mint path -> reverts inside USDO.mint() via USDOExceedsTotalSupplyCap (https://docs.openeden.com/usdo/usdo-token/minting-workflow)