Reference: contracts
The launch deployment was three contracts plus a shared base; the Ramses migration and the treasury desks added several more. Signatures are abbreviated; the authoritative source is contracts/ and the interfaces under contracts/interfaces/. Parameter values live in parameters.md, and every live address in addresses.md.
HAM — the token (contracts/HAM.sol)
UUPS-upgradeable rebasing ERC-20. Ownership: Ownable2StepUpgradeable. Deployed behind an ERC1967Proxy. The burn/blacklist upgrade preserves slots 0–8, adds isBlacklisted at slot 9, and reduces the storage gap from 44 to 43 slots; it has no reinitializer.
| Function | Access | Notes |
|---|---|---|
balanceOf(addr) | view | Display balance (fragments, 18 dec); scales every rebase. |
balanceOfUnderlying(addr) | view | Rebase-invariant share (internal units, 1e24). |
totalSupply() | view | Derived from initSupply × scalingFactor. |
hamsScalingFactor() | view | Global rebase multiplier (genesis 1e18). |
transfer / approve / allowance | public | Standard ERC-20, denominated in fragments. |
mint(to, amount) | rebaser or incentivizer | Mints fragments; increases initSupply. |
rebase(epoch, indexDelta, positive) | rebaser only | Moves the scaling factor within bounds. |
burn(account, amount) | owner | Burns any holder's HAM, including a blacklisted holder. A full displayed-balance burn also clears hidden internal dust. |
setBlacklisted(account, blocked) | owner | Toggles account blocking. A blocked account cannot send, receive, approve, spend via transferFrom, or receive a mint. |
isBlacklisted(account) | view | Current blacklist status; defaults to false after the upgrade. |
setRebaser(addr) / setIncentivizer(addr) | owner | Set the two privileged roles. |
upgradeToAndCall(impl, data) | owner | UUPS upgrade — no timelock. |
Events: standard Transfer / Approval, plus Rebase(epoch, previousScalingFactor, newScalingFactor), Mint, NewRebaser, NewIncentivizer, and BlacklistUpdated(account, blocked). Rebase outcomes are also observable via the rebaser's RebaseExecuted and the changed hamsScalingFactor.
Blacklist checks apply even to zero-value operations. transferFrom checks the caller/spender as well as the sender and recipient. Existing allowances are preserved while either participant is blocked and become usable again after unblocking. Rebases and read functions remain available, so a blocked balance continues to scale. Operators should use the repository runbook at docs/operations/upgrade-ham-burn-blacklist.md for storage and deployment steps.
WHAM — the non-rebasing wrapper (contracts/leverage/WHAM.sol)
Wraps rebasing HAM into a fixed-balance ERC-20 so that pools, vaults and any other contract assuming balances move only on transfer can hold it. This is the token in the live pool, and the unit BackingLens quotes backing in.
| Function | Notes |
|---|---|
decimals() | 24 — a wHAM share is a HAM internal unit (INTERNAL_DECIMALS = 1e24), not an 18-decimal fragment. |
balanceOf(addr) | Does not change at a rebase. |
deposit(fragments) → shares | Wrap. Shares are minted off the measured underlying delta, so a fee-on-transfer or dust edge cannot over-mint. |
withdraw(shares) → fragments | Unwrap at the current scaling factor. |
previewDeposit(fragments) · previewWithdraw(shares) | Quote either direction without transacting. |
excessUnderlying() | Underlying held by the wrapper beyond what its shares claim (e.g. donated HAM). |
24 decimals, not 18
Every integration error with wHAM is this one. A reader defaulting to 18 is wrong by 10^6. State the decimals explicitly wherever wHAM is registered and read decimals() back to confirm.
Because a share is a fixed quantity of internal units, holding wHAM through a positive rebase means the HAM it unwraps to grows — the appreciation shows up in the redemption rate, not the balance.
HAMRebaser — rebase policy (contracts/HAMRebaser.sol)
The live rebaser is HAMRebaserV2
HAM.rebaser() returns 0x08E5cEf25c7fe9dbcf584d1Bd95de5cDD55962F5 (HAMRebaserV2, deployed 2026-08-10 for the Ramses migration). The surface below documents V1 and is broadly shared, but V2 adds the seigniorage-sale path and a stale-TWAP-window bound. V1 0xa55fC153…b4Da is inert — the token will not accept a rebase from it. See Networks & addresses.
Ownership: Ownable2Step + ReentrancyGuard. Holds immutable yam (HAM), reserveToken, uniswapPair, isToken0, and usdPegMode.
Views
| Function | Notes |
|---|---|
getCurrentExchangeRate() | Peg-denominated rate. USD mode: TWAP × reserveUsd. Use this, not getCurrentTWAP, for USD-mode price. |
getCurrentTWAP() | Pool TWAP only (reserve per HAM); returns 0 if no time elapsed. |
getReserveUsdPrice() | Primary reserve/USD feed (USD mode); reverts on bad/stale answer. |
computeOffPegPerc(rate) | (offPegPerc, positive) vs targetRate, capped at MAX_RATE. |
inRebaseWindow() · epoch · targetRate · usdPegMode · rebasingActive | state getters |
Rebase flow
| Function | Access | Notes |
|---|---|---|
init_twap() | owner | Snapshot TWAP; once. |
activate_rebasing() | anyone | After rebaseDelay warmup; one-way. |
commitRebase(hash) | keeper | In-window; hash = keccak256(committer, salt, minReserveOut). |
revealAndRebase(salt, minReserveOut) | keeper | 25–50 blocks after commit; USD mode enforces oracle-drift bound. |
cancelCommit() / cancelExpiredCommit() | committer·owner / anyone | Recover a stuck commit. |
forceRebase(minReserveOut) | owner | Emergency, owner-only; bypasses commit/reveal (and its sandwich protection) but still requires the rebase window and interval. |
Admin — setKeeper, setTreasury, setDeviationThreshold, setRebaseLag, setRebaseMintPerc, setMaxSlippageFactor, setRebaseTimingParameters, setRebaseDelay, secondary-oracle setters; and USD-mode: proposeReserveUsdOracles → applyReserveUsdOracles (24h timelock) / cancelReserveUsdOracleProposal, setMaxReserveOracleAge, setMaxCommitRevealOracleDrift. setTargetRate reverts in USD mode.
Key events: RebaseExecuted, UsdPegPriceComputed, RebaseCommitted, ReserveUsdOraclesProposed, NewTreasury, KeeperSet.