The machine itself

The Contract

GadflyFund.sol, deployed and verified on Robinhood Chain. One file, no imports, no proxy, no upgrade path: 9,355 bytes of runtime that will do exactly this, and only this, forever. This page is the engineering record.

ChainRobinhood Chain · chainId 4663 (Arbitrum Nitro)
Deployed3 September 2026
Compilersolc v0.8.24+commit.e11b9ed9
Settingsoptimizer on, 200 runs · EVM paris · no viaIR
Runtime size9,355 bytes
Deployment gas1,952,841
LicenseMIT · source verified on Blockscout
Dependenciesnone · zero imports, zero libraries, zero inheritance

Design notes

The contract is deliberately primitive. There is no proxy and no upgrade mechanism, because a record book that can be upgraded is not a record book. There is no inheritance and no external dependency, so the verified source on the explorer is the entire trusted computing base: 400-odd lines, readable in one sitting. State is append-only by construction; the ABI exposes no function that deletes or rewrites a stored record, and the erased/restored counters can only increment.

Storage is packed where packing is free. Timestamps are uint64 (sufficient until the year 292 billion), money is uint128 integer cents (the fund thinks in stamps, not wei), and the share's state machine shares slot 2 with both of its counters: state, timesErased, and timesRestored occupy seventeen bytes of one word. Reverts use custom errors rather than strings, which is cheaper and gives every failure mode a stable four-byte name; BadReveal() is 0x8ff14e0d, and the tests assert it by selector.

Every mutation emits an event carrying its full payload, so the fund's complete history is reconstructible from logs alone, without reading contract storage at all; the site does exactly this for transaction references. Access is a single operator address checked by one modifier. This is the honest shape of the trust model rather than a limitation of it: multisigs and timelocks would decorate the operator's power without diminishing it, and the whitepaper's position is that decoration is the enemy.

Exactly one function enforces something an operator could not fake: revealLetter recomputes keccak256 over the submitted text and refuses anything that does not match the stored commitment. The check is three lines. The rest of the contract exists to give those three lines a place to live.


The load-bearing code

Verbatim from the verified source. The commit locks thirty-two bytes; the reveal must reproduce them from raw text, byte for byte, or revert.

/// Lock the hash of a letter's full text before it is mailed.
function commitLetter(bytes32 commitment) external onlyOperator returns (uint256 id) {
    if (commitment == bytes32(0)) revert EmptyCommitment();
    _letters.push();
    id = _letters.length - 1;
    Letter storage l = _letters[id];
    l.commitment = commitment;
    l.committedAt = uint64(block.timestamp);
    emit LetterCommitted(id, commitment, block.timestamp);
}
/// Publish the full text. The contract checks it against the commitment.
/// This check is the only statement in the system that does not require
/// trusting the operator.
function revealLetter(uint256 id, string calldata fullText, string calldata uri)
    external
    onlyOperator
{
    if (id >= _letters.length) revert NothingCommitted();
    Letter storage l = _letters[id];
    if (l.revealedAt != 0) revert AlreadyRevealed();
    if (keccak256(bytes(fullText)) != l.commitment) revert BadReveal();
    l.revealedAt = uint64(block.timestamp);
    l.uri = uri;
    emit LetterRevealed(id, l.commitment, uri, block.timestamp);
}

Notes for the auditor: the text arrives as calldata and is hashed directly from it (no memory copy of the string is retained); the commitment cannot be zero, so an unset slot can never pass; a letter can be revealed exactly once (AlreadyRevealed guards the overwrite); and the out-of-bounds id reverts with NothingCommitted rather than a raw panic, so every failure mode of the rite has a name. Measured cost on the reference tests: commit ≈ 92k gas, reveal ≈ 34–61k depending on text length.


Storage layout

From forge inspect storage-layout. Slot 2 packs the state machine and both Theseus counters into one word.

NameTypeSlotOffsetBytes
operatoraddress0020
targetstring1032
shareStateenum ShareState201
timesEraseduint64218
timesRestoreduint64298
totalSpentCentsuint2563032
totalReceivedCentsuint2564032
_lettersLetter[]5032
_expensesExpense[]6032
_incomeIncome[]7032
_meetingsMeeting[]8032

Custom errors

Every failure mode has a stable four-byte name. The tests assert reverts by selector.

BadReveal()0x8ff14e0drevealed text does not hash to the commitment
NothingCommitted()0x835a24b1letter id does not exist
AlreadyRevealed()0xa89ac151a letter reveals exactly once
EmptyCommitment()0xaac1fcc6the zero hash cannot be committed
NotOperator()0x7c214f04caller is not the operator
NotHolding()0x9e4c8d94cannot erase what is already erased
NotErased()0xcedb69cbcannot restore what is held
ZeroAddress()0xd92e233dguards operator transfer and withdrawals
EthTransferFailed()0x6d963f88treasury ETH send failed
TokenTransferFailed()0x045c4b02treasury token send failed

Function selectors

The complete external surface: 29 functions, from forge inspect methodIdentifiers.

commitLetter(bytes32)0x3821fd6dwrite · operator
revealLetter(uint256,string,string)0x114a47ccwrite · operator · trustless check
recordMailing(uint256,bytes32)0xfff84a82write · operator
recordReply(uint256,bytes32)0x5114a30dwrite · operator
markErased(string)0x2e8acda7write · operator
markRestored(string)0x947c9f2awrite · operator
recordExpense(bytes32,uint128,string)0x6bec9137write · operator
recordIncome(bytes32,uint128,string)0x3b5d9bc4write · operator
recordMeeting(uint16,bool,string,bool,bool,bytes32)0x8437d31dwrite · operator
withdrawEth(address,uint256)0x1b9a91a4write · operator
withdrawToken(address,address,uint256)0x01e33667write · operator
setOperator(address)0xb3ab15fbwrite · operator
counters()0xf7210633view
getLetter(uint256)0x96e583a9view
lettersCount()0x798ebe2fview
getExpense(uint256)0x735e5db6view
expensesCount()0x4f5b35dcview
getIncome(uint256)0x85791510view
incomeCount()0xafb7afbbview
getMeeting(uint256)0x2d703f88view
meetingsCount()0x0a3b2f2cview
operator()0x570ca735view
target()0xd4b83992view
inceptionAt()0x8708b006view
shareState()0x4745fe4fview
timesErased()0x997f6526view
timesRestored()0xe862e5beview
totalSpentCents()0xf9138dc8view
totalReceivedCents()0x26a1cefdview

Events

One event per mutation; the fund's full history reconstructs from logs alone.

LetterCommitted(uint256,bytes32,uint256)0xfafaabdc21d3c93564291d137ecea0ca9cb86f926fc41b4624d16a49ae526f18
LetterRevealed(uint256,bytes32,string,uint256)0x6f0fa5e12a63798ff2b929f883fe9934a96e978e1f370f61d3e9731daa802b06
LetterMailed(uint256,bytes32,uint256)0x4dc236515560be8d8e27ec1fec69584299a7320a0d86785d1bc587c0d17fe1f6
ReplyRecorded(uint256,bytes32,uint256)0x4ee27fc2420d15032356a858cd3e9da588ba06f45bfc63d9481445c4e16b1b1f
ShareErased(uint256,string,uint256)0x8756aa1eec173086ce3b55e03689c79e7de61adb32e9c27dd1221601217a98b6
ShareRestored(uint256,string,uint256)0x76fbdb7b27dbfbd06cca7e2ce30f3c2cc49fa1ed191e732db84fbd2ada11f5f6
ExpenseRecorded(uint256,bytes32,uint128,string,uint256)0x07a3cf0a95a77c87ac5ea69d8fb65870568a47fb2cdb6a2154d0d93468fb2a6d
IncomeRecorded(uint256,bytes32,uint128,string,uint256)0xb251e795488b0cd921501e9628c3efe62117da6d8cb666c761518fe37cff284e
MeetingRecorded(uint256,uint16,bool,bool,bool,uint256)0xfa842dbb070ba38d1b52a05571888b265ceae375fce9d597d336e86dcf2fdc92
Received(address,uint256)0x88a5966d370b9919b20f3e2c13ff65706f196a4e32cc2c12bf57088f88525874
EthWithdrawn(address,uint256)0x8455ae6be5d92f1df1c3c1484388e247a36c7e60d72055ae216dbc258f257d4b
TokenWithdrawn(address,address,uint256)0x8210728e7c071f615b840ee026032693858fbcd5e5359e67e438c890f59e5620
OperatorChanged(address,address)0xd58299b712891143e76310d5e664c4203c940a67db37cf856bdaa3c5c76a802c

Measured gas

From forge test --gas-report over the 24-test suite (incl. 256-run fuzz campaigns). Min/avg/max in gas units.

FunctionMinAvgMax
deployment1,952,841
commitLetter23,66091,70192,472
revealLetter26,19446,60261,428
recordMailing24,23935,37655,527
recordReply24,19635,33355,484
markErased24,23936,61750,344
markRestored24,21628,97632,774
recordExpense24,674122,343184,733
recordIncome24,58382,218139,853
recordMeeting25,22295,336162,985
withdrawEth23,88841,52259,862
setOperator23,78225,35828,499

Tests

The Foundry suite runs 24 tests against the fund: the commit–reveal equality (matching text passes; one altered character reverts BadReveal; an unknown id reverts NothingCommitted; a second reveal reverts AlreadyRevealed), a 256-run fuzz campaign asserting that for arbitrary text the exact bytes verify and any mutation reverts, the erased→restored round trip with counter monotonicity, expense and income accumulation with event assertions, treasury deposit and withdrawal, and operator gating on every mutator. The suite is in the repository at test/GadflyFund.t.sol and completes in under twenty milliseconds.


The full verified source

The complete verified source, byte-identical to what the explorer serves. Reading time: one coffee. Trusted computing base: this, and nothing else.

GadflyFund.sol · 14,171 characters
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

/*
 * ────────────────────────────────────────────────────────────────────────────
 *  GADFLY FUND
 *  The smallest activist fund in the world.
 *  AUM: one share. Fully exercised.
 *
 *  Deployed on Robinhood Chain (chainId 4663).
 *
 *  The contract is a notarized diary that grew a coin pocket. The one share
 *  lives off-chain in the operator's custody; the treasury that pays for
 *  the stamps lives here, fed by the GADFLY token's trading tax, and is
 *  withdrawable by the operator alone, without restriction, as disclosed.
 *  It records six kinds of facts:
 *
 *    1. Letters, under commit-reveal. The keccak256 hash of a letter's full
 *       UTF-8 text is locked on-chain BEFORE the letter is mailed. After
 *       mailing, the full text is revealed and the contract checks that the
 *       published text hashes to the locked commitment. This equality is the
 *       only trustless moment in the entire system: it proves that the letter
 *       the public reads is the letter the CEO received. Everything else is
 *       testimony.
 *    2. Mailing receipts and replies, as document hashes (testimony).
 *    3. The state of the one share: HOLDING or ERASED, with counters for how
 *       many times corporate arithmetic (reverse splits) deleted the share
 *       and how many times the fund bought it back. The Theseus clause.
 *    4. Expenses, in integer US cents, by category (testimony).
 *    5. Income, likewise: token-tax sweeps and anything else that funds
 *       the rites (testimony).
 *    6. Annual-meeting attendance and the one question asked each year
 *       (testimony).
 *
 *  This file is the canonical contract: the tests, the deploy script, and
 *  the site's ABI are all derived from it.
 * ────────────────────────────────────────────────────────────────────────────
 */

contract GadflyFund {
    // ── Errors ──────────────────────────────────────────────────────────────

    error NotOperator();
    error ZeroAddress();
    error EmptyCommitment();
    error NothingCommitted();
    error AlreadyRevealed();
    error BadReveal();
    error NotHolding();
    error NotErased();
    error EthTransferFailed();
    error TokenTransferFailed();

    // ── Types ───────────────────────────────────────────────────────────────

    enum ShareState {
        HOLDING,
        ERASED
    }

    struct Letter {
        bytes32 commitment;  // keccak256 of the letter's full UTF-8 text
        uint64 committedAt;  // block timestamp of the commit
        uint64 revealedAt;   // 0 while the letter is still sealed
        uint64 mailedAt;     // 0 until a mailing receipt is recorded
        uint64 repliedAt;    // 0 while the company is silent
        bytes32 receiptHash; // hash of the registered-mail receipt scan
        bytes32 replyHash;   // hash of the company's reply scan; 0 = silence
        string uri;          // where the revealed full text is published
    }

    struct Expense {
        bytes32 category;    // short tag, e.g. bytes32("POSTAGE")
        uint128 amountCents; // US cents; the fund thinks in stamps
        uint64 recordedAt;
        string memo;
    }

    struct Income {
        bytes32 category;    // e.g. bytes32("TAX") for token-tax sweeps
        uint128 amountCents; // USD value as recorded by the operator
        uint64 recordedAt;
        string memo;
    }

    struct Meeting {
        uint16 year;              // meeting year, e.g. 2025
        bool attendedInPerson;
        bool questionReadAloud;   // was the question read / asked on the floor
        bool answered;            // did any answer follow, of any quality
        uint64 recordedAt;
        bytes32 evidenceHash;     // hash of the operator's notes or exhibits
        string question;          // the one question, verbatim
    }

    // ── Events ──────────────────────────────────────────────────────────────

    event LetterCommitted(uint256 indexed id, bytes32 commitment, uint256 at);
    event LetterRevealed(uint256 indexed id, bytes32 commitment, string uri, uint256 at);
    event LetterMailed(uint256 indexed id, bytes32 receiptHash, uint256 at);
    event ReplyRecorded(uint256 indexed id, bytes32 replyHash, uint256 at);
    event ShareErased(uint256 timesErased, string note, uint256 at);
    event ShareRestored(uint256 timesRestored, string note, uint256 at);
    event ExpenseRecorded(
        uint256 indexed id, bytes32 indexed category, uint128 amountCents, string memo, uint256 at
    );
    event IncomeRecorded(
        uint256 indexed id, bytes32 indexed category, uint128 amountCents, string memo, uint256 at
    );
    event Received(address indexed from, uint256 amount);
    event EthWithdrawn(address indexed to, uint256 amount);
    event TokenWithdrawn(address indexed token, address indexed to, uint256 amount);
    event MeetingRecorded(
        uint256 indexed id,
        uint16 indexed year,
        bool attendedInPerson,
        bool questionReadAloud,
        bool answered,
        uint256 at
    );
    event OperatorChanged(address indexed previousOperator, address indexed newOperator);

    // ── Storage ─────────────────────────────────────────────────────────────

    address public operator;
    string public target; // ticker of the company whose one share the fund holds
    uint64 public immutable inceptionAt;

    ShareState public shareState; // HOLDING at deployment
    uint64 public timesErased;
    uint64 public timesRestored;

    uint256 public totalSpentCents;
    uint256 public totalReceivedCents;

    Letter[] private _letters;
    Expense[] private _expenses;
    Income[] private _income;
    Meeting[] private _meetings;

    // ── Modifiers ───────────────────────────────────────────────────────────

    modifier onlyOperator() {
        if (msg.sender != operator) revert NotOperator();
        _;
    }

    constructor(string memory target_) {
        operator = msg.sender;
        target = target_;
        inceptionAt = uint64(block.timestamp);
    }

    // ── Letters: the one trustless moment ───────────────────────────────────

    /// Lock the hash of a letter's full text before it is mailed.
    function commitLetter(bytes32 commitment) external onlyOperator returns (uint256 id) {
        if (commitment == bytes32(0)) revert EmptyCommitment();
        _letters.push();
        id = _letters.length - 1;
        Letter storage l = _letters[id];
        l.commitment = commitment;
        l.committedAt = uint64(block.timestamp);
        emit LetterCommitted(id, commitment, block.timestamp);
    }

    /// Publish the full text. The contract checks it against the commitment.
    /// This check is the only statement in the system that does not require
    /// trusting the operator.
    function revealLetter(uint256 id, string calldata fullText, string calldata uri)
        external
        onlyOperator
    {
        if (id >= _letters.length) revert NothingCommitted();
        Letter storage l = _letters[id];
        if (l.revealedAt != 0) revert AlreadyRevealed();
        if (keccak256(bytes(fullText)) != l.commitment) revert BadReveal();
        l.revealedAt = uint64(block.timestamp);
        l.uri = uri;
        emit LetterRevealed(id, l.commitment, uri, block.timestamp);
    }

    /// Record the hash of the registered-mail receipt. Testimony, not proof:
    /// the chain cannot watch a letter enter a mailbox.
    function recordMailing(uint256 id, bytes32 receiptHash) external onlyOperator {
        if (id >= _letters.length) revert NothingCommitted();
        Letter storage l = _letters[id];
        l.mailedAt = uint64(block.timestamp);
        l.receiptHash = receiptHash;
        emit LetterMailed(id, receiptHash, block.timestamp);
    }

    /// Record the hash of a reply, should one ever arrive.
    function recordReply(uint256 id, bytes32 replyHash) external onlyOperator {
        if (id >= _letters.length) revert NothingCommitted();
        Letter storage l = _letters[id];
        l.repliedAt = uint64(block.timestamp);
        l.replyHash = replyHash;
        emit ReplyRecorded(id, replyHash, block.timestamp);
    }

    // ── The Theseus clause ──────────────────────────────────────────────────

    /// A reverse split (or similar corporate act) deleted the share.
    function markErased(string calldata note) external onlyOperator {
        if (shareState != ShareState.HOLDING) revert NotHolding();
        shareState = ShareState.ERASED;
        timesErased += 1;
        emit ShareErased(timesErased, note, block.timestamp);
    }

    /// The fund bought the share back. Same fund, new plank.
    function markRestored(string calldata note) external onlyOperator {
        if (shareState != ShareState.ERASED) revert NotErased();
        shareState = ShareState.HOLDING;
        timesRestored += 1;
        emit ShareRestored(timesRestored, note, block.timestamp);
    }

    // ── The ledger ──────────────────────────────────────────────────────────

    function recordExpense(bytes32 category, uint128 amountCents, string calldata memo)
        external
        onlyOperator
        returns (uint256 id)
    {
        _expenses.push(
            Expense({
                category: category,
                amountCents: amountCents,
                recordedAt: uint64(block.timestamp),
                memo: memo
            })
        );
        id = _expenses.length - 1;
        totalSpentCents += amountCents;
        emit ExpenseRecorded(id, category, amountCents, memo, block.timestamp);
    }

    function recordIncome(bytes32 category, uint128 amountCents, string calldata memo)
        external
        onlyOperator
        returns (uint256 id)
    {
        _income.push(
            Income({
                category: category,
                amountCents: amountCents,
                recordedAt: uint64(block.timestamp),
                memo: memo
            })
        );
        id = _income.length - 1;
        totalReceivedCents += amountCents;
        emit IncomeRecorded(id, category, amountCents, memo, block.timestamp);
    }

    // ── The treasury ────────────────────────────────────────────────────────
    // The token's trading tax lands here, in GADFLY and eventually in ETH.
    // The operator withdraws it to buy stamps and seats, and records what
    // everything was worth via recordIncome / recordExpense. The withdrawals
    // are unrestricted by design and disclosed as such: this fund's deepest
    // dependency has always been the operator's honesty.

    receive() external payable {
        emit Received(msg.sender, msg.value);
    }

    function withdrawEth(address payable to, uint256 amount) external onlyOperator {
        if (to == address(0)) revert ZeroAddress();
        (bool ok,) = to.call{value: amount}("");
        if (!ok) revert EthTransferFailed();
        emit EthWithdrawn(to, amount);
    }

    function withdrawToken(address token, address to, uint256 amount) external onlyOperator {
        if (to == address(0)) revert ZeroAddress();
        // transfer(address,uint256); tolerate ERC-20s that return nothing.
        (bool ok, bytes memory data) =
            token.call(abi.encodeWithSelector(0xa9059cbb, to, amount));
        if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) {
            revert TokenTransferFailed();
        }
        emit TokenWithdrawn(token, to, amount);
    }

    // ── The question ────────────────────────────────────────────────────────

    function recordMeeting(
        uint16 year,
        bool attendedInPerson,
        string calldata question,
        bool questionReadAloud,
        bool answered,
        bytes32 evidenceHash
    ) external onlyOperator returns (uint256 id) {
        _meetings.push(
            Meeting({
                year: year,
                attendedInPerson: attendedInPerson,
                questionReadAloud: questionReadAloud,
                answered: answered,
                recordedAt: uint64(block.timestamp),
                evidenceHash: evidenceHash,
                question: question
            })
        );
        id = _meetings.length - 1;
        emit MeetingRecorded(id, year, attendedInPerson, questionReadAloud, answered, block.timestamp);
    }

    // ── Operator ────────────────────────────────────────────────────────────

    function setOperator(address newOperator) external onlyOperator {
        if (newOperator == address(0)) revert ZeroAddress();
        emit OperatorChanged(operator, newOperator);
        operator = newOperator;
    }

    // ── Views ───────────────────────────────────────────────────────────────

    function lettersCount() external view returns (uint256) {
        return _letters.length;
    }

    function getLetter(uint256 id) external view returns (Letter memory) {
        if (id >= _letters.length) revert NothingCommitted();
        return _letters[id];
    }

    function expensesCount() external view returns (uint256) {
        return _expenses.length;
    }

    function getExpense(uint256 id) external view returns (Expense memory) {
        return _expenses[id];
    }

    function incomeCount() external view returns (uint256) {
        return _income.length;
    }

    function getIncome(uint256 id) external view returns (Income memory) {
        return _income[id];
    }

    function meetingsCount() external view returns (uint256) {
        return _meetings.length;
    }

    function getMeeting(uint256 id) external view returns (Meeting memory) {
        return _meetings[id];
    }

    /// One call for the site's front page.
    function counters()
        external
        view
        returns (
            uint256 letters_,
            uint256 expenses_,
            uint256 meetings_,
            uint64 erased_,
            uint64 restored_,
            ShareState state_,
            uint256 totalSpentCents_
        )
    {
        return (
            _letters.length,
            _expenses.length,
            _meetings.length,
            timesErased,
            timesRestored,
            shareState,
            totalSpentCents
        );
    }
}