The Vault
RWAVault (vSPHYNX): an ERC-4626 vault over USDG that holds tokenized stocks and reverts any order breaching its written caps. How shares are priced, how NAV is computed, what previewTrade checks, and the exits that always work.
RWAVault is the custody layer. It holds USDG and allowlisted Stock Tokens, issues vSPHYNX shares against them, and is the only contract that can move value. It has exactly one door for trading, executeTrade(), and that door is guarded by the same previewTrade() anyone can call for free.
Shares and NAV
The vault is a standard ERC-4626 over USDG with a 6-decimal offset: one USDG of NAV is 1,000,000 share units, so vSPHYNX has 12 decimals. The offset makes the first-depositor inflation attack economically pointless (an attacker would need to donate a million times the victim's deposit to move the price by one unit).
function totalAssets() public view override returns (uint256 nav) {
nav = usdgBalance();
for (uint256 i = 0; i < _allowlist.length; i++) nav += positionValue(_allowlist[i]);
}
function positionValue(address token) public view returns (uint256) {
uint256 bal = IERC20(token).balanceOf(address(this));
if (bal == 0) return 0;
return bal.mulDiv(oracle.priceE18(token), 1e30); // 18-dec units × 18-dec price → 6-dec USDG
}NAV is cash plus every allowlisted position at the oracle's price, which is a 5-minute TWAP from the token's Uniswap V3 pool (see Oracle & Execution). Because the oracle reverts on a stale or deviating price, totalAssets() reverts too, and with it every deposit, withdrawal and order. The vault would rather refuse than value the book wrong. redeemInKind() does not need a price and keeps working.
What previewTrade checks, in order
The check is a pure view. It short-circuits at the first rule broken, so the verdict names the first reason, not all of them.
Paused → ZeroAmount → NotAllowed
Cheap gates first. The token must be on the owner's allowlist (max 16 entries).
Unfunded
NAV must be non-zero. Everything below is a percentage of NAV.
MaxDailyOrders
Executed orders today (UTC) must be below maxDailyOrders. Applies to buys and sells.
Sells stop here
A sell only needs held ≥ amountIn. Reducing risk is never refused by a risk cap.
DailyLossHalt
If NAV is below the day's opening NAV by more than dailyLossHaltBps, buys are frozen for the day. The opening NAV is captured on the first order of the day.
MissingStop
stopPriceE18 must be > 0, < mark, and ≥ mark × (1 − stopLossBps). A nominal $0.01 stop fails.
PerTradeCap
amountIn ≤ NAV × perTradeBps.
Concentration
Current position value + amountIn ≤ NAV × maxConcentrationBps.
MaxPositions
If this would open a new token, distinct positions must be below maxOpenPositions.
CashBuffer
Cash after the buy ≥ NAV × cashBufferBps.
NoAveragingIntoLoser
If the token is already held and mark < average cost, refused unless leftSideException is set.
Execution
executeTrade() is onlyExecutor and nonReentrant. It re-runs the check, reverts with GuardrailBreach(violation) on anything but None, then approves the adapter for exactly amountIn and calls swap(). The vault measures its own balance before and after; if it received less than minAmountOut it reverts SlippageNotMet regardless of what the adapter claimed. Cost basis is average-cost: a buy adds amountIn to costUsdg, a sell removes the proportional share.
Exits
maxWithdraw)redeemInKind() burns shares, then transfers balance × shares × (1 − exitFee) / supply of USDG and of each allowlisted token. The exit fee (max 1%, 0% at launch) is not transferred anywhere: it simply stays, raising the share price for everyone left. Cost basis is scaled down with the units that leave so the average cost of the remaining position is unchanged.
Owner powers, and their limits
| Owner can | Owner cannot |
|---|---|
| Set the executor, oracle, adapter, guardrails contract | Withdraw or transfer any asset |
| Change the deposit cap | Mint shares |
| Pause deposits and orders | Block redeemInKind |
| Add or remove tokens from the allowlist (max 16) | Trade (only the executor can) |
| Set the exit fee, up to 1% | Set a management or performance fee (none exist) |
Events
event TradeExecuted(address indexed token, bool isBuy, uint256 amountIn, uint256 amountOut, uint256 priceE18, uint256 navAfter);
event RedeemedInKind(address indexed owner, address indexed receiver, uint256 shares, uint256 usdgOut);
event AllowlistSet(address token, bool allowed);
event DepositCapSet(uint256 cap); event ExitFeeSet(uint16 bps);
event ExecutorSet(address); event OracleSet(address); event AdapterSet(address); event GuardrailsSet(address);
// plus ERC-4626 Deposit / Withdraw and Pausable Paused / UnpausedThe Trade terminal builds its Trades and Deposits tabs from TradeExecuted, Deposit, Withdraw and RedeemedInKind. Addresses and ABIs in Contracts.