← AERE Academy
Intermediate

Build a DeFi Primitive on Chain 2800

A working ERC-4626 vault with deposit and withdraw, wired to a live price oracle. You will read a real vault and a real oracle that are on chain today, reproduce their arithmetic by hand, then write and deploy your own.

Who this is for. Developers who have deployed a Solidity contract before and can read a JSON-RPC response. You do not need prior DeFi experience. If you have never deployed anything, start with Deploy Your First Smart Contract.

How long. Around two hours to read and run the verification steps. Longer if you deploy.

What you will know at the end. What the four ERC-4626 entry points actually compute and why the share price formula has an offset in it; how to read a live vault's accounting and check it against the formula yourself; how to attach a price feed without letting it corrupt your share price; what a feed that refuses to answer looks like and why that is the correct behaviour; and which design choices open up when a block is final in one slot instead of after a confirmation count.

Chain ID
2800 (0xAF0)
Height at measurement
13,212,535
Block interval
0.54 to 0.56 s
Base fee
1 Gwei
Validators
9, quorum 6
Measured
2026-08-10 11:18 UTC

Every figure above is a snapshot taken from https://rpc.aere.network at block 13,212,535, 2026-08-10 11:18:20 UTC. Height and interval move; the interval is given as a range because that is what three different sampling windows returned on the day. The exact calls that produce every figure are in Verify it yourself, so you can re-take the snapshot rather than trust this one.

Read this before you plan anything

Four things about chain 2800 today that change what is worth building on it. None of them are hidden and all of them are checkable with the calls in the last section.

1Point your tools at the chain

Chain 2800 runs the standard EVM, so Foundry, Hardhat, ethers and viem work unmodified. It is a superset rather than an exact match: it adds five precompiles that Ethereum does not have, at addresses 0x0aE1 through 0x0aE5, which step 6 covers. Nothing else in your toolchain has to change. Confirm the chain ID before anything else, because a contract deployed against the wrong chain ID is a silent, expensive mistake.

curl -s -X POST https://rpc.aere.network \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}'
{"jsonrpc":"2.0","id":1,"result":"0xaf0"}

0xaf0 is 2800. To add the network to an EIP-3085 compatible wallet, this is the parameter object:

{
  "chainId": "0xAF0",
  "chainName": "AERE Network",
  "nativeCurrency": { "name": "AERE", "symbol": "AERE", "decimals": 18 },
  "rpcUrls": ["https://rpc.aere.network", "https://rpc2.aere.network"],
  "blockExplorerUrls": ["https://explorer.aere.network"]
}

There are two public read endpoints, and both are named above. Point eth_chainId at each and both return 0xaf0; point eth_blockNumber at each and they track within a block or two of one another. Read the note in the footer before you treat that as independence, because it is not.

AERE has no token contract. It is the chain's native coin, the same way ETH is on Ethereum. Balances come from eth_getBalance, not from an ERC-20 call. If a page or a listing offers you "the AERE token address", it is describing something else. A vault, however, needs an ERC-20, which is what the next step is about.

2What a vault has to get right

ERC-4626 is a standard interface for a contract that takes one ERC-20 in, gives a second ERC-20 back as a receipt, and lets you exchange the receipt for the original later. The receipt is called a share, the thing deposited is the asset. Everything else in the standard exists so that unrelated contracts can integrate with your vault without reading your source.

The interface has four state-changing entry points, arranged as two pairs. In each pair one function fixes the asset amount and the other fixes the share amount:

FunctionYou specifyDirection
deposit(assets, receiver)assets inin
mint(shares, receiver)shares outin
withdraw(assets, receiver, owner)assets outout
redeem(shares, receiver, owner)shares inout

Each has a preview* twin that returns what the call would produce, and a max* twin that returns the largest input that would not revert. Integrators call the previews; if your previews disagree with what your entry points actually do, integrations break in ways that are hard to trace back to you.

The accounting identity

A vault is a single ratio, totalAssets() / totalSupply(), plus rules for changing it. Deposits and withdrawals must not move the ratio; only yield arriving, or a loss, may move it. If a deposit can move the share price, someone will deposit in order to move it.

The naive conversion is shares = assets * totalSupply / totalAssets, and it fails on an empty vault. The first depositor can be handed one share for a large deposit, then donate assets directly to the vault to inflate the ratio, so that the second depositor's deposit rounds down to zero shares and their assets are absorbed. This is the ERC-4626 inflation attack. The fix used in practice, and in the live vault you are about to read, is to add virtual assets and virtual shares to both sides:

// OpenZeppelin ERC4626 with _decimalsOffset() = N
shares = assets * (totalSupply() + 10**N) / (totalAssets() + 1)
assets = shares * (totalAssets() + 1)      / (totalSupply() + 10**N)

The virtual share count makes the attacker's donation dilute their own position faster than it dilutes the victim's, so the attack costs more than it returns. You are not asked to take that on faith. In the next step you read the live numbers and reproduce both formulas exactly.

3Read a live ERC-4626 vault

There is an ERC-4626 vault on chain 2800 at 0x9B580f9118AF4421b270cDb279CF18c03E1573a2. Read it before writing your own, because reading tells you things the standard does not. Confirm you are at the right contract first: name() returns "Staked AERE" and symbol() returns "sAERE".

It is used here as a reference implementation to inspect, and it is holding a seed balance rather than user deposits. You can check that claim rather than accept it: totalAssets() on the vault and balanceOf(vault) on the asset return the same number, so the vault holds its assets idle and has no strategy deployed behind it. This course is not suggesting you put anything into it.

# asset() -> selector 0x38d52e0f
curl -s -X POST https://rpc.aere.network -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_call","params":[{"to":"0x9B580f9118AF4421b270cDb279CF18c03E1573a2","data":"0x38d52e0f"},"latest"],"id":1}'
0x0000000000000000000000007e84d7d66d5da4cfe46da67cdeeb05b323e1f5e8

The asset is 0x7e84d7d66d5da4cfE46Da67CDEeB05B323e1f5e8, the wrapped form of the native coin. Its name() reads "Wrapped AERE", symbol() reads "WAERE", decimals() is 18, and it follows the familiar wrapper shape: deposit() is payable and takes native coin in, withdraw(uint256) sends it back out.

Now the four numbers that define the vault's state. Same call, different selector:

CallSelectorRaw result, exactly as returnedReads as
decimals()0x313ce5670x000000000000000000000000000000000000000000000000000000000000001824
totalSupply()0x18160ddd0x00000000000000000000000000000000000000000000000000000000000003e81000
totalAssets()0x01e1d1140x00000000000000000000000000000000000000000000000000000000000007d02000
convertToShares(1e18)0xc6e6f5920x00000000000000000000000000000000000000000000001b1e5c92ee8ffde7b1500249875062468765617

Every result comes back as one 32-byte word, zero padded on the left, which is why each is 64 hex digits long. Arguments go in the same shape: convertToShares(1e18) is the selector 0xc6e6f592 followed by 1e18 padded to 32 bytes, which is the long data string used in section 8.

The first surprise is decimals(), and it is 24, not 18. Nothing in ERC-4626 says shares carry the same number of decimals as the asset. Here the virtual-share offset is 6, so the share token is 18 + 6 = 24 decimals. Any front end that hardcodes 18 will display this vault's balances a million times too large. Call decimals() on the share token. Do not infer it from the asset.

Reproduce the share price by hand

You now have every input the formula needs: totalSupply 1000, totalAssets 2000, offset 6. Compute what convertToShares(1e18) should return and compare it with what the chain returned.

node -e '
const totalSupply = 1000n, totalAssets = 2000n, offset = 10n ** 6n;
const shares = (10n ** 18n) * (totalSupply + offset) / (totalAssets + 1n);
console.log(shares.toString());
'
500249875062468765617 ← identical to the chain, digit for digit

Run the inverse as well. convertToAssets(1e24) returns 1999000999000999000999, and (10n**24n) * (totalAssets + 1n) / (totalSupply + offset) gives the same value. Two independent directions, both exact, from numbers you read yourself. That is what a verifiable claim looks like, and it is the standard the rest of this course holds itself to.

These live values are a snapshot. If your reads differ from the table, the vault's state changed, which is normal. Recompute with the numbers you read; it is the agreement between the formula and the chain that matters, not the specific digits.

4Write the vault

With the accounting understood, the contract is short, because the standard's hard parts are the ones you should not be reimplementing. Install OpenZeppelin v5, then:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IERC20}  from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20}   from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";

/// A vault that takes WAERE in and issues transferable share receipts.
contract SimpleVault is ERC4626 {
    constructor(IERC20 asset_)
        ERC20("Simple Vault WAERE", "svWAERE")
        ERC4626(asset_)
    {}

    // Virtual shares against the inflation attack. 6 is the value the
    // live vault in step 3 uses, and the reason its decimals() is 24.
    function _decimalsOffset() internal pure override returns (uint8) {
        return 6;
    }
}

You did not write deposit or withdraw, and that is the point: inherited, all four entry points and all eight twins stay consistent with each other by construction. What you are responsible for is everything the base class cannot know.

What the base class cannot know

5Deploy it and make the first deposit

The steps below need a funded account. As stated at the top, there is no public way to obtain AERE today, so treat this section as the exact procedure for when you have funds, not as something you can run right now.

# 1. Deploy against WAERE as the asset.
#    Current Foundry needs --broadcast to actually send; older
#    versions send without it and reject the flag. Check yours.
forge create src/SimpleVault.sol:SimpleVault \
  --rpc-url https://rpc.aere.network \
  --constructor-args 0x7e84d7d66d5da4cfE46Da67CDEeB05B323e1f5e8 \
  --private-key $PK --broadcast

# 2. Wrap native AERE into WAERE (deposit() is payable)
cast send 0x7e84d7d66d5da4cfE46Da67CDEeB05B323e1f5e8 "deposit()" \
  --value 1ether --rpc-url https://rpc.aere.network --private-key $PK

# 3. Approve the vault to pull that WAERE
cast send 0x7e84d7d66d5da4cfE46Da67CDEeB05B323e1f5e8 \
  "approve(address,uint256)" $VAULT 1000000000000000000 \
  --rpc-url https://rpc.aere.network --private-key $PK

# 4. Check what you will get BEFORE you commit to it
cast call $VAULT "previewDeposit(uint256)(uint256)" 1000000000000000000 \
  --rpc-url https://rpc.aere.network

# 5. Deposit
cast send $VAULT "deposit(uint256,address)" 1000000000000000000 $ME \
  --rpc-url https://rpc.aere.network --private-key $PK

# 6. Exit: redeem burns shares, withdraw names the assets you want out
cast call $VAULT "maxRedeem(address)(uint256)" $ME --rpc-url https://rpc.aere.network
cast send $VAULT "redeem(uint256,address,address)" $SHARES $ME $ME \
  --rpc-url https://rpc.aere.network --private-key $PK
Step 4 is not optional politeness. previewDeposit is the vault's own answer, computed by the same code that will run in step 5. Comparing the preview against your own calculation is how you catch a misconfigured vault before it holds your money rather than after.

6Attach a price oracle

A vault denominates everything in its asset. The moment you want a position limit in dollars, a risk threshold, or a display value, you need a price, and a price is an external claim about the world that has to enter the chain through somebody. That somebody is your real counterparty risk.

Chain 2800 has a multi-reporter median feed at 0xca69AA961D836516010Ae669a223Ce249490ACb1. Its interface is small:

interface IAereOracleV2 {
    function DECIMALS() external view returns (uint8);
    function maxStaleness() external view returns (uint64);
    function minContributors() external view returns (uint256);
    function getPrice(bytes32 symbol)
        external view returns (uint128 price, uint64 timestamp, uint256 contributors);

    error InsufficientQuorum(uint256 fresh, uint256 required);
}

Read its three configuration values live before you design against it:

CallSelectorRaw result, exactly as returnedMeaning
DECIMALS()0x2e0f26250x0000000000000000000000000000000000000000000000000000000000000008prices are scaled by 10^8
minContributors()0x2bae82360x00000000000000000000000000000000000000000000000000000000000000033 fresh reporters required
maxStaleness()0x87cf46960x000000000000000000000000000000000000000000000000000000000000012c300 seconds

Now ask it for a price

Symbols are bytes32, right padded ASCII. "BTC/USD" is 0x4254432f55534400.... Ask:

curl -s -X POST https://rpc.aere.network -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_call","params":[{"to":"0xca69AA961D836516010Ae669a223Ce249490ACb1","data":"0x31d98b3f4254432f55534400000000000000000000000000000000000000000000000000"},"latest"],"id":1}'
{"jsonrpc":"2.0","id":1,"error":{"code":3,"message":"Execution reverted", "data":"0x6a1ee67a 0000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000003"}}

It reverts, and this is the most instructive call in the course. The data field arrives as one unbroken hex string; it is split across three lines above so you can see its structure. 0x6a1ee67a is the four-byte selector of InsufficientQuorum(uint256,uint256), and the two 32-byte words after it decode to 0 and 3: zero fresh reporters, three required. The feed is deployed and correct and is telling you it has no current opinion, rather than handing back the last number it happened to hold.

Design for this, not around it. A feed that reverts is a feed you can build on. A feed that returns a stale number with no way to tell is one that will eventually price a liquidation with a number from an hour ago. Note also what the quorum rule prevents: with a single fresh reporter, a median is not a median, it is that one reporter's number. Requiring three fresh contributors is what makes the word median mean anything.

Wire it into the vault correctly

The important rule first: the oracle must not touch your share price. In ERC-4626 totalAssets() is denominated in the asset, so conversions between shares and assets need no price at all. If you make convertToShares depend on an external feed, then whoever can move that feed can mint or redeem shares at a price of their choosing. Keep the price on the reporting and risk side.

contract PricedVault is SimpleVault {
    IAereOracleV2 public immutable oracle;
    bytes32       public immutable feedSymbol;
    uint64        public immutable maxAge;

    error PriceUnavailable();
    error PriceStale(uint64 age, uint64 allowed);
    error PriceAhead();
    error OracleHasNoCode();

    constructor(IERC20 asset_, IAereOracleV2 oracle_, bytes32 sym, uint64 maxAge_)
        SimpleVault(asset_)
    {
        // Checked once, here, because catch below cannot be relied on to
        // cover a wrong address. Cheaper than checking on every read.
        if (address(oracle_).code.length == 0) revert OracleHasNoCode();
        oracle = oracle_; feedSymbol = sym; maxAge = maxAge_;
    }

    /// Returns a price or reverts. It NEVER substitutes a default.
    function priceOrRevert() public view returns (uint256 price, uint8 dec) {
        try oracle.getPrice(feedSymbol) returns (uint128 p, uint64 ts, uint256) {
            uint64 nowTs = uint64(block.timestamp);
            if (ts > nowTs)            revert PriceAhead();
            uint64 age = nowTs - ts;
            if (age > maxAge)          revert PriceStale(age, maxAge);
            if (p == 0)               revert PriceUnavailable();
            return (uint256(p), oracle.DECIMALS());
        } catch {
            // The feed reverted. On this chain today that is
            // InsufficientQuorum, measured in step 6. Whatever the
            // reason, the answer to the caller is the same: no price.
            revert PriceUnavailable();
        }
    }

    /// Reporting only. Share accounting above does not read this.
    function totalAssetsQuoted() external view returns (uint256) {
        (uint256 p, uint8 dec) = priceOrRevert();
        return totalAssets() * p / (10 ** dec);
    }
}

Four details in that function are deliberate, and each is a bug in code that omits it:

Optional: the post-quantum precompiles, and what is actually proven about them

Chain 2800 exposes five native precompiles that Ethereum does not have. They are callable read-only by anyone, including from inside your contract via staticcall, which is useful if your primitive needs to check a signature produced outside the EVM's usual scheme.

AddressSchemeStanding of the standard
0x0aE1Falcon-512NIST selected it; the standard, FN-DSA, is not published yet
0x0aE2Falcon-1024same, not published yet
0x0aE3ML-DSA-44published NIST standard, FIPS 204
0x0aE4SLH-DSA-128spublished NIST standard, FIPS 205
0x0aE5SHAKE256, extendable-output hashpublished NIST standard, FIPS 202
Two of the five are not standardized yet, and the page will not blur that. Calling all five "NIST post-quantum verifiers" would be wrong: Falcon was selected by NIST but its standard has not been published, so 0x0aE1 and 0x0aE2 implement a scheme whose final specification could still move. Treat them accordingly. Only 0x0aE3, 0x0aE4 and 0x0aE5 correspond to published standards.

What this course actually demonstrates is the hash, because it is the one whose correct answer is published and so can be checked against something outside our control. It fits on a single line:

curl -s -X POST https://rpc.aere.network -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"0x0000000000000000000000000000000000000ae5","data":"0x0000000000000000000000000000000000000000000000000000000000000020616263"},"latest"]}'
0x483366601360a8771c6863080cc4114d8db44530f8f1e1ee4f94ea37e78b5739

The calldata is an output length of 32 as a big-endian 32-byte word, followed by 616263, which is ASCII "abc". The answer is the FIPS-202 known-answer value for SHAKE256("abc", 32), so you can compare it against any local library and against the published standard. Two more calls make it a real test rather than one lucky hit. Change the length word from 0x20 to 0x11 and you get 0x483366601360a8771c6863080cc4114d8d, which is the first 17 bytes of the same value, as an extendable-output function requires. Change the input from 616263 to 616264, ASCII "abd", and the answer changes completely, to 0x5edd927ba15c9dbc3db67a9ab4397ad21da31824e0acb628eefda936b63014a9.

Do not test for these with eth_getCode. It returns 0x for all five, because a precompile lives in the client and has no bytecode. Presence is shown by behaviour instead, and there is a call that separates the two cases cleanly: send four bytes of nonsense such as 0xdeadbeef to 0x0aE1 through 0x0aE4 and each returns a 32-byte zero word, meaning "that is not a valid signature", while the same call to an address with nothing at it, say 0x0000000000000000000000000000000000000afe, returns empty data. Present and refusing looks different from absent, and that difference is the evidence.

What this section does not prove

The SHAKE256 calls above check a hash against a published known answer, which is a real check. The four signature verifiers get no such treatment here: this course never feeds them a valid signature, so nothing on this page shows that they accept a correct one. All the nonsense-input call establishes is that something is there and answers. Before you depend on 0x0aE1 through 0x0aE4, verify them yourself with a known-good keypair and signature of your own making. Note also that the input encoding for these verifiers is specific and not guessable from the interface, so read the SDK rather than assuming the usual "message then signature" layout.

The activation height for all five, block 9,189,161, is stated here from our own records and is not reproducible from a public endpoint, because the endpoint no longer serves state that far back. Section 8 shows the call that demonstrates this limit. What you can check is that they answer correctly now.

7What one-slot finality actually changes

Chain 2800 runs QBFT. A block is committed by a quorum of the validator set within its slot, and committed blocks are not reorganized. Sampling three windows ending at the same height on 2026-08-10, the interval came out at 0.5433 s over the last 2,000 blocks, 0.5506 s over the last 20,000 and 0.5552 s over the last 200,000, so call it a little over half a second and re-measure rather than quoting a digit you did not take yourself. The configured target is not published here because no public call returns it; what you can have is the measurement, and section 8 shows how to take it. Both halves of this matter, and they matter for different reasons.

The half that is about reorganizations, not speed

On a chain with probabilistic finality, a block can be replaced by a longer competing chain, so a careful protocol waits for confirmations before treating anything as settled. That wait is not a user-experience cost, it is a design constraint that reaches into your contracts. A liquidation executed on a price from block N can be undone if block N is replaced, and an adversary who can influence which fork wins can choose whether their liquidation happened. Protocols answer this with confirmation delays, with grace periods, and with conservative parameters that assume the recent past is provisional.

With single-slot finality that entire category of defence is unnecessary, because the situation it defends against does not arise. A settled trade is settled. This is a property of the consensus algorithm, and it is the same property whether the validator set is nine machines or nine hundred, which is exactly why the block time is not the interesting number and the validator set is.

The half that is about the interval

Now combine that interval with the oracle's 300 second staleness window, which you read from the feed in step 6. At a little over half a second per block, 300 seconds spans roughly 540 to 550 blocks. A price is allowed to be several hundred blocks old before the feed refuses it, and you can tighten your own maxAge a long way below the ceiling and still leave reporters room to publish. On a chain with a twelve second interval the same 300 seconds is about 25 blocks, and a tighter window starts to collide with ordinary publication jitter.

Concretely, three things become practical:

What it does not change

Fast finality is not a substitute for the things that actually break DeFi protocols. It does not make the oracle honest; the feed is still an external claim, and on this chain it currently declines to answer for lack of a fresh quorum, which you must handle. It does not fix rounding, and a rounding error compounds faster on a chain producing more blocks per hour, not slower. It does not remove the inflation attack, which is arithmetic and is defeated by the offset in step 2. And it does not make a ten-validator set operated by one organisation into a decentralized one. Weigh that trust boundary on its own terms; the block time has nothing to say about it.

8Verify it yourself

Everything asserted above is reproducible with the calls below. Run them against https://rpc.aere.network. Each one names what a correct answer looks like, and where a check can be made to fail, the failing form is given too, because a check that has never returned a negative cannot be trusted to detect one.

1. Chain identity and current height

curl -s -X POST https://rpc.aere.network -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}'
curl -s -X POST https://rpc.aere.network -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'

Expect 0xaf0 and a height above the 13,212,535 recorded here at 11:18:20 UTC on 2026-08-10. Run the same two calls against https://rpc2.aere.network, the second public endpoint, and expect the same chain ID and a height within a block or two.

2. Re-measure the block interval

Do not take our figure on trust. Read two headers far apart and divide the timestamp difference by the height difference.

for B in 0xc93e50 0xc98d20; do
  curl -s -X POST https://rpc.aere.network -H "Content-Type: application/json" \
    -d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getBlockByNumber\",\"params\":[\"$B\",false],\"id\":1}"
done

Those two heights are 13,188,688 and 13,208,864, and their timestamps were 1786347540 and 1786358699. That is 11159 seconds over 20176 blocks, which is 0.5531 s per block. Those exact two headers are still on the chain, so this one reproduces digit for digit rather than landing near.

Then take windows of your own, and take more than one. Three windows ending at height 13,212,090 gave 0.5433 s over 2,000 blocks, 0.5506 s over 20,000 and 0.5552 s over 200,000. The figure moves with the window, which is why this page quotes a range rather than a single digit. Two things follow. A single pair of adjacent blocks is not a measurement. And the average across the chain's whole life is a different and slower number, 0.62 s, because the chain did not always run at this rate; if you want today's rate, sample today.

3. Confirm the validator set from the chain

curl -s -X POST https://rpc.aere.network -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"qbft_getValidatorsByBlockNumber","params":["latest"],"id":1}'

Returns nine addresses. Count them yourself; do not take the count from any document, including this one. Quorum and fault tolerance follow from that count by formula rather than by assertion: QBFT needs ceil(2N/3) validators to commit a block and tolerates floor((N-1)/3) faulty ones, so at N=10 the quorum is 7 and f is 3. Those two formulas are the ones in the consensus client this chain runs, not a general rule of thumb, and at N=7 they would give 5 and 2.

4. Reproduce the vault arithmetic

# totalSupply, totalAssets, convertToShares(1e18)
V=0x9B580f9118AF4421b270cDb279CF18c03E1573a2
for D in 0x18160ddd 0x01e1d114 \
  0xc6e6f5920000000000000000000000000000000000000000000000000de0b6b3a7640000; do
  curl -s -X POST https://rpc.aere.network -H "Content-Type: application/json" \
    -d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_call\",\"params\":[{\"to\":\"$V\",\"data\":\"$D\"},\"latest\"],\"id\":1}"
done

Feed the first two into assets * (totalSupply + 10**6) / (totalAssets + 1) and the result must equal the third, exactly. Then run it the other way: convertToAssets, selector 0x07a2d13a, called with 1e24 padded to 32 bytes, must equal shares * (totalAssets + 1) / (totalSupply + 10**6), which is 1999000999000999000999 at the state above. One direction agreeing could be luck with the rounding; both agreeing is the formula. If either fails, then either the offset is not 6 on that vault or you read the values at different heights; pin a block number in every call and repeat.

5. The oracle, in both directions

O=0xca69AA961D836516010Ae669a223Ce249490ACb1
# positive: a configuration read that must succeed
curl -s -X POST https://rpc.aere.network -H "Content-Type: application/json" \
  -d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_call\",\"params\":[{\"to\":\"$O\",\"data\":\"0x2bae8236\"},\"latest\"],\"id\":1}"
# negative: a price read that must revert while no quorum is fresh
curl -s -X POST https://rpc.aere.network -H "Content-Type: application/json" \
  -d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_call\",\"params\":[{\"to\":\"$O\",\"data\":\"0x31d98b3f4254432f55534400000000000000000000000000000000000000000000000000\"},\"latest\"],\"id\":1}"

The first returns a 32-byte word ending in 03. The second returns an error whose data is 0x6a1ee67a followed by two 32-byte words, the first all zeros and the second ending in 03. The pair is the point: the same contract answers one call and refuses the other, which is how you know the refusal is a decision and not a dead address. Ask for a symbol that does not exist and you get the same refusal, which tells you the feed is empty rather than that "BTC/USD" specifically is unknown.

6. Check for yourself that a block does not get replaced

curl -s -X POST https://rpc.aere.network -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0xc98d20",false],"id":1}' \
  | grep -o '"hash":"[^"]*"'

Record the hash, wait, run it again. It stays the same. This is evidence over the window you observed, not a proof about all future blocks, and it is worth stating that way rather than overclaiming from one check.

7. The precompiles, positive and negative

Run the SHAKE256 call from step 6 and confirm it matches your local library's SHAKE256("abc", 32) and the published FIPS-202 value. Then change one hex digit of the input, 616263 to 616264, and confirm the answer changes completely. A test that only ever passes has told you nothing.

Then separate presence from absence on the other four. Send four bytes of nonsense to each and compare against an address with nothing at it:

for A in 0x0000000000000000000000000000000000000ae1 \
         0x0000000000000000000000000000000000000ae4 \
         0x0000000000000000000000000000000000000afe; do
  curl -s -X POST https://rpc.aere.network -H "Content-Type: application/json" \
    -d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_call\",\"params\":[{\"to\":\"$A\",\"data\":\"0xdeadbeef\"},\"latest\"],\"id\":1}"
done

The two precompiles return a 32-byte zero word, meaning "not a valid signature". The third address returns empty data, because nothing is there. If all three returned the same thing, this check would be measuring nothing, which is exactly why the empty address is in the loop.

8. Find the limit of what you can check

A verification section that only shows things working is advertising. This call shows you where the public endpoint stops being able to answer:

# header of an old block: served
curl -s -X POST https://rpc.aere.network -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0x8c3729",false],"id":1}'
# state at that same old block: not served
curl -s -X POST https://rpc.aere.network -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_call","params":[{"to":"0x9B580f9118AF4421b270cDb279CF18c03E1573a2","data":"0x18160ddd"},"0x8c3729"],"id":1}'

0x8c3729 is block 9,189,161. The first call returns a header. The second returns an error, because the endpoint keeps recent state, not all of it. This is why the precompile activation height in step 6 is stated from our records rather than proved to you, and it is also a limit worth knowing before you design anything that reads far back in history.