# How it works (/docs/concepts/how-it-works)
Each Solidon meme is one NEAR contract. It is a token, a pool, a treasury ledger and a set of capital rules at the same time. Trading fees become treasury cash. The contract sends that cash to its own addresses on three chains and signs, through NEAR Chain Signatures, the swaps that turn it into the meme's thesis stock.
Everything described here runs on testnets: NEAR testnet, Base Sepolia, Robinhood Chain testnet and Solana devnet. The stock tokens are Solidon synthetic tokens priced from the Chainlink feeds on Robinhood Chain mainnet. The USDC is test USDC with no value.
## The flow [#the-flow]
M[\"Meme contract on NEAR: NEP-141 + AMM + mNAV engine\"]\nM -- \"0.8% fee\" --> T[(\"Treasury cash on NEAR\")]\nT -- \"fund_chain\" --> B[\"Bridge: keeper on testnet, Intents 1Click on mainnet\"]\nB -- \"USDC\" --> E1[\"Base treasury\"]\nB -- \"USDC\" --> E2[\"Robinhood Chain treasury\"]\nB -- \"USDC\" --> S1[\"Solana treasury\"]\nM -- \"sign approve and swap\" --> MPC[\"MPC signer v1.signer-prod.testnet\"]\nMPC -- \"signature, checked by the contract\" --> M\nM -- \"signed tx\" --> K[\"Anyone broadcasts\"]\nK --> E1\nK --> E2\nK --> S1\nE1 -- \"Uniswap v3\" --> N1[\"Stock\"]\nE2 -- \"Uniswap v3\" --> N2[\"Stock\"]\nS1 -- \"Raydium CLMM\" --> N3[\"Stock\"]" />
### A trader buys the meme [#a-trader-buys-the-meme]
The trader sends USDC to the meme contract with `ft_transfer_call` and `msg: {"action":"buy","min_out":"..."}`. The contract takes a 1% fee from the USDC: 0.8% goes to treasury cash, 0.2% is credited to the creator. The rest goes into the constant-product pool and the trader receives meme tokens. See [The meme token](/docs/concepts/token).
### Fees build treasury cash on NEAR [#fees-build-treasury-cash-on-near]
Treasury cash is USDC held by the meme contract on NEAR. The contract keeps 20% of NAV on NEAR as a buffer for buybacks and redemptions.
### Anyone calls fund_chain [#anyone-calls-fund_chain]
`fund_chain(chain)` reads a fresh oracle price, then sends the NEAR cash above the 20% buffer (at most one swap's worth, `max_swap_usdc`) to the factory's bridge account. The amount is booked as *in transit* for that chain and stays in NAV. A failed transfer returns it to NEAR cash.
### The bridge delivers USDC to the treasury [#the-bridge-delivers-usdc-to-the-treasury]
On testnet the bridge is a keeper account (`keeper.solidon.testnet`) that delivers test USDC to the treasury address on the target chain. On mainnet this leg is NEAR Intents / 1Click. See [NEAR Intents](/docs/concepts/intents).
### The arrival is credited [#the-arrival-is-credited]
- **Base:** anyone calls `credit_cash` with the USDC `Transfer` log. The contract asks the MPC network to verify that exact log (`verify_foreign_transaction`). Only then is the chain's USDC ledger credited.
- **Robinhood Chain and Solana:** the factory's relayer account calls `report_cash`.
Every credit is capped by the cash in transit for that chain and is booked once. See [Treasuries](/docs/concepts/treasuries).
### Anyone calls buy_stock [#anyone-calls-buy_stock]
`buy_stock(chain)` (EVM) or `buy_stock_solana(blockhash, tick_arrays)` reads the oracle and builds the swap inside the contract, with `minAmountOut` set to the oracle value minus 1%. The contract asks `v1.signer-prod.testnet` for a signature, verifies it against its own treasury key, and stores the signed transaction.
### Anyone broadcasts [#anyone-broadcasts]
Any relayer can read the signed transaction with `get_signed_tx` and send it to the chain. The swap runs on Uniswap v3 (Base Sepolia, Robinhood Chain testnet) or Raydium CLMM (Solana devnet). The stock lands at the treasury address. The ledger books the `minAmountOut` it signed for.
### The mNAV engine acts [#the-mnav-engine-acts]
Once per hour anyone can call `rebalance()`. The contract compares the pool's 30-minute TWAP with NAV per token and issues, holds, buys back or opens redemptions. See [mNAV](/docs/concepts/mnav).
## The actors [#the-actors]
| Actor | What it is | What it can do |
| --- | --- | --- |
| Trader | Any NEAR account | Buy, sell, and redeem while the redemption window is open |
| Creator | The account that launched the meme | Seeds the pool once, claims the 0.2% creator fee with `claim_usdc()` |
| Factory | `solidon.testnet` | Deploys each meme as a sub-account (for example `honey.solidon.testnet`), holds the immutable stock list and chains config |
| Meme contract | One per meme | NEP-141 token, AMM pool, mNAV engine, treasury ledger. Builds and requests signatures for every treasury transaction. No owner method |
| MPC signer | `v1.signer-prod.testnet` (NEAR Chain Signatures) | Signs for the meme's derived EVM (ECDSA) and Solana (EdDSA) addresses. Verifies Base logs for `credit_cash` |
| Oracle | A Pyth-interface contract on NEAR | Gives the stock price. On testnet it is a mock oracle fed live Chainlink prices (Robinhood Chain mainnet) by the keeper |
| Bridge | The factory's `bridge` account | Delivers USDC from NEAR to a treasury chain. Testnet: keeper account. Mainnet: Intents / 1Click |
| Relayer | The factory's `relayer` account | Reports cash arrivals on Robinhood Chain and Solana, sets the Solana token accounts. Capped by cash in transit |
| Broadcaster | Anyone | Sends signed transactions to the chains. The official keeper is a convenience, not a trust point |
| Caller of `rebalance()` | Anyone | Triggers the mNAV engine once per hour and earns a 0.05% tip of the action value |
## One meme, three chains, live [#one-meme-three-chains-live]
The test meme `e2etest.solidon.testnet` holds NVDA on all three chains. Every treasury step was signed by the NEAR MPC at the meme contract's request; the keeper only broadcast and bridged. The full list of transactions is in `docs/DEPLOYMENTS.md` under "End to end" and on [Deployments](/docs/reference/deployments).
| Chain | Ledger NVDA (booked minimum) | On-chain NVDA |
| --- | --- | --- |
| Base Sepolia | 0.02965417 | 0.03032966 |
| Robinhood testnet | 0.01060760 | 0.01082210 |
| Solana devnet | 0.02770371 | 0.02831835 |
The ledger is below the on-chain balance on every chain, as designed for swaps that land.
# NEAR Intents (/docs/concepts/intents)
NEAR Intents moves value between chains by letting solvers fill a swap request. Solidon uses its 1Click API in two places: to let a user pay from another chain, and, on mainnet, to carry treasury cash from NEAR to the treasury chains.
NEAR Intents runs on mainnet only. Solidon runs on testnets, so no Intents swap is executed. The app requests live 1Click quotes from mainnet with `dry: true`: real routes, real prices and fees, but no deposit address and no swap.
## What is live and what is not [#what-is-live-and-what-is-not]
| Use | Status |
| --- | --- |
| "Pay from another chain" quotes in the app | Live: real 1Click dry quotes from mainnet |
| Executing a cross-chain payment into USDC on NEAR | Mainnet only. Built into the app behind `NEXT_PUBLIC_NETWORK=mainnet`; not used on testnet |
| Treasury bridge leg (NEAR cash to Base, Robinhood Chain, Solana) | Testnet: a trusted keeper account. Mainnet design: Intents / 1Click |
| Routing tokenized stocks through 1Click | Not possible: no tokenized stock is routable through 1Click on any chain. The treasury buys the stock locally |
## Pay from another chain [#pay-from-another-chain]
On a meme page, **Buy** → **Pay from another chain** asks 1Click for a quote from an asset on another chain, for example SOL, ETH on Base, BTC or USDC on Arbitrum, into USDC on NEAR. USDC on NEAR is the asset a Solidon buy is paid in.
The app talks to 1Click through a narrow server proxy (`/api/oneclick/tokens`, `/api/oneclick/quote`, `/api/oneclick/status`). The destination asset is fixed to USDC on NEAR (`nep141:17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1`), so the proxy cannot be used as an open relay. Unless the app is built with `NEXT_PUBLIC_NETWORK=mainnet`, every quote is forced to dry.
On mainnet the same component requests a deposit address, the user sends the origin asset there, and the app follows the swap status until the USDC lands in the user's NEAR account. The user then buys the meme with it.
### What the spikes measured [#what-the-spikes-measured]
From live dry quotes (`docs/SPIKE.md` S6, S11, S17):
- User entry into USDC on NEAR: quotes work from $20 (the smallest size tried).
- Outbound from USDC on NEAR (the treasury leg): quotes work from $0.50 to Base, Robinhood Chain (as ETH) and Solana.
- Indicative times: SOL about 20 seconds, USDC on Base about 35 seconds, BTC about 13 minutes.
- Fee about 0.2% without a 1Click JWT. A JWT is optional (`ONECLICK_JWT`, server-only).
- A recipient NEAR account that does not exist makes every quote fail with a generic `400`.
## Where Intents fits on mainnet [#where-intents-fits-on-mainnet]
N[\"USDC on NEAR\"]\nN -- \"buy\" --> M[\"Meme contract\"]\nM -- \"fund_chain: cash above the 20% buffer\" --> BR[\"Bridge leg: Intents 1Click\"]\nBR -- \"USDC\" --> T[\"Treasuries on Base, Robinhood Chain, Solana\"]" />
1. **Entry from any chain.** A user pays with BTC, SOL, ETH or USDC from another chain and receives USDC on NEAR, then buys the meme.
2. **The treasury bridge leg.** `fund_chain(chain)` sends cash above the 20% buffer to the factory's bridge account, booked as in transit. On mainnet that account's job is done by an Intents / 1Click swap from USDC on NEAR to USDC on the treasury chain. The spikes measured a full cycle (cash to the treasury chain, buy stock, prove it) at about $0.02 to $0.05 on Base, Robinhood Chain or Solana, above $0.50.
## Open work [#open-work]
- **A contract-driven Intents swap.** Today the bridge account is trusted to deliver. The candidate design: the meme registers an MPC-derived ed25519 key on `intents.near` and MPC-signs its own NEP-413 intents, so nobody has to supply a 1Click deposit address. Mainnet only, not built.
- **Redeem to another chain.** Paying a redemption straight to a chain of choice through Intents is listed as later work.
See [Security](/docs/security) for how the bridge trust assumption is bounded.
# mNAV (/docs/concepts/mnav)
mNAV is the ratio between what the market pays for a meme and what its treasury is worth. Solidon borrows the idea from treasury companies such as MicroStrategy: when the shares trade at a premium to the asset held, the company issues shares to buy more of the asset. In Solidon the rule is written in the meme contract, runs automatically, and is the same for everyone.
## Definitions [#definitions]
**NAV per token** is the treasury value divided by the circulating supply:
```text
NAV per token = ( sum over chains of
stock units x oracle price
+ USDC on the chain
+ USDC in transit to the chain )
+ USDC cash on NEAR
---------------------------------
circulating supply
```
**mNAV** is the pool price divided by NAV per token:
```text
mNAV = meme price in the pool (30-minute TWAP) / NAV per token
```
Equivalently, meme market cap divided by treasury value.
- **Circulating supply** is total supply minus the pool's meme inventory.
- **One price per stock.** The stock on every chain is valued at the same oracle price.
- **Cash in transit counts.** USDC sent from NEAR to a treasury chain stays in NAV while it travels, so moving cash never changes NAV.
- **Stock is booked, not read.** Stock units are the `minAmountOut` the contract signed for. See [Treasuries](/docs/concepts/treasuries).
## The four zones [#the-four-zones]
| mNAV | Contract action | Limit |
| --- | --- | --- |
| above 2.0 | Mint new tokens and sell them into the pool; the USDC proceeds go to treasury cash and later buy stock | At most 1% of circulating supply per 24 hours. Only if every new token is sold at or above NAV |
| 1.0 to 2.0 | No action. The premium moves freely | None |
| below 1.0 | Buy memes from the pool with treasury cash and burn them | Only cash above the 20% buffer. Never sells stock to fund a buyback |
| below 0.9 | Open the redemption window: holders burn tokens and receive NAV per token in USDC | Paid from NEAR cash only. The buyback rule also still applies |
| stale oracle | No action | Price older than 15 minutes |
The thresholds are stored in basis points in the contract's `Params` (`issuance_mnav_bps: 20000`, `buyback_mnav_bps: 10000`, `redemption_mnav_bps: 9000`) and can be read with `get_params`. There is no method that changes them.
## How one rebalance runs [#how-one-rebalance-runs]
### Anyone calls rebalance() [#anyone-calls-rebalance]
`rebalance()` takes 100 TGas and can be called by any account. It fails if less than one hour has passed since the last action.
### The contract reads the oracle [#the-contract-reads-the-oracle]
It calls `get_price_no_older_than(price_id, 900)` on the oracle. If no price fresher than 15 minutes exists, the outcome is `StalePrice` and nothing changes. Pool trading keeps working.
### It computes NAV and mNAV [#it-computes-nav-and-mnav]
NAV per token uses the fresh price. The pool price is the 30-minute TWAP, built from checkpoints taken every 5 minutes. If the pool has no TWAP yet, the outcome is `NoMarket`.
### It applies the zone rule [#it-applies-the-zone-rule]
Issuance above 2.0, buyback below 1.0, redemption window flag below 0.9. The caller receives a tip of 0.05% of the action value, paid from treasury cash.
## Why each rule exists [#why-each-rule-exists]
### Issuance above 2.0, capped at 1% per day [#issuance-above-20-capped-at-1-per-day]
- **The premium can run.** Issuance starts only at a high premium, so a rally is not capped early.
- **Issuance is accretive.** The contract sells only enough new tokens to bring the pool toward 2.0 times NAV, and it checks that the proceeds per new token are at least NAV. Every issuance therefore adds more value than it dilutes, and stock units per token rise. This is the MicroStrategy flywheel, enforced by code.
- **No sudden dilution.** The cap is 1% of circulating supply per 24-hour epoch. A parameter sweep of thresholds 2.0, 2.5, 3.0 and caps 0.5%, 1% kept the floor identical, and a 0.5% cap hurt the bull case, so 2.0 and 1% were kept.
### Hold between 1.0 and 2.0 [#hold-between-10-and-20]
The premium belongs to the market. The contract does nothing in this band, so the meme can move harder than the stock in both directions.
### Buyback below 1.0, from surplus cash only [#buyback-below-10-from-surplus-cash-only]
When the pool trades below NAV, the contract buys memes from the pool and burns them. It spends only NEAR cash above the 20% buffer and never sells stock to do it.
Tokens bought back come from the pool's inventory, not from holders, so circulating supply does not fall and each buyback lowers NAV per token. An earlier rule let buybacks spend cash down to 10% of NAV, after which a stock sale refilled the buffer and the next buyback spent it again. In the stock crash scenario that rule ended with +56.4% stock units per token and +0.9% NAV per token; the adopted rule ended with +68.8% and +3.9%. Redemption, which is value-neutral, carries the floor instead. See [Simulation](/docs/simulation).
### Redemption below 0.9 [#redemption-below-09]
Holders burn tokens with `redeem(amount, min_usdc)` and receive NAV per token in USDC from treasury cash. Redemption at NAV is value-neutral for holders who stay. The window opens only when a rebalance measures mNAV below 0.9, so the floor is stated as "back at 0.9 or above within 2 hours". Worst case across all simulation runs: minimum mNAV 0.87, at most 1 hour below 0.9.
Each redemption reads a fresh price. If the price is stale, the window has closed, `min_usdc` is not met, or cash is short, the tokens are returned. See [Redeem](/docs/guides/redeem).
### 30-minute TWAP and 1-hour cooldown [#30-minute-twap-and-1-hour-cooldown]
A single large trade cannot push the pool price far enough, for long enough, to trigger issuance or a buyback. The engine reads an average over 30 minutes and acts at most once per hour.
### 15-minute oracle staleness stop [#15-minute-oracle-staleness-stop]
Equity prices do not always update, for example while the stock market is closed. If the price is older than 15 minutes, every mNAV action stops: no issuance, no buyback, no redemption, no `fund_chain` and no `buy_stock`.
## Locked parameters [#locked-parameters]
| Parameter | Value |
| --- | --- |
| Issuance threshold | mNAV above 2.0 |
| Max issuance | 1% of circulating supply per 24 hours |
| Buyback threshold | mNAV below 1.0 |
| Redemption threshold | mNAV below 0.9 |
| Cash buffer on NEAR | 20% of NAV |
| Pool price for mNAV | 30-minute TWAP |
| Oracle staleness limit | 15 minutes |
| Rebalance cooldown | 1 hour |
| Caller tip | 0.05% of the action value |
| Target leverage | 1.0x (MVP) |
If the stock falls, NAV falls with it. The floor removes the extra loss from the hype premium collapsing below NAV; it does not protect against the stock itself.
# The meme token (/docs/concepts/token)
Each meme is a NEAR sub-account of the factory, for example `honey.solidon.testnet`. One contract holds the token, its pool and its treasury. There is no owner method and no admin key; launched memes have no access key at all.
## Token standard [#token-standard]
| Property | Value |
| --- | --- |
| Standard | NEP-141 (NEAR fungible token), with on-chain metadata |
| Decimals | 18 |
| Initial supply | 1,000,000,000 tokens |
| Initial holder | The meme contract itself, as pool inventory |
| Icon | Stored on-chain at launch; meme icons are immutable |
At launch the whole supply is minted to the contract as the pool's meme inventory. The creator receives no tokens. Anyone who wants tokens buys them from the pool.
**Circulating supply** is total supply minus the pool's inventory. NAV per token and the issuance cap use it.
## Launch and seed [#launch-and-seed]
### Create the meme [#create-the-meme]
The creator calls `create_meme` on the factory with a name, a symbol, a thesis stock from the factory's list and an optional icon, and attaches at least `launch_cost()` (4.46 NEAR on the current testnet factory). The factory deploys the meme contract as a sub-account with locked parameters. See [Launch a meme](/docs/guides/launch).
### Seed the pool [#seed-the-pool]
The creator sends the first USDC liquidity once, with `ft_transfer_call` on USDC and `msg: {"action":"seed"}`. Only the creator can seed, and only once. The pool then holds the full supply against the seed, which sets the starting price. HONEY was seeded with 1,000 USDC.
### Open the treasury [#open-the-treasury]
Anyone calls `init_treasury()` and `init_solana_treasury()` to derive the meme's treasury addresses. See [Treasuries](/docs/concepts/treasuries).
The factory lists eight stocks: NVDA, AAPL, MSFT, GOOGL, AMZN, META, TSLA and SPCX. The list and every stock venue are fixed when the factory is initialized. See [Stocks](/docs/reference/stocks).
## The built-in pool [#the-built-in-pool]
The pool is a constant-product AMM (x times y = k) inside the meme contract, between the meme inventory and USDC. Because it lives in the same contract, the mNAV engine reads its price directly. Every reserve change records a price observation for the 30-minute TWAP.
| Action | Call | Notes |
| --- | --- | --- |
| Buy | `ft_transfer_call` on USDC with `msg: {"action":"buy","min_out":""}` | Full refund of the USDC if the output is below `min_out` |
| Sell | `sell(amount, min_out)` with 1 yoctoNEAR | USDC is paid out; if the transfer fails, the amount becomes claimable |
| Quote | `quote_buy(usdc_in)`, `quote_sell(meme_in)` | Views, include the fee |
| Pool state | `get_pool` | Reserves, treasury cash, spot price and TWAP |
## Fee split [#fee-split]
The pool fee is 1% of the USDC side of every trade.
| Part | Rate | Destination |
| --- | --- | --- |
| Treasury | 0.8% | Treasury cash on NEAR, later used to buy stock |
| Creator | 0.2% | Credited to the creator, withdrawn with `claim_usdc()` |
| Total | 1.0% | |
- **On a buy** the fee is taken from the USDC paid in, before the swap.
- **On a sell** the fee is taken from the USDC the pool pays out.
Prices are stored as USDC raw units per meme raw unit, scaled by 1e30.
## How supply changes [#how-supply-changes]
Only three things change the supply after launch. None of them is a manual mint.
| Event | Effect | Trigger |
| --- | --- | --- |
| Issuance | Mints new tokens into the pool and moves the USDC they sell for into treasury cash | `rebalance()` with mNAV above 2.0, at most 1% of circulating supply per 24 hours, only if accretive |
| Buyback | Buys tokens from the pool with surplus cash and burns them | `rebalance()` with mNAV below 1.0 |
| Redemption | A holder burns tokens and receives NAV per token in USDC | `redeem(amount, min_usdc)` while mNAV is below 0.9 |
Details and rationale are on [mNAV](/docs/concepts/mnav).
## Sybil resistance [#sybil-resistance]
There are no per-account rewards. Every mechanic, from fees to redemption, is proportional to the token amount, so splitting holdings across accounts gains nothing.
# Treasuries (/docs/concepts/treasuries)
Every meme owns two treasury addresses outside NEAR: one EVM address used on both Base and Robinhood Chain, and one Solana address. Both are derived from the meme's NEAR account by the NEAR MPC network (Chain Signatures). No person holds a key to them. Only the meme contract can ask the MPC network to sign for them.
## Derived addresses [#derived-addresses]
| Treasury | Signature scheme | MPC domain | Derivation path | Set by |
| --- | --- | --- | --- | --- |
| EVM (Base Sepolia and Robinhood Chain testnet) | ECDSA (secp256k1) | 0 | `evm-treasury` | `init_treasury()`, anyone, once |
| Solana devnet | EdDSA (Ed25519) | 1 | `solana-treasury` | `init_solana_treasury()`, anyone, once |
The MPC signer is `v1.signer-prod.testnet`. The contract reads its derived public key and stores the resulting address. The same EVM address is used on every EVM chain, with a separate nonce per chain. On Solana the relayer then sets the treasury's USDC and stock token accounts with `set_solana_accounts`, from canonical bumps.
Example, HONEY (`honey.solidon.testnet`):
- EVM treasury on Robinhood Chain testnet: [`0x51b1197be727ce2fb229c4919eca30e5568c802a`](https://explorer.testnet.chain.robinhood.com/address/0x51b1197be727ce2fb229c4919eca30e5568c802a)
- Solana treasury: [`9gyosY8RFLdET2AGVj5XU14tpRGLFrun2VrpfZ1tAsLP`](https://explorer.solana.com/address/9gyosY8RFLdET2AGVj5XU14tpRGLFrun2VrpfZ1tAsLP?cluster=devnet)
## What the contract signs [#what-the-contract-signs]
The contract builds every treasury transaction itself, from config that was locked when the factory was initialized. A caller cannot supply calldata, a recipient or a token.
| Method | Chain | Transaction built by the contract |
| --- | --- | --- |
| `approve_router(chain)` | Base, Robinhood Chain | `approve(router, max)` on that chain's USDC, for the locked Uniswap v3 SwapRouter02 |
| `buy_stock(chain)` | Base, Robinhood Chain | SwapRouter02 `exactInputSingle` of up to `max_swap_usdc` USDC into the meme's locked stock, `minAmountOut` = oracle value minus 1% |
| `buy_stock_solana(blockhash, tick_arrays)` | Solana | A legacy message with `CreateIdempotent` for the stock token account and Raydium CLMM `swap_v2` of up to `max_swap_usdc` USDC into the stock, `minAmountOut` = oracle value minus 1% |
| `resign_solana(blockhash, tick_arrays)` | Solana | The last Solana buy again, same amount and `minAmountOut`, with a new blockhash |
On Solana, two inputs come from the caller: a recent blockhash and the pool's tick arrays. Every other account comes from locked config and the contract's own derivations. A wrong blockhash or tick array can only make the swap fail on Solana; it cannot redirect funds.
The testnet cap is 5 USDC per swap per chain, so the 0.3% pool fee plus price impact stays inside the 1% slippage allowed against the oracle. Larger balances are swapped over several transactions.
## Signature verification before storing [#signature-verification-before-storing]
The contract does not trust the MPC response blindly. On every signature it runs:
- `ecrecover` on EVM, and
- `ed25519_verify` on Solana, over the exact message,
and stores the signed transaction only if the signature matches its own treasury key. A wrong or malicious signature is discarded.
One signature is in flight at a time across all chains. If a signing callback never completes, anyone can call `clear_stuck_signature()` after 300 blocks to free the slot; the nonce is not consumed.
## Anyone broadcasts [#anyone-broadcasts]
Signed transactions are public. `get_signed_tx(chain, nonce)` returns them ready to send: `0x` hex on EVM, base64 on Solana. Any account can broadcast them. The official keeper does this as a convenience; if it stops, anyone else can take over. The treasury needs a little ETH (Base, Robinhood Chain) and SOL (Solana) for gas, which anyone can top up.
## Cash arrivals [#cash-arrivals]
USDC leaves NEAR through `fund_chain(chain)` and is booked as *in transit* for that chain. When it arrives, the chain's USDC ledger is credited out of that in-transit amount.
Anyone calls `credit_cash(chain, tx_hash, log_index, log)` with the USDC `Transfer` log from the transaction receipt. The contract checks that the log is a `Transfer` from the chain's USDC to the treasury, computes the MPC payload hash itself, and calls `verify_foreign_transaction` on `v1.signer-prod.testnet` (domain 3) with that `expected_payload_hash`. The MPC answers only if its nodes see that exact log. A wrong claim gets no answer. Each log is credited once.
Robinhood Chain is not in the MPC network's foreign-chain list, so the contract cannot prove an arrival there. The factory's relayer account calls `report_cash(chain, tx_hash, amount)`. Only the relayer can call it, once per transaction, and the credit is capped by the cash in transit.
The relayer calls `report_cash("solana", signature, amount)`. The MPC network can prove a Solana account's state, but a balance is not an arrival: while a signed buy has not landed, the USDC balance still holds cash the ledger already moved to stock, so a credit from a balance could count it twice. Crediting from MPC-proven state is a follow-up.
Because every credit is capped by cash already counted in NAV, a credit can only move value from "in transit" to "on the chain". It cannot create value.
## The ledger books the minimum [#the-ledger-books-the-minimum]
Stock holdings are booked, not read. When a swap is signed, the ledger records its `minAmountOut`, the least it can receive. A swap that lands leaves the chain slightly above the ledger. The app shows the ledger next to the real explorer balance for each chain.
A signed EVM swap can revert on-chain, for example when the pool has drifted more than 1% from the oracle ("Too little received"). The ledger booked `minAmountOut` at signing, so the stock figure is overstated while the USDC stays unspent in the treasury. Total value is unchanged, but its split is wrong. The chain's USDC ledger reads 0, so that USDC is not spent again. The app flags the chain "Less on-chain". This is the case today for HONEY on Base and Robinhood Chain, from two swaps that reverted before the automatic re-peg of the synthetic pools ran. Base example: [`0x9609b7…`](https://sepolia.basescan.org/tx/0x9609b7e27e2f741c4286a6c6a1a6dd04867a4d7f6c1b60c092aea942ef7e27fb).
Planned fix: book EVM swaps only after the MPC verifies the swap's `Transfer` log on Base, and give relayer chains a capped "swap failed" report that returns the booking to USDC.
## Expired Solana buys [#expired-solana-buys]
A Solana transaction expires about 60 to 90 seconds after its blockhash. If a signed buy never lands, anyone can call `resign_solana` once 200 NEAR blocks have passed since it was signed and nothing else is in flight. The same buy is signed again with a new blockhash, and the ledger is not touched again. The 200-block wait (at least 2 minutes) is longer than a blockhash lifetime, so the old and new transaction can never both land.
## Proven live [#proven-live]
| What | Evidence |
| --- | --- |
| MPC-signed EVM transactions mined on Base Sepolia and Robinhood Chain testnet | `docs/SPIKE.md` S23 |
| MPC EdDSA controls a Solana address; hand-built Raydium swap signed by the MPC and finalized on devnet | `docs/SPIKE.md` S21, S25 |
| The contract's own expected payload hash for a Base USDC `Transfer` log, signed by the live testnet MPC | `docs/SPIKE.md` S26 |
| One meme holding NVDA on all three chains, every step signed at the contract's request | `docs/DEPLOYMENTS.md`, "End to end" |
See [Security](/docs/security) for the full threat model and [Contracts reference](/docs/reference/contracts) for method details.
# FAQ (/docs/faq)
No. Everything runs on testnets: NEAR testnet, Base Sepolia, Robinhood Chain testnet and Solana devnet. The one live mainnet piece is the **Pay from another chain** panel: it requests live NEAR Intents 1Click quotes (dry quotes) to show the mainnet route. On testnet it issues no deposit address and swaps nothing.
No. Testnets have no real stock tokens. On every treasury chain the stock is a Solidon synthetic token in its own pool (Uniswap v3 on Base Sepolia and Robinhood Chain testnet, Raydium CLMM on Solana devnet). Each pool was seeded at the reference price from the official Chainlink feeds on Robinhood Chain mainnet, and the keeper feeds those Chainlink prices to the mock oracle on NEAR. The test USDC has no value. Addresses are in [Deployments](/docs/reference/deployments).
No person. Each meme's treasury addresses (one EVM address for Base and Robinhood Chain, one Solana address) are derived from the NEAR MPC network (`v1.signer-prod.testnet`) for the meme contract's own account. Only that contract can request a signature for them. It builds every transaction itself from locked config and checks each signature (`ecrecover` on EVM, `ed25519_verify` on Solana) before storing it. Anyone can broadcast the signed transaction.
Launched meme contracts have no access key at all.
No method lets anyone move treasury funds. The contract only signs an approve to one locked router and a swap into its locked stock (plus USDC sent to the bridge by `fund_chain`).
Two trusted roles remain on testnet, stated openly:
- **The bridge is a keeper account.** `fund_chain` sends NEAR cash above the 20% buffer, at most one swap's worth, to the keeper, which delivers it as USDC on the treasury chain. On mainnet that leg is NEAR Intents / 1Click.
- **The relayer reports cash on Robinhood Chain and Solana.** Only Base arrivals are proven by the MPC. A relayer report is capped by the cash in transit, once per transaction, so it cannot create value.
During development the factory account still has a full-access key, which could redeploy the factory (not the memes already launched).
NAV falls with it. The floor is a NAV floor, not a price guarantee. What the rules remove is the extra loss from the hype premium collapsing: below 1.0× mNAV the contract buys back with surplus cash, and below 0.9× holders can redeem tokens for NAV per token in USDC. See [Redeem](/docs/guides/redeem).
The ledger books the minimum. When the contract signs a swap, it books the swap's `minAmountOut` (oracle value minus 1%). A swap that lands leaves the chain a little above the ledger.
A swap that reverts on-chain (for example when the pool has drifted more than 1% from the oracle: "Too little received") stays booked, while its USDC sits unspent in the treasury. Total value is unchanged, but the split between stock and USDC is wrong until holdings are proven. The app flags that chain **Less on-chain**. This is the case today for HONEY on Base and Robinhood Chain, from two swaps that reverted before the automatic re-peg ran ([Base example](https://sepolia.basescan.org/tx/0x9609b7e27e2f741c4286a6c6a1a6dd04867a4d7f6c1b60c092aea942ef7e27fb)).
The planned fix: book EVM swaps only after the MPC verifies the swap's `Transfer` log on Base, and give relayer chains a capped "swap failed" report.
| Fee | Amount | Goes to |
| --- | --- | --- |
| Pool fee, every buy and sell | 1% | 0.8% treasury, 0.2% creator |
| Launch | `launch_cost()`, 4.46 NEAR today | The new meme's storage |
| Token registration, first buy with your own wallet | 0.00125 NEAR | Storage on the meme contract |
| Rebalance tip | 0.05% of what a rebalance moves | The caller, from treasury cash |
| Pay from another chain | Route cost shown in the 1Click quote | NEAR Intents 1Click |
Creator fees and tips are credited as claimable USDC. Claim them with **Claim … USDC** in the trade panel, or **Claim** on the Dashboard.
Two NEAR features carry the design:
- **Chain Signatures** let one NEAR contract own addresses on Base, Robinhood Chain and Solana and sign their transactions itself. No custodian, no bridge contract per chain.
- **NEAR Intents (1Click)** let users enter from other chains (SOL, ETH on Base, BTC, USDC on Arbitrum and more) arriving as USDC on NEAR, and carry treasury cash to the treasury chains on mainnet.
The MPC can also verify foreign-chain logs, which is how Base cash arrivals are proven.
Anyone. `rebalance()` is public and runs at most once per hour. The caller earns 0.05% of what it moves. The Solidon keeper calls it when due, and the app shows a **Run rebalance** button when the cooldown is over. See [Rebalance](/docs/guides/rebalance).
No. Every parameter (fees, thresholds, buffer, cooldown, tip) is set when the factory deploys the meme and is readable with `get_params`. The meme has no owner and no setter. The factory's stock list is fixed at factory init.
mNAV uses a 30-minute TWAP of the pool price, not spot. The rules run at most once an hour, issuance is capped at 1% of circulating supply per day, and every mNAV action stops if the oracle price is older than 15 minutes. Each treasury swap allows at most 1% slippage against the oracle.
No. Tokenized stocks are regulated securities in many places. This is a testnet prototype with synthetic tokens.
# Launch a meme (/docs/guides/launch)
Anyone can launch a meme from the **Launch** tab of the app. The flow has three steps, shown at the top of the page: **Create** (name it, pick its stock), **Seed** (add the first USDC), **Open treasury** (derive its addresses). Each step is one transaction, or two when a storage registration is needed.
If you leave halfway, the page keeps the meme in its URL (`/launch?meme=`), and the "Your memes" panel lists the memes you created so you can continue their setup.
## What it costs [#what-it-costs]
| Item | Amount |
| --- | --- |
| Launch cost | `launch_cost()` on the factory, 4.46 NEAR today. It pays for the meme contract's storage (its code plus 20,000 bytes of state) |
| Seed | Your USDC, which becomes the pool's first liquidity |
| USDC registration for the meme | 0.00125 NEAR, only if the meme is not yet registered on USDC |
| Gas | Three transactions |
On testnet, **Get test funds** gives you NEAR for gas and 1,000 test USDC. The launch cost is more than the faucet's 0.5 NEAR, so fund the launching account with testnet NEAR first (for example from the NEAR testnet faucet).
### Step 1: Create (`create_meme`) [#step-1-create-create_meme]
Fill in the form under "Create your meme":
| Field | Rule |
| --- | --- |
| Name | Free text, up to 40 characters in the app |
| Ticker | 2 to 10 ASCII letters or digits. The meme's account is the ticker in lowercase under the factory, for example `floor.solidon.testnet`. The app checks it live: "Free: …" or "That ticker is taken" |
| Which stock does it believe in? | One of the factory's listed stocks: NVDA, AAPL, MSFT, GOOGL, AMZN, META, TSLA, SPCX |
| Icon (optional) | Any image. The app crops it square, resizes it to 64×64 and stores it on-chain in the token metadata as a data URL, 8 KB max |
The icon is part of the token metadata and no method changes it. Check the preview on the right before you create. (`honeypot.solidon.testnet` was abandoned for this reason: it launched with a cropped icon and was replaced by `$HONEY`.)
Click **Create TICKER for 4.46 NEAR**. The app calls the factory:
```json title="create_meme on solidon.testnet (deposit: launch_cost, 300 TGas)"
{ "name": "The Floor Is Lava", "symbol": "FLOOR", "thesis_stock": "TSLA", "icon": "data:image/webp;base64,…" }
```
The factory creates the sub-account, deploys the meme code, and calls `new` with the locked parameters and the stock's venues on every chain. All 1,000,000,000 tokens are minted to the meme's own pool. If the deploy fails, the deposit is refunded to you. On success the factory logs a `launch` event.
The toast reads "Creating TICKER", then "TICKER is live".
### Step 2: Seed the pool [#step-2-seed-the-pool]
Only the creator can seed, and only once. Enter the amount in "You seed USDC" (quick amounts 100, 500, 1,000 or MAX; default 1,000).
The opening price is the seed divided by 1,000,000,000: a 1,000 USDC seed opens at $0.000001 per token.
Click **Seed the pool with … USDC**. The app sends:
1. `storage_deposit` on `usdc.solidon.testnet` for the meme (0.00125 NEAR), only if it is not yet registered.
2. `ft_transfer_call` on `usdc.solidon.testnet` to the meme with `msg` `{"action":"seed"}`.
The contract refuses a seed from anyone but the creator ("Only the creator can seed") and a second seed ("Pool is already seeded"). The toast reads "Seeding the pool", then "Pool seeded". Trading opens now. Until then the meme page shows "Trading has not opened yet".
### Step 3: Open the treasury (`init_treasury`) [#step-3-open-the-treasury-init_treasury]
Click **Open the treasury**. The app calls `init_treasury()` on the meme ("Free apart from gas"). Anyone can call it, once.
The meme asks the MPC signer `v1.signer-prod.testnet` for the public key derived for its own account (path `evm-treasury`) and stores the resulting EVM address. That one address is the treasury on Base Sepolia and on Robinhood Chain testnet.
The toast reads "Opening the treasury", then "Treasury opened".
## What "live" means [#what-live-means]
After the three steps the page shows **All three steps done** and "TICKER is live.": the meme is seeded and tradable, its treasury address exists, and every trade now feeds the balance sheet. Buttons: **Open TICKER** and **Launch another**.
The remaining setup needs no action from you. These methods are public, and the Solidon keeper calls them for every meme it finds:
| Call | Effect |
| --- | --- |
| `init_solana_treasury()` | Derives the Solana treasury address (EdDSA, path `solana-treasury`) |
| `set_solana_accounts(...)` | Relayer only: sets the Solana treasury's USDC and stock token accounts |
| `approve_router(chain)` | MPC-signed approval of the chain's Uniswap v3 SwapRouter02 for USDC |
| `fund_chain(chain)`, `buy_stock(chain)`, `buy_stock_solana(...)` | Moves cash above the 20% buffer to a chain and buys the stock |
| `rebalance()` | Runs the mNAV rules once an hour |
## Example: $FLOOR, "The Floor Is Lava" [#example-floor-the-floor-is-lava]
`floor.solidon.testnet` holds TSLA. It was launched on 13 Nov 2026 from the app, seeded with 100 USDC.
| Step | Transaction |
| --- | --- |
| `create_meme` (`name` "The Floor Is Lava", `symbol` "FLOOR", `thesis_stock` "TSLA", icon) | [`FYksRfSV…`](https://testnet.nearblocks.io/txns/FYksRfSVMa3NogLoxszJU3QMNiRgpDe366ZYTZYJ6oN6) |
| Seed: `ft_transfer_call` of 100 USDC, `msg` `{"action":"seed"}` | [`8omCZa5M…`](https://testnet.nearblocks.io/txns/8omCZa5MGSYmBJS77bwhNjmv8CsvCGyUBqeHbtdg7bg1) |
| `init_treasury` | [`9PZPgpfY…`](https://testnet.nearblocks.io/txns/9PZPgpfYhfBQ7kS3NSyuDUZbKqjzNWN9YVbtqMKugT9c) |
Its EVM treasury is `0x68c1e3adc85bf1fcb60697dbef01db4521dbbb72`. The hosted keeper is configured to leave FLOOR alone (`KEEPER_SKIP_MEMES`), so its cash stays on NEAR and buybacks and redemptions can be shown by hand. Its open methods still work for anyone. See [Rebalance](/docs/guides/rebalance).
## Launch from the CLI [#launch-from-the-cli]
```bash
NET="network-config testnet"
COST=$(near contract call-function as-read-only solidon.testnet launch_cost json-args '{}' $NET now 2>/dev/null | tr -d '"')
near contract call-function as-transaction solidon.testnet create_meme \
json-args '{"name":"Elon Doge","symbol":"ELONDOGE","thesis_stock":"SPCX","icon":null}' \
prepaid-gas '300.0 Tgas' attached-deposit '4.46 NEAR' sign-as .testnet $NET sign-with-keychain send
```
Then register the meme on `usdc.solidon.testnet` with `storage_deposit`, seed it with `ft_transfer_call` and `msg` `{"action":"seed"}`, and call `init_treasury` on the meme.
# Rebalance (/docs/guides/rebalance)
`rebalance()` runs a meme's mNAV rules. It is public: any NEAR account can call it, at most once per hour per meme, and the caller earns a small tip when the call moves value. No backend is needed for the rules to run. The Solidon keeper is one caller among many.
## What one call does [#what-one-call-does]
In detail:
1. **Cooldown.** The call fails with "Rebalance cooldown has not ended" until one hour has passed since the last evaluated rebalance. The first call on a new meme is always allowed.
2. **Price.** The meme reads its stock's price from the oracle with `get_price_no_older_than` (15 minutes). With no fresh price the outcome is `stale_price` and the cooldown is not used.
3. **TWAP.** mNAV uses the pool's 30-minute average price. A pool younger than that window gives `no_market`, and the cooldown is not used.
4. **Evaluate.** The contract records NAV and mNAV, starts the cooldown, sets the redemption window (open below 0.9×), and takes at most one action:
| mNAV | Outcome | Limits |
| --- | --- | --- |
| above 2.0× | `issued`: sells just enough new tokens into the pool to bring the price toward 2.0× NAV. The USDC goes to treasury cash and later buys stock | At most 1% of circulating supply per 24 hours. Only if every new token sells at or above NAV (accretive) |
| 1.0× to 2.0× | `no_action` | |
| below 1.0× | `bought_back`: buys tokens from the pool with cash above the 20% buffer and burns them | Never sells stock |
Every evaluated call logs a `rebalance` event with the caller, `mnav_bps`, NAV, TWAP, `redemption_open`, the outcome and the tip.
## The tip [#the-tip]
The caller earns **0.05%** of the USDC value an issuance or buyback moved, capped by treasury cash. The tip is credited as claimable USDC on the meme, not sent. Claim it with **Claim … USDC** in the trade panel (`claim_usdc`).
A call that ends in `no_action` moves nothing and pays no tip.
## Running it from the app [#running-it-from-the-app]
On a meme page, under the mNAV gauge, one line reads: "Anyone can run the rules once an hour, for a 0.05% tip on what they move." On the right it shows one of:
| Shown | Meaning |
| --- | --- |
| **Run rebalance** | The cooldown is over and the oracle price is fresh. Click to call `rebalance()` with 100 TGas |
| Next in … | The cooldown is still running |
| Waits for a fresh … price | The oracle price is older than 15 minutes |
| **Connect to run** | No wallet connected |
After the call, the toast reads "Running the rules", then one of:
| Toast title | Outcome |
| --- | --- |
| Issued new tokens | "Sold … new SYMBOL into the pool for $…, now treasury cash for more TICKER." plus your tip |
| Bought back tokens | "Bought and burned … SYMBOL for $…." plus your tip |
| Rules ran: nothing to do | mNAV was inside the hold zone, or nothing was allowed within the caps and the buffer. "No tip without a move." |
| Nothing ran: stale price | No price within the allowed age. The cooldown is not used |
| Nothing ran: no average price yet | The pool is younger than its average-price window. The cooldown is not used |
When a tip was paid, the toast adds "Your tip, $…, is claimable in the trade panel."
## Running it from the CLI [#running-it-from-the-cli]
```bash
near contract call-function as-transaction honey.solidon.testnet rebalance json-args '{}' \
prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as .testnet network-config testnet sign-with-keychain send
```
Read the state first with `get_treasury`: `last_rebalance`, `next_rebalance` (Unix seconds), `last_mnav_bps`, `redemption_open`, `epoch_cap` and `epoch_issued`.
## The keeper [#the-keeper]
The Solidon keeper (`backend/keeper`) runs on the hosted testnet deployment. For every meme the factory lists, it calls `rebalance()` with 100 TGas whenever the cooldown allows and the oracle price is fresh. It also pushes Chainlink prices to the testnet oracle and runs the treasury steps (`init_treasury`, `approve_router`, `fund_chain`, `buy_stock` and the Solana equivalents).
The keeper holds no special power over the rules. If it stops, anyone can call `rebalance()` and collect the tips. Memes listed in `KEEPER_SKIP_MEMES` are left alone: on the hosted deployment that is `floor.solidon.testnet`, so its rules can be run by hand from the app.
# Redeem and buybacks (/docs/guides/redeem)
The meme may trade above the value of its treasury (the premium is hype), but the contract does not let it stay far below. Two rules defend NAV. Both run inside `rebalance()`, which anyone can call once an hour (see [Rebalance](/docs/guides/rebalance)).
| mNAV at the last rebalance | What happens | Who acts |
| --- | --- | --- |
| below 1.0× | Buyback: the contract buys tokens from the pool with surplus cash and burns them | The contract, during `rebalance()` |
| below 0.9× | Redemption window opens: holders can burn tokens for NAV per token in USDC | Each holder, with `redeem` |
mNAV here is the pool's 30-minute TWAP price divided by NAV per token, at a fresh oracle price.
## The cash buffer [#the-cash-buffer]
Treasury cash on NEAR is USDC kept ready for buybacks and redemptions, so neither has to wait for a stock sale on another chain.
- **Target: 20% of NAV.** Only cash above this buffer is sent to the treasury chains to buy stock, and only cash above it funds buybacks.
- **Refill line: 10% of NAV.** The spec schedules a partial stock sale to refill the buffer to 20% when it falls below this line.
The meme page shows the buffer under **Details → Balance sheet and cash buffer**, with one of these lines: "Above target: only this surplus can fund buybacks.", "Below target but above the refill line: new fees rebuild the buffer.", or "Below the refill line: a stock sale refills the buffer."
## Buyback, below 1.0× [#buyback-below-10]
When a rebalance sees mNAV below 1.0×, the contract:
1. Computes how much USDC would lift the pool price to NAV.
2. Spends the smaller of that amount and the cash above the 20% buffer.
3. Buys tokens from its own pool with it and burns them (`ft_burn`, memo `buyback`).
It never sells stock to fund a buyback. The simulation showed that selling stock to refill cash destroys NAV, so the adopted rule spends only surplus cash. If there is no surplus, the rebalance does nothing.
As a holder you do nothing. You see the result in **Activity** and, if you ran the rebalance yourself, in the toast "Bought back tokens" ("Bought and burned … SYMBOL for $…").
## Redemption, below 0.9× [#redemption-below-09]
A rebalance that sees mNAV below 0.9× sets the redemption window open. The next rebalance closes it again if mNAV has recovered. The window state is public: `get_treasury` returns `redemption_open`, and **Details → Rules** shows "Redemption window open" or "Redemption window closed".
### What a holder does [#what-a-holder-does]
### Open the Redeem tab [#open-the-redeem-tab]
While the window is open, the trade panel shows a third tab, **Redeem**, with the line: "The redemption window is open: burn tokens for NAV per token in USDC from the cash buffer." The tab does not exist while the window is closed.
### Enter the amount [#enter-the-amount]
Type the amount in "You burn" (or **MAX**). The panel shows "You get ≈ … USDC at the last NAV". Details show **Minimum received** (the estimate minus your max slippage) and **Paid at**: "NAV with a fresh oracle price".
### Burn [#burn]
Click **Burn … SYMBOL for ~… USDC**. This calls `redeem(amount, min_usdc)` on the meme with 1 yoctoNEAR attached. The toast reads "Redeeming SYMBOL", then "Redemption sent. USDC arrives if the oracle price is fresh; otherwise your tokens are returned."
### What the contract does [#what-the-contract-does]
1. Refuses if the window is closed ("Redemption window is closed").
2. Takes your tokens and reads a fresh oracle price (no older than 15 minutes).
3. Computes NAV per token at that price and pays `amount × NAV` in USDC from treasury cash on NEAR, then logs `redeem` and burns the tokens.
4. Returns your tokens instead (event `redeem_refunded`) if the price is stale, the payout is below your `min_usdc`, the window closed in the meantime, or treasury cash on NEAR cannot cover the payout.
If the USDC transfer itself fails, the amount becomes claimable and appears in the trade panel as "… USDC waiting for you".
Redemption at NAV is value-neutral for everyone who stays: each token leaves with exactly its share of the treasury.
If the stock falls, NAV falls with it, and so does what a redemption pays. What the rules remove is the extra loss from the hype premium collapsing below NAV.
## Seeing it live [#seeing-it-live]
On most memes the keeper sends cash above the buffer to the treasury chains and runs the rules every hour. `floor.solidon.testnet` ($FLOOR, holding TSLA) is excluded from the hosted keeper, so its cash stays on NEAR and a buyback or a redemption can be triggered by hand with **Run rebalance** when its mNAV is below 1.0× or 0.9×.
# Trade a meme (/docs/guides/trade)
Every meme trades against USDC in a constant-product pool inside its own contract. There is no external DEX. Prices, fees and the mNAV rules all live in the same contract.
On testnet the USDC is `usdc.solidon.testnet`, a mock token with 6 decimals and no value. Get 1,000 of it with **Get test funds** (see [Quickstart](/docs/quickstart)).
## Buy [#buy]
Open a meme from **Markets**. The trade panel has a **Buy** tab and a **Sell** tab (and **Redeem** while the redemption window is open).
| Field | Meaning |
| --- | --- |
| You pay | USDC amount. Prefilled with 10. **MAX** fills your whole balance |
| You get | Estimated tokens out, after the fee, from the pool's current reserves |
| Details → Minimum received | The least you accept. The contract refuses the trade below it |
| Details → Average price | USDC paid per token for this trade |
| Details → Price impact, fee included | Average price against the current spot price |
| Details → Fee | `1% (0.8% treasury, 0.2% creator)` |
| Details → Max slippage | 0.5%, 1% (default) or 3%. Sets Minimum received |
| Details → Network | NEAR testnet |
The button says exactly what happens: **Buy ~… SYMBOL for 10 USDC**. If something blocks the trade, the button says why instead: "Enter an amount", "Not enough USDC", "Amount too small".
After you confirm, a toast shows "Buying SYMBOL", then "Bought SYMBOL" with a link **The transaction on NearBlocks**. If you run out of USDC, the panel shows "Out of USDC? It is free on testnet." with a **Get test funds** button.
### What the buy transaction contains [#what-the-buy-transaction-contains]
A buy is one call on the USDC token, not on the meme:
```json title="ft_transfer_call on usdc.solidon.testnet (1 yoctoNEAR attached)"
{
"receiver_id": "honey.solidon.testnet",
"amount": "10000000",
"msg": "{\"action\":\"buy\",\"min_out\":\"\"}"
}
```
The USDC contract calls the meme's `ft_on_transfer`. The meme then:
1. Splits the 1% fee: 0.8% to treasury cash, 0.2% credited to the creator as claimable USDC.
2. Swaps the rest through the pool (`x * y = k`).
3. Refuses with "Slippage: output below min_out" if the output is below `min_out`. Any refusal refunds the full USDC through the token standard.
4. Sends the tokens to you and logs a `buy` event.
With your own wallet, the first buy of a meme also calls `storage_deposit` on the meme (0.00125 NEAR) so you can hold its token. The wallet asks twice. The demo wallet signs both without popups.
## Sell [#sell]
On the **Sell** tab, enter an amount in "You sell" (or **MAX**). The panel shows "You get ≈ … USDC", with the same Details rows.
The button reads **Sell … SYMBOL for ~… USDC**. It calls `sell(amount, min_out)` on the meme with 1 yoctoNEAR attached. The contract:
1. Swaps your tokens through the pool.
2. Takes the same 1% fee from the USDC out (0.8% treasury, 0.2% creator).
3. Refuses if the net USDC is below `min_out`.
4. Sends you the USDC with `ft_transfer`.
The success toast says "Sold SYMBOL. USDC is on its way; if the transfer fails it becomes claimable." Claimable USDC appears in the trade panel as "… USDC waiting for you" with a **Claim … USDC** button, which calls `claim_usdc`.
## Pay from another chain [#pay-from-another-chain]
Under the Buy tab, **Pay from another chain** opens a NEAR Intents 1Click quote: any asset 1Click routes into USDC on NEAR, the asset a Solidon buy is paid in.
- Common assets are listed first: SOL, ETH on Base, ETH on Arbitrum, BTC, and USDC on Base, Solana and Arbitrum. **Search all assets** lists the rest.
- Enter an amount in "You send". About 500 ms after you stop typing, the app requests a quote and shows "You get ≈ … USDC on NEAR".
- Details show **Minimum received**, **Route cost**, **Arrives in** and **To**.
The panel reads: "Mainnet route preview — on testnet use Get test funds. Quotes are live; no deposit address is issued and nothing is swapped." Quotes are real 1Click mainnet quotes requested with `dry: true`. A testnet account does not exist on mainnet, so the preview quotes to `intents.near` as a stand-in recipient.
On a mainnet build, the same panel offers **Get a … deposit address**, an optional refund address, and then follows the swap: "Waiting for your deposit", "Deposit seen, waiting for confirmations", "Swapping into USDC on NEAR", "Done: the USDC is in your NEAR account".
## Where the fees go [#where-the-fees-go]
| Share | Destination | Effect |
| --- | --- | --- |
| 0.8% | Treasury cash on NEAR | Raises NAV. Cash above the 20% buffer is sent to the treasury chains and buys stock |
| 0.2% | Creator, claimable USDC | Claimed with `claim_usdc` |
See [How it works](/docs/concepts/how-it-works) for how treasury cash becomes stock on Base, Robinhood Chain and Solana.
# Hackathon submission (/docs/hackathon)
## Submission facts [#submission-facts]
| Item | Value |
| --- | --- |
| Event | NEAR HACKATHON 2026 |
| Tracks | `defi` (Unstoppable DeFi) and `memecoins` (Memecoins With Mechanics) |
| Team size | 1 |
| Author | yeheskieltame |
| Repository created | 8 Nov 2026, inside the hackathon window |
| Baseline code | None. Every commit is new work |
| Submission tag | `hackathon-submission` on the submitted commit. No `pre-hackathon` tag is needed because there is no baseline |
| Deadline | 14 Nov 2026 23:59 UTC (15 Nov 2026 06:59 WIB) |
| Live demo | [solidon.xyz](https://solidon.xyz), app at [app.solidon.xyz](https://app.solidon.xyz), read API at [api.solidon.xyz/health](https://api.solidon.xyz/health) |
| Networks | NEAR testnet, Base Sepolia, Robinhood Chain testnet, Solana devnet. NEAR Intents 1Click quotes from mainnet (dry) |
| License | MIT (`LICENSE`) |
## Other project by the same author [#other-project-by-the-same-author]
**NYXERON** is an independent project by the same author, submitted separately. The rules allow at most two projects per person, provided they are independent and name each other.
## How Solidon maps to the tracks [#how-solidon-maps-to-the-tracks]
| Criterion | Evidence in Solidon |
| --- | --- |
| `defi`: cross-chain settlement | Enter from any chain via NEAR Intents 1Click (live dry quotes in the app); treasury cash sent to Base, Robinhood Chain and Solana, through Intents on mainnet |
| `defi`: no custodian | Treasuries at contract-owned addresses via Chain Signatures; `rebalance()` callable by anyone |
| `defi`: exchange and vault | Internal AMM pool and a per-meme stock vault on three chains |
| `memecoins`: mechanic in the contract | mNAV engine, issuance, buyback and redemption all run in the meme contract |
| `memecoins`: locked parameters | Parameters locked at launch, publicly readable, no admin key |
| `memecoins`: treasury with rules | 20% cash buffer, 1% per day issuance cap, redemption window |
| `memecoins`: simulation | 60-day simulation across five scenarios. See [Simulation](/docs/simulation) |
| `memecoins`: sybil resistance | No per-account rewards; every mechanic is proportional to token amount |
## Reproduce it [#reproduce-it]
Start with [Quickstart](/docs/quickstart). Every claim on this site has a "do X, see Y" path: a view call, an explorer link, a test, or the simulation script. The full list of live transactions is on [Deployments](/docs/reference/deployments).
## Tools and disclosure [#tools-and-disclosure]
### AI [#ai]
- **Claude Code** was used throughout for code, tests and docs, under the author's direction and review.
- **GPT Image (ChatGPT)** generated the mascot and meme art.
### Libraries [#libraries]
| Area | Libraries |
| --- | --- |
| Contracts | near-sdk, cargo-near |
| Backend | near-api-js, viem, @solana/web3.js, Raydium SDK v2 (devnet pool setup only) |
| Frontend | Next.js, Tailwind CSS, shadcn/ui, Motion, @hot-labs/near-connect |
| Simulation | Python standard library |
### Services [#services]
| Purpose | Service |
| --- | --- |
| Signing | NEAR Chain Signatures (`v1.signer-prod.testnet`) |
| Prices | Chainlink price feeds (Robinhood Chain mainnet) |
| Cross-chain entry | NEAR Intents 1Click (quotes) |
| Chain access | FastNear and dRPC (NEAR), Alchemy (EVM, Solana) |
### Assets [#assets]
- The hero background is a licensed MotionSites gradient.
- Stock, chain and platform logos are trademarks of their owners. Sources are listed in `frontend/public/logos/SOURCES.md`.
## Testnet and legal notice [#testnet-and-legal-notice]
Everything runs on testnets. The stock tokens are synthetic, priced from the official Chainlink feeds on Robinhood Chain mainnet, and the test USDC has no value. Tokenized stocks are regulated securities in many places. Solidon is a testnet prototype and is not for US persons.
# Introduction (/docs)
Solidon is a memecoin launchpad on NEAR. Every meme it launches is a small treasury company: trading fees buy a tokenized stock (NVDA, TSLA, AAPL and five more) held on Base Sepolia, Robinhood Chain testnet and Solana devnet at the same time. The NEAR contract signs every treasury transaction itself through NEAR Chain Signatures, and its capital rules are locked in code with no admin key.
Everything runs on testnets. The stock tokens are synthetic, priced from the official Chainlink feeds on Robinhood Chain mainnet, and the test USDC has no value.
- Landing: [solidon.xyz](https://solidon.xyz)
- App: [app.solidon.xyz](https://app.solidon.xyz)
- Read API: [api.solidon.xyz/health](https://api.solidon.xyz/health)
## The problem [#the-problem]
Memecoins have attention but nothing underneath. When the hype fades, holders are left with a token backed by nothing.
Treasury companies such as MicroStrategy do the opposite. They hold an asset and grow the amount held per share by issuing shares at a premium. But they can only be bought on stock exchanges, and their issuance is decided by management, not by code.
Tokenized stocks are also split across chains (Base, Robinhood Chain, Solana). Users on NEAR cannot reach them easily.
## What Solidon does [#what-solidon-does]
Solidon gives every meme the treasury-company engine. Hype becomes stock per token, and the meme gets a NAV floor that anyone can verify on-chain.
**mNAV** is the meme's market cap divided by its treasury value. The contract acts on it at most once an hour, from a 30-minute TWAP and a fresh oracle price:
| mNAV | Action, enforced by the contract |
| --- | --- |
| above 2.0× | Issue new tokens (at most 1% of supply per day) and buy more stock |
| 1.0× to 2.0× | Hold |
| below 1.0× | Buy back with cash above the 20% buffer (never sells stock to do it) |
| below 0.9× | Open redemptions: burn tokens for NAV per token in USDC |
## Three ideas [#three-ideas]
### 1. A treasury per meme [#1-a-treasury-per-meme]
Each meme is its own NEAR contract (for example `honey.solidon.testnet`): a NEP-141 token, a meme/USDC pool, and a treasury. 0.8% of every trade goes to the treasury, and the treasury buys the meme's thesis stock.
### 2. Treasuries on three chains through Chain Signatures [#2-treasuries-on-three-chains-through-chain-signatures]
Each meme derives one EVM address (used on Base and Robinhood Chain) and one Solana address from the NEAR MPC network. Only the meme contract can request signatures for them. The contract builds each transaction from locked config (an approval to one router, a swap into its stock), checks the signature, and anyone can broadcast it.
### 3. mNAV rules locked in the contract [#3-mnav-rules-locked-in-the-contract]
Every parameter is set at launch and readable by anyone. There is no owner and no setter. `rebalance()` is public: anyone can run the rules once per hour and earn a small tip.
## Where to go next [#where-to-go-next]
# Try it in five minutes (/docs/quickstart)
This walkthrough uses the hosted app at [app.solidon.xyz](https://app.solidon.xyz). Everything is on NEAR testnet. You need no extension and no real money.
### Open the app [#open-the-app]
Go to [app.solidon.xyz](https://app.solidon.xyz). The header has three tabs: **Markets**, **Launch** and **Dashboard**. Markets lists every meme the factory `solidon.testnet` launched.
### Get a funded wallet [#get-a-funded-wallet]
Click **Try a demo wallet** in the header (or **Connect wallet**, then **Start with a demo wallet**).
- The app generates an ed25519 key in your browser and uses its implicit NEAR testnet account.
- The Solidon faucet creates the account and sends 0.5 NEAR for gas plus 1,000 test USDC. You sign nothing and pay no gas.
- A toast shows "Creating and funding your demo wallet", then "Test NEAR and 1,000 USDC sent", with links to each transaction on NearBlocks.
- The header shows a **Demo** chip with your account. Its menu has **View on NearBlocks**, **Export key**, **Disconnect (keep the key)** and **Forget demo wallet**.
The key lives only in this browser. Transactions sign without popups.
Click **Connect wallet** and pick a NEAR wallet under "Or use your own wallet". Use a testnet account.
Then click **Get test funds** in the header. The server-side faucet pays, so you need no NEAR first:
- 0.5 NEAR for gas, only if your account holds less than 0.3 NEAR.
- 1,000 test USDC (`usdc.solidon.testnet`), with storage registration.
The toast reads "Test NEAR and 1,000 USDC sent" or "1,000 test USDC minted". One request per account every 10 minutes.
You see your USDC balance in the header.
### Open HONEY [#open-honey]
On **Markets**, open **HONEY** (Honeypot, `honey.solidon.testnet`). Its treasury holds NVDA.
You see the price, **mNAV**, **Treasury value**, an mNAV gauge, the trade panel on the right, and "Where the treasury is": one row per chain plus "Cash on NEAR".
### Buy 10 USDC of HONEY [#buy-10-usdc-of-honey]
In the trade panel, stay on **Buy**. "You pay" is prefilled with 10 USDC. The line under it says "You get ≈ … HONEY · fee 1%".
Click **Buy ~… HONEY for 10 USDC**.
- Demo wallet: it signs at once ("Signing").
- Your own wallet: approve in the wallet. The first buy also registers your account on HONEY (0.00125 NEAR storage), so the wallet asks twice.
The toast goes from "Buying HONEY" to "Bought HONEY". Your HONEY balance appears in the panel, and the treasury value rises by the 0.8% treasury fee.
### See the transaction on the explorer [#see-the-transaction-on-the-explorer]
Click **The transaction on NearBlocks** in the success toast. You see a `ft_transfer_call` on `usdc.solidon.testnet` with receiver `honey.solidon.testnet` and `msg` `{"action":"buy","min_out":"…"}`, then the meme's `buy` event. See [Trade](/docs/guides/trade) for the details.
### Details: Treasury per chain [#details-treasury-per-chain]
Scroll to **Details** and open **Treasury per chain**. For Base Sepolia, Robinhood Chain testnet and Solana devnet you see:
- The treasury address (only this contract can sign for it) and whether the router is approved.
- A proof label: **MPC-proven** (Base) or **Relayer-reported** (Robinhood Chain, Solana).
- **Ledger** (what the contract booked) against **On-chain** (read live by your browser), with a verdict: "Matches", "Covered, +…" or "Short by …".
A chain whose on-chain balance is below the ledger shows "Less on-chain" in "Where the treasury is". HONEY shows it on Base and Robinhood Chain today, from two swaps that reverted. See the [FAQ](/docs/faq).
### Details: Holders [#details-holders]
Open **Holders**. Every holder is read from `ft_balance_of`. Your account is tagged **You**. "Pool reserve" is the tokens held by the contract's own pool.
### Dashboard [#dashboard]
Open **Dashboard**. You see **Portfolio value** (your memes at pool price), **At NAV** (the treasury assets behind your tokens), **USDC**, and **Claimable** (creator fees, caller tips and failed payouts). Each position lists Balance, At pool price, At NAV and Share of treasury.
### Launch your own meme [#launch-your-own-meme]
Open **Launch**. Name it, pick a ticker and a stock, seed the pool, open the treasury. Three transactions. See [Launch a meme](/docs/guides/launch).
## Read the live state without the app [#read-the-live-state-without-the-app]
| Do | See |
| --- | --- |
| Open [`solidon.testnet` on NearBlocks](https://testnet.nearblocks.io/address/solidon.testnet) | The factory and the memes it launched |
| `near contract call-function as-read-only honey.solidon.testnet get_treasury_chains json-args '{}' network-config testnet now` | HONEY's treasury per chain: address, nonce, USDC, NVDA booked, cash in transit |
| Open [api.solidon.xyz/health](https://api.solidon.xyz/health) | The read API is up |
# Read API (/docs/reference/api)
The indexer (`backend/keeper/src/indexer.js`) records one price-history point per meme per interval and every `EVENT_JSON` log of the factory and its memes. `backend/keeper/src/api.js` serves them as JSON.
| | |
| --- | --- |
| Base URL | `https://api.solidon.xyz` |
| Local | `http://localhost:8788` (`INDEXER_PORT`) |
| Methods | `GET`, `HEAD`; `OPTIONS` answers 204; anything else 405 |
| Auth | none |
| CORS | `access-control-allow-origin: *`, `access-control-allow-methods: GET, OPTIONS`, `access-control-allow-headers: content-type` |
| Caching | `cache-control: no-store` on every response. Holders are cached server-side for 60 s per meme. |
| Content type | `application/json; charset=utf-8` |
The API is read-only. The process that serves it loads no key and sends no transaction.
## Routes [#routes]
| Route | Query | Answer |
| --- | --- | --- |
| `GET /health` | none | Indexer status |
| `GET /v1/memes` | none | Every tracked meme with metadata and its newest snapshot |
| `GET /v1/memes/:id/history` | `since` (unix seconds, default 0), `limit` (1 to 2000, default 2000) | Snapshots, ascending, thinned evenly to at most `limit` |
| `GET /v1/memes/:id/events` | `limit` (1 to 500, default 50), `before` (block height) | Events of the meme, newest first |
| `GET /v1/memes/:id/holders` | `limit` (1 to 500, default 50) | Holders, biggest first, from live NEAR views |
| `GET /v1/accounts/:id/activity` | `limit` (1 to 500, default 50), `before` (block height) | Events across all memes that involve the account, newest first |
`:id` must match `^[a-z0-9._-]{2,64}$`. Meme routes return 404 for an account the indexer does not track (the factory's memes, from `get_memes`). The API lists every meme the factory returns, including the test memes the web app hides (`e2etest`, `honeypot`).
### Errors [#errors]
Errors are `{"error": ""}`:
| Status | When |
| --- | --- |
| 400 | Bad parameter (`limit must be between 1 and 2000`), invalid account id, bad URL encoding |
| 404 | Unknown meme (`unknown meme `) or route (`no such route`) |
| 405 | Method other than `GET`, `HEAD`, `OPTIONS` |
| 500 | Internal error |
| 502 | Holders only: NEAR RPC unavailable (`NEAR RPC unavailable, try again`) |
## GET /health [#get-health]
```bash
curl -s https://api.solidon.xyz/health
```
```json
{
"ok": true,
"last_block": 277608489,
"memes": 5,
"lag_s": 20,
"events": 555,
"transactions": 770,
"backfilled": 6,
"tracked": 6,
"last_snapshot": 1794630782
}
```
| Field | Meaning |
| --- | --- |
| `ok` | `false` before the first event poll, or when the last poll is older than `3 * INDEXER_POLL_SECS + 60` seconds |
| `last_block` | Final NEAR block at the last finished poll |
| `lag_s` | Seconds since the last finished poll |
| `events` | Stored events across all accounts |
| `transactions` | Indexed transactions |
| `backfilled`, `tracked` | Accounts fully backfilled, out of the factory plus every meme |
| `last_snapshot` | Unix time of the last history snapshot |
## GET /v1/memes [#get-v1memes]
```bash
curl -s https://api.solidon.xyz/v1/memes
```
```json
{
"memes": [
{
"id": "honey.solidon.testnet",
"symbol": "HONEY",
"name": "Honeypot",
"creator": "solidon-launch2.testnet",
"ticker": "NVDA",
"latest": {
"t": 1794630782,
"price": 0.000011843000782134,
"twap": 0.000011843000782134,
"nav": 4.06123708214e-7,
"mnav": 29.161067,
"stock_price": 161.40001379,
"oracle_stale": false,
"treasury_value": 295.977105,
"circulating": 728785586.7885972,
"chains": {
"base": "11708036",
"robinhood": "112317573164105596",
"solana": "5697440"
}
},
"events": 195
}
]
}
```
`latest` is the newest snapshot, or `null` before the first one. `events` is the number of stored events for the meme. The array is trimmed here; the live response holds every meme.
### Snapshot fields [#snapshot-fields]
| Field | Type | Meaning |
| --- | --- | --- |
| `t` | number | Unix seconds |
| `price` | number | Pool spot price, USD per whole meme token |
| `twap` | number \| null | 30-minute TWAP, `null` while the pool is younger than the window |
| `nav` | number \| null | Treasury value / circulating supply, USD per token |
| `mnav` | number \| null | (TWAP, else spot) / NAV, as `rebalance` computes it |
| `stock_price` | number \| null | Oracle price of the thesis stock, USD |
| `oracle_stale` | boolean | `true` when the fresh read (`get_price_no_older_than`) returned nothing and `get_price_unsafe` was used |
| `treasury_value` | number \| null | NEAR cash + USDC on and in transit to every chain + stock on every chain, USD |
| `circulating` | number | Circulating supply, whole tokens |
| `chains` | object | Stock held per chain, raw units of that chain's venue token (8 decimals on Base and Solana, 18 on Robinhood) |
Values are computed exactly as the contract's `Meme::assets` and `Meme::nav` (`backend/keeper/src/history.js`).
## GET /v1/memes/:id/history [#get-v1memesidhistory]
```bash
curl -s "https://api.solidon.xyz/v1/memes/honey.solidon.testnet/history?limit=2"
```
```json
{
"meme": "honey.solidon.testnet",
"history": [
{
"t": 1794380675,
"price": 0,
"twap": null,
"nav": null,
"mnav": null,
"stock_price": 172.82606149,
"oracle_stale": false,
"treasury_value": 0,
"circulating": 0,
"chains": { "base": "0", "robinhood": "0", "solana": "0" }
},
{
"t": 1794630782,
"price": 0.000011843000782134,
"twap": 0.000011843000782134,
"nav": 4.06123708214e-7,
"mnav": 29.161067,
"stock_price": 161.40001379,
"oracle_stale": false,
"treasury_value": 295.977105,
"circulating": 728785586.7885972,
"chains": { "base": "11708036", "robinhood": "112317573164105596", "solana": "5697440" }
}
],
"thinned": true
}
```
- Points with `t >= since` are selected, then thinned evenly to `limit`, always keeping the first and the newest point. `thinned` is `true` when points were dropped.
- `limit` above 2000 is rejected with 400.
- Storage: every point from the last 24 hours is kept; older points are compacted to one per 15 minutes.
- History starts at the first indexer run. The contracts keep no price history, so there are no earlier points.
## GET /v1/memes/:id/events [#get-v1memesidevents]
```bash
curl -s "https://api.solidon.xyz/v1/memes/honey.solidon.testnet/events?limit=2"
```
```json
{
"meme": "honey.solidon.testnet",
"events": [
{
"block_height": 277607788,
"timestamp": 1794630345,
"tx_hash": "9unCEdGi1kxsZhixzdUY4SX7FdqExSEdjJ535VTqA92Z",
"receipt_id": "2GGC5HtBXYfQqBZy56bzpbPun1Ybnjp1jYBwZAvcwsma",
"signer": "keeper.solidon.testnet",
"predecessor": "honey.solidon.testnet",
"receiver": "honey.solidon.testnet",
"log_index": 0,
"data_index": 0,
"event": "rebalance",
"standard": "solidon",
"version": "1.0.0",
"data": {
"caller": "keeper.solidon.testnet",
"mnav_bps": "291610",
"nav": "406123708214",
"twap": "11843000782134",
"redemption_open": false,
"outcome": "no_action",
"tip": "0"
}
}
],
"more": true
}
```
- Page backwards with `before` = the last `block_height` you received.
- `receiver` is the contract that emitted the event: the meme, the factory, or the USDC contract. NEP-141 events of USDC that name a meme (buys, payouts, `fund_chain` transfers) are stored under that meme.
- One record per item of the log's `data` array (`data_index`). Failed receipts emit no logs, so failed trades do not appear.
- Event names and fields: [Contracts, Events](/docs/reference/contracts#events).
## GET /v1/memes/:id/holders [#get-v1memesidholders]
```bash
curl -s "https://api.solidon.xyz/v1/memes/honey.solidon.testnet/holders?limit=3"
```
```json
{
"meme": "honey.solidon.testnet",
"holders": [
{ "account": "solidon-launch2.testnet", "balance": "441964285714285714285714284", "share": 0.433567 },
{ "account": "solidon-alice.testnet", "balance": "183889147947937141707060708", "share": 0.180395 },
{ "account": "solidon-frank.testnet", "balance": "37410443083619633438874869", "share": 0.036699 }
],
"pool_reserve": "290582276511498352598087637",
"total_supply": "1019367863300095556102907722",
"updated_at": 1794630796
}
```
- Candidates are every account named in the meme's indexed events plus the creator; each is asked `ft_balance_of`. Zero balances are dropped.
- `balance` is raw (18 decimals). `share` = balance / total supply, truncated to 6 decimals, or `null` if the supply is unknown.
- The meme contract's own balance is the pool inventory and is reported as `pool_reserve`, not as a holder.
- Results are cached for 60 s per meme; at most 3 views run in parallel, with retries on RPC rate limits.
## GET /v1/accounts/:id/activity [#get-v1accountsidactivity]
```bash
curl -s "https://api.solidon.xyz/v1/accounts/solidon-alice.testnet/activity?limit=2"
```
```json
{
"account": "solidon-alice.testnet",
"events": [
{
"block_height": 277358928,
"timestamp": 1794484223,
"tx_hash": "CWTz3cCmULuHpnGM9tPiSDmLqVh7C7sBNiCEBMaKbWKm",
"receipt_id": "HwBoFZYBicmMncZhAfimJWs8a1xrLg8NhamoaX5p9eHq",
"signer": "solidon-alice.testnet",
"predecessor": "usdc.solidon.testnet",
"receiver": "honey.solidon.testnet",
"log_index": 1,
"data_index": 0,
"event": "buy",
"standard": "solidon",
"version": "1.0.0",
"data": {
"account": "solidon-alice.testnet",
"usdc_in": "900000000",
"meme_out": "104551375261073980932937751"
}
}
],
"more": true
}
```
An event involves the account when it is the `signer`, the `predecessor`, or named in `data` under `account`, `owner_id`, `old_owner_id`, `new_owner_id`, `caller`, `creator` or `account_id`. The account does not need to be a meme. Page with `before`.
## How the data is collected [#how-the-data-is-collected]
| Source | Interval | What |
| --- | --- | --- |
| NEAR RPC views | `SNAPSHOT_SECS` (default `LOOP_SECS`, 60 s) | Per meme: `get_pool`, `get_treasury`, `get_treasury_chains`, and the oracle price; one history point |
| FastNear transactions API (`https://tx.test.fastnear.com`) | `INDEXER_POLL_SECS` (20 s) | Transactions of the factory and every meme (`POST /v0/account`), fetched with receipts and logs (`POST /v0/transactions`, 20 per call) |
The first run backfills each account from its creation; `data/cursor.json` lets a restart resume. Data lives in `backend/keeper/data/` (`INDEXER_DATA`): `history/.jsonl`, `events/.jsonl`, `cursor.json`. See [Keeper](/docs/reference/keeper) for the env vars and [Run locally](/docs/reference/run-locally) to start it.
# Contracts (/docs/reference/contracts)
Solidon is two Rust contracts on NEAR (near-sdk), in one cargo workspace under `contracts/`:
| Crate | Account | Role |
| --- | --- | --- |
| `factory` | `solidon.testnet` | Deploys each meme as `.solidon.testnet`. Fixes USDC, oracle, treasury chains and stock list at init. No owner. |
| `meme` | `.solidon.testnet` | NEP-141 token, internal meme/USDC pool, mNAV engine, multichain treasury via Chain Signatures. No owner, no setter. |
| `mock-ft` | `usdc.solidon.testnet` | Testnet USDC stand-in (NEP-141, 6 decimals, open `mint`). |
| `mock-pyth` | `pyth.solidon.testnet` | Testnet oracle with the Pyth read interface and an open `set_price`. |
Conventions used on this page:
- `U128` values are JSON strings (`"1000000"`). USDC amounts are raw units with 6 decimals; meme amounts are raw units with 18 decimals.
- Prices returned by the pool (`spot_price`, `twap_price`, `nav`) are USDC raw units per meme raw unit, scaled by `1e30`.
- `chain` is a name from the factory's chains config: `base`, `robinhood` or `solana`.
- Ratios are basis points: `10000` = 100% = 1.0x.
- "Anyone" means any NEAR account. Who calls does not change the outcome; the contract checks every input.
Neither contract has an owner method. Every parameter is fixed at `new` and readable through views. See [Security](/docs/security).
## Factory [#factory]
### Change methods [#change-methods]
| Method | Args | Deposit | Caller | Effect |
| --- | --- | --- | --- | --- |
| `new` | `usdc: AccountId`, `oracle: AccountId`, `chains: ChainsConfig`, `stocks: Stock[]` | none | deployer, once (`#[init]`) | Stores the immutable config. Requires at least one stock. |
| `create_meme` | `name: string`, `symbol: string`, `thesis_stock: string`, `icon: string \| null` | at least `launch_cost()` yoctoNEAR | anyone | Creates `.`, transfers the whole deposit to it, deploys the meme code and calls its `new`. `symbol` must be 2 to 10 ASCII letters or digits and not yet launched; `thesis_stock` must be a listed ticker. On failure the deposit is refunded to the caller. |
| `on_create` | `meme`, `creator`, `deposit` | none | factory only (`#[private]`) | Callback. On success records the meme and emits `launch`; otherwise refunds the deposit. |
### View methods [#view-methods]
| Method | Args | Returns |
| --- | --- | --- |
| `launch_cost` | none | `U128`: storage cost of the meme code plus 20,000 bytes of state. Live value: `4459990000000000000000000` (4.46 NEAR). |
| `get_memes` | `from_index?: u32` (default 0), `limit?: u32` (default 50) | `AccountId[]` in launch order |
| `get_usdc` | none | USDC token account |
| `get_oracle` | none | Oracle account |
| `get_chains` | none | `ChainsConfig` |
| `get_stocks` | none | `Stock[]` |
### Config types [#config-types]
`EvmChain`: `{name, chain_id, usdc, usdc_decimals, swap_router, pool_fee, max_fee_per_gas, max_priority_fee_per_gas, max_swap_usdc, proof}`. `proof` is `{"Mpc": {"foreign_chain": "Base", "finality": "Latest" | "Safe" | "Finalized"}}` or `"Relayer"`.
`SolanaChain`: `{name, usdc_mint, usdc_token_program, stock_token_program, clmm_program, amm_config, max_swap_usdc}`. The USDC program must be SPL Token and the stock program Token-2022; `max_swap_usdc` must fit a `u64`.
`Stock`: `{ticker, price_id, venues: [{chain, token, decimals, solana}]}`. `price_id` is 64 lowercase hex characters. One venue per chain; `solana` is `{pool, usdc_vault, stock_vault, observation, bitmap_extension}` on the Solana venue and `null` elsewhere. `decimals` at most 18.
The live config is on the [Deployments](/docs/reference/deployments) page.
## Meme: token and pool [#meme-token-and-pool]
The meme is a standard NEP-141 token (`ft_transfer`, `ft_transfer_call`, `ft_total_supply`, `ft_balance_of`, `ft_metadata`) with NEP-145 storage management (`storage_deposit`, `storage_withdraw`, `storage_unregister`, `storage_balance_bounds`, `storage_balance_of`). Decimals: 18. At `new`, 1,000,000,000 tokens are minted to the meme contract itself as pool inventory.
A buyer must be registered on the meme (`storage_deposit`) before a buy, or the transfer of meme tokens panics and the USDC is refunded. The meme must be registered on the USDC contract before any USDC can be sent to it.
### Buying and seeding: `ft_transfer_call` on USDC [#buying-and-seeding-ft_transfer_call-on-usdc]
USDC enters through `ft_transfer_call` on the USDC contract with `receiver_id` = the meme. The meme's `ft_on_transfer` accepts only the factory-fixed USDC and parses `msg`:
| `msg` | Caller | Effect |
| --- | --- | --- |
| `{"action":"seed"}` | the creator, once | Sets the USDC reserve to the full amount. Panics if already seeded or amount is 0. Emits `seed`. |
| `{"action":"buy","min_out":""}` | anyone | Takes the 1% fee on the USDC side (0.8% to treasury cash, 0.2% claimable by the creator), swaps the rest on `x * y = k`, sends meme tokens to the sender. Panics if output is 0 or below `min_out`. Emits `buy`. |
Any panic refunds the full USDC amount through `ft_resolve_transfer`. Attach 1 yoctoNEAR; the web app uses 150 TGas.
```bash
near contract call-function as-transaction usdc.solidon.testnet ft_transfer_call \
json-args '{"receiver_id":"honey.solidon.testnet","amount":"10000000","msg":"{\"action\":\"buy\",\"min_out\":\"1\"}"}' \
prepaid-gas '150.0 Tgas' attached-deposit '1 yoctoNEAR' \
sign-as .testnet network-config testnet sign-with-keychain send
```
### Change methods [#change-methods-1]
| Method | Args | Deposit | Caller | Effect |
| --- | --- | --- | --- | --- |
| `sell` | `amount: U128`, `min_out: U128` | 1 yoctoNEAR | holder | Sells `amount` meme into the pool. Fee taken from the USDC output (same split as a buy). Panics if net output is 0 or below `min_out`. Emits `sell`, then pays USDC; a failed payout becomes claimable (`payout_failed`). |
| `claim_usdc` | none | none | any account with a claimable balance | Withdraws USDC credited to the caller: creator fees, rebalance tips, failed payouts. Panics with `Nothing to claim`. |
### View methods [#view-methods-1]
| Method | Args | Returns |
| --- | --- | --- |
| `get_pool` | none | `{meme_reserve, usdc_reserve, treasury_usdc, spot_price, twap_price}`. `twap_price` is `null` while the pool is younger than the 30-minute window. |
| `quote_buy` | `usdc_in: U128` | Meme out after fees |
| `quote_sell` | `meme_in: U128` | USDC out after fees |
| `claimable_of` | `account_id` | `U128` USDC claimable |
| `get_params` | none | `Params` (below) |
| `get_info` | none | `{creator, usdc, oracle, stock}` |
### Locked parameters (`get_params`) [#locked-parameters-get_params]
| Field | Value | Meaning |
| --- | --- | --- |
| `pool_fee_bps` | 100 | 1% fee on every trade, USDC side |
| `treasury_fee_bps` | 80 | Of which 0.8% to treasury cash |
| `creator_fee_bps` | 20 | Of which 0.2% claimable by the creator |
| `issuance_mnav_bps` | 20000 | Issue new tokens above mNAV 2.0 |
| `max_issuance_bps_per_epoch` | 100 | At most 1% of circulating supply per epoch |
| `epoch_secs` | 86400 | Epoch: 24 h |
| `buyback_mnav_bps` | 10000 | Buy back and burn below mNAV 1.0 |
| `redemption_mnav_bps` | 9000 | Redemption window opens below mNAV 0.9 |
| `cash_buffer_bps` | 2000 | 20% of treasury value kept as cash on NEAR |
| `cash_refill_bps` | 1000 | Refill threshold, 10% |
| `twap_window_secs` | 1800 | 30-minute TWAP |
| `oracle_max_age_secs` | 900 | Price older than 15 minutes is stale |
| `rebalance_cooldown_secs` | 3600 | One rebalance per hour |
| `caller_tip_bps` | 5 | 0.05% of the rebalance amount to the caller |
| `target_leverage_bps` | 10000 | 1.0x |
## Meme: mNAV engine [#meme-mnav-engine]
| Method | Args | Deposit | Caller | Effect |
| --- | --- | --- | --- | --- |
| `rebalance` | none | none | anyone, once per cooldown | Attach 100 TGas. Reads the oracle (`get_price_no_older_than`, 900 s), then evaluates mNAV = TWAP / NAV: above 2.0 issues tokens into the pool (capped by the epoch, only if accretive), below 1.0 buys back and burns with cash above the 20% buffer, and sets the redemption window (open below 0.9). Credits the 0.05% tip to the caller. Emits `rebalance`. Panics if the cooldown has not ended. |
| `redeem` | `amount: U128`, `min_usdc: U128` | 1 yoctoNEAR | holder, while the window is open | Attach 100 TGas (the web app uses 150). Burns `amount`, reads a fresh price, pays NAV per token in USDC from treasury cash. Refunds the tokens on a stale price, on `min_usdc` not met, or on insufficient cash. Emits `redeem` or `redeem_refunded`. |
| `get_treasury` | none | view | anyone | `{stock_units, treasury_usdc, circulating, redemption_open, last_rebalance, next_rebalance, last_nav, last_mnav_bps, epoch_cap, epoch_issued}` |
`rebalance` returns an `Outcome`: `"cooldown"`, `"stale_price"` (no state change, cooldown not consumed), `"no_market"` (pool younger than the TWAP window or nothing circulating, cooldown not consumed), `{"issued": {meme, usdc}}`, `{"bought_back": {meme, usdc}}`, or `"no_action"`.
See [mNAV](/docs/concepts/mnav) for the formulas and [Rebalance](/docs/guides/rebalance) and [Redeem](/docs/guides/redeem) for walkthroughs.
## Meme: treasury [#meme-treasury]
Each meme owns one EVM address (MPC path `evm-treasury`, secp256k1 domain 0), shared by Base and Robinhood Chain, and one Solana address (path `solana-treasury`, EdDSA domain 1). One signature is in flight at a time across all chains. See [Treasuries](/docs/concepts/treasuries).
The Gas column is the prepaid gas the contract's doc comment asks for, or what the keeper attaches.
### Setup [#setup]
| Method | Args | Caller | Gas | Effect |
| --- | --- | --- | --- | --- |
| `init_treasury` | none | anyone, once | 100 TGas | Reads `derived_public_key` from the MPC and stores the EVM treasury address. Emits `treasury` with `evm_address`. |
| `init_solana_treasury` | none | anyone, once | 100 TGas | Reads the derived EdDSA key and stores the Solana treasury address. Emits `treasury` with `solana_address`. |
| `approve_router` | `chain: string` | anyone, once per EVM chain | 300 TGas | MPC-signs `approve(swap_router, max)` on that chain's USDC. Emits `evm_tx_signed` (or `evm_tx_rejected`). |
| `set_solana_accounts` | `usdc_account_bump: u8`, `stock_account_bump: u8` | relayer only (may correct) | 30 TGas | Stores the treasury's USDC (SPL Token) and stock (Token-2022) associated token accounts. Emits `solana_accounts`. |
### Cash flow [#cash-flow]
| Method | Args | Caller | Gas | Effect |
| --- | --- | --- | --- | --- |
| `fund_chain` | `chain: string` | anyone | 100 TGas | Reads a fresh price, then sends NEAR cash above the 20% buffer, at most `max_swap_usdc`, to the factory's `bridge` with memo `solidon fund `. Booked as `in_transit` (still counted in NAV). Emits `cash_sent` or `fund_chain_skipped`; a failed transfer returns the cash (`cash_returned`). |
| `credit_cash` | `chain: string`, `tx_hash: string`, `log_index: u64`, `log: {block_hash, block_number, transaction_index, address, topics, data}` | anyone, chains with `proof: Mpc` (Base) | 300 TGas | Checks the log is a USDC `Transfer` to the treasury, then calls `verify_foreign_transaction` (domain 3) on the MPC. On success moves `min(value, in_transit)` into the chain's USDC ledger. Once per log. Emits `cash_credited` (`proof: "mpc"`) or `cash_rejected`. |
| `report_cash` | `chain: string`, `tx_hash: string`, `amount: U128` | relayer only, chains with `proof: Relayer` (Robinhood) and Solana | 30 TGas | Same credit, capped by `in_transit`, once per `tx_hash`. `amount` is in the chain's raw USDC units. On Solana `tx_hash` is the base58 transaction signature. Emits `cash_credited` (`proof: "relayer"`). |
### Stock buys [#stock-buys]
| Method | Args | Caller | Gas | Effect |
| --- | --- | --- | --- | --- |
| `buy_stock` | `chain: string` | anyone, EVM chain with a venue, router approved | 300 TGas | Fresh price, then MPC-signs a Uniswap v3 SwapRouter02 `exactInputSingle` of up to `max_swap_usdc` of the chain's USDC into the thesis stock, `minOut` = oracle value minus 1%. The ledger books `minOut` at signing. Emits `evm_tx_signed`, or `buy_stock_skipped` on a stale price. |
| `buy_stock_solana` | `blockhash: string`, `tick_arrays: [i32, u8][]` | anyone | 300 TGas | Fresh price, then MPC-signs (EdDSA) a Raydium CLMM `swap_v2` of up to `max_swap_usdc` USDC into the stock, `minOut` = oracle value minus 1%. `blockhash`: recent, base58. `tick_arrays`: 1 to 3 `[start_index, bump]` pairs in the swap direction, nearest first. Emits `solana_tx_signed`. |
| `resign_solana` | `blockhash: string`, `tick_arrays: [i32, u8][]` | anyone, 200 NEAR blocks after the last Solana signature | 300 TGas | Signs the last Solana buy again with the same amounts and a new blockhash. The ledger is not touched. |
| `clear_stuck_signature` | none | anyone, 300 blocks after a request | default | Frees a signing slot whose callback never completed. The nonce is not consumed. Emits `signature_cleared`. |
### Treasury views [#treasury-views]
| Method | Args | Returns |
| --- | --- | --- |
| `get_treasury_chains` | none | `{address, mpc, signing, chains: [{config, venue, nonce, approved, usdc, stock, in_transit}], solana: {config, venue, address, usdc_account, stock_account, seq, usdc, stock, in_transit, last_buy} \| null}`. `usdc` and `stock` are raw units of the chain's tokens; `in_transit` is 6-decimal USDC. |
| `get_signed_tx` | `chain: string`, `nonce: u64` | Signed raw transaction or `null`: `0x` hex on EVM chains, base64 on Solana (ready for `sendTransaction` with `encoding: base64`). |
Anyone can broadcast a signed transaction; the keeper's broadcaster does it automatically. See [Keeper](/docs/reference/keeper).
### Internal callbacks [#internal-callbacks]
These are `#[private]` and can only be called by the contract itself: `on_payout`, `on_rebalance_price`, `on_redeem_price`, `on_fund_price`, `on_funded`, `on_cash_verified`, `on_treasury_key`, `on_solana_treasury_key`, `on_buy_price`, `on_solana_buy_price`, `on_signed`, `ft_resolve_transfer`.
## Read-only calls [#read-only-calls]
All views work with near-cli-rs and no account:
```bash
NET="network-config testnet"
near contract call-function as-read-only solidon.testnet get_memes json-args '{}' $NET now
near contract call-function as-read-only solidon.testnet launch_cost json-args '{}' $NET now
near contract call-function as-read-only solidon.testnet get_chains json-args '{}' $NET now
near contract call-function as-read-only honey.solidon.testnet get_pool json-args '{}' $NET now
near contract call-function as-read-only honey.solidon.testnet get_treasury json-args '{}' $NET now
near contract call-function as-read-only honey.solidon.testnet get_treasury_chains json-args '{}' $NET now
near contract call-function as-read-only honey.solidon.testnet quote_buy json-args '{"usdc_in":"10000000"}' $NET now
near contract call-function as-read-only honey.solidon.testnet get_signed_tx json-args '{"chain":"base","nonce":0}' $NET now
```
Example `get_treasury` of `floor.solidon.testnet`:
```json
{
"circulating": "497487437185929648241206030",
"epoch_cap": "0",
"epoch_issued": "0",
"last_mnav_bps": "0",
"last_nav": "0",
"last_rebalance": 0,
"next_rebalance": 3600,
"redemption_open": false,
"stock_units": "0",
"treasury_usdc": "800000"
}
```
## Events [#events]
Every event is a NEP-297 log line `EVENT_JSON:{"standard":"solidon","version":"1.0.0","event":"","data":[{...}]}`. The meme also emits standard NEP-141 events (`ft_mint` at launch and on issuance, `ft_burn` on buyback and redemption, `ft_transfer`).
| Event | Emitter | `data` fields |
| --- | --- | --- |
| `launch` | factory | `meme`, `creator` |
| `seed` | meme | `account`, `usdc_in` |
| `buy` | meme | `account`, `usdc_in`, `meme_out` |
| `sell` | meme | `account`, `meme_in`, `usdc_out` |
| `payout_failed` | meme | `account`, `amount` |
| `rebalance` | meme | `caller`, `mnav_bps`, `nav`, `twap`, `redemption_open`, `outcome`, `tip` |
| `redeem` | meme | `account`, `meme_in`, `usdc_out` |
| `redeem_refunded` | meme | `account`, `meme` |
| `treasury` | meme | `evm_address` or `solana_address` |
| `solana_accounts` | meme | `usdc_account`, `stock_account` |
| `cash_sent` | meme | `chain`, `amount`, `bridge`, `to` |
| `fund_chain_skipped` | meme | `chain`, `reason` |
| `cash_returned` | meme | `chain`, `amount` |
| `cash_credited` | meme | `chain`, `tx_hash`, `amount`, `proof` (`mpc` adds `log_index`) |
| `cash_rejected` | meme | `chain`, `tx_hash`, `log_index` |
| `buy_stock_skipped` | meme | `chain`, `reason` |
| `evm_tx_signed` | meme | `chain`, `nonce`, `kind`, `raw` |
| `evm_tx_rejected` | meme | `chain`, `nonce` |
| `solana_tx_signed` | meme | `chain`, `seq`, `kind`, `tx` (base64) |
| `solana_tx_rejected` | meme | `chain`, `seq` |
| `signature_cleared` | meme | `chain`, `nonce` |
`kind` is `"Approve"`, `{"Swap": {"usdc_in", "min_stock_out"}}` or `"Resign"`.
Example `rebalance` event, as served by the [read API](/docs/reference/api):
```json
{
"event": "rebalance",
"standard": "solidon",
"version": "1.0.0",
"data": {
"caller": "keeper.solidon.testnet",
"mnav_bps": "291610",
"nav": "406123708214",
"twap": "11843000782134",
"redemption_open": false,
"outcome": "no_action",
"tip": "0"
}
}
```
## Testnet helpers [#testnet-helpers]
| Contract | Method | Args | Note |
| --- | --- | --- | --- |
| `usdc.solidon.testnet` (`mock-ft`) | `mint` | `account_id`, `amount` | Open to anyone; registers the account if needed. Plus NEP-141 and NEP-145. |
| `pyth.solidon.testnet` (`mock-pyth`) | `set_price` | `price_id`, `price: I64`, `expo: i32`, `publish_time?: i64` | Open to anyone; the keeper pushes Chainlink prices. |
| `pyth.solidon.testnet` | `get_price_no_older_than` | `price_id`, `age: u64` | Returns `{price, conf, expo, publish_time}` or `null` |
| `pyth.solidon.testnet` | `get_price_unsafe` | `price_identifier` | Same, without the age check |
# Deployments (/docs/reference/deployments)
Everything runs on testnets. Stock tokens are synthetic (Solidon-deployed), priced from the official Chainlink feeds on Robinhood Chain mainnet; the test USDC has no value. Source files: `docs/DEPLOYMENTS.md` and the venue registries in `contracts/stocks/`.
| Surface | URL |
| --- | --- |
| Web app | https://app.solidon.xyz |
| Landing | https://solidon.xyz |
| Read API | https://api.solidon.xyz (see [API](/docs/reference/api)) |
## NEAR testnet [#near-testnet]
| Account | Contract | Role |
| --- | --- | --- |
| [`solidon.testnet`](https://testnet.nearblocks.io/address/solidon.testnet) | `factory` | Launches memes. 3 treasury chains, 8 stocks. `launch_cost` = 4.45999 NEAR. |
| [`usdc.solidon.testnet`](https://testnet.nearblocks.io/address/usdc.solidon.testnet) | `mock-ft` | USDC stand-in, 6 decimals, open `mint` |
| [`pyth.solidon.testnet`](https://testnet.nearblocks.io/address/pyth.solidon.testnet) | `mock-pyth` | Oracle with the Pyth read interface; prices pushed by the keeper from Chainlink |
| [`keeper.solidon.testnet`](https://testnet.nearblocks.io/address/keeper.solidon.testnet) | none | The factory's `bridge` and `relayer`; runs the [keeper](/docs/reference/keeper) |
| [`v1.signer-prod.testnet`](https://testnet.nearblocks.io/address/v1.signer-prod.testnet) | NEAR MPC | Chain Signatures signer (secp256k1 domain 0, EdDSA domain 1, foreign-tx verification domain 3) |
| [`creator.solidon.testnet`](https://testnet.nearblocks.io/address/creator.solidon.testnet) | none | Demo creator account |
Factory config, as returned by `get_chains`:
```json
{
"mpc": "v1.signer-prod.testnet",
"bridge": "keeper.solidon.testnet",
"relayer": "keeper.solidon.testnet",
"evm": [
{
"name": "base",
"chain_id": 84532,
"usdc": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"usdc_decimals": 6,
"swap_router": "0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4",
"pool_fee": 3000,
"max_fee_per_gas": "100000000",
"max_priority_fee_per_gas": "1000000",
"max_swap_usdc": "5000000",
"proof": { "Mpc": { "foreign_chain": "Base", "finality": "Latest" } }
},
{
"name": "robinhood",
"chain_id": 46630,
"usdc": "0xbf4479C07Dc6fdc6dAa764A0ccA06969e894275F",
"usdc_decimals": 18,
"swap_router": "0x3ce954107b1a675826b33bf23060dd655e3758fe",
"pool_fee": 3000,
"max_fee_per_gas": "100000000",
"max_priority_fee_per_gas": "10000000",
"max_swap_usdc": "5000000000000000000",
"proof": "Relayer"
}
],
"solana": {
"name": "solana",
"usdc_mint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"usdc_token_program": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"stock_token_program": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
"clmm_program": "devi51mZmdwUJGU9hjN27vEz64Gps7uUefqxg27EAtH",
"amm_config": "GVSwm4smQBYcgAJU7qjFHLQBHTc4AdB3F2HbZp6KqKof",
"max_swap_usdc": "5000000"
}
}
```
`max_swap_usdc` is 5 USDC per swap and per `fund_chain` transfer on every chain.
## Memes [#memes]
`get_memes` on the factory returns five memes. Two are test launches that the factory cannot unlist; the web app hides them from Markets and landing totals, but they stay reachable by URL.
| Meme | Symbol | Thesis | Creator | Note |
| --- | --- | --- | --- | --- |
| [`honey.solidon.testnet`](https://testnet.nearblocks.io/address/honey.solidon.testnet) | `HONEY` ("Honeypot") | NVDA | `solidon-launch2.testnet` | Featured meme |
| [`spydia.solidon.testnet`](https://testnet.nearblocks.io/address/spydia.solidon.testnet) | `SPYDIA` | NVDA | `solidon-launch.testnet` | Live |
| [`floor.solidon.testnet`](https://testnet.nearblocks.io/address/floor.solidon.testnet) | `FLOOR` ("The Floor Is Lava") | TSLA | implicit account `19d6c2e1…3e27` | Floor demo: the keeper skips it (`KEEPER_SKIP_MEMES`), so its cash stays on NEAR for manual buyback and redemption |
| [`e2etest.solidon.testnet`](https://testnet.nearblocks.io/address/e2etest.solidon.testnet) | `E2ETEST` | NVDA | `creator.solidon.testnet` | Hidden. End-to-end proof run in `docs/DEPLOYMENTS.md` |
| [`honeypot.solidon.testnet`](https://testnet.nearblocks.io/address/honeypot.solidon.testnet) | `HONEYPOT` | NVDA | `solidon-launch.testnet` | Hidden. Abandoned first launch with a cropped, immutable icon; holds only its seed |
## Treasury addresses [#treasury-addresses]
Each meme has one EVM address (the same on Base Sepolia and Robinhood testnet) and one Solana address, both derived by the MPC from the meme account. Read them with `get_treasury_chains`.
| Meme | EVM treasury (Base, Robinhood) | Solana treasury |
| --- | --- | --- |
| HONEY | `0x51b1197be727ce2fb229c4919eca30e5568c802a` ([Base](https://sepolia.basescan.org/address/0x51b1197be727ce2fb229c4919eca30e5568c802a), [Robinhood](https://explorer.testnet.chain.robinhood.com/address/0x51b1197be727ce2fb229c4919eca30e5568c802a)) | [`9gyosY8RFLdET2AGVj5XU14tpRGLFrun2VrpfZ1tAsLP`](https://explorer.solana.com/address/9gyosY8RFLdET2AGVj5XU14tpRGLFrun2VrpfZ1tAsLP?cluster=devnet) |
| SPYDIA | `0xc8c8407717e388ed21e5b2fd5a047c6743318c50` ([Base](https://sepolia.basescan.org/address/0xc8c8407717e388ed21e5b2fd5a047c6743318c50), [Robinhood](https://explorer.testnet.chain.robinhood.com/address/0xc8c8407717e388ed21e5b2fd5a047c6743318c50)) | [`5DdM4FKEsvLwjr2t2GZGxpwg8D3RN9HQoCo4Lpd51Xmr`](https://explorer.solana.com/address/5DdM4FKEsvLwjr2t2GZGxpwg8D3RN9HQoCo4Lpd51Xmr?cluster=devnet) |
| FLOOR | `0x68c1e3adc85bf1fcb60697dbef01db4521dbbb72` ([Base](https://sepolia.basescan.org/address/0x68c1e3adc85bf1fcb60697dbef01db4521dbbb72), [Robinhood](https://explorer.testnet.chain.robinhood.com/address/0x68c1e3adc85bf1fcb60697dbef01db4521dbbb72)) | not initialised |
| E2ETEST | `0x582be22a62a466284817298848be90bf8a6f37e4` ([Base](https://sepolia.basescan.org/address/0x582be22a62a466284817298848be90bf8a6f37e4), [Robinhood](https://explorer.testnet.chain.robinhood.com/address/0x582be22a62a466284817298848be90bf8a6f37e4)) | [`BnhYneFsvh45UDJy7B7CVwvdAev1p1gNzGjWdf9KERNV`](https://explorer.solana.com/address/BnhYneFsvh45UDJy7B7CVwvdAev1p1gNzGjWdf9KERNV?cluster=devnet) |
| HONEYPOT | `0xcfb5fe7a549f3886bbb671fb3efe2b3488910b11` | `Fx3V5mfKsQj9MJ1Y2TnXKYZQxfm3PmVzaYEecAnC4je3` |
HONEY's Solana token accounts: USDC `GFqkQg9GC7yYbeFLUkCGPXJK1ZLacig2GUzrnk3ADMt5`, NVDAx `FR7UKctLZvR8qJBriNUmRA2WThDWZ6XXNpFuMAwxAhAz`.
```bash
near contract call-function as-read-only honey.solidon.testnet get_treasury_chains json-args '{}' network-config testnet now
```
## Base Sepolia (chain 84532) [#base-sepolia-chain-84532]
| Contract | Address |
| --- | --- |
| USDC (Circle, 6 decimals) | [`0x036CbD53842c5426634e7929541eC2318f3dCF7e`](https://sepolia.basescan.org/address/0x036CbD53842c5426634e7929541eC2318f3dCF7e) |
| Uniswap v3 SwapRouter02 | [`0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4`](https://sepolia.basescan.org/address/0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4) |
| Uniswap v3 factory | [`0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24`](https://sepolia.basescan.org/address/0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24) |
| NonfungiblePositionManager | [`0x27F971cb582BF9E50F397e4d29a5C7A34f11faA2`](https://sepolia.basescan.org/address/0x27F971cb582BF9E50F397e4d29a5C7A34f11faA2) |
Cash arrivals on Base are proven by the MPC (`credit_cash`). Stock tokens are Solidon `MockERC20` (owner-only mint); pools are USDC/stock, fee 0.3%.
| Ticker | Symbol | Token | Decimals | Pool |
| --- | --- | --- | --- | --- |
| NVDA | `NVDAc` | [`0x8f13563130684478E617C1797308A081Cb2c2654`](https://sepolia.basescan.org/address/0x8f13563130684478E617C1797308A081Cb2c2654) | 8 | [`0x93A765030deB0352d22EbBe8335A2B79aF153129`](https://sepolia.basescan.org/address/0x93A765030deB0352d22EbBe8335A2B79aF153129) |
| AAPL | `AAPLc` | [`0xEbF01E9d3d578AD3af778C281E56aDA483ebB9bD`](https://sepolia.basescan.org/address/0xEbF01E9d3d578AD3af778C281E56aDA483ebB9bD) | 8 | [`0x0Fa222947220935106bA9C8Fed888B8Bfd74ca4e`](https://sepolia.basescan.org/address/0x0Fa222947220935106bA9C8Fed888B8Bfd74ca4e) |
| MSFT | `MSFTc` | [`0x4fE929688f9A5598bE4559daC9B92a9A05cD2265`](https://sepolia.basescan.org/address/0x4fE929688f9A5598bE4559daC9B92a9A05cD2265) | 8 | [`0x1791E60Fd6CDB6294366b8C92e34946a5a2B6e16`](https://sepolia.basescan.org/address/0x1791E60Fd6CDB6294366b8C92e34946a5a2B6e16) |
| GOOGL | `GOOGLc` | [`0x12e763e9C1ceeb82f9AaCdf8a87b152bE05A7eeF`](https://sepolia.basescan.org/address/0x12e763e9C1ceeb82f9AaCdf8a87b152bE05A7eeF) | 8 | [`0x07914c5774361f222bdCd991f0b872b7CF955A03`](https://sepolia.basescan.org/address/0x07914c5774361f222bdCd991f0b872b7CF955A03) |
| AMZN | `AMZNc` | [`0x9b931cb9EE0A70b88d89b3c21687d4a6ccee32a3`](https://sepolia.basescan.org/address/0x9b931cb9EE0A70b88d89b3c21687d4a6ccee32a3) | 8 | [`0x3F35161b0E85E3FFdA761c49Ad469b607B67Fc58`](https://sepolia.basescan.org/address/0x3F35161b0E85E3FFdA761c49Ad469b607B67Fc58) |
| META | `METAc` | [`0x845A964b112a33B6A0881b36e121fdDFFDe2477d`](https://sepolia.basescan.org/address/0x845A964b112a33B6A0881b36e121fdDFFDe2477d) | 8 | [`0x6861ADbEE1c0BBb1aaC9f7166fBe9828E26BdDD5`](https://sepolia.basescan.org/address/0x6861ADbEE1c0BBb1aaC9f7166fBe9828E26BdDD5) |
| TSLA | `TSLAc` | [`0x431190511D177106Fa49588f7e1Dc3B845c95Bc5`](https://sepolia.basescan.org/address/0x431190511D177106Fa49588f7e1Dc3B845c95Bc5) | 8 | [`0xfF7b55617F5D24686139526588977A02eE5A59a4`](https://sepolia.basescan.org/address/0xfF7b55617F5D24686139526588977A02eE5A59a4) |
| SPCX | `SPCXc` | [`0x486FECA986219f7d4eC52Cbf56d9ca14D4955035`](https://sepolia.basescan.org/address/0x486FECA986219f7d4eC52Cbf56d9ca14D4955035) | 8 | [`0xda6BFf78b5513b6818EDE36C1769363d99F07f9f`](https://sepolia.basescan.org/address/0xda6BFf78b5513b6818EDE36C1769363d99F07f9f) |
## Robinhood Chain testnet (chain 46630) [#robinhood-chain-testnet-chain-46630]
| Contract | Address |
| --- | --- |
| USDC (18 decimals) | [`0xbf4479C07Dc6fdc6dAa764A0ccA06969e894275F`](https://explorer.testnet.chain.robinhood.com/address/0xbf4479C07Dc6fdc6dAa764A0ccA06969e894275F) |
| Uniswap v3 SwapRouter02 | [`0x3ce954107b1a675826b33bf23060dd655e3758fe`](https://explorer.testnet.chain.robinhood.com/address/0x3ce954107b1a675826b33bf23060dd655e3758fe) |
| Uniswap v3 factory | [`0x911b4000d3422f482f4062a913885f7b035382df`](https://explorer.testnet.chain.robinhood.com/address/0x911b4000d3422f482f4062a913885f7b035382df) |
| NonfungiblePositionManager | [`0x54A0EF7da351cb8fD1998E7945cc51E5825fB233`](https://explorer.testnet.chain.robinhood.com/address/0x54A0EF7da351cb8fD1998E7945cc51E5825fB233) |
Cash arrivals on Robinhood are reported by the relayer (`report_cash`). The faucet's official TSLA and AMZN trade at arbitrary testnet prices, so all eight stocks here are Solidon `MockERC20` tokens. Pools: fee 0.3%, each with a small USDC float for the fork's up-front protocol fee.
| Ticker | Symbol | Token | Decimals | Pool |
| --- | --- | --- | --- | --- |
| NVDA | `NVDA` | [`0x82289af84B9a0faf598a4D2052E16c8D1619f7f4`](https://explorer.testnet.chain.robinhood.com/address/0x82289af84B9a0faf598a4D2052E16c8D1619f7f4) | 18 | [`0x7AD201DB77530864213f30Bd87Ddc2c520D86E07`](https://explorer.testnet.chain.robinhood.com/address/0x7AD201DB77530864213f30Bd87Ddc2c520D86E07) |
| AAPL | `AAPL` | [`0x3ab373732E7bE9fD387e767F0e36040677a14057`](https://explorer.testnet.chain.robinhood.com/address/0x3ab373732E7bE9fD387e767F0e36040677a14057) | 18 | [`0x9f34D4E076DD19a36C36ff28DeA2F8C671D28eD0`](https://explorer.testnet.chain.robinhood.com/address/0x9f34D4E076DD19a36C36ff28DeA2F8C671D28eD0) |
| MSFT | `MSFT` | [`0x8d26425Ea6B44032938b290Bab5C032CaA747cdD`](https://explorer.testnet.chain.robinhood.com/address/0x8d26425Ea6B44032938b290Bab5C032CaA747cdD) | 18 | [`0x2016FfecC830012aaDB3B378b4D3b59829045fed`](https://explorer.testnet.chain.robinhood.com/address/0x2016FfecC830012aaDB3B378b4D3b59829045fed) |
| GOOGL | `GOOGL` | [`0xaf95AE21417c4a1489C7C49990a9C03DD3C6B98C`](https://explorer.testnet.chain.robinhood.com/address/0xaf95AE21417c4a1489C7C49990a9C03DD3C6B98C) | 18 | [`0x116B775D35C0A53708b515156eF962171411bddd`](https://explorer.testnet.chain.robinhood.com/address/0x116B775D35C0A53708b515156eF962171411bddd) |
| AMZN | `AMZN` | [`0x9f555Fa3247304bEd0033EC31c8d6A88c36c6A37`](https://explorer.testnet.chain.robinhood.com/address/0x9f555Fa3247304bEd0033EC31c8d6A88c36c6A37) | 18 | [`0x7CEE8466204C628edBE122597DdB993753893cbF`](https://explorer.testnet.chain.robinhood.com/address/0x7CEE8466204C628edBE122597DdB993753893cbF) |
| META | `META` | [`0x4d65dBeD417B42Ecef039aF0931948bC07507D53`](https://explorer.testnet.chain.robinhood.com/address/0x4d65dBeD417B42Ecef039aF0931948bC07507D53) | 18 | [`0x4CFbc8761a81367C2BEAf301dc2DaAB142bD7fAE`](https://explorer.testnet.chain.robinhood.com/address/0x4CFbc8761a81367C2BEAf301dc2DaAB142bD7fAE) |
| TSLA | `TSLA` | [`0x3D3E8Cf6F9F92b9a1574cdB4C5686261cF52b47B`](https://explorer.testnet.chain.robinhood.com/address/0x3D3E8Cf6F9F92b9a1574cdB4C5686261cF52b47B) | 18 | [`0x16916cACEb6be1107eF7AEDa3d8c5cE34C098F9D`](https://explorer.testnet.chain.robinhood.com/address/0x16916cACEb6be1107eF7AEDa3d8c5cE34C098F9D) |
| SPCX | `SPCX` | [`0xf0c264689aC444FFe95b9F03e8eFF2bA36E508E0`](https://explorer.testnet.chain.robinhood.com/address/0xf0c264689aC444FFe95b9F03e8eFF2bA36E508E0) | 18 | [`0x1d285a27509Fa8eeC6ab8eac73635F4b270afB81`](https://explorer.testnet.chain.robinhood.com/address/0x1d285a27509Fa8eeC6ab8eac73635F4b270afB81) |
## Solana devnet [#solana-devnet]
| Account | Address |
| --- | --- |
| USDC mint (Circle devnet, 6 decimals) | [`4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`](https://explorer.solana.com/address/4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU?cluster=devnet) |
| Raydium CLMM program (devnet) | [`devi51mZmdwUJGU9hjN27vEz64Gps7uUefqxg27EAtH`](https://explorer.solana.com/address/devi51mZmdwUJGU9hjN27vEz64Gps7uUefqxg27EAtH?cluster=devnet) |
| Raydium AMM config (0.05% fee tier, tick spacing 10) | [`GVSwm4smQBYcgAJU7qjFHLQBHTc4AdB3F2HbZp6KqKof`](https://explorer.solana.com/address/GVSwm4smQBYcgAJU7qjFHLQBHTc4AdB3F2HbZp6KqKof?cluster=devnet) |
| SPL Token program (USDC) | `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA` |
| Token-2022 program (stocks) | `TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb` |
Cash arrivals on Solana are reported by the relayer (`report_cash` with the transaction signature). Stocks are Solidon Token-2022 mints with on-chain metadata. Each pool's vaults, observation and bitmap extension accounts are in `contracts/stocks/solana-devnet.json`.
| Ticker | Symbol | Mint | Decimals | Raydium CLMM pool |
| --- | --- | --- | --- | --- |
| NVDA | `NVDAx` | [`88Bseqdd57C3sD5E8SaLPfwQ18e47RvzCXsRApiwSqbB`](https://explorer.solana.com/address/88Bseqdd57C3sD5E8SaLPfwQ18e47RvzCXsRApiwSqbB?cluster=devnet) | 8 | [`FEstNywAgrVXixWbLkFaqqMjT7jbdjwmkoPdmudtRCqU`](https://explorer.solana.com/address/FEstNywAgrVXixWbLkFaqqMjT7jbdjwmkoPdmudtRCqU?cluster=devnet) |
| AAPL | `AAPLx` | [`FtYHpjhYBFVrUE1inx7w48Wp1s7AzxnsFptvpgZcGkW5`](https://explorer.solana.com/address/FtYHpjhYBFVrUE1inx7w48Wp1s7AzxnsFptvpgZcGkW5?cluster=devnet) | 8 | [`8HtcC4dszMQcnrPvow8iYF6aNDTigtFsZ8PP4BiRypsV`](https://explorer.solana.com/address/8HtcC4dszMQcnrPvow8iYF6aNDTigtFsZ8PP4BiRypsV?cluster=devnet) |
| MSFT | `MSFTx` | [`Bah7NyVk64eL7ADuHuJYwqKeG6vQTD5YRPQKLMtzG4SP`](https://explorer.solana.com/address/Bah7NyVk64eL7ADuHuJYwqKeG6vQTD5YRPQKLMtzG4SP?cluster=devnet) | 8 | [`EhyAxindqL3HiK6VPXjpJtrxKHgh2kJkePML3r5cXsdv`](https://explorer.solana.com/address/EhyAxindqL3HiK6VPXjpJtrxKHgh2kJkePML3r5cXsdv?cluster=devnet) |
| GOOGL | `GOOGLx` | [`XLYZw9YeVrUNsp4dAVVYayMdT5jWrmk6wFjUNWu8AnH`](https://explorer.solana.com/address/XLYZw9YeVrUNsp4dAVVYayMdT5jWrmk6wFjUNWu8AnH?cluster=devnet) | 8 | [`5dRjF2Zpc7crjrqp8NzrpntYnhGTVcLFN5QumB2iftu9`](https://explorer.solana.com/address/5dRjF2Zpc7crjrqp8NzrpntYnhGTVcLFN5QumB2iftu9?cluster=devnet) |
| AMZN | `AMZNx` | [`D4GVCzUpbJRkqQ4ymbpXxKTC5jCxWK1gRvDXwRAHERZk`](https://explorer.solana.com/address/D4GVCzUpbJRkqQ4ymbpXxKTC5jCxWK1gRvDXwRAHERZk?cluster=devnet) | 8 | [`6pRZ79bnqssPy3HhyN3TijrCjXoqzZ4auD77kcM5mVhK`](https://explorer.solana.com/address/6pRZ79bnqssPy3HhyN3TijrCjXoqzZ4auD77kcM5mVhK?cluster=devnet) |
| META | `METAx` | [`66aQ8CyXmgYkvreikf76EzETQC4oVgv9bbdxbPgFrKj8`](https://explorer.solana.com/address/66aQ8CyXmgYkvreikf76EzETQC4oVgv9bbdxbPgFrKj8?cluster=devnet) | 8 | [`EgV8KMGkUbzpLZUru3ivLi9Cx7YgxxhtopxcDFQC6JLA`](https://explorer.solana.com/address/EgV8KMGkUbzpLZUru3ivLi9Cx7YgxxhtopxcDFQC6JLA?cluster=devnet) |
| TSLA | `TSLAx` | [`229y3sTgdfFBEWGxmuvQ4yvpj8puVrpmeSFNp7ZRkmSq`](https://explorer.solana.com/address/229y3sTgdfFBEWGxmuvQ4yvpj8puVrpmeSFNp7ZRkmSq?cluster=devnet) | 8 | [`8cZfaL77SdxBZPc3SVPe7mrmNpqPJa61ixqqjsBqqm2o`](https://explorer.solana.com/address/8cZfaL77SdxBZPc3SVPe7mrmNpqPJa61ixqqjsBqqm2o?cluster=devnet) |
| SPCX | `SPCXx` | [`8mdbWErQneCkM6jpE5epezYrk8W6uhjjG9nnPwxLQDKp`](https://explorer.solana.com/address/8mdbWErQneCkM6jpE5epezYrk8W6uhjjG9nnPwxLQDKp?cluster=devnet) | 8 | [`3uwogu11qDBBw7mSVFHdTFq1YXnMrgaah1bYjuAJCF5K`](https://explorer.solana.com/address/3uwogu11qDBBw7mSVFHdTFq1YXnMrgaah1bYjuAJCF5K?cluster=devnet) |
## Older contracts [#older-contracts]
Memes launched by earlier factory code (BSC era, before 10 Nov 2026) still exist, because memes have no access key, but they are not listed by the current factory: `nvda.solidon.testnet`, `elondoge.solidon.testnet`, and the Chain Signatures test account `chainsig-e2e.solidon.testnet`. The keeper gives memes without `get_treasury_chains` only `rebalance()`.
During development the factory account still has a full-access key, which could redeploy the factory. Launched memes have no access key. See [Security](/docs/security).
# Keeper (/docs/reference/keeper)
The keeper (`backend/keeper/keeper.js`, Node.js 20+) is a convenience process. Every treasury method it calls is open to anyone and checked by the contract; the keeper only decides when to call. It also plays two trusted testnet roles: it feeds the mock oracle, and its NEAR account `keeper.solidon.testnet` is the factory's `bridge` and `relayer`.
Run at most one keeper against a factory. Its `state.json` keys every bridge transfer by NEAR receipt id; a second keeper with its own state could deliver the same transfer twice. To move a keeper, stop it and move (never copy) its `state.json`.
## The loop [#the-loop]
One cycle runs every `LOOP_SECS` (60 s). Steps run in this order; `--only=` selects a subset.
| Step (`--only` name) | When | What it does |
| --- | --- | --- |
| `oracle` | every `ORACLE_INTERVAL_SECS` (300 s) | Reads `latestRoundData()` of the 8 Chainlink proxies on Robinhood Chain mainnet and calls `set_price` on the mock oracle for each, in one NEAR transaction. Skips a feed older than `ORACLE_MAX_FEED_AGE_SECS` (25 h) or non-positive, so the contracts' 15-minute staleness brake applies. Refuses to write to `pyth-oracle.near` / `pyth-oracle.testnet` or any non-`.testnet` account. |
| `repeg` | every `REPEG_EVERY_MIN` minutes (default 0 = off; 30 in production) | Moves every synthetic stock pool whose next buy fills more than `THRESHOLD_PCT` above Chainlink to `TARGET` x Chainlink, adding `DEPTH_USD` of stock liquidity. EVM pools in-process (`src/repeg-evm.js`), Solana pools via `backend/solana-spike/3_repeg.js`. Runs before any buy, so the contract's `minOut` (oracle minus 1%) can fill. |
| `broadcast` | every cycle | For each meme and EVM chain, sends `get_signed_tx(chain, nonce)` for every nonce between the treasury's on-chain nonce and the meme's next nonce, and waits for the receipt. Tops up treasury gas from the keeper wallet below `TREASURY_GAS_MIN_ETH`. |
| `bridge` | every cycle | Lists USDC transfers to the keeper on NearBlocks, re-reads each one from NEAR RPC (`EXPERIMENTAL_receipt`), parses the memo `solidon fund `, and sends the same amount of that chain's testnet USDC to the treasury, capped by the meme's `in_transit` minus what was already delivered. Then proves it: `credit_cash` on Base (MPC-verified log, 300 TGas), `report_cash` on Robinhood and Solana (30 TGas). The signed delivery is saved before broadcast, so a restart resumes instead of paying twice. |
| `treasury` | every cycle, per meme | `init_treasury` if no address; per EVM chain with a venue: `approve_router` if not approved, `buy_stock` if the chain holds USDC and the price is fresh, `fund_chain` when nothing is in transit, the chain's USDC is spent, it is the chain holding the fewest shares, and the estimated cash above the 20% buffer is at least `FUND_MIN_USDC`. Then `rebalance` (100 TGas) when the cooldown is over and the price is fresh; a `no_market` answer is retried at most every 600 s. Signing steps wait while a signature is in flight. |
| `solana` | every cycle, per meme with a Solana venue | `init_solana_treasury`; `set_solana_accounts` with the canonical ATA bumps (also corrects wrong ones); keeps treasury SOL above `TREASURY_SOL_MIN`; checks the last signed buy first (landed: done; valid blockhash: resend; expired: `resign_solana` after 200 NEAR blocks, at most `SOLANA_MAX_RESIGNS` times); then `buy_stock_solana` with up to 3 tick arrays and a recent blockhash, broadcast at once; then `fund_chain("solana")` under the EVM rule. |
The keeper never calls `clear_stuck_signature`. Memes on code without `get_treasury_chains` only get `rebalance()`.
## Indexer and API [#indexer-and-api]
`keeper.js` also starts the indexer and the [read API](/docs/reference/api) on their own timers, so they never delay the treasury loop. `--no-api` turns them off; `--once` never starts them. `node indexer.js` runs them alone: that process loads no key and never signs.
## Flags [#flags]
```bash
node keeper.js [--once] [--dry-run] [--no-api] [--meme=a.testnet,b.testnet] [--only=oracle,broadcast,bridge,treasury,solana,repeg]
node indexer.js [--meme=...]
node repeg.js [--chain=base,robinhood,solana] [--ticker=NVDA,...] [--dry-run]
```
| Flag | Effect |
| --- | --- |
| `--once` | One cycle, then exit. No indexer, no API. |
| `--dry-run` | Real reads, no writes: every transaction is logged as `DRY ... would ...`. No key needed. |
| `--no-api` | Do not start the indexer and its HTTP API (use when `indexer.js` runs separately, so only one process writes `data/`). |
| `--meme=` | Only these memes (comma separated). Default: every meme of the factory. |
| `--only=` | Only these steps. Default: all. |
| `--help`, `-h` | Print usage. |
npm scripts: `npm start` (`node keeper.js`), `npm run once`, `npm run dry-run` (`--once --dry-run`), `npm test`.
## Environment variables [#environment-variables]
The keeper reads `backend/.env` and `contracts/.env` (values already in the environment win) and never prints secrets; RPC URLs and keys are redacted from logs. Template: `backend/.env.example`. Every non-secret value has a testnet default.
### Accounts and keys [#accounts-and-keys]
| Variable | Default | Meaning |
| --- | --- | --- |
| `FACTORY` | `solidon.testnet` | Factory whose memes the keeper serves |
| `KEEPER_ACCOUNT` | `keeper.` | NEAR account of the keeper: the factory's `bridge` and `relayer` |
| `KEEPER_NEAR_PRIVATE_KEY` | `~/.near-credentials/testnet/.json` | Secret. NEAR key of the keeper account |
| `KEEPER_EVM_PRIVATE_KEY` | `EVM_DEPLOYER_PRIVATE_KEY` from `contracts/.env` | Secret. Keeper EVM wallet: bridge deliveries, treasury gas, re-peg (must own the `MockERC20` stocks). Without it, deliveries and gas top-ups are off. |
| `KEEPER_EVM_ADDRESS` | `EVM_DEPLOYER_ADDRESS` | Expected EVM address; the keeper refuses to start if the key does not match |
| `KEEPER_SOLANA_KEYPAIR` | `backend/keeper/.secrets/solana-keeper.json` | Secret. Devnet keypair, created on first run |
| `ALCHEMY_API_KEY` | none | Secret. Default RPC for Base Sepolia, Robinhood testnet and mainnet, Solana devnet |
| `NEARBLOCKS_API_KEY` | none | Secret, optional. Raises the NearBlocks rate limit |
| `PYTH_API_KEY` | none | Secret, optional. Redacted from logs |
| `SOLIDON_ENV`, `SOLIDON_CONTRACTS_ENV` | `backend/.env`, `contracts/.env` | Paths of the two env files |
### RPC endpoints [#rpc-endpoints]
| Variable | Default | Meaning |
| --- | --- | --- |
| `NEAR_RPC` | `https://test.rpc.fastnear.com` | NEAR RPC for views |
| `NEAR_RPC_SEND` | `NEAR_RPC` | NEAR RPC for transactions. Must support `broadcast_tx_commit` (dRPC fails it); keep FastNear here |
| `BASE_SEPOLIA_RPC` | Alchemy `base-sepolia`, else `https://sepolia.base.org` | Base Sepolia |
| `HOOD_TESTNET_RPC` | Alchemy `robinhood-testnet`, else `https://rpc.testnet.chain.robinhood.com` | Robinhood Chain testnet |
| `HOOD_MAINNET_RPC` | Alchemy `robinhood-mainnet` | Chainlink reads. Required for the oracle step |
| `SOLANA_DEVNET_RPC` | Alchemy `solana-devnet`, else `https://api.devnet.solana.com` | Solana devnet |
| `NEARBLOCKS_API` | `https://api-testnet.nearblocks.io` | Bridge transfer discovery |
Views and transactions use separate endpoints because views are many and transactions are few but need an RPC that waits for the outcome. Production uses `NEAR_RPC=https://near-testnet.drpc.org` for views and `NEAR_RPC_SEND=https://test.rpc.fastnear.com` for transactions.
### Loop and thresholds [#loop-and-thresholds]
| Variable | Default | Meaning |
| --- | --- | --- |
| `LOOP_SECS` | `60` | Cycle interval |
| `ORACLE_INTERVAL_SECS` | `300` | Oracle push interval; must stay under the contracts' 900 s staleness limit |
| `ORACLE_MAX_FEED_AGE_SECS` | `90000` (25 h) | Older Chainlink answers are not pushed |
| `KEEPER_SKIP_MEMES` | empty | Comma-separated memes the keeper leaves alone (production: `floor.solidon.testnet`, so its cash stays on NEAR for a manual buyback and redemption demo) |
| `FUND_MIN_USDC` | `1` | Smallest `fund_chain` amount or chain cash the keeper acts on |
| `TREASURY_GAS_MIN_ETH` | `0.0002` | Top up an EVM treasury below this |
| `TREASURY_GAS_TOPUP_ETH` | `0.0005` | Amount sent; `0` disables top-ups |
| `RECEIPT_TIMEOUT_SECS` | `120` | Wait for an EVM receipt |
| `CREDIT_MAX_ATTEMPTS` | `6` | Retries of a failed `credit_cash` before it is left as `credit-failed` |
| `KEEPER_STATE` | `backend/keeper/state.json` | Bridge transfer state |
| `TREASURY_SOL_MIN` | `0.01` | Top up a Solana treasury below this |
| `TREASURY_SOL_TOPUP` | `0.02` | Top-up target |
| `KEEPER_SOL_MIN` | `0.1` | Below this the keeper requests a 1 SOL devnet airdrop |
| `SOLANA_CONFIRM_SECS` | `60` | Wait for a Solana confirmation |
| `SOLANA_MAX_RESIGNS` | `5` | `resign_solana` attempts for one failing buy |
### Re-peg [#re-peg]
| Variable | Default | Meaning |
| --- | --- | --- |
| `REPEG_EVERY_MIN` | `0` (off) | Run a re-peg pass every N minutes |
| `THRESHOLD_PCT` | `0.5` | Re-peg a pool whose next buy fills this many percent above Chainlink |
| `TARGET` | `0.995` | New pool price as a fraction of Chainlink |
| `DEPTH_USD` | `50000` | New one-sided stock liquidity, USD |
| `SOLANA_PAYER_KEYPAIR` | `backend/solana-spike/.secrets/payer.json` | Secret. Devnet payer, mint authority of the Solana stocks |
| `SOLANA_SPIKE_STATE` | the spike's `state.json` | Writable copy of the Solana spike state |
| `DRY_RUN` | unset | For `repeg.js`: same as `--dry-run` |
### Indexer [#indexer]
| Variable | Default | Meaning |
| --- | --- | --- |
| `INDEXER_PORT` | `8788` | HTTP port of the read API |
| `INDEXER_DATA` | `backend/keeper/data` | `history/`, `events/`, `cursor.json` |
| `INDEXER_POLL_SECS` | `20` | Event poll interval |
| `SNAPSHOT_SECS` | `LOOP_SECS` | One history point per meme per interval |
| `FASTNEAR_TX_API` | `https://tx.test.fastnear.com` | FastNear transactions API |
## Deployment [#deployment]
Production runs on a VPS as two containers of one image (`backend/keeper/Dockerfile`, `node:24-alpine`, build context = repo root) from `backend/deploy/compose.yml`, behind a shared Caddy that terminates HTTPS for `api.solidon.xyz`.
| Service | Command | Notes |
| --- | --- | --- |
| `api` | `node indexer.js` | Read-only, no key. `INDEXER_DATA=/state/data`. Caddy proxies `api.solidon.xyz` to `solidon-api:8788`. |
| `keeper` | `node keeper.js --no-api` | Secrets from `/opt/solidon/secrets.env`. `REPEG_EVERY_MIN=30`, `NEAR_RPC=https://near-testnet.drpc.org`, `NEAR_RPC_SEND=https://test.rpc.fastnear.com`, `KEEPER_SKIP_MEMES=floor.solidon.testnet`, `KEEPER_STATE=/state/state.json`. |
```bash
# ship the code, then (re)build and start both services
git archive --format=tar HEAD backend/keeper backend/solana-spike backend/deploy contracts/stocks \
| ssh root@ 'rm -rf /opt/solidon/repo && mkdir -p /opt/solidon/repo && tar -x -C /opt/solidon/repo'
ssh root@ 'cd /opt/solidon/repo/backend/deploy && docker compose up -d --build'
# one re-peg pass by hand, plan only
docker compose exec keeper node repeg.js --dry-run
```
Host layout: `/opt/solidon/state` (indexer data, `state.json`), `/opt/solidon/secrets` (Solana keypairs, read-only mount), `/opt/solidon/secrets.env` (keys, mode 600). See `backend/deploy/README.md`.
## Trust [#trust]
| Step | Trust required |
| --- | --- |
| Oracle | The mock oracle is trusted on testnet (anyone can set it). The contracts read whichever Pyth-interface oracle the factory fixed at init. |
| Broadcast | None. Transactions are MPC-signed and checked by the contract; anyone can broadcast. |
| Bridge | The testnet bridge is trusted to deliver. On mainnet this leg is NEAR Intents / 1Click. |
| Cash proof | Base arrivals are MPC-verified. Robinhood and Solana arrivals are relayer-reported. |
| Treasury, Solana buys | None. Open methods; the contract builds every transaction from locked config. Wrong live Solana inputs only make a buy fail. |
| Re-peg | Testnet only; the pools and tokens are Solidon's own. |
See [Security](/docs/security) for the full threat model.
# Run locally (/docs/reference/run-locally)
Everything below runs against the live testnet deployment (`solidon.testnet`). You do not need to deploy contracts to use the app or the indexer locally.
## Requirements [#requirements]
| Part | Needs |
| --- | --- |
| Web app (`frontend/`) | Node.js 20+, npm |
| Keeper and indexer (`backend/keeper/`) | Node.js 20+, npm |
| Contracts (`contracts/`) | Rust with the `wasm32-unknown-unknown` target, cargo-near 0.23+ |
| Simulation (`simulation/`) | Python 3.10+, standard library only |
| CLI reads (optional) | near-cli-rs (`near`) |
## Web app [#web-app]
### Install and start [#install-and-start]
```bash
cd frontend
npm install
npm run dev # landing http://localhost:3000, app http://localhost:3000/app and http://app.localhost:3000
```
Production build: `npm run build && npm run start`. Lint: `npm run lint`.
### Configure (optional) [#configure-optional]
Copy `frontend/.env.example` to `frontend/.env.local`. The defaults target NEAR testnet on a single host, so no variable is required to browse and trade with your own wallet.
| Variable | Kind | Purpose |
| --- | --- | --- |
| `NEXT_PUBLIC_NETWORK` | public | NEAR network (`testnet`) |
| `NEXT_PUBLIC_FACTORY` | public | Factory account (`solidon.testnet`) |
| `NEXT_PUBLIC_USDC` | public | USDC token (`usdc.solidon.testnet`) |
| `NEXT_PUBLIC_NEAR_RPC` | public | NEAR RPCs, comma separated, tried in order |
| `NEXT_PUBLIC_NEARBLOCKS_API` | public | NearBlocks API base |
| `NEXT_PUBLIC_BASE_RPC`, `NEXT_PUBLIC_ROBINHOOD_RPC`, `NEXT_PUBLIC_SOLANA_RPC` | public | Bypass the app's `/api/rpc/` read proxy with a public URL |
| `NEXT_PUBLIC_INDEXER_URL` | public | Read API base, default `http://localhost:8788` |
| `NEXT_PUBLIC_APP_URL`, `NEXT_PUBLIC_SITE_URL` | public | Two-domain setup (app subdomain and landing) |
| `ALCHEMY_API_KEY` | server-only | Upstream of `/api/rpc/` |
| `BASE_RPC_URL`, `ROBINHOOD_RPC_URL`, `SOLANA_RPC_URL` | server-only | Per-chain upstreams of `/api/rpc/` |
| `ONECLICK_JWT` | server-only, optional | NEAR Intents 1Click API key for `/api/oneclick/*` |
| `FAUCET_ACCOUNT_ID`, `FAUCET_PRIVATE_KEY` | server-only secret | Testnet faucet account and its key; needed for the demo wallet and "Get test funds" |
| `FAUCET_DAILY_NEAR`, `FAUCET_IP_PER_HOUR` | server-only | Faucet limits |
`NEXT_PUBLIC_*` values are read at build time and are public. Never commit the faucet key.
## Indexer and read API [#indexer-and-read-api]
```bash
cd backend/keeper
npm install
node indexer.js # http://localhost:8788, no key, never signs
curl -s http://localhost:8788/health
```
The first run backfills every meme's events from the FastNear transactions API; history starts at the first run. Point the app at it with `NEXT_PUBLIC_INDEXER_URL=http://localhost:8788`. Data goes to `backend/keeper/data/` (`INDEXER_DATA`). Routes: [API](/docs/reference/api).
## Keeper [#keeper]
```bash
cd backend/keeper
npm install
node keeper.js --once --dry-run # real reads, logs every transaction it would send; no key needed
node repeg.js --dry-run # synthetic pools vs Chainlink, plan only
```
The production keeper already serves `solidon.testnet`. A live keeper needs the keys of `keeper.solidon.testnet` and must be the only one running. Use `--dry-run`, or deploy your own factory with your own keeper account.
Variables a live keeper needs (names only; full list with defaults on [Keeper](/docs/reference/keeper)):
| Variable | Why |
| --- | --- |
| `KEEPER_NEAR_PRIVATE_KEY` (or `~/.near-credentials/testnet/.json`) | Sign NEAR transactions as the keeper |
| `KEEPER_EVM_PRIVATE_KEY` (or `EVM_DEPLOYER_PRIVATE_KEY` in `contracts/.env`) | Bridge deliveries, treasury gas, EVM re-peg |
| `ALCHEMY_API_KEY` (or `HOOD_MAINNET_RPC`) | Chainlink reads for the oracle step; default RPC for every chain |
| `KEEPER_SOLANA_KEYPAIR` | Devnet wallet; created on first run if missing |
| `SOLANA_PAYER_KEYPAIR` | Solana re-peg only |
| `FACTORY`, `KEEPER_ACCOUNT` | Point at your own factory |
Put them in `backend/.env` (template `backend/.env.example`; gitignored).
## Contracts [#contracts]
```bash
cd contracts
./build.sh # builds mock-ft, mock-pyth, meme, factory to target/near/ (the factory embeds meme.wasm)
cargo test # unit tests + NEAR sandbox integration tests
```
Run `./build.sh` before `cargo test`: the factory embeds `target/near/meme/meme.wasm` and the sandbox tests deploy the built wasm files.
`./deploy-testnet.sh` deploys a factory with the keychain of the signing account; `DRY_RUN=1 ./deploy-testnet.sh` prints the init args built from `contracts/stocks/` and sends nothing.
## Keeper tests [#keeper-tests]
```bash
cd backend/keeper
npm test # node --test "test/*.test.js": logic, treasury and bridge flows, Solana re-sign, indexer, history API, holders, re-peg
```
No network, no key.
## Simulation [#simulation]
```bash
python3 simulation/sim.py
```
Runs the 60-day simulation (five scenarios, 12 seeds each) with the contract's rules, writes `simulation/RESULTS.md` and `simulation/charts/*.svg`, and exits non-zero if any SPEC check fails. Takes about 2 seconds. See [Simulation](/docs/simulation).
## Read the chain without running anything [#read-the-chain-without-running-anything]
```bash
NET="network-config testnet"
near contract call-function as-read-only solidon.testnet get_memes json-args '{}' $NET now
near contract call-function as-read-only honey.solidon.testnet get_treasury_chains json-args '{}' $NET now
curl -s https://api.solidon.xyz/v1/memes
```
# Stocks (/docs/reference/stocks)
The factory lists eight stocks, fixed at init. A creator picks one as the meme's thesis stock in `create_meme(thesis_stock)`; it cannot be changed. Read the list with `get_stocks` on `solidon.testnet`.
Every stock token on this page is synthetic: a Solidon-deployed token with a Solidon-seeded pool, priced from the real Chainlink feed. They are not tokenized shares and have no value.
## The list [#the-list]
| Ticker | Company | Oracle feed id (`price_id`) | Chainlink proxy (Robinhood Chain mainnet) |
| --- | --- | --- | --- |
| NVDA | NVIDIA | `b1073854ed24cbc755dc527418f52b7d271f6cc967bbf8d8129112b18860a593` | `0x379EC4f7C378F34a1B47E4F3cbeBCbAC3E8E9F15` |
| AAPL | Apple | `49f6b65cb1de6b10eaf75e7c03ca029c306d0357e91b5311b175084a5ad55688` | `0x6B22A786bAa607d76728168703a39Ea9C99f2cD0` |
| MSFT | Microsoft | `d0ca23c1cc005e004ccf1db5bf76aeb6a49218f43dac3d4b275e92de12ded4d1` | `0x45C3C877C15E6BA2EBB19eA114Ea508d14C1Af2E` |
| GOOGL | Alphabet | `5a48c03e9b9cb337801073ed9d166817473697efff0d138874e0f6a33d6d5aa6` | `0xF6f373a037c30F0e5010d854385cA89185AE638b` |
| AMZN | Amazon | `b5d0e0fa58a1f8b81498ae670ce93c872d14434b72c364885d4fa1b257cbb07a` | `0xD5a1508ceD74c084eBf3cBe853e2C968fB2a651C` |
| META | Meta Platforms | `78a3e3b8e676a8f73c439f5d749737034b139bbbe899ba5775216fba596607fe` | `0x7C38C00C30BEe9378381E7B6135d7283356D71b1` |
| TSLA | Tesla | `16dad506d7db8da01c87581c87ca897a012a153557d4d578c3b9c9e1bc0632f1` | `0x4A1166a659A55625345e9515b32adECea5547C38` |
| SPCX | SpaceX | `8a593d6edde7a3095213c88116d8840d01e93c2ddeb800bc891772eb8b93bb94` | `0xB265810950ba6c5C0Ff821c9963014a56fD8Bffb` |
Sources: `contracts/stocks/feeds.json` (feed ids) and `CHAINLINK_PROXIES` in `backend/keeper/src/config.js`.
## Pricing [#pricing]
### Chainlink is the reference [#chainlink-is-the-reference]
The keeper reads `latestRoundData()` from the official Chainlink proxies on Robinhood Chain mainnet. These are 24/5 equity feeds with 8 decimals.
### The keeper writes the mock oracle [#the-keeper-writes-the-mock-oracle]
Every `ORACLE_INTERVAL_SECS` (300 s) the keeper calls `set_price(price_id, price, expo)` on `pyth.solidon.testnet` for all eight stocks in one NEAR transaction. The feed id is the Pyth id of `Equity.US./USD`, used as the oracle key. A Chainlink answer older than 25 hours (weekend or broken feed) is not pushed.
### The contract reads one price per stock [#the-contract-reads-one-price-per-stock]
The meme calls `get_price_no_older_than(price_id, 900)`. A price older than 15 minutes is stale: `rebalance`, `redeem`, `fund_chain` and the stock buys then do nothing. The same price values the stock held on every chain.
### Buys are bounded by the oracle [#buys-are-bounded-by-the-oracle]
`buy_stock` and `buy_stock_solana` sign a swap with `minOut` = oracle value of the USDC in, minus 1%. If the pool fills worse than that, the swap reverts on the destination chain.
The mock oracle is open on testnet (anyone can call `set_price`), so testnet mNAV numbers are a demo, not a market signal. See [mNAV](/docs/concepts/mnav).
## Tokens per chain [#tokens-per-chain]
| Chain | Token standard | Symbol | Decimals | Venue | Pool fee | Cash proof |
| --- | --- | --- | --- | --- | --- | --- |
| Base Sepolia | ERC-20 (`MockERC20`) | `NVDAc`, `AAPLc`, ... | 8 | Uniswap v3 (SwapRouter02 `exactInputSingle`) | 0.3% | MPC-verified (`credit_cash`) |
| Robinhood Chain testnet | ERC-20 (`MockERC20`) | `NVDA`, `AAPL`, ... | 18 | Uniswap v3 (SwapRouter02 `exactInputSingle`) | 0.3% | Relayer (`report_cash`) |
| Solana devnet | Token-2022 mint with metadata | `NVDAx`, `AAPLx`, ... | 8 | Raydium CLMM (`swap_v2`) | 0.05% | Relayer (`report_cash`) |
Every listed stock has one venue on each of the three chains. Each token address, pool and explorer link is on [Deployments](/docs/reference/deployments).
USDC per chain: Circle USDC (6 decimals) on Base Sepolia, the testnet USDC (18 decimals) on Robinhood, Circle devnet USDC (6 decimals) on Solana. The contract normalises all of them to 6 decimals in NAV.
### How the pools were seeded [#how-the-pools-were-seeded]
| Chain | Script | Liquidity |
| --- | --- | --- |
| Base Sepolia, Robinhood testnet | `contracts/evm/deploy-testnet-stocks.py` | One-sided stock liquidity worth $50,000, from the reference price at creation to about 2x |
| Solana devnet | `backend/solana-spike/1_setup.js` | Same, on Raydium CLMM; vault, observation and bitmap accounts checked by `contracts/stocks/solana-accounts.py` |
Reference prices at creation (10 Nov 2026), USD:
| NVDA | AAPL | MSFT | GOOGL | AMZN | META | TSLA | SPCX |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 177.56 | 350.44 | 544.15 | 290.38 | 268.14 | 642.58 | 376.77 | 141.18 |
On Solana the NVDA reference was 176.59.
### Re-peg [#re-peg]
One-sided liquidity starts at the reference price. When the stock falls, a buy still fills at the range edge, above the oracle, and the treasury's swap fails its `minOut`. The keeper's re-peg pass (every 30 minutes in production) moves any pool whose next buy fills more than 0.5% above Chainlink to 0.995 x Chainlink and adds $50,000 of new stock liquidity. A pool below the oracle is left alone. See [Keeper](/docs/reference/keeper).
## Registries [#registries]
| File | Content |
| --- | --- |
| `contracts/stocks/feeds.json` | Oracle feed id per ticker |
| `contracts/stocks/base-sepolia.json` | USDC, Uniswap v3 factory, SwapRouter02, position manager, and per stock: token, symbol, decimals, pool, fee, reference price, tick range |
| `contracts/stocks/hood-testnet.json` | Same for Robinhood Chain testnet |
| `contracts/stocks/solana-devnet.json` | USDC mint and token programs, Raydium CLMM program and AMM config, and per stock: mint, pool, `usdc_vault`, `stock_vault`, `observation`, `bitmap_extension` |
`contracts/deploy-testnet.sh` builds the factory's init args from these files; `DRY_RUN=1 ./deploy-testnet.sh` prints them without sending.
# Security and trust (/docs/security)
Solidon is a testnet prototype. This page lists what a user has to trust, what the contract enforces, and what is not solved yet. The source of truth is the threat model in `docs/SPEC.md`.
## Trust assumptions [#trust-assumptions]
### No admin key [#no-admin-key]
No method lets anyone move treasury funds or change a parameter. Launched memes have no access key at all (for example, `honey.solidon.testnet` has an empty access key list). The treasury addresses can only be signed for by the meme contract through the NEAR MPC network, and the contract signs only an approve to its chain's locked router and a swap into its locked stock. Every parameter is readable with `get_params`.
### Holdings are booked, not read [#holdings-are-booked-not-read]
The stock ledger records each swap's `minAmountOut` when the swap is signed. That is conservative for a swap that lands. A signed EVM swap that reverts is not unbooked yet: its USDC stays in the treasury, but the stock figure is overstated until holdings are proven. The app shows the explorer balance next to the ledger and flags such a chain "Less on-chain". See [Treasuries](/docs/concepts/treasuries).
### Cash on Robinhood Chain and Solana is relayer-reported [#cash-on-robinhood-chain-and-solana-is-relayer-reported]
Cash arriving on Base is proven by MPC verification of the USDC `Transfer` log. Robinhood Chain is not in the MPC's foreign-chain list. Solana is, but only as account state, and a proven balance is not a proven arrival. Arrivals on both are booked on the word of the factory's relayer account, capped by the cash in transit and once per transaction, so a report cannot create value.
### The bridge delivers [#the-bridge-delivers]
Cash in transit counts in NAV from the moment it leaves NEAR. On testnet the bridge is a trusted keeper account (`keeper.solidon.testnet`, which is also the relayer). On mainnet that leg is NEAR Intents / 1Click. Exposure is bounded: each transfer is capped by `max_swap_usdc`, and only cash above the 20% buffer leaves NEAR, so redemptions are untouched.
### The oracle [#the-oracle]
On testnet the oracle is a mock with a Pyth read interface, fed live Chainlink prices from Robinhood Chain mainnet by the keeper. Testnet mNAV numbers are therefore a demo, not a market signal. The mainnet price source is still open: Pyth equity feeds need a paid grant, and Chainlink logs verified through the MPC are the candidate.
## Manipulation brakes [#manipulation-brakes]
| Brake | Value | What it stops |
| --- | --- | --- |
| Pool price for mNAV | 30-minute TWAP | One large trade triggering issuance or a buyback |
| Rebalance cooldown | 1 hour | Repeated actions in a short window |
| Issuance cap | 1% of circulating supply per 24 hours | Sudden dilution |
| Oracle staleness stop | 15 minutes | Any mNAV action, `fund_chain` or `buy_stock` on an old price |
| Max slippage per swap | 1% against the oracle (`minAmountOut`) | Front-running and sandwiching the treasury swap |
| Per-chain swap cap | 5 USDC per transaction on testnet | Pool fee and price impact exceeding the 1% slippage |
The floor defends NAV, not the price. If the stock falls, NAV falls with it. Buyback starts below 1.0 times NAV and redemption below 0.9 times NAV. The UI and README state this.
## Threat model [#threat-model]
| Threat | Mitigation |
| --- | --- |
| Creator or admin drains the treasury | No admin key. The treasury addresses can only be signed for by the meme contract via MPC, and the contract only signs an approve and a swap to each chain's locked router |
| Stock ledger drifts from the on-chain balance | The ledger records `minAmountOut`, not an estimate. The dashboard shows the real explorer balance per chain next to the ledger. Upgrade: OutLayer TEE reads the balance and attests to it |
| A fake or replayed cash arrival inflates a chain's USDC ledger | Every credit is capped by that chain's cash in transit, so it only moves value already counted in NAV. Each (chain, tx, log) is credited once. On Base the log must be a `Transfer` from the chain's USDC to the treasury, and the MPC must confirm the contract's own payload hash |
| The relayer misreports cash on Robinhood Chain or Solana | Only the factory's relayer can report, only for relayer chains, once per transaction, capped by the cash in transit. Worst case it books in-transit cash as arrived early: NAV is unchanged, and a swap signed against it reverts on-chain |
| A signed Solana buy expires without landing | `resign_solana` signs the same buy with a new blockhash, without touching the ledger, only when nothing is in flight and 200 NEAR blocks have passed, so the old and new transaction can never both land. Residual: only the last Solana buy can be signed again |
| A signed EVM swap reverts on-chain | Found live on 11 Nov 2026 (HONEY, Base, [`0x9609b7…`](https://sepolia.basescan.org/tx/0x9609b7e27e2f741c4286a6c6a1a6dd04867a4d7f6c1b60c092aea942ef7e27fb)). Total value is unchanged but the stock is overstated; the chain's USDC ledger reads 0, so the USDC is not spent again. Flagged "Less on-chain". Fix planned (see Known gaps) |
| Wrong live inputs for a Solana buy (blockhash, tick arrays) | Every other account comes from locked config and the contract's own derivations, so a wrong input only makes the swap fail. Recovery is `resign_solana` |
| Wrong Solana token accounts | Set by the relayer from canonical bumps (`set_solana_accounts`), which may correct them. A wrong one only makes buys fail |
| The bridge keeps or loses cash in transit | Bounded per transfer by `max_swap_usdc`; only cash above the 20% buffer leaves NEAR. Mainnet uses Intents / 1Click; the testnet keeper is a trusted stand-in |
| Pool price manipulation to trigger issuance or buyback | 30-minute TWAP, 1-hour cooldown, issuance capped at 1% of supply per day |
| Stale oracle while the stock market is closed | All mNAV actions stop if the price is older than 15 minutes. Pool trading keeps working |
| Bank run on redemptions | Redemptions only draw on the cash buffer. A redemption that cash cannot cover is refunded |
| Stock token issuer blocks, pauses or upgrades the token | Accepted trust assumption. Stock sales stop on that chain, the stock is still valued at the oracle price, and redemptions use the USDC buffer on NEAR |
| Fake token with a stock symbol | The factory's stock list is immutable: every venue (chain, token, decimals) comes from the registries in `contracts/stocks/` and is fixed at factory init |
| Relayer withholds transactions | Anyone can broadcast a signed transaction |
| Front-running the swap | `minAmountOut` from the oracle price with at most 1% slippage; each swap capped per chain |
| A callback panics on an unexpected MPC or oracle response and locks the contract | Callbacks that hold state parse raw results with a size bound and never panic: a bad response refunds or frees the slot. `clear_stuck_signature` frees a signing slot after 300 blocks |
| The MPC network returns a wrong or malicious signature | `ecrecover` (EVM) or `ed25519_verify` (Solana) on every signature; the transaction is stored only if it matches the treasury key |
| Nobody pays gas for the treasury | Liveness only. Anyone can top up ETH or SOL; on mainnet Intents can deliver it with the cash |
| Premium bubble then crash | Buyback below 1.0, redemption below 0.9. Issuance is accretive, so a bubble leaves holders with more stock per token |
| Users read the floor as a price guarantee | UI and README state that the floor is a NAV floor |
## Sybil resistance [#sybil-resistance]
No per-account rewards exist. Every mechanic is proportional to token amount, so splitting holdings across accounts gains nothing.
## Known gaps and next steps [#known-gaps-and-next-steps]
Today a reverted EVM swap stays booked. Next step: book Base swaps only after the MPC verifies the swap's `Transfer` log, as cash already is, and give relayer chains a capped "swap failed" report that returns the booking to USDC.
The MPC can verify Base logs and Solana account state, but not Robinhood Chain. Proven holdings would replace the booked ledger on Base and Solana. A TEE keeper on OutLayer that reads and attests balances is later work.
Solana arrivals are relayer-reported because a proven balance can double count while a signed buy has not landed. Crediting from MPC-proven state is a follow-up.
The spec schedules a partial stock sale when the NEAR buffer falls below 10% of NAV, and a redemption queue when the buffer runs out. The contract carries the 10% refill parameter but has no stock-sale method yet; a redemption that cash cannot cover is refunded.
The testnet bridge is a trusted keeper. The mainnet design uses Intents / 1Click; a contract-driven intent signed by an MPC-derived key on `intents.near` would remove the need to trust whoever supplies the deposit address. Not built.
Pyth equity feeds need a paid grant. Chainlink logs verified through the mainnet MPC are the candidate and need one mainnet run.
The testnet factory account `solidon.testnet` still has a full-access key during development. Launched memes have no access key and copy their stock and chains config at launch, so the key cannot change an existing meme. Before a mainnet deployment the factory's keys are to be deleted so the "no admin" claim holds for the factory too.
## Legal [#legal]
Tokenized stocks are regulated securities in many places. Robinhood Stock Tokens may not be offered, sold or delivered to US persons. Solidon is a testnet prototype with synthetic tokens and is not for US persons.
# 60-day simulation (/docs/simulation)
The simulation tests the core claim: a Solidon meme moves in the same direction as its stock, more aggressively, with a floor at NAV. It runs the contract's locked parameters against simulated stock prices and meme demand. The parameters were locked only after every check passed (9 Nov 2026).
## Method [#method]
The script `simulation/sim.py` mirrors the meme contract: the x times y = k pool with a 1% USDC-side fee (0.8% to the treasury), treasury cash and stock units, and the mNAV engine with the locked parameters. It uses the Python standard library only and deterministic seeds.
| Piece | How it is modelled |
| --- | --- |
| Day 0 | End of a 90-day launch: a fresh pool seeded with 10,000 USDC and the full supply, then hype buying. The treasury is built only by pool fees and issuance |
| Traders | Move the meme by 1.5 times the stock's move, plus a hype drift per scenario and 5% per day noise |
| Sellers | Redeem at NAV while the redemption window is open and the pool pays less; sell the rest |
| Arbitrageurs | Buy below NAV in the pool and redeem, limited by the cash on hand |
| `rebalance()` | Every hour: 30-minute TWAP, then issuance (mNAV above 2.0, capped at 1% of circulating supply per 24 hours, accretive only), buyback (mNAV below 1.0, cash above the 20% buffer only), redemption window (mNAV below 0.9) |
| Treasury | Cash above 20% of NAV buys stock; cash below 10% sells stock to refill to 20%. Every stock trade loses 1% to slippage |
| Oracle | Stale after 15 minutes without an update; no mNAV action while stale |
| Steps | 5-minute steps over 60 days |
| Seeds | 12 per scenario. Tables show the median across seeds; "Min mNAV" and "Longest below 0.9" are the worst seed |
## Scenarios [#scenarios]
| Scenario | Stock path | Meme demand | What must hold |
| --- | --- | --- | --- |
| Steady bull | +30% over 60 days | Rising | Meme return above stock return; stock units per token rise after each issuance |
| Bull then hype fades | +30% over 30 days, then flat | Rising, then falling | Meme falls back toward NAV; mNAV back at 0.9 or above within 2 hours of dropping below |
| Stock crash | -30% | Panic selling | Meme falls with the stock; redemption brings mNAV back to 0.9 or above within 2 hours |
| Sideways chop | Plus or minus 5% swings | Random | No runaway issuance; supply changes stay within the epoch cap |
| Weekend gap | Flat, oracle stale for 48 hours | Random | No mNAV action during staleness; the pool keeps trading |
## Results [#results]
Median of 12 seeds per scenario, from `simulation/RESULTS.md`.
| Scenario | Meme | Stock | NAV/token | Corr | Beta | Meme max DD | Stock max DD | Min mNAV | Longest below 0.9 | Supply | Stock units/token |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Steady bull | +31.1% | +28.7% | +47.9% | 0.55 | 1.36 | -22.1% | -8.0% | 1.07 | 0 h | +7.1% | +13.1% |
| Bull then hype fades | -24.7% | +28.7% | +56.6% | 0.50 | 1.34 | -46.1% | -9.6% | 0.87 | 1 h | -1.6% | +29.1% |
| Stock crash | -46.9% | -31.6% | +3.9% | 0.59 | 1.35 | -54.4% | -36.0% | 0.87 | 1 h | -22.6% | +68.8% |
| Sideways chop | -19.6% | -4.7% | +22.1% | 0.66 | 1.43 | -39.9% | -18.5% | 0.88 | 1 h | +1.2% | +34.7% |
| Weekend gap | -19.0% | +0.0% | +18.5% | n/a | n/a | -30.5% | +0.0% | 0.88 | 1 h | +1.7% | +22.9% |
All 13 checks from `docs/SPEC.md` pass:
| Scenario | Check | Result |
| --- | --- | --- |
| All five | Every issuance is accretive (all seeds) | pass |
| Steady bull | Median meme return above median stock return | pass |
| Steady bull | Median beta to the stock above 1 | pass |
| Steady bull | Stock units per token rise (all seeds) | pass |
| Bull then hype fades | mNAV back at 0.9 or above within 2 hours of dropping below (all seeds) | pass |
| Stock crash | Meme falls with the stock (median return below 0, median correlation above 0) | pass |
| Stock crash | mNAV back at 0.9 or above within 2 hours of dropping below (all seeds) | pass |
| Sideways chop | Supply growth per day within the 1% cap (all seeds) | pass |
| Weekend gap | No mNAV action while the oracle is stale (all seeds) | pass |
## Findings [#findings]
- **Same direction, more aggressive.** Beta to the stock is 1.34 to 1.43 in every scenario with stock movement, and correlation is positive.
- **Stock per token grows in every scenario.** Accretive issuance and buybacks below NAV raise stock units per token, from +13.1% (steady bull) to +68.8% (stock crash).
- **The floor holds.** Worst case across all runs: minimum mNAV 0.87, at most 1 hour below 0.9. The floor is stated as "back at 0.9 or above within 2 hours" because the redemption window only opens once mNAV is already below 0.9.
- **The meme can still lose.** In the fade, crash, chop and weekend scenarios the meme price ends lower than it started, while NAV per token ends higher. The premium paid at launch is what falls. The floor defends NAV, not the hype premium paid on entry.
- **Parameter sweep.** Thresholds 2.0, 2.5, 3.0 times caps 0.5%, 1% kept the floor identical; a 0.5% cap hurt the bull case. 2.0 and 1% were kept.
### The finding that changed the spec: a buyback must not sell stock [#the-finding-that-changed-the-spec-a-buyback-must-not-sell-stock]
Tokens bought back come from the pool's inventory, not from holders, so circulating supply does not fall and each buyback lowers NAV per token. Under the first version of the rule, a buyback could spend cash down to the 10% refill level; the refill then sold stock to top cash back up, and the next buyback spent it again. The adopted rule: a buyback only spends cash above the 20% buffer and never triggers a stock sale. Redemption, which is value-neutral, carries the floor.
| Stock crash, median of 12 seeds | Stock units per token, day 60 vs day 0 | NAV/token | Worst min mNAV |
| --- | --- | --- | --- |
| First rule (cash down to 10%) | +56.4% | +0.9% | 0.89 |
| Adopted (cash above 20% only) | +68.8% | +3.9% | 0.87 |
The contract implements the adopted rule (`buy_back` in `contracts/meme/src/mnav.rs`).
## Rerun it [#rerun-it]
```bash
python3 simulation/sim.py
```
Python 3.10 or newer, standard library only, about 2 seconds. It rewrites `simulation/RESULTS.md` and the charts in `simulation/charts/`, and exits non-zero if any check fails, so it doubles as a test.