GravFi
GRAVFI · Finance documentation

Capital has gravity.

GravFi pulls fragmented capital into Robinhood Chain. An asset is locked in a single-purpose vault on Ethereum, a message is delivered by a configured messenger, and a GravFi representation (gToken) is minted 1:1 on Robinhood Chain. The reverse path burns the gToken and unlocks the collateral. Proof of Gravity compares the two sides on-chain, continuously.

What the interface promises

  • Every number is read from a contract or an RPC. There is no seeded TVL, no sample transaction, no placeholder address.
  • A feature whose configuration is missing is disabled and labelled NOT CONFIGURED / NOT DEPLOYED.
  • A reserve that cannot be read is UNVERIFIED, never “100% backed”.
  • Bridge stages derive from receipts, events and contract views — not timers.

Architecture

src/config        env · chains · assets · contracts · brand   (single source of configuration)
src/lib/chain     viem public clients (browser → /api/rpc/<chain> relay)
src/lib/reserves  Proof of Gravity engine (pinned-block reads on both chains)
src/lib/bridge    BridgeAdapter interface + adapters/native · adapters/layerzero
src/lib/swap      SwapAdapter interface + adapters/uniswapV2
src/lib/liquidity factory/pair enumeration
src/lib/history   HistoryAdapter: rpc-logs (default) · indexer (when configured)
src/lib/router    Gravity Router: bridge leg + swap leg composition
src/hooks         TanStack Query + wagmi hooks; no transaction logic in components
contracts/        Foundry workspace: GravVault · GravToken · GravBridge · IGravMessenger

Bridge

GravBridge is deployed once per chain. On the source chain each route points at a GravVault; on Robinhood Chain each route points at a GravToken. The bridge does not implement a messaging protocol: it calls IGravMessenger.send and accepts receiveMessage only from the messenger it was configured with. A LayerZero OApp, a native rollup messenger or a test double can sit behind that interface.

wrap    user → approve(vault) → bridge.bridgeOut{value: fee}(USDC, amount, recipient)
        vault.depositFrom(user, amount) → totalLocked += received
        messenger.send(dstEid, abi.encode(nonce, gUSDC, recipient, received))
        … remote messenger → bridge.receiveMessage(srcEid, payload)
        gUSDC.mintFromBridge(recipient, received)

unwrap  user → bridge.bridgeOut{value: fee}(gUSDC, amount, recipient)   // burns caller's gUSDC
        … → vault.withdrawTo(recipient, amount)   // subject to the rolling withdraw limit

Frontend adapters live in src/lib/bridge/adapters. The native adapter quotes the messaging fee from the bridge contract, prepares the write for the wallet (it never signs), and tracks completion via processed(nonce) and the BridgeCompleted event on the far chain. The LayerZero adapter reports NOT CONFIGURED: no LayerZero endpoint is deployed on Robinhood Chain (checked on-chain), so the live transport is GravRelayMessenger — an operator-signed relayer. The Bridge page says so on every pending message, and an account with RELAYER_ROLE can deliver from the page or with npm run relayer. Deploy everything from /app/deploy with your own wallet.

Proof of Gravity

For each asset, at a pinned block per chain:

source       token.balanceOf(vault)   vault.totalLocked()   token.decimals()
destination  gToken.totalSupply()     gToken.decimals()

backingBps = balanceOf(vault) · 10_000 / totalSupply     (decimals aligned)
≥ 100.00%  FULLY BACKED · 99–100%  WATCH · < 99%  UNDERCOLLATERALIZED
unreadable → UNVERIFIED · unconfigured → AWAITING DEPLOYMENT

The inspector shows every raw read with its contract, method, value and block, with copy and explorer actions. The classification lives in src/lib/reserves/engine.ts and is never stored.

Swap & liquidity

Swaps run through a router exposing the Uniswap V2 interface on Robinhood Chain. Route discovery tries the direct pair, then a two-hop route through WETH. Quotes come from getAmountsOut; price impact is derived from pair reserves along the route. Quotes older than 30 seconds are re-fetched before signing. The liquidity explorer enumerates the factory and omits USD TVL and volume because no price source is configured.

Gravity Router

The router composes the bridge leg and the swap leg into one plan with per-stage availability, fees and outputs. It is explicit that execution requires two signatures on two chains and is not atomic.

Configuration

NEXT_PUBLIC_APP_ENV selects the network pair: mainnet Ethereum (1) → Robinhood Chain (4663); anything else → Sepolia (11155111) → Robinhood Chain Testnet (46630). All addresses come from .env; see .env.example. The full activation sequence is in the README.

Security

  • Roles: DEFAULT_ADMIN (routes, messenger, limits), PAUSER (pause/unpause), BRIDGE_ROLE (vault deposit/withdraw, token mint/burn) — granted only to the bridge contract.
  • No public mint; no tx.origin; SafeERC20; ReentrancyGuard on every fund-moving path; Pausable everywhere.
  • Vault accounting uses the amount actually received, so fee-on-transfer tokens cannot inflate supply.
  • Replay protection by nonce on the receiving bridge; wrong-source and unauthorized-messenger reverts.
  • Invariant tests: wrapped supply ≤ locked collateral; exact accounting including in-flight messages; vault holds what it reports.
  • Contracts are experimental until independently audited.