Role inversion
Suggest an editMany 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.
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.
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.