
The CeDeFi Ledger Playbook
A Practical Guide to Tracking CeDeFi Yield, Gas, and Slashing in a Core Ledger
CeDeFi breaks standard ledgers because digital asset balances can change without transactions. Learn how to design a Core Ledger to shadow-ledger on-chain protocols.
Building a CeDeFi platform requires you to commit a fundamental engineering sin called Maturity Mismatch. Your users treat their accounts like a savings bank. They expect their principal to be static, safe, and available on demand. But you are taking that capital and deploying it into decentralized protocols that are volatile, complex, and fundamentally asynchronous. You are putting a simple “Bank” interface over a complex “Hedge Fund” backend.
The core of this complexity is the Omnibus Yield Pool. For the unit economics to work, you cannot open a separate DeFi wallet for every single user. Instead, you take deposits from 10,000 different users, pool them into a single massive omnibus wallet, and deploy that entire pile into a protocol like Aave or Lido via a single smart contract transaction. This creates a disconnection between ownership and reality.
- Internal Reality: Your database knows that Alice owns 9.1% of the pile and Ben owns 90.9%.
- External Reality: The blockchain just sees one giant pile of 1,000,000 aUSDC belonging to You.
When the protocol pays yield, it doesn’t send 10,000 little payments to your users. It sends one lump sum of reward tokens to your omnibus wallet.
Suddenly, you have an extra 100 aUSDC in your wallet. The blockchain can’t tell you whose money earned it. It is just there. Your entire engineering challenge is figuring out how to fairly break that 100 aUSDC lump sum back down into micro-payouts for Alice and Ben without losing money on gas fees or rounding errors.
This is how to design a Core Ledger that solves that problem.
Why You Need Hierarchical Paths
Most engineers start by building a ledger that looks like a standard database table. You have a column for UserID, a column for Asset, and a column for Balance. This is called a single-entry ledger, and not only is it a bad way to record stored funds, it fails catastrophically when you are investing money.
In a yield platform, a single user’s money exists in multiple states simultaneously. Ben has 1,000 aUSDC. Some of it is sitting idle in a wallet. Some of it is locked in a staking contract. Some of it is unclaimed reward dust. If you use a simple database, you have to write complex queries with multiple JOIN statements just to calculate Ben’s net worth. You end up managing state with tags or boolean flags like is_staked=true. As your product grows, these flags multiply and become a nightmare to maintain.
The better solution is to use Hierarchical Paths. The idea is similar to how a computer file system works. You organize money into folders and sub-folders directly inside the account name. Instead of a flat list, we structure the account name to represent the lifecycle of the investment. A typical path follows the logic of Class:Entity:Context:State. This results in account names that tell a story:
user:ben:wallet:idle(State: Liquid | Yield: 0%): This holds the uninvested capital sitting in Ben’s main wallet. It is available for immediate withdrawal but isn’t generating returns.user:ben:aave:principal(State: Staked | Yield: Active): This represents the portion of funds actively locked inside the Aave lending protocol. It is exposed to smart contract risk but is generating interest.
This hierarchical structure allows you to query your financial data using wildcards rather than complex code. Want to know exactly how much principal is at risk in Aave across the entire company? You query *:aave:principal. Want to know Ben’s total balance across all strategies? You query user:ben:*. By baking the structure into the account name, you don't need to update your database schema every time you add a new strategy. You just create a new path. It turns your ledger into a flexible map of your entire operation.
Capturing The “Silent” Yield
The hardest part of building a yield platform is detecting that you actually earned money. In traditional finance, a bank sends you a transaction when they pay interest. In DeFi, nobody sends you anything. If you hold stETH (Staked ETH), your balance updates passively via a protocol-wide state change. If you hold cTokens (Compound), the exchange rate between the token and the underlying asset simply shifts.
Your ledger cannot passively wait for a web-hook that will never come. You need to build a state observer, an active service that runs on a schedule (for example, every day at midnight). Its job is to look at the blockchain, look at your internal ledger, and calculate the difference.
- Poll the Chain: The observer asks the smart contract for the current balance of your omnibus wallet.
- Poll the Ledger: It checks the total balance recorded in your internal “realized” accounts.
- Calculate the Delta: It subtracts the internal balance from the external balance. The difference is your new yield.
Once the observer finds this difference, it has to explain it to the ledger. It creates a synthetic transaction, a ledger entry that doesn’t correspond to a user deposit or withdrawal, but strictly records the “drift” that happened on-chain.
In a strict double-entry system, funds cannot simply appear. Every credit requires a matching debit. Since these funds originate from outside your system, we need to model that external source as a valid account. We call this the world account. This account acts as the universal counterparty for the smart contracts and banking rails that sit outside your ledger.
| Transaction | Source | Destination | Amount |
| Daily Yield Sweep | world | 100.00 aUSDC | |
platform:omnibus:yield | 100.00 aUSDC |
Now the yield exists in the ledger. It has been pulled from the invisible on-chain state into a visible internal account. Only now can you safely distribute it to users.
The “Fair Share” Problem (Weighted Math)
Now that you have the $100 yield in your hand, who gets it?
This is where the Omnibus model gets tricky. Because the $100 was generated by the entire pool, you cannot simply split it evenly. You have to distribute it Pro-Rata.
If you just divide by the user count, you are stealing from your whales. You need to visualize the pool ownership to understand the math.
Step 1: Determine Ownership
Imagine your Omnibus Pool holds $1,100 total. Ben deposited $1,000, and Alice deposited $100.
Omnibus Pool Composition
| Owner | Amount | Ownership |
| Ben | 1,000.00 aUSDC | 90.9% |
| Alice | 100.00 aUSDC | 9.1% |
| Total | 1,100.00 aUSDC | 100% |
Step 2: Apply Ownership Calculations to the Yield
When the $100 reward arrives, the ledger must apply those owner percentages to the new money.
Yield Calculation on $100 Reward
| Owner | Ownership | Reward |
| Ben | 100.00 aUSDC × 90.9% | 90.90 aUSDC |
| Alice | 100.00 aUSDC × 9.1% | 9.10 aUSDC |
In a SQL database, calculating this is expensive. You have to run a script that loops through every single user, looks up their balance, calculates their percentage of the total pool, determines their specific dollar amount, and then updates their row. To do this accurately, you have to lock the entire table. If a single user makes a deposit or withdrawal while the script is running, the total pool changes and your calculation becomes invalid. So you freeze operations. And if this script crashes halfway through, you corrupt your data.
A Core Ledger solves this with weighted distribution. Instead of running a loop, you instruct the ledger to check the balances of the principal accounts at the exact moment of the harvest. The ledger dynamically calculates the weight of every user relative to the total pool and splits the reward in a single atomic action.
This ensures that the payout is mathematically perfect based on the exact exposure each user had at that moment.
The “Gas Station” Problem
But before you pay Ben and Alice, you have to pay the network.
When you harvest yield or withdraw funds from a protocol, you pay Gas Fees. A common mistake is treating Gas as a generic operating expense that the company pays for. If you do this, you will bleed money as you scale. Gas should be treated as a direct cost of the revenue itself. Using the weighted logic we just defined, we can construct a sophisticated transaction that pays the network first and then distributes the remainder to the users.
We take the $100 yield, subtract the $5 gas fee, and then share the remaining $95 into the user accounts based on their pro-rata weights.
| Transaction | Source | Destination | Amount |
| Harvest & Distribute | platform:omnibus:yield | 100.00 aUSDC | |
platform:expenses:gas | 5.00 aUSDC | ||
user:ben:aave:rewards | 86.36 aUSDC (90.9% of remaining) | ||
user:alice:aave:rewards | 8.64 aUSDC (9.1% of remaining) |
By explicitly visualizing the gas as a destination in the movement, you create an immutable record that proves why the user received slightly less than the gross APY. And by using weighted distribution, you ensure Ben and Alice get exactly their fair share without needing a complex python script to figure it out.
The “Nuclear” Scenario: Handling Slashing
Okay, we have covered how to handle it when the pool grows. Now we have to talk about the uncomfortable part: what happens when the pool shrinks?
With digital assets, the quantity of tokens you hold can actually go down. In Proof-of-Stake protocols, a validator can be penalized for a technical error. This is called slashing. When this happens, the protocol burns a portion of the tokens in the pool, creating a dangerous gap. Your internal ledger says you are holding 1,100 aUSDC, but because of the penalty, the real wallet on the blockchain actually holds less than that. If you do not fix this immediately, you will accidentally bankrupt your company.
Here is why: If you let users continue to withdraw their full balance, the first few people will get paid in full. But because the pool is smaller than the total claims, the last few people will try to withdraw and find an empty wallet. They get zero.
To prevent this unfair race to the exit, you need to apply the exact same weighted math we used for profits, but in reverse. If Ben owns 90.9% of the pool, he has to absorb 90.9% of the penalty. This effectively means every single user takes a loss proportional to their share of the overall balance. If the pool burns 5% of its tokens, the ledger must burn 5% of everyone’s balance.
| Transaction | Source | Destination | Amount |
| Slashing Event | user:ben:aave:principal | 49.60 aUSDC (90.9% of the loss) | |
user:alice:aave:principle | 5.40 aUSDC (9.1% of the loss) | ||
world | 55.00 aUSDC |
By running this transaction, you are telling the truth. You are acknowledging that the tokens are gone. This ensures that if Ben wants to withdraw the next second, he can withdraw the remaining 950.40 aUSDC safely, without stealing the liquidity that belongs to Alice.
Don’t Build This From Scratch
The architecture described here covers hierarchical paths, synthetic events, and weighted distributions. This is the gold standard for institutional DeFi.
Attempting to build this logic into a standard application database is a trap. You would need to write complex scripts to handle the rounding logic, manage the state observers, and ensure that your multi-user updates don't lock your database and grind your application to a halt.
That is where Formance comes in. We provide this architecture as a set of pre-built primitives.
We give you the Programmable Ledger to natively handle the hierarchical paths, allowing you to spin up new strategies instantly just by using a new name. We provide Numscript so you can write the complex weighted distribution logic as atomic rules rather than fragile code. And we offer the framework to connect your ledger to the blockchain, automating the state observer pattern so you never miss a reward or a slash.
If you are tired of tracking millions of dollars of yield in a spreadsheet, let’s talk about your ledger design!
