Docs

How a club works

Everything Henge does, in the order it happens — and every contract it touches, none of them ours.

A club

A club is a Safe 1.4.1 account created by Safe's own SafeProxyFactory with the SafeL2 singleton. Every member is an owner; the club's votes to act is Safe's threshold. There is no module, no guard, and no contract of Henge's anywhere: one transaction, createProxyWithNonce(SafeL2, setup(members, votes, …), salt), builds it.

The club's name rides along after that call's own arguments — the bytes HENGE followed by the ABI-encoded name. Solidity ignores trailing calldata, so Safe never sees it; the page reads it back from the creation transaction.

The factory has made 2,615 Safes on Robinhood Chain so far. The code at every Safe address Henge uses is byte-for-byte the code at the same address on Arbitrum and Base (below).

Proposals, on chain

Anything a club does is one Safe transaction: a single call, or several through Safe's MultiSendCallOnly (which refuses delegatecalls). Its EIP-712 hash — domain (chainId 4663, club), every gas field zero, and the nonce it will run at — is what members vote on.

To propose, a member calls the club's own approveHash(hash), which is also their yes vote, and appends HENGE ‖ abi.encode(to, value, data, operation, nonce, note) to the calldata. Safe reads the first 36 bytes and ignores the rest. Every other member's page finds the ApproveHash event, fetches that transaction, decodes the appendix, re-derives the hash from it and discards anything that does not match. The proposal cannot be edited after the fact: a different description is a different hash, which nobody voted for.

What a proposal does is then read from its calldata, step by step: approvals (only to Uniswap's router, only for listed tokens), swaps (only paying the club), transfers, and changes to members or votes. A step Henge cannot read is shown as such, and Henge will not offer a yes vote on it.

Votes

A yes is approveHash(hash) from the member's wallet; Safe records it in its own approvedHashes mapping, which is what the page reads. A no is a yes for Safe's standard rejection: an empty transaction from the club to itself at the same nonce.

When enough members approved either, anyone sends execTransaction with one "approved" signature per member (r = member, s = 0, v = 1), sorted by address. Safe accepts each only if that member approved the hash on chain — or is the sender, which is how the deciding vote and the execution are one click. Carrying out a rejection spends the nonce, so the proposal it rejected can never run.

Proposals run in nonce order. The page numbers a new one after every proposal already waiting; if one stalls, voting it down clears the way.

Shares

A member's share is the USDG they sent to the club (Transfer events from that member's address to the club), divided by what all current members sent. Stocks moved in directly and dollars from non-members count for nobody — they are a gift to the club. A club nobody has put dollars into is shared equally.

A payout sends each member their share of a fraction of every holding, in kind, rounded down (the dust stays). A member who leaves is paid their share of every holding and removed with removeOwner, in one transaction; the votes needed drop only if they would otherwise exceed the members left. Both keep every remaining member's share exactly as it was.

Shares count dollars, not timing. A member who joins after the club has grown gets a share of that growth unless the club agrees otherwise; clubs that chip in equal amounts at the same time, like dues, never meet the problem.

Trades

A buy is approve(router, exactly the total) followed by one Uniswap exactInputSingle per stock, from USDG, paid to the club, with amountOutMinimum 3% under Uniswap's own quote when proposed. A sale approves exactly the shares sold and swaps them to USDG for the club. Each stock trades in its deepest USDG pool on Robinhood Chain (42 stocks in data/markets.json).

If the price moves past a floor before the votes are in, the swap reverts, Safe reverts the whole transaction (GS013), and nothing is spent. The page checks every proposal that is next in line against today's prices — simulated as the club, with the vote count set aside — and says whether it would go through.

What the page reads

No server and no database. A club's history is its own event log (ApproveHash, ExecutionSuccess, and SafeMultiSigTransaction, which SafeL2 emits with every executed transaction's full contents), USDG's Transfer events into it, and the transactions that published its proposals. Your clubs are found by asking every SafeL2 Safe the factory has made whether your wallet is an owner. Prices are each pool's own slot0; quotes are Uniswap's QuoterV2.

Risks

  • A club does what its votes say. Enough members can vote to send everything to one of them. Choose members you trust and a number of votes no small group reaches alone.
  • Stale proposals. A trade's floor protects the club from a bad price; it also means a slow vote can expire a good trade. Vote it down and propose again.
  • Shares are by dollars in. See above.
  • The stocks. Robinhood's stock tokens can be paused, blocklisted and burned by Robinhood. Uniswap pools can be thin; the page shows the cost of every trade before anyone votes.
  • This page. It builds transactions your wallet signs. It is open, tested (below) and needs no server, but read what your wallet shows before you sign.

Addresses

Every contract Henge calls, and whether its code is identical to the same address on Arbitrum and Base (read at block 76,893,115, 30 Sep 2026). Uniswap's periphery and USDG are deployed per chain.

ContractAddressSame code elsewhere
USDG (Global Dollar, Paxos)0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168per chain
Uniswap SwapRouter020xCaf681a66D020601342297493863E78C959E5cb2per chain
Uniswap QuoterV20x33e885eD0Ec9bF04EcfB19341582aADCb4c8A9E7per chain
Uniswap v3 Factory0x1f7d7550B1b028f7571E69A784071F0205FD2EfAper chain
Multicall30xcA11bde05977b3631167028862bE2a173976CA11yes
Safe 1.4.1 SafeProxyFactory0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67yes
Safe 1.4.1 SafeL2 (singleton)0x29fcB43b46531BcA003ddC8FCB67FFE91900C762yes
Safe 1.4.1 CompatibilityFallbackHandler0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99yes
Safe 1.4.1 MultiSendCallOnly0x9641d764fc13c8B624c04430C7356C1C7C8102e2yes

Tests

The last run: 15/15 properties and 194 checks passed against live state at block 76,903,740 (30 Sep 2026). Each property builds its transactions with the page's own js/henge.js and runs them with eth_simulateV1 against the real Safe contracts and Uniswap pools, from test wallets with real keys. Nothing is broadcast.

Then a sabotage sweep plants 34 bugs, one at a time, in a copy of js/henge.js — a trade paid to the wrong wallet, a floor dropped, a proposal whose description differs from what is voted on, a payout that favours one member — and requires the property named for each one to fail. 34/34 were caught by the property named for them.

The browser run: 8/8 journeys (40 checks) clicked through the real pages in Chrome as four test members and a stranger, against a private copy of the live chain (01 Oct 2026).

PropertyWhat it provesChecks
create3 members, 2 votes, SafeL2 + standard fallback, no modules, name on chain; 307k gas11
rulesa duplicate member (GS204), more votes than members (GS201) and zero votes (GS202) are refused by the page AND by Safe; a club without its founder or a name by the page15
publishhash == Safe's getTransactionHash; ApproveHash(hash, proposer) emitted and recorded; the appendix decodes exactly; a swapped description, another club and a bare vote all read as nothing10
membersstrangers cannot propose or vote (GS030); one vote of two is refused (GS020), a claimed vote never cast is refused (GS025); the second member votes-and-executes in one tx; replay refused (GS025)18
buy$700 out, 1.74125 NVDA and 0.89820 AAPL in, each ≥ its floor, allowance back to 013
floora floor 1% above the quote: Safe refuses the whole transaction (GS013), nothing moves; the same buy at a 3% floor fills; trades without a floor are never built10
sellsold exactly 0.422808 TSLA for 149.869963 USDG ≥ floor; allowance 010
rejecta rejection at the same nonce (Safe's own hash) reached 2 votes and spent the nonce; the struck proposal is refused forever (GS025) even with 2 yes; the next one runs14
orderthe next proposal takes nonce+1; with full votes it is refused (GS025) until the one before it runs, then it runs10
sharesfrom USDG's own Transfer events: 550/300/200 of 1,050; the stranger's $999 counts for nobody; shares sum to exactly 115
payouthalf of the dollars AND half of the NVDA went out 50/30/20, each amount exact to the unit; the club kept the other half17
leaveb was paid exactly 30% of the dollars and of the NVDA and removed in one vote; a and c keep 5:2; b can no longer vote (GS030); the head of the owner list leaves too, threshold drops to 1 of 119
settingsd joined with 3-of-4, proposed; 2 votes were refused (GS020); the third carried it; the page refuses duplicates, impossible thresholds and the club as its own member12
describebuys, sales, sends and the rejection read back exactly; a swap paying someone else, a foreign approval, a foreign delegatecall and a module switch are all flagged as unreadable10
previewslot 4 is the threshold; with the page's override an affordable send previews OK and an unaffordable one GS013; without it the same call is GS020; nothing moves10