122 lines
5.4 KiB
TypeScript
122 lines
5.4 KiB
TypeScript
import * as React from "react";
|
|
import { CoreShellDiagram } from "@/src/components/notes/CoreShellDiagram";
|
|
import {
|
|
NoteFigure,
|
|
NoteList,
|
|
NoteParagraph,
|
|
NoteSection,
|
|
} from "@/src/components/notes/NoteProse";
|
|
import type { Note } from "./types";
|
|
|
|
const Body: React.FC = () => (
|
|
<>
|
|
<NoteParagraph>
|
|
I am building a liquidation system that runs across 7 chains. It is
|
|
written in Rust, with Solidity executors on chain. It is built to run
|
|
unattended, which changes how much you can rely on manual checking: almost
|
|
all of the confidence has to come from tests that run before the code
|
|
does. The architecture that makes this workable is a functional core with
|
|
imperative shells.
|
|
</NoteParagraph>
|
|
|
|
<NoteSection>The shape</NoteSection>
|
|
<NoteParagraph>
|
|
A detector per chain indexes borrower positions and computes health
|
|
factors. That work is split in two. The math lives in a pure domain crate:
|
|
health factor, fixed-point arithmetic, and a closed-form optimum that is
|
|
checked against an exact swap engine. The crate has no network, no clock,
|
|
no randomness and no global state. Everything arrives as arguments and
|
|
leaves as return values.
|
|
</NoteParagraph>
|
|
<NoteParagraph>
|
|
Around that core sit thin shells. A shell reads state from a chain,
|
|
converts it into plain data, calls the core, and acts on the answer. It
|
|
contains almost no decisions, so there is little in it that can be wrong
|
|
in an interesting way.
|
|
</NoteParagraph>
|
|
|
|
<NoteFigure caption="Data flows down; only the shells touch the network.">
|
|
<CoreShellDiagram />
|
|
</NoteFigure>
|
|
|
|
<NoteSection>The on-chain side</NoteSection>
|
|
<NoteParagraph>
|
|
The executor contract does one job in one transaction. It takes a flash
|
|
loan, liquidates the position, swaps the collateral, repays the loan, and
|
|
reverts if the trade would not be profitable. Because the transaction is
|
|
atomic, a failed attempt leaves nothing behind except the gas for the
|
|
attempt. No own capital is at risk per attempt, which is a property of the
|
|
design rather than of careful operation.
|
|
</NoteParagraph>
|
|
<NoteParagraph>
|
|
The same idea applies on chain as off chain: the contract is a small,
|
|
closed piece of logic with explicit inputs. The off-chain core decides
|
|
whether to try; the contract has the final say.
|
|
</NoteParagraph>
|
|
|
|
<NoteSection>Testing the edge, not the middle</NoteSection>
|
|
<NoteParagraph>
|
|
Fixed-point arithmetic has cliffs. Past a certain input size a
|
|
multiplication no longer fits in the integer type. If the code wraps
|
|
around, a hugely healthy position can suddenly look unhealthy, or the
|
|
other way round, and nothing crashes to tell you.
|
|
</NoteParagraph>
|
|
<NoteParagraph>
|
|
One spec in the domain crate tests the exact overflow cliff of the health
|
|
factor. It pins the behavior one step below the cliff and at the cliff,
|
|
and requires the result to saturate instead of wrapping. This kind of case
|
|
is where an optimizing refactor or a generated change breaks things
|
|
silently, and it is trivial to write because the core is a pure function:
|
|
call it with the boundary values and compare the result.
|
|
</NoteParagraph>
|
|
|
|
<NoteSection>Why the split pays off</NoteSection>
|
|
<NoteList>
|
|
<li>
|
|
<strong>The core is testable without a network.</strong> Specs run fast,
|
|
with no node, no fork and no flaky timing. That makes spec-first
|
|
practical for the part with the most arithmetic.
|
|
</li>
|
|
<li>
|
|
<strong>Mutation testing can prove the specs.</strong> Pure functions
|
|
give mutation tools clean targets. If a mutant of the health factor
|
|
survives, a spec is missing, and the gate says so. That is a stronger
|
|
statement than a coverage percentage.
|
|
</li>
|
|
<li>
|
|
<strong>Shells are proven by end-to-end specs.</strong> They are not
|
|
pure, so they are not tested like the core. They are exercised as a
|
|
whole against the behavior they are meant to produce, and because they
|
|
hold so little logic, those specs stay small.
|
|
</li>
|
|
</NoteList>
|
|
|
|
<NoteSection>Where it hurts</NoteSection>
|
|
<NoteParagraph>
|
|
The discipline has a cost. You have to keep deciding where a line of code
|
|
belongs, and the temptation to put a small calculation in the shell
|
|
because it is right there is constant. Every such shortcut moves logic
|
|
into the part that is hardest to test. The gates described in the note on
|
|
mechanical spec-first enforcement help here: a gate that forbids methods,
|
|
mutable references and unwrap pushes the code toward the style the core
|
|
depends on.
|
|
</NoteParagraph>
|
|
<NoteParagraph>
|
|
The pattern is not specific to this project. Any system where money,
|
|
permissions or irreversible actions sit behind a thin layer of I/O gains
|
|
from keeping the decision logic pure and the I/O boring.
|
|
</NoteParagraph>
|
|
</>
|
|
);
|
|
|
|
export const functionalCoreImperativeShell: Note = {
|
|
slug: "functional-core-imperative-shell-multi-chain",
|
|
title: "Functional core, imperative shell in a multi-chain system",
|
|
summary:
|
|
"How a pure Rust core, thin shells and an atomic on-chain executor make a 7-chain liquidation system testable.",
|
|
publishedOn: "2026-10-02",
|
|
readingMinutes: 4,
|
|
tags: ["architecture", "rust", "web3"],
|
|
Body,
|
|
};
|