Create a market with a pull request
You propose a market by adding one JSON config to the Precept repository. CI turns the config into a deployment plan. When Templar merges the pull request, CI deploys that exact plan and hands the market to you.
Read Market policy, Before you create a market and Reserve parameters first.
Warning: most values freeze when the market goes live. To fix a mistake after that, you deploy a new market and move users to it.
What you need
- A market admin address on the target network: a
G...account or aC...smart account. A multisig is the safer choice. - An oracle admin address, if different from the market admin. Read Choose the oracle admin first.
- The contract address and on-chain decimals of each asset.
- A price source for each asset.
- A keeper to refresh your proxy oracle.
- Python 3, to run the config check on your computer. This step is optional.
Both admin addresses must already exist on the network, or the config check fails.
Steps
- Fork the repository and create a branch.
- Add one file:
registry/configs/<name>.json. Use lowercase letters, numbers and hyphens, and start with a letter or number. Setnamein the file to the same value. - Copy the template and change the values. See What to change.
Check the config. Warning: this reads your admin addresses from the network, so use the correct
--network.python3 registry/registry.py check-config registry/configs/<name>.json --network mainnetOpen the pull request against
main. Put only one config in it, because a failed check on one config also blocks the other. CI runs the same check and comments a plan with itsplan sha256. The plan sends nothing.- Read the plan comment. If it says failed or No plan, fix the config and push. Every push creates a new plan.
- Get review from Templar. Templar reviews the config and plan against the market policy. The merge approves exactly that plan.
- Wait for the deployment. On merge, CI deploys in a protected environment and comments the contract addresses. Templar's deployer key signs and pays for these transactions.
- Wait for price feeds, if CI stops. A refresh step stops the run until each asset has an accepted price. If you deploy your own Lazer source, get its address from the plan or deploy comment, and push its first update. Then ask Templar to resume the run. Steps already sent are not sent again.
- Start your keeper. Follow Run your keeper, and confirm that every asset returns
Accepted. - Accept the admin role. From the market admin wallet, call
accept_admin()on the market within 10 days. If the comment listsaccept_ownership()calls, sign them too. - Activate the market. Warning: reserve settings freeze at this step. Check every value first, and confirm that each asset has a working price. Then call
set_statuswith status 0. CI never activates a market. - Check the registry record. CI opens a separate pull request that adds your contracts to
registry/mainnet.jsonl. You do not write this record yourself. - Ask Templar about the app. The app lists official markets. See what makes a market official.
To rehearse first, use "network": "testnet". The testnet rehearsal config has working testnet asset addresses.
If the plan is refused after the merge, because the chain changed under it, open a new pull request that touches the config. A merged pull request cannot be reopened.
Warning: do not edit a deployed config by accident. A merged change to any file in registry/configs/ starts a new deployment.
Template: XLM/USDC with a proxy oracle
Save this as registry/configs/my-xlm-usdc.json. It lends USDC against XLM on mainnet. CI deploys a new proxy oracle for this market.
{
"name": "my-xlm-usdc",
"network": "mainnet",
"proposed_by": "<Your organization> (<date>)",
"pool": {
"admin": "REPLACE_WITH_MARKET_ADMIN_ADDRESS",
"name": "<Your organization> - XLM/USDC",
"oracle": null,
"backstop_take_rate": 0,
"max_positions": 4,
"min_collateral": "0"
},
"reserves": [
{
"asset": "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75",
"label": "usdc",
"config": {
"index": 0,
"decimals": 7,
"c_factor": 0,
"l_factor": 10000000,
"util": 9000000,
"max_util": 9900000,
"r_base": 1000,
"r_one": 799000,
"r_two": 1200000,
"r_three": 1200000,
"reactivity": 0,
"supply_cap": "100000000000000",
"min_position": "10000000",
"enabled": true
}
},
{
"asset": "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA",
"label": "xlm",
"config": {
"index": 1,
"decimals": 7,
"c_factor": 8000000,
"l_factor": 0,
"util": 8000000,
"max_util": 9000000,
"r_base": 1000,
"r_one": 0,
"r_two": 0,
"r_three": 0,
"reactivity": 0,
"supply_cap": "1000000000000000",
"min_position": "500000000",
"enabled": true
}
}
],
"target_status": 0,
"notes": [
"NOT LIVE: do not deposit into, borrow from or loop against this market until its contracts appear in mainnet.jsonl and the launch is announced.",
"Oracle: this market's own runtime, reading Templar's shared Lazer source and the RedStone wrapper. One fresh source serves a price.",
"supply_cap is an example: 10,000,000 USDC and 100,000,000 XLM. It freezes at activation.",
"usdc interest: 0.01% at U=0, 8% at 90%, 20% at 95%, 32% at 100%. xlm is collateral only."
],
"oracle": {
"lazer_verifier": "CACZ3GBAKUPIAFRILUFO27J5RUH5GJ2VSJ46LP6GJYSKGDRTQ5MS3HCH",
"admin": "REPLACE_WITH_ORACLE_ADMIN_ADDRESS",
"assets": {
"usdc": {
"min_sources": 1,
"max_age_secs": 3600,
"sources": [
{
"kind": "sep40",
"contract": "CDXOHZDLLF6AMQ5NLZQOWRSBQIJBFPC4KWQ6SOLKQSQOP2EYDQC4E5IB",
"asset": {
"Other": "7"
}
},
{
"kind": "sep40",
"contract": "CBMGLKUQZVSAIL5CPDDAWSUY7MAKXISHMOZEVLMBUWBMFGHRJSR4WYRF",
"asset": {
"Stellar": "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75"
}
}
]
},
"xlm": {
"min_sources": 1,
"max_age_secs": 1800,
"sources": [
{
"kind": "sep40",
"contract": "CDXOHZDLLF6AMQ5NLZQOWRSBQIJBFPC4KWQ6SOLKQSQOP2EYDQC4E5IB",
"asset": {
"Other": "23"
}
},
{
"kind": "sep40",
"contract": "CBMGLKUQZVSAIL5CPDDAWSUY7MAKXISHMOZEVLMBUWBMFGHRJSR4WYRF",
"asset": {
"Stellar": "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA"
}
}
]
}
}
}
}
The check rejects the two REPLACE_WITH values until you add real addresses.
What to change
| Field | What to set |
|---|---|
name, proposed_by, pool.name |
Your own names |
pool.admin |
The final market admin |
pool.oracle |
Leave null. CI deploys the adapter |
pool.backstop_take_rate |
Keep 0. A market must use 0 |
pool.max_positions |
Cover every pairing of reserves. 4 suits two reserves |
pool.min_collateral |
Effective collateral in 7-decimal USD units. 10000000000 is 1,000 USD, and 0 relies on min_position |
reserves[].asset, label, decimals |
The asset, a short name, and its on-chain decimals |
reserves[].config |
See Reserve parameters. index must match the list order |
supply_cap |
Size it to the collateral's market depth. The template values are examples |
target_status |
Keep 0. It records intent, and you still activate the market yourself |
notes |
Your reasoning. Notes do not change the deployment |
oracle.admin |
Receives the governance admin role of the new oracle. See Choose the oracle admin |
oracle.assets |
One entry for each reserve label |
Oracle setup
The template uses the same two sources as Templar's XLM/USDC market:
| Source | Contract | USDC key | XLM key |
|---|---|---|---|
| Pyth Lazer, through Templar's shared source | CDXOHZDLLF6AMQ5NLZQOWRSBQIJBFPC4KWQ6SOLKQSQOP2EYDQC4E5IB |
{"Other": "7"} |
{"Other": "23"} |
| RedStone wrapper | CBMGLKUQZVSAIL5CPDDAWSUY7MAKXISHMOZEVLMBUWBMFGHRJSR4WYRF |
{"Stellar": "<USDC token>"} |
{"Stellar": "<XLM token>"} |
min_sourcesis how many fresh sources a price needs. At 1, one fresh source serves a price and an outage of the other does not stop the market. A higher value requires more agreement, but stops the market sooner.max_age_secsis the oldest source price the proxy accepts, from 60 to 86,400 seconds. The template uses 1 hour for USDC and 30 minutes for XLM. Set it above the refresh interval, or the price expires between refreshes.oracle.runtimereuses an existing proxy oracle instead of deploying one. Leave it out unless its governance admin has agreed. The proxy serves every market that shares it."kind": "lazer"deploys a new Lazer source with afeed_idthat you must keep updated. It needslazer_verifier. The template does not deploy one.
Templar's keeper infrastructure pushes Templar's shared Lazer source, and RedStone pushes its wrapper. Neither one refreshes your proxy. You must run your own keeper.
Run your keeper
A keeper is a scheduled job that copies source prices into your proxy and keeps its ledger entries alive. All calls here are permissionless. Liquidators also update prices when they need to liquidate, but do not rely on them to keep prices fresh.
Warning: if refreshes stop, prices expire. Borrows, collateral withdrawals, new liquidations and write-offs that need a price then stop.
Warning: do not use an admin key for the keeper. The keeper needs no role, so give it a dedicated account that holds only enough XLM for fees.
- Install Stellar CLI 28.1.0, and fund the keeper account.
Set the addresses. Get yours from the deploy comment or
registry/mainnet.jsonl. The batcher address is Templar's stateless batcher on mainnet. The commands use this template's assets, so replace the asset list in steps 3 and 4 with every reserve in your config.export RT=<your proxy_runtime> GOV=<your proxy_governance> AD=<your pool_oracle_adapter> export MARKET=<your pool> BACKSTOP=<your backstop> export BATCH=CAOAWPLNVWVHR5TMNLNT2EWXLVXCNUWVFIMV4C7OJWDGCKFI7RTUB5EU export USDC=CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75 export XLM=CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA inv() { stellar contract invoke --network mainnet --source keeper "$@"; }Refresh every asset. Each result must be
Accepted.inv --id "$BATCH" -- refresh_many --oracle "$RT" \ --assets '[{"Stellar":"'"$USDC"'"},{"Stellar":"'"$XLM"'"}]'Extend the ledger entries. The asset results and the governance and adapter results must be
true. Afalseresult is expected for a contract with noextend_ttlfunction, such as the market and the backstop. The batcher still renews their instance and code.inv --id "$BATCH" -- extend_ttl_many --oracle "$RT" \ --assets '[{"Stellar":"'"$USDC"'"},{"Stellar":"'"$XLM"'"}]' inv --id "$BATCH" -- extend_ttl_contracts \ --contracts '["'"$GOV"'","'"$AD"'","'"$MARKET"'","'"$BACKSTOP"'"]'Schedule both jobs from at least two independent hosts. As a starting point, refresh at no more than a third of your shortest
max_age_secs, which is every 10 minutes for this template. Extend entries daily.- Set
max_age_secsabove each source's update interval plus your keeper interval, with margin. A cached price expires when its earliest contributing source would age out, so a source near its limit leaves little time. - Alert on any status other than
Accepted, on an unexpectedfalseTTL result, on a failed job, and on a low keeper balance. On a failure, read theRefreshFailurecode in the oracle runbook. - Restore any archived entry before you batch it, because an archived entry fails the whole batch.
To rehearse on testnet, use --network testnet and your testnet addresses. The registry records no batcher contract on testnet, so call refresh --asset and extend_ttl --asset on $RT for each asset, and extend_ttl on $GOV and $AD.
If you use "kind": "lazer", the keeper must also feed your Lazer source before each refresh. You need Pyth Lazer access. Fetch a leEcdsa update with the price, exponent and feedUpdateTimestamp properties, then call update_price_feeds --payload <hex> on the source.
Choose the oracle admin
Warning: this is one of the most important choices in your config. The oracle admin can change the proxy's parameters, including its sources, maximum ages, circuit breakers, code and roles. If that key is compromised, an attacker can feed your market bad prices. Bad prices can trigger false liquidations or let someone borrow too much, so user funds are at risk. The market's fixed rules do not protect against this.
- Use a multisig or smart account, not a single hot key.
- Use independent signers on separate devices, with hardware-backed keys.
- Do not reuse the market admin's keys for the oracle admin.
- Keep a proposal delay long enough to detect and cancel a bad change.
- Write down who can sign, how to replace a signer, and who to call in an incident.
If you leave out oracle.admin, the market admin receives this role.
Registry records for other deployments
CI records a deployment made from a config. For a contract deployed another way, add a record to the registry in a pull request:
python3 registry/registry.py add --network mainnet records.jsonl
python3 registry/registry.py check
The registry README gives the record format and all config bounds.