23, On-chain · Contracts

Contracts Reference

Every SPHYNX contract on Robinhood Chain mainnet (chainId 4663): address, role, owner, the functions you will call, and how to build, test and redeploy from the repo.

Deployed 2026-09-20 at block 26,015,235 by forge script script/Deploy.s.sol:Deploy. The addresses below are the contents of onchain/deployments/latest.json, which the site, the CLI and the dashboard all import. If that file changes, everything follows.

ContractAddressOwner
RWAVault (vSPHYNX)0x510Af4fC7fA571e5549258541a9374dE3D894F28deployer
SessionKeyExecutor0x87a6F83D1375401e1BfFca9e8055228033788713deployer
GuardrailConfig0x7Ec7A870361E75A44E5549b57Cd437742e509be2deployer
UniswapV3Oracle0xBf4fbd55eB70DC6424d839B9F6fDbc693A63cCe5deployer
UniswapV3Adapter0x18bdc0EE9C2d33eeAbC5fe422126310cf3df13bCdeployer
DeskRegistry0x685915EB0226757bFeae58c5f5BdD3f5F493Eac4none (subject-owned)
Deployer / owner / agent0x21BFa4F43D78f388219c0743CCb9dCa98bD1244aEOA

Robinhood Chain mainnet · chainId 4663 · explorer robinhoodchain.blockscout.com

GuardrailConfig

solidity
function caps() view returns (Caps)   // (perTradeBps, maxConcentrationBps, maxOpenPositions, maxDailyOrders, stopLossBps, dailyLossHaltBps, cashBufferBps)
function setCaps(uint16,uint16,uint8,uint8,uint16,uint16,uint16) onlyOwner

// hard ceilings the owner cannot exceed
MAX_PER_TRADE_BPS      = 5000   // 50%
MAX_CONCENTRATION_BPS  = 5000
MAX_STOP_LOSS_BPS      = 2500   // stop no deeper than 25%
MIN_CASH_BUFFER_BPS    = 500    // at least 5% cash after a buy
CapLaunchMeaning
perTradeBps1500Max single order, % of NAV
maxConcentrationBps2500Max % of NAV in one token after the order
maxOpenPositions6Distinct tokens held
maxDailyOrders4Executed orders per UTC day
stopLossBps800A buy's stop must be within 8% below mark
dailyLossHaltBps500Buys freeze once NAV is 5% below the day's open
cashBufferBps1000Min % of NAV in USDG after a buy

RWAVault

solidity
// ERC-4626 (asset = USDG, shares 12 dec)
deposit(uint256 assets, address receiver) returns (uint256 shares)
mint / withdraw / redeem / previewDeposit / convertToAssets / maxDeposit / maxWithdraw …
redeemInKind(uint256 shares, address receiver) returns (uint256 usdgOut)   // always works

// the riddle
previewTrade(Trade) view returns (Violation)      // enum index, see below
quoteNotional(Trade) view returns (uint256)       // USDG value of an order
executeTrade(Trade) onlyExecutor returns (uint256 amountOut)

// reads
totalAssets() / navUsdg() / usdgBalance() / openPositions() / allowlist()
positions(address) → (costUsdg, stopPriceE18)
positionValue(address) / isAllowed(address) / today() → (day, orders, openNav)
depositCap() / exitFeeBps() / paused() / executor() / oracle() / adapter() / guardrails()

// owner
setExecutor / setOracle / setAdapter / setGuardrails / setDepositCap / setExitFee(≤100) / setAllowed / pause / unpause

struct Trade { address stockToken; bool isBuy; uint256 amountIn; uint256 minAmountOut; uint256 stopPriceE18; bool leftSideException; }
enum Violation { None, Unfunded, DailyLossHalt, PerTradeCap, MaxDailyOrders, Concentration, MaxPositions,
                 CashBuffer, NoAveragingIntoLoser, MissingStop, NotAllowed, ZeroAmount, InsufficientPosition, Paused }

SessionKeyExecutor

solidity
execute(Trade) returns (uint256 amountOut)     // caller must hold a live session
sessions(address) → (active, expiry, maxNotionalPerTrade, maxTrades, tradesUsed, maxCumNotional, cumNotionalUsed, buysAllowed, sellsAllowed)
isLive(address) view returns (bool)
tokenAllowed(address agent, address token) view returns (bool)
vault() view returns (address)

// owner
grant(address agent, uint64 expiry, uint256 maxNotionalPerTrade, uint32 maxTrades, uint256 maxCumNotional, bool buys, bool sells, address[] tokens)
revoke(address agent)
setTokenAllowed(address agent, address token, bool)

errors: NoSession SessionExpired SideNotAllowed TokenNotInSession TradeTooLarge TradeCountExhausted BudgetExhausted BadExpiry

UniswapV3Oracle · UniswapV3Adapter

solidity
// oracle
priceE18(address token) view returns (uint256)   // 5m TWAP, reverts SpotDeviates / NoPool
spotE18(address token) view returns (uint256)
twapSeconds() / maxSpotDeviationBps() / poolOf(address)
setPool(address token, IUniswapV3Pool) onlyOwner   setParams(uint32, uint16) onlyOwner

// adapter
swap(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, address to) returns (uint256)
quote(address tokenIn, address tokenOut, uint256 amountIn) returns (uint256)   // eth_call it
poolOf(address) / usdg()
setPool(address token, IUniswapV3Pool) onlyOwner

DeskRegistry

solidity
subjectFor(address owner, bytes32 salt) pure returns (bytes32)
attest(bytes32 salt, uint64 epoch, uint256 nav, int256 realizedPnl, bytes32 snapshotHash, string uri) returns (uint256 index)
attestAs(bytes32 subject, …)                 // for a delegated attester
setAttester(bytes32 salt, address attester)
count(bytes32 subject) / at(bytes32 subject, uint256 i) / latest(bytes32 subject) / attesterOf(bytes32)

// SPHYNX vault subject (seeded at deploy with epoch 0 "sphynx:genesis"):
// salt    = keccak256("sphynx-vault:" ‖ vault)
// subject = 0x0a85fbcdf8264204fe7ff101875e2511923318d68b156a42f0763d36df7c9c0e

Anyone can own a subject; SPHYNX's is derived from the deployer and the vault address. Nothing on the registry can be edited or removed. It is the on-chain counterpart of the desk's logs/*.jsonl, and the place refusals and vetoes will be published.

Build, test, deploy

onchain/
forge build
forge test                                            # 29 unit tests on mocks
FORK_RPC=$RPC forge test --mc Fork -vv                # deploys on a mainnet fork, buys + sells NVDA through the real pool

cp .env.example .env                                  # DEPLOYER_PRIVATE_KEY, AGENT_PRIVATE_KEY, AGENT, optional OWNER
forge script script/Deploy.s.sol:Deploy --rpc-url $RPC --broadcast -vv
node scripts/sync-site.mjs                            # addresses → public/app/desk-state.json

The deploy script refuses to run off chainId 4663, checks USDG.decimals() == 6 and that the NVDA pool's token1() is NVDA before broadcasting, deploys the six contracts, registers the three pools on both oracle and adapter, allowlists the three tokens, grants the agent a 30-day session, seeds the registry, optionally transfers ownership to OWNER, and writes deployments/latest.json.

Repo layout

text
onchain/
├── src/
│   ├── Guardrails.sol            shared types: Caps, Trade, Violation
│   ├── GuardrailConfig.sol
│   ├── RWAVault.sol
│   ├── SessionKeyExecutor.sol
│   ├── UniswapV3Oracle.sol
│   ├── UniswapV3Adapter.sol
│   ├── PostedPriceOracle.sol     alternative owner-fed oracle (not deployed)
│   ├── DeskRegistry.sol
│   ├── interfaces/               IPriceOracle, ISwapAdapter, IUniswapV3
│   ├── libraries/TickMath.sol    port of v3-core sqrt-price math
│   └── mocks/                    MockERC20, MockRouter, MockSwapAdapter (tests only)
├── script/Deploy.s.sol
├── test/Desk.t.sol               29 tests: every Violation, sessions, exits, registry
├── test/Fork.t.sol               mainnet fork round trip
├── deployments/latest.json       the addresses everything reads
├── scripts/sync-site.mjs
└── desk.sh                       read-only CLI