Files
mintel.me/apps/web/src/content/notes/functional-core-imperative-shell.tsx
T

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,
};