Skip to content
 
 

Repository files navigation

Soroban Keeper Network

The decentralized automation & upkeep layer for the Stellar/Soroban ecosystem. Chainlink Keepers — but native to Soroban.

CI License: Apache-2.0 Built on Soroban Live on Testnet

🟢 Live on Stellar testnet: CDJOYHBS7C2PVJS47BTRDLGBNG2YOE43VX6Y3EWIZPPPKOPRNYQQ54U4 — a full register → claim → execute → withdraw run is traced on-chain in docs/DEMO.md.


Documentation

Doc What's inside
Live demo Deployed testnet contract + full on-chain transaction trace
Architecture Components, task lifecycle, storage, money invariants, trust model
Deploying & running Testnet deploy walkthrough and keeper-bot operator guide
Deployments Canonical record of on-chain addresses
Contributing How to pick up an issue and open your first PR
Changelog Notable changes

Quick start: make help lists every common command (build, test, fmt, lint, wasm, optimize, bot).


Problem & Solution

The Problem

Every DeFi protocol running on Soroban has time-sensitive operations that must be triggered by an external agent:

  • Liquidations — health factor drops below threshold → position must be liquidated
  • Oracle price pushes — off-chain price must be written on-chain every N seconds
  • Funding rate updates — perpetuals markets need periodic rate settlements
  • LP rebalancing — concentrated liquidity positions fall outside active range
  • TTL extensions — Soroban's storage expiry model means contract data expires unless refreshed

Today, each protocol runs its own centralised bot, creating:

Pain Impact
Single point of failure Missed liquidations → bad debt, insolvency
High ops burden Every team re-invents the same infrastructure
No economic incentives Bots run at a loss; sustainability risk
Opaque No on-chain record of who executed what and when

The Solution — Soroban Keeper Network

A shared, permissionless, on-chain coordination layer where:

  • dApps register automation tasks with an XLM reward bounty.
  • Anyone can run a keeper bot to claim and execute tasks, earning rewards.
  • The registry contract enforces fairness, handles escrow, and emits events.
  • No trust required — keepers are economically incentivised, not whitelisted.
┌─────────────────────────────────────────────────────────┐
│                    dApp / Protocol                      │
│  (lending protocol, DEX, perps, oracle aggregator...)   │
└────────────────┬────────────────────────────────────────┘
                 │  register_task(reward, calldata, deadline)
                 ▼
┌─────────────────────────────────────────────────────────┐
│              KeeperRegistry Contract                    │
│  ┌──────────────┐  ┌─────────────┐  ┌───────────────┐  │
│  │ Task Storage │  │  Fee Logic  │  │  Auth / Pause │  │
│  └──────────────┘  └─────────────┘  └───────────────┘  │
└────────────────┬────────────────────────────────────────┘
                 │  events: TaskRegistered, TaskClaimed, TaskExecuted
                 ▼
┌──────────────────────────────────────────────────────────────┐
│                 Off-Chain Keeper Bots (permissionless)        │
│  Bot A   Bot B   Bot C   ... (anyone can run one)            │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ 1. Listen to events                                    │  │
│  │ 2. claim_task(task_id)                                 │  │
│  │ 3. Execute underlying action (liquidate, push price…)  │  │
│  │ 4. execute_task(task_id, proof)                        │  │
│  │ 5. withdraw_rewards()                                  │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

Key Features

MVP (v1 — This Repo)

  • Task Registry — any Soroban contract or EOA registers tasks with XLM reward
  • Permissionless claiming — first keeper to claim wins lock rights
  • Lock period — prevents spam claims while giving the claimer time to execute
  • Re-claim after lock expiry — unresponsive keepers lose their lock
  • Execution proof — keepers submit a tx hash / state witness for transparency
  • Reward escrow — XLM held in contract until task is executed or expired
  • Auto-expiry — permissionless expire_task refunds owner after deadline
  • Task cancellation — owner can cancel a Pending task and receive refund
  • Protocol fee — configurable basis-point fee taken from rewards
  • Upgradeable — admin can upgrade WASM via Soroban's native pattern
  • Pause/unpause — emergency circuit breaker
  • Full event logTaskRegistered, TaskClaimed, TaskExecuted, TaskExpired, TaskCancelled

Phase 2 (Roadmap)

  • On-chain execution verifier interface — target contracts implement IKeeperVerifier and the registry calls them to verify execution succeeded
  • Batch task registration — register multiple tasks in one transaction
  • EIP-like task conditions — on-chain checkUpkeep callback before claiming
  • Keeper reputation scores — slash stake for missed executions
  • Keeper staking — stake XLM or governance token for priority and dispute resolution
  • Governance token ($KPRS) — vote on fee parameters, upgrades, whitelists
  • Treasury contract — protocol fees flow to stakers
  • Subgraph / indexer — TheGraph-style event indexing for analytics

Phase 3 (Vision)

  • Cross-contract task composition — chain multiple operations as a single task
  • Decentralized oracle integration — task conditions driven by Reflector/Band
  • SDK libraries — TypeScript + Rust SDKs so dApps integrate in < 1 hour
  • Keeper DAO — fully on-chain governance of protocol parameters
  • Stellar Community Fund grant round — sustained ecosystem funding

Architecture Diagram

┌────────────────────────────────────────────────────────────────────────────┐
│                        Soroban Keeper Network                              │
├────────────────────────────────────────────────────────────────────────────┤
│                                                                            │
│  ┌─────────────────────────────────────────────────────────────────────┐  │
│  │                    KeeperRegistry Contract                          │  │
│  │                                                                     │  │
│  │  Instance Storage (hot, short-TTL)                                  │  │
│  │  ┌──────────┬─────────┬────────┬─────────────┬─────────────┬──────┐ │  │
│  │  │  Admin   │ FeeBps  │ Paused │ TaskCounter  │ RewardToken │ Fees │ │  │
│  │  └──────────┴─────────┴────────┴─────────────┴─────────────┴──────┘ │  │
│  │                                                                     │  │
│  │  Persistent Storage (task lifetime)                                 │  │
│  │  ┌────────────────────────────────────────────────────────────┐    │  │
│  │  │  Task(id) → { owner, type, calldata, reward, deadline,     │    │  │
│  │  │               status, claimer, claim_ledger, lock_ledgers } │    │  │
│  │  └────────────────────────────────────────────────────────────┘    │  │
│  │  ┌────────────────────────────────────────────────────────────┐    │  │
│  │  │  KeeperReward(address) → i128  (claimable balance)         │    │  │
│  │  └────────────────────────────────────────────────────────────┘    │  │
│  │                                                                     │  │
│  │  External (Token)                                                   │  │
│  │  ┌────────────────────────────────────────────────────────────┐    │  │
│  │  │  SAC / XLM token contract (transfer, balance)              │    │  │
│  │  └────────────────────────────────────────────────────────────┘    │  │
│  └─────────────────────────────────────────────────────────────────────┘  │
│                                                                            │
│  ┌────────────────┐    ┌─────────────────────────┐    ┌───────────────┐   │
│  │  dApp Contract │───▶│  register_task (XLM dep) │───▶│  TaskRegistered│  │
│  └────────────────┘    └─────────────────────────┘    │     Event     │   │
│                                                        └───────┬───────┘   │
│  ┌────────────────┐    ┌─────────────────────────┐            │           │
│  │  Keeper Bot A  │───▶│  claim_task             │◀───────────┘           │
│  └────────────────┘    └─────────────────────────┘                        │
│         │              ┌─────────────────────────┐    ┌───────────────┐   │
│         └─────────────▶│  execute_task + proof   │───▶│ TaskExecuted  │   │
│                        └─────────────────────────┘    │     Event     │   │
│                                                        └───────────────┘   │
└────────────────────────────────────────────────────────────────────────────┘

Product Requirements Document (PRD)

User Stories

dApp Developers / Protocol Owners

As a... I want to... So that...
Lending protocol Register a liquidation task when a position is undercollateralised My protocol remains solvent without running my own bot
Oracle provider Register periodic price-push tasks with a time deadline Prices stay fresh without centralised infrastructure
Perp DEX Register funding rate settlement tasks every 8 hours Settlement never misses even if my team is offline
AMM Register LP rebalancing tasks with custom calldata Liquidity is always in range without manual intervention
Any Soroban contract Cancel a task if the underlying condition resolves itself I don't pay keepers for work that's no longer needed

Keeper Operators

As a... I want to... So that...
Keeper Listen to on-chain events and claim profitable tasks I earn XLM rewards for providing upkeep
Keeper See the reward amount before claiming I can calculate profitability vs gas
Keeper Re-claim a task if the original claimer vanished No task is permanently stuck
Keeper Withdraw my accumulated balance in one transaction I minimise transaction overhead

Protocol/Admin

As a... I want to... So that...
Admin Pause the registry in emergencies No new tasks can be registered during an incident
Admin Upgrade the WASM hash Bug fixes and new features can be deployed without redeployment
Admin Adjust fee basis points Protocol economics can be tuned by governance
Admin Sweep accumulated fees to treasury Revenue flows to stakeholders

Functional Requirements

FR-1: Task Registration

  • register_task MUST escrow the full reward amount from the caller.
  • Task ID MUST be monotonically increasing and globally unique.
  • deadline MUST be strictly in the future at registration time.
  • calldata MUST NOT exceed MAX_CALLDATA_LEN (1024 bytes), rejected with CalldataTooLarge otherwise. Empty calldata is accepted.
  • reward MUST be greater than zero.
  • MUST emit TaskRegistered event with (task_id, owner, reward, deadline).

FR-2: Task Claiming

  • claim_task MUST be callable by any address (permissionless).
  • MUST reject if task is not in Pending or Claimed (with expired lock) state.
  • MUST reject if deadline has passed.
  • MUST record the claimer address and claim_ledger.
  • A second keeper MUST be able to claim after lock_ledgers have elapsed.
  • MUST emit TaskClaimed event.

FR-3: Task Execution

  • execute_task MUST only be callable by the current claimer.
  • MUST reject if task deadline has passed.
  • MUST credit (reward * (10000 - fee_bps) / 10000) to the keeper's balance.
  • Protocol fee MUST remain in the contract (swept separately by admin).
  • MUST emit TaskExecuted with net reward and proof bytes.
  • Task status MUST transition to Executed (immutable after this point).

FR-4: Task Cancellation

  • cancel_task MUST only be callable by the task owner.
  • MUST only be callable when task is in Pending state.
  • MUST refund the full reward to the owner.
  • MUST emit TaskCancelled.

FR-5: Task Expiry

  • expire_task MUST be callable by anyone.
  • MUST only succeed when ledger.timestamp >= task.deadline.
  • MUST refund the full reward to the task owner.
  • MUST emit TaskExpired.

FR-6: Reward Withdrawal

  • withdraw_rewards MUST transfer the keeper's full credited balance.
  • MUST zero the balance before transfer (CEI pattern).
  • MUST emit RewardsWithdrawn.
  • MUST revert if balance is zero.

FR-7: Admin Controls

  • pause/unpause MUST gate register_task, claim_task, execute_task, and increase_reward — all four open new escrow or reward exposure.
  • pause/unpause MUST NOT gate cancel_task, expire_task, or withdraw_rewards — these only let already-escrowed value flow back to whoever already owns it, which must always stay available so an admin pause can never become a fund freeze. Read-only views are likewise never gated.
  • extend_deadline is currently not gated by pause in the deployed code (own bug, tracked separately from this requirement) — it changes no funds either way, but the intent was likely for it to follow register/claim/execute. See the pause/unpause doc comment in contracts/keeper-registry/src/lib.rs and the test_pause_policy_matrix_entry_point_by_entry_point test in contracts/keeper-registry/src/test.rs for the authoritative, verified matrix.
  • set_fee_bps MUST reject values > 10 000.
  • transfer_admin MUST require auth from BOTH current admin AND new admin.
  • upgrade MUST use deployer().update_current_contract_wasm.

Non-Functional Requirements

Security

  • All state-mutating functions require address.require_auth().
  • No re-entrancy vectors: token transfers happen after all state mutations (CEI pattern).
  • No unchecked arithmetic — Rust's checked_* methods or overflow-checks = true.
  • Admin cannot drain escrowed task rewards; only sweeps protocol fees.
  • Upgrade requires admin auth — no anonymous upgrades.

Gas Efficiency

  • Instance storage for hot/shared data (admin, counter, flags).
  • Persistent storage for per-task data with explicit TTL management.
  • No unbounded iteration — no Vec<task_id> scanned in O(n); queries are by key.
  • Events are the query primitive for off-chain indexers.

Scalability

  • Task IDs are u64 — supports 18 quintillion tasks.
  • Reward balance is aggregated per keeper — single persistent entry regardless of tasks executed.
  • Storage TTL managed per entry; expired tasks are naturally evicted by the ledger.

Liveness

  • Tasks with expired lock periods are always re-claimable.
  • expire_task is permissionless — anyone can trigger it to unblock a stuck task.
  • Contract pause does not affect reward withdrawal (keepers can always pull earned funds).

Technical Specifications

Storage Model

Key Type Storage TTL Default when unset
Admin Address Instance Instance lifetime
FeeBps u32 Instance Instance lifetime 0 (see DEFAULT_FEE_BPS)
Paused bool Instance Instance lifetime false
TaskCounter u64 Instance Instance lifetime 0
RewardToken Address Instance Instance lifetime
Task(u64) Task struct Persistent task.ttl_ledgers
KeeperReward(Address) i128 Persistent ~1 year (6.3M ledgers) 0

Task.calldata is capped at MAX_CALLDATA_LEN = 1024 bytes, enforced at register_task. save_task re-writes the whole Task struct (including calldata) on every lifecycle mutation — claim_task, execute_task, the permissionless expire_task, increase_reward, extend_deadline — and those calls are frequently made by a keeper or third party, not the task owner. An unbounded calldata would let an owner push arbitrarily large re-serialisation and storage cost onto whoever touches the task next. 1024 bytes comfortably covers a realistic encoded contract call — a target Address (~40 bytes XDR), a function Symbol (up to 32 bytes), and several scalar or address arguments — with headroom for XDR/Vec overhead. Empty calldata is accepted, since some task types (e.g. a TtlExtension against a well-known key) need no extra encoded parameters.

Events

All events use two-topic format (verb_symbol, noun_symbol) for efficient filtering.

Event Topics Data
TaskRegistered ("reg", "task") (task_id, owner, reward, deadline)
TaskClaimed ("claim", "task") (task_id, keeper, ledger_seq)
TaskExecuted ("exec", "task") (task_id, keeper, net_reward, proof)
TaskExpired ("exp", "task") (task_id,)
TaskCancelled ("cancel", "task") (task_id, owner)
RewardsWithdrawn ("withdraw", "reward") (keeper, amount)

Task Lifecycle State Machine

              register_task()
NONE ─────────────────────────────────▶ PENDING
                                           │
               ┌──────────────────────────┘│
               │ claim_task()              │ cancel_task()
               ▼                          ▼
            CLAIMED                    CANCELLED
               │
       ┌───────┴──────────┐
       │ execute_task()   │ expire_task() (deadline passed)
       ▼                  ▼
    EXECUTED           EXPIRED

    (re-claim possible if lock_ledgers elapsed without execute)

Integration Guide

How Other Soroban Contracts Call This

Step 1 — Approve the reward amount (ERC-20 / SEP-41 style):

// In your dApp contract, approve the registry to transfer reward tokens
token_client.approve(
    &env.current_contract_address(), // from: your contract
    &registry_contract_id,           // spender: the registry
    &reward_amount,
    &(env.ledger().sequence() + 1000), // expiry ledger
);

Step 2 — Register the task:

// Cross-contract call to register a task
let registry = KeeperRegistryClient::new(&env, &registry_contract_id);
let task_id = registry.register_task(
    &env.current_contract_address(), // owner
    &TaskType::Liquidation,
    &calldata,                        // encoded liquidation params
    &reward_amount,                   // XLM in stroops
    &(env.ledger().timestamp() + 3600), // deadline: 1 hour from now
    &17_280u32,                       // TTL: ~1 day
    &120u32,                          // lock: ~10 minutes
);

Step 3 — React to execution (optional Phase 2 — verifier interface):

// Your contract implements this trait (Phase 2 only)
pub trait IKeeperVerifiable {
    fn verify_execution(env: Env, task_id: u64, proof: Bytes) -> bool;
}

Tokenomics

Phase 1 — XLM Rewards

  • Task owners deposit XLM (or any SAC-wrapped token) as the reward.
  • Keepers earn reward * (1 - fee_bps/10000) per task.
  • Protocol fee (fee_bps) is configurable by admin (default 3%).
  • Fees accumulate in the contract; admin sweeps to a treasury address.

Phase 2 — Governance Token ($KPRS)

Attribute Value
Name Keeper Token
Symbol KPRS
Total Supply 100,000,000
Distribution 40% Keepers (emissions over 4 years), 20% Team (4-year vest), 20% Ecosystem fund, 10% Early supporters, 10% Treasury
Utility Vote on fee params, propose upgrades, stake for priority queue
Emissions Proportional to tasks executed and stake weight

Deployment & Usage

Prerequisites

# Rust + WASM target
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add wasm32-unknown-unknown

# Soroban CLI
cargo install --locked stellar-cli --features opt

# Node.js ≥ 18 (for keeper bot)
node --version

Local Development

git clone https://github.com/soroban-tooling/soroban-keeper-network
cd soroban-keeper-network

# Run all tests
cargo test --all --features testutils

# Build WASM
cargo build --release --target wasm32-unknown-unknown --package keeper-registry

Testnet Deployment

# Fund a testnet account
stellar keys generate --global deployer
stellar keys fund deployer --network testnet

export DEPLOYER_SECRET_KEY=$(stellar keys show --secret deployer)
export ADMIN_ADDRESS=$(stellar keys address deployer)

# Deploy
./scripts/deploy.sh testnet

Running the Keeper Bot

cd examples/keeper-bot
npm install
cp .env.example .env
# Edit .env with your secret key and contract ID
npm run start:testnet

Security Considerations & Audit Plan

Known Design Decisions

  1. No on-chain execution verification (MVP) — The registry trusts the claimer to submit proof. A malicious keeper could claim-and-execute-fake. Phase 2 adds an optional verifier callback.
  2. Fee sweep is manual — Protocol fees are batched and swept by admin. In Phase 2 this flows automatically to a staking/treasury contract.
  3. No slashing (MVP) — Unresponsive keepers lose their lock but face no economic penalty. Phase 2 introduces staking + slashing.

Security Properties

  • No re-entrancy — State transitions happen before token transfers (CEI pattern throughout).
  • Auth on all mutations — Every write function calls address.require_auth().
  • Overflow protectionoverflow-checks = true in release profile + checked_* arithmetic.
  • Bounded storage — No dynamic Vec in storage; all reads are O(1) by key.
  • Upgrade is admin-gated — WASM upgrade requires admin auth; new WASM must be pre-uploaded.

Audit Plan

Phase Scope Target
Pre-audit Internal review + fuzzing Q3 2026
Formal audit keeper-registry contract Q4 2026
Ongoing Automated invariant testing with cargo-fuzz Continuous

Security issues should be reported per SECURITY.md.


Stellar Community Fund / SDF Grant Readiness

This project is designed to qualify for:

  • Stellar Community Fund (SCF) — Open source infrastructure grant
  • SDF Build program — Soroban DeFi tooling
  • Meridian hackathon — Infrastructure track

Grant readiness checklist:

  • Open source (Apache-2.0)
  • On Soroban / Stellar ecosystem
  • Novel infrastructure (no equivalent exists)
  • Composable — designed to be used by other protocols
  • Fully documented + testable
  • Roadmap beyond MVP

Contributing

See CONTRIBUTING.md for the full guide including branch strategy, commit conventions, and PR process.


License

Apache-2.0 — see the LICENSE file for full terms.

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages