Oracles with Soroban Smart Contracts: A Practical and Flexible On-Chain Framework

The latest release of soroban-kit 0.1.8 is now available, and it brings a set of tools designed to streamline both synchronous and asynchronous cross-contract communication, particularly for building more robust oracle systems with soroban—exciting topic!

In this article, I’m going to walk you through the new soroban-kit::oracle module step-by-step. You will learn how to build a flexible and extensible oracle system leveraging the pub/sub pattern for asynchronous communication between multiple on-chain oracle subscribers and brokers.

We are implementing this system for our Soroban-powered smart contracts on Litemint, specifically for feeding royalty contracts with market prices.

How It Works

soroban-kit 0.1.8 introduces two procedural attribute macros, oracle_broker and oracle_subscriber.

Applying these macros to your contract struct automatically generates a lightweight messaging framework that enables both synchronous and asynchronous cross-contract communication, as demonstrated in the following diagram:

Sequence diagram - soroban-kit::oracle

By leveraging the publisher-subscriber pattern, we can enable multiplicity between oracle subscribers, brokers and publishers. Not only this enhances deployment flexibility but also allows for a gradual increase in system resilience and robustness—mitigating the risks associated with reliance on a single point.

Deployment diagram - soroban-kit::oracle

The best part is, setting up your contracts for this requires only a single line of code, all thanks to Rust’s sophisticated macro system. For those curious about the inner workings and mechanics, take a look at oracle.rs.

💡 Tip: Alternatively, you can run cargo expand command to view the expanded code generated by the macros.

// Implement the oracle broker interface for your contract.
#[contract]
#[oracle_broker(Bytes, Bytes)]
pub struct OracleBrokerContract;
// Implement the oracle subscriber interface for your contract.
#[contract]
#[oracle_subscriber(Bytes, Bytes)]
pub struct OracleSubscriberContract;

These macros also allow you to customize the types for both the topic and the data. We use Bytes in this example but any built-in type and user-defined type is supported, Soroban transparently provides for all our serialization needs!

After setting it up, all that’s required is to implement the soroban-kit::oracle::Events trait to manage and process the workflow.

Let’s go!

Getting the Code on Github

The code for the example oracle project is located in this GitHub repo.

It consists of 2 smart contracts:

  • Oracle Broker Contract (see lib.rs)
    • Handling sync/async topic-based subscriber requests.
    • Handling publishing requests.
    • Collecting subscriber fees.
    • Managing envelopes for async routing.
    • Managing publishers whitelist.
  • Oracle Subscriber Contract (see lib.rs)
    • Routing topic-based data requests to broker contract.
    • Handling sync/async responses.
    • Managing brokers whitelist.
    • Providing data reconciliation.

Note that our example data reconciliation function simply replaces the old data with the new but more elaborate mechanisms can be implemented, including price aggregation, average computation, validation and coalescing of results, based on specific use cases.

First, clone the repository. Then, execute the following command to compile the contracts:

$ soroban contract build

Once compiled, you’ll find two oracle contracts ready to deploy in the target directory.

💡 Tip: While they are already compact, using the optimization command (soroban contract optimize) can shrink their sizes even more (to less than 3.5K for the broker contract and 2.5K for the subscriber one):

$ ls -lh broker.optimized.wasm subscriber.optimized.wasm
-rwxrwxrwx 1 3.4K Dec 22 22:29 broker.optimized.wasm
-rwxrwxrwx 1 2.5K Dec 22 22:29 subscriber.optimized.wasm

Step-by-Step Deployment and Setup

For our scenario below, we will need 2 subscribers and 2 brokers.

Remember, you always have the option to deploy additional nodes at any time. soroban-kit::oracle ensures consistency for communication with decoupled, events-driven, interactions between your contracts (as long as the types are consistent, they can communicate).

We assume that the following network and identities are pre-configured (soroban config) and funded. Follow this link if you need instructions.

TypeNameDescription
NetworkTESTNETStellar Testnet
AccountADMINContracts admin
AccountALICESubscriber operator
AccountBOBPublisher

To deploy the oracle broker contracts and simultaneously save their IDs, use the following commands:

$ soroban contract deploy \
--wasm target/wasm32-unknown-unknown/release/broker.wasm \
--source ADMIN --network TESTNET > BROKER1

$ soroban contract deploy \
--wasm target/wasm32-unknown-unknown/release/broker.wasm \
--source ADMIN --network TESTNET > BROKER2

Do the same for the oracle subscriber contracts:

$ soroban contract deploy \
--wasm target/wasm32-unknown-unknown/release/subscriber.wasm \
--source ADMIN --network TESTNET > SUBSCRIBER1

$ soroban contract deploy \
--wasm target/wasm32-unknown-unknown/release/subscriber.wasm \
--source ADMIN --network TESTNET > SUBSCRIBER2

Setup the ADMIN for all contracts:

$ soroban contract invoke --id $(cat BROKER1) \
--source ADMIN --network TESTNET -- set_admin --admin ADMIN

$ soroban contract invoke --id $(cat BROKER2) \
--source ADMIN --network TESTNET -- set_admin --admin ADMIN

$ soroban contract invoke --id $(cat SUBSCRIBER1) \
--source ADMIN --network TESTNET -- set_admin --admin ADMIN

$ soroban contract invoke --id $(cat SUBSCRIBER2) \
--source ADMIN --network TESTNET -- set_admin --admin ADMIN

Whitelist the publisher (BOB) for both oracle brokers:

$ soroban contract invoke --id $(cat BROKER1) \
--source ADMIN --network TESTNET -- allow_publisher --publisher BOB

$ soroban contract invoke --id $(cat BROKER2) \
--source ADMIN --network TESTNET -- allow_publisher --publisher BOB

Whitelist the brokers (BROKER1 and BROKER2) for both oracle subscribers:

$ soroban contract invoke --id $(cat SUBSCRIBER1) \
--source ADMIN --network TESTNET -- allow_broker --broker $(cat BROKER1)

$ soroban contract invoke --id $(cat SUBSCRIBER2) \
--source ADMIN --network TESTNET -- allow_broker --broker $(cat BROKER1)

$ soroban contract invoke --id $(cat SUBSCRIBER1) \
--source ADMIN --network TESTNET -- allow_broker --broker $(cat BROKER2)

$ soroban contract invoke --id $(cat SUBSCRIBER2) \
--source ADMIN --network TESTNET -- allow_broker --broker $(cat BROKER2)

Executing a Test Scenario

Everything is now ready! ALICE is initiating data requests for Topic 21 through SUBSCRIBER1 and SUBSCRIBER2 to both BROKER1 and BROKER2.

Here’s a deployment diagram to provide a clearer visual representation of the setup:

Alice deployment setup

Here are the commands to do so:

$ soroban contract invoke --id $(cat SUBSCRIBER1) \
--source ALICE --network TESTNET -- request \
--subscriber ALICE --topic 21 --broker $(cat BROKER1)

> null

$ soroban contract invoke --id $(cat SUBSCRIBER2) \
--source ALICE --network TESTNET -- request \
--subscriber ALICE --topic 21 --broker $(cat BROKER1)

> null

$ soroban contract invoke --id $(cat SUBSCRIBER1) \
--source ALICE --network TESTNET -- request \
--subscriber ALICE --topic 21 --broker $(cat BROKER2)

> null

$ soroban contract invoke --id $(cat SUBSCRIBER2) \
--source ALICE --network TESTNET -- request \
--subscriber ALICE --topic 21 --broker $(cat BROKER2)

> null

BOB is setup to publish data exclusively to BROKER2, and note that BROKER1 will not receive this data. Here’s the command:

soroban contract invoke --id $(cat BROKER2) \
--source BOB --network TESTNET -- publish --topic 21 \
--publisher BOB --data 21000000

Because both subscribers were connected to BROKER2, they have received and can now serve the data synchronously. Here are some example commands:

$ soroban contract invoke --id $(cat SUBSCRIBER1) \
--source ADMIN --network TESTNET -- get_data --topic 21

> 21000000

$ soroban contract invoke --id $(cat SUBSCRIBER2) \
--source ADMIN --network TESTNET -- get_data --topic 21

> 21000000

Conclusion

Oracles serve as bridges between blockchains and external data sources. There are many key challenges in implementing Oracle services, including decentralization, synchronicity, decoupling and multiplicity.

soroban-kit proposes a lightweight solution for implementing the pub/sub messaging pattern to help address these challenges for cross-contract communication. Allowing you to deploy nodes gradually to achieve resilience and robustness for your oracle system.

I hope you found this enjoyable! Stay tuned for more detailed tutorials showcasing the full range of soroban-kit features!

If you have any questions, don’t hesitate to reach out. I’m usually active on X @FredericRezeau and Discord where you can find me in the LitemintStellar Global and Stellar Developers servers!

Feel free to reach out on GitHub too.

Happy coding!