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 a C... 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

  1. Fork the repository and create a branch.
  2. Add one file: registry/configs/<name>.json. Use lowercase letters, numbers and hyphens, and start with a letter or number. Set name in the file to the same value.
  3. Copy the template and change the values. See What to change.
  4. 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 mainnet
    
  5. Open 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 its plan sha256. The plan sends nothing.

  6. Read the plan comment. If it says failed or No plan, fix the config and push. Every push creates a new plan.
  7. Get review from Templar. Templar reviews the config and plan against the market policy. The merge approves exactly that plan.
  8. 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.
  9. 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.
  10. Start your keeper. Follow Run your keeper, and confirm that every asset returns Accepted.
  11. Accept the admin role. From the market admin wallet, call accept_admin() on the market within 10 days. If the comment lists accept_ownership() calls, sign them too.
  12. 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_status with status 0. CI never activates a market.
  13. 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.
  14. 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_sources is 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_secs is 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.runtime reuses 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 a feed_id that you must keep updated. It needs lazer_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.

  1. Install Stellar CLI 28.1.0, and fund the keeper account.
  2. 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 "$@"; }
    
  3. Refresh every asset. Each result must be Accepted.

    inv --id "$BATCH" -- refresh_many --oracle "$RT" \
      --assets '[{"Stellar":"'"$USDC"'"},{"Stellar":"'"$XLM"'"}]'
    
  4. Extend the ledger entries. The asset results and the governance and adapter results must be true. A false result is expected for a contract with no extend_ttl function, 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"'"]'
    
  5. 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.

  6. Set max_age_secs above 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.
  7. Alert on any status other than Accepted, on an unexpected false TTL result, on a failed job, and on a low keeper balance. On a failure, read the RefreshFailure code in the oracle runbook.
  8. 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.

  1. Use a multisig or smart account, not a single hot key.
  2. Use independent signers on separate devices, with hardware-backed keys.
  3. Do not reuse the market admin's keys for the oracle admin.
  4. Keep a proposal delay long enough to detect and cancel a bad change.
  5. 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.

results matching ""

    No results matching ""