Sign in

Role inversion

Suggest an edit

Many roles in a design are inherited from whoever had the data first, and on Solana these roles are often the expensive ones. Role inversion moves such a job to the party that needs the result. The program only checks that the job was done correctly.

Roles nobody chose

Most roles in a design are never chosen. They follow from who has the data first. The oracle has the prices, so it writes them on-chain and pays for every update. The bridge has the message, so it calls the receiving program. The program stores a sorted list, so the program searches it.

On Solana, these default roles can be costly. Writing every price on-chain costs a fee each time, even if nobody reads that price. To call another program, the caller has to list every account that program will use, and often the caller cannot know them. Searching a long list on-chain costs compute units on every insert, while a transaction has a fixed compute budget.

Hand the role to whoever needs it

Role inversion moves an expensive job from the party that does it by default to the party that needs the result. The program keeps only one job, which is checking that the work was done correctly. That is usually much cheaper, because checking a result costs less than producing it.

You have already seen a small version of this in the lecture on constraints behind patterns. The client already finds the right accounts, so the program can leave the search to the client. The client says "here is the position", and the program only agrees or disagrees. Role inversion applies the same idea to any role in a design.

Pyth: from writing prices to checking them

Pyth is a price oracle. It used to work in the push model, where Pyth's data publishers sent every price update on-chain themselves and paid the fees. Each update of each feed, for example SOL/USD, was a transaction, whether or not anyone needed that price at that moment. A program read whatever value was stored in the price account, and that value was only as fresh as the last update.

Now Pyth works in the pull model, where the user who needs the price brings it. A trader downloads the latest signed price from Pyth's off-chain service and puts it in the same transaction as their trade. The transaction first checks that the price really came from Pyth and saves it, and then the trade reads it. The trade uses a price that is only seconds old. The trader now pays for the update, but only for the price they use, at the moment they use it. Pyth no longer pays for updates that nobody reads, and its on-chain role turned from writing prices into checking them.

Pyth push versus pull price updatesTwo vertical flows side by side. On the left, push: Pyth publishers send a transaction for every update and pay for it, the price account stores it, and a trader's transaction reads it later, so the price is as old as the last push. On the right, pull: Pyth signs updates off-chain, the trader fetches one and puts it in a single transaction that first checks and saves the price and then runs the trade, so the price is seconds old at the trade. Who brings the price Push: Pyth writes every price Pyth publishers tx per update, publishers pay price account read later trader's tx price is as old as the last push Pull: the trader brings the price Pyth, off-chain signed update trader one transaction 1. check + save the price 2. trade reads it price is seconds old at the trade With pull, the party that needs the price brings it, and the program only checks it.

Wormhole: the receiver collects the message

Wormhole is a cross-chain messaging bridge. The obvious design is for the bridge to call the receiving program when a message arrives. On Solana that is hard. To call a program, the bridge has to list every account that program needs, and every receiver needs different accounts.

So Wormhole does not call the receiver. A relayer or the user submits the signed message to the Wormhole program, which checks the signatures and saves the message in an account. Later, someone sends a transaction to the receiving program with the receiver's own accounts and the saved message. The receiver checks that the Wormhole program owns that account, that the message came from the expected chain and sender, and that it has not processed this message before. The bridge never needs to know which accounts any receiver uses.

LayerZero: the receiver lists its own accounts

LayerZero solves the same problem differently. Its delivery service, called the executor, does call the receiving program. To do that, it sends a transaction to your program, and that transaction has to list every account your program will read or write. The executor does not know your program, so it cannot know that list.

So your program tells it. Your program has one extra instruction, lz_receive_types, that changes nothing and only returns the list of accounts it needs to receive a message. The executor simulates this instruction, meaning it runs it as a test without sending it to the network, and reads the list. Then it builds the real transaction with those accounts and sends it.

Monaco: the client finds the place

Monaco is a prediction market, and it keeps its order book sorted. A new order has to go into the right place. Finding that place on-chain means reading the book one order at a time, and the longer the book, the more compute units that costs.

So Monaco lets the client find the place. The client reads the order book off-chain, where reading is free, and tells the program "put my order after this one". The program checks only the order right before the new one and the order right after it. If the new order fits between them, the program accepts it. If the client pointed to the wrong place, or another order landed first and changed the neighbors, the check fails and the client retries with a fresh position. The check costs the same whether the book holds ten orders or ten thousand.

Not every flip works

Symmetry runs index funds. An index fund holds several tokens in target shares, and when prices move, it has to trade to bring the shares back. This is called rebalancing.

Normally the fund is a taker, which means it trades against prices that someone else offers. Symmetry V2 flipped this and made the fund a maker, a party that offers prices for others to trade against. The fund sent its prices to aggregators, the services that find the best price for a swap and route it there. The idea was that other people's trades would move the fund back toward its targets.

It sounded smart. In practice, the rebalance happened only when someone else decided to trade. The protocol now depended on the aggregators, and it had to wait until an aggregator agreed to route trades to it.

The difference between Symmetry V2 and Pyth comes down to one question. After the flip, does the party that needs the action still decide when it happens? For Pyth, yes. The trader needs the price, and the trader decides when to bring it. For Symmetry V2, no. The fund needed the rebalance, and other people decided when it happened. The table below asks the same question of every example.

Five role inversions and who holds controlA table of five protocols. Pyth moved from the oracle writing every price to the trader bringing it while the program checks it. Wormhole moved from the bridge calling the receiver to the bridge storing the message and the receiver reading it. LayerZero moved from the executor needing to know the accounts to the receiver listing them while the executor follows the list. Monaco moved from the program searching the book to the client searching and the program checking. In all four, the party that needs the action stays in control. Symmetry V2 moved from the fund trading to rebalance to other people's trades rebalancing the fund, and the fund lost control. Five inversions and who holds control protocol inherited role after the flip in control? Pyth publishers oracle writes every price trader brings it, program checks yes Wormhole bridge calls the receiver bridge stores, receiver reads yes LayerZero executor must know accounts receiver lists them, executor follows yes Monaco program searches the book client searches, program checks yes Symmetry V2 fund trades to rebalance others' trades rebalance the fund no An inversion works when the party that needs the action still decides when it happens.

Find the role you inherited

Take a design you are working on and write down who does each job. For each one, ask why that party does it. If the only answer is "because it had the data first", and the job is expensive, try inverting it. Find who actually needs the action, and check whether the program's part can shrink to a single check. Before you choose this design, check one more thing. After the inversion, the party that needs the action must still decide when it happens. If it does, the party that benefits also pays, and your program stays small. If it does not, you removed a cost but now depend on someone you do not control.