The pieces
One deployment (contracts/script/DeployArc.s.sol) puts up:
| contract | role |
|---|---|
EdenDock, the dock | the one-transaction creation (a docking), with the chosen clauses; a first buy paid in native USDC included |
EdenRing, the ring (the hook) | takes each coin's fee on the pair-asset side of every swap, and runs the coin's clauses |
EdenVault, the vault | holds every coin's liquidity forever, harvests and splits fees |
| 15 clause modules | the covenant's clauses (see the end of this chapter) |
PairRegistry | pair-asset eligibility: two floors, plus admit/refuse rulings |
DepthComposite | asks every venue and keeps the best answer |
| the V3, Slipstream, V4 and V2 depth gates | depth on Uniswap V3, Aero (Arc) and Archery, Uniswap v4, and V2-style venues (Uniswap V2, DYORSwap) |
The on-chain vocabulary is the contracts' own: a creation is a docking, its creator its founder, the fee route its payout mode. contracts/RENAMES.md in the repo maps each to the role above.
The coin itself
Every coin created here is the same coin contract, and every one shows as verified without anyone submitting it. The coin takes no constructor arguments (it calls the dock back for its name, ticker and supply), so every coin has the same bytecode apart from a few values set at creation; verifying one on the explorer is enough for it to recognise all the others, including those not made yet.
What that verified source says the coin cannot do: no mint, no transfer tax, no blacklist, no pause, no owner. One thing is set at creation: if the coin's covenant watches transfers (Portion, Ember), the coin reports each transfer to the ring (its watcher()), which can refuse one that breaks a clause. A coin without such a clause has no watcher and calls nobody.
External contracts
| Uniswap v4 PoolManager | 0x8366a39CC670B4001A1121B8F6A443A643e40951 |
| Uniswap v4 StateView (read only) | 0xF3334192D15450CdD385c8B70e03f9A6bD9E673b |
| Uniswap v4 Quoter | 0x8Dc178eFB8111BB0973Dd9d722ebeFF267c98F94 |
| Uniswap Universal Router (2.1.2) | 0x8702463e73f74d0b6765aBceb314Ef07aCb92650 |
| Permit2 | 0x000000000022D473030F116dDEE9F6B43aC78BA3 |
| Uniswap V3 factory (measurement only) | 0xf0db7b58379503491d857dB50AC9ece64c653918 |
| Aero (Arc) and Archery CL factories (measurement only) | 0xb89Df768aF2CFE637ceB352c587Fe8edAf491d03, 0xC481038C013FE96F38CE7A2dC417b2B1B78b16A4 |
| Uniswap V2 and DYORSwap factories (measurement only) | 0x89e5DB8B5aA49aA85AC63f691524311AEB649eba, 0x942Bd5BFdc5317C5507e326f8EB4BB6058AB5C10 |
| USDC (the ERC-20 view of the gas token, 6 decimals) | 0x3600000000000000000000000000000000000000 |
| WETH (bridged), cirBTC, EURC | 0x128cC466B61f542da60c70e3aA11c10e19B84EDB, 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0, 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 |
| Chainlink ETH/USD, BTC/USD, EURC/USD (8 decimals) | 0x50FCDD99D6762D1C170DC6A9111db944AEE6D364, 0xa109B535C70C8Be9995be64Bb6751AcDB27e03De, 0x361b95c10b76Ca3f35C686d423e43A951755Bf23 |
| KyberSwap router (the USDC leg of a first buy on a token pair) | 0x6131B5fae19EA4f9D964eAc0408E4408b66337b5 |
The anchors are USDC (at par; native USDC counts as the ERC-20), WETH, cirBTC and EURC. Anything that reads a Chainlink feed calls decimals() rather than assuming it, and accepts an answer up to 30 hours old: every feed on Arc updates on a 24-hour heartbeat.
USDC as the gas token
Arc pays gas in USDC, and the same dollars exist twice: as the native balance (sent as value, 18 decimals) and as the ERC-20 at 0x3600… (6 decimals). One balance, two views. A coin paired with native USDC (the default, quote == address(0), Uniswap v4's currency zero) takes its first buy, pays its fees and pays holders as plain value transfers; a coin paired with the ERC-20 is bought with the same dollars through an approval. There is no wrapped native asset and nothing to wrap.
Calls
Before creating
// Never reverts. Says whether the asset would be accepted, and if not, why.
registry.verdict(address quote)
returns (bool pairable, Ruling ruling, Reading reading) // Ruling: Measure / Admit / Refuse
// The floor this asset must clear: $10,000 if listed, $25,000 otherwise.
registry.floorFor(address quote) returns (uint256)
Registering a v4 pool
A v4 pool is the hash of its key, and the key holds a 160-bit hook address: verifiable, never enumerable. So a v4 pool is registered once for the gate to see it. Anyone can do it, for everyone.
v4Gate.watchPool(PoolKey key) returns (address token)
v4Gate.watchDocking(address token) returns (address) // for a coin created here
v4Gate.glance(PoolKey key) returns (uint256 usdE8, address anchor) // read only
V2-style venues, Uniswap V3 and the Slipstream factories are looked up directly; a coin with a market there counts as soon as its address is pasted.
Creating
dock.dock(DockConfig cfg, Rulebook rules) payable returns (address token, bytes32 poolId)
// First buy paid in native USDC on a token pair: an allowed router (KyberSwap, the Universal Router)
// swaps it into the pair asset, inside the creation. Against the ERC-20 USDC nothing is swapped.
dock.dockWithUsdc(DockConfig cfg, UsdcFirstBuy swap, Rulebook rules) payable returns (address token, bytes32 poolId)
dock.predictCoin(address founder, bytes32 salt) returns (address)
dock.dockFee() returns (uint256) // 5 USDC, in wei (18 decimals)
DockConfig is { name, symbol, manifestURI, supply, quote, tradeFee, tickSpacing, tickLower, tickUpper, premineBps, payoutMode, payee, firstBuyQuote, firstBuyMinOut, salt }. Beyond the obvious:
| field | role |
|---|---|
premineBps | share of supply the creator keeps, capped at 20% |
payoutMode | 0 (Keep) pays the creator, 1 (Burn) buys the coin back and burns it, 2 (Share) pays the holders (final, needs a paymaster) |
payee | zero means the creating wallet |
firstBuyQuote | pair-asset amount spent on the pool's first trade; needs an approval to the dock first (sent as value on a native USDC pair) |
firstBuyMinOut | the least accepted for that buy; the floor against the pool and the creator disagreeing on the opening price |
UsdcFirstBuy is { router, data, minOut }: the allowed router that turns the USDC into the pair asset, its calldata, and the least it may deliver (zero router and empty data on a USDC pair).
Rulebook is { RulePick[] rules; bool matures; int24 maturityTick }: up to six { rule, params } pairs (an allowed module and its ABI-encoded params), and optionally the price at which every clause lifts for good (the release). With no clauses, the list is empty.
Afterwards
// Anyone can call. Funds only go to the recorded payee or to the pot, the burn desk
// and the house, or to the burn.
vault.harvest(address token)
// Founder only. Takes effect at the next harvest. Share (2) is final.
vault.setPayoutMode(address token, PayoutMode mode)
vault.setPayee(address token, address payee)
// Reads
dock.dockingOf(address token) // public mapping: an unnamed tuple
vault.dockingOf(address token) returns (Docking) // includes the PoolKey
vault.payoutModeOf(address token) returns (PayoutMode)
vault.payeeOf(address token) returns (address)
vault.deferred(address token, address who) returns (uint256) // a payout that couldn't go out
vault.withdraw(address token) // withdraw it; withdraw(address(0)) for native USDC
vault.burnDesk() returns (address) // where the protocol share goes
// All in the pair asset
vault.harvestable(address token) returns (uint256) // taken by the ring, not harvested yet
vault.lifetimeFees(address token) returns (uint256) // everything taken since creation
vault.rateOf(address token) returns (uint24) // the coin's rate, in hundredths of a bip
The hook
Every pool opens with no Uniswap LP fee and the ring as its hook. The hook takes the coin's rate on the pair-asset leg of every swap, never on the coin:
| swap | the pair asset is | the hook takes |
|---|---|---|
| buy, exact input | the amount paid | in × rate, before the swap |
| buy, exact output | what the pool charges | in × rate / (1 − rate) on top, after the swap |
| sell, exact input | what the pool pays out | out × rate from it, after the swap |
| sell, exact output | the amount asked for | out × rate / (1 − rate) extra to the pool, before the swap |
Every row comes to the same rate of the gross pair-asset amount. The fee is minted to the vault as a Uniswap v4 claim (ERC-6909) during the swap, so no token moves mid-swap and no token can block trading; harvest turns it into the pair asset and splits it. Only the dock can open a pool on the ring, and the rate and the covenant are written once, then. A clause can add a rate to a swap (Cooling toll, Storm toll): taken in the same place, capped at 50% in total, booked to the coin. The one exception is the Crown's cut, which the ring credits to whoever wears the crown, who redeems it with ring.redeem(currency, to). The vault's own buyback swaps are the only ones the ring does not charge.
Only the vault can add liquidity to a pool. A third party's range position would be a limit order, a way to trade against the pool without a swap and so without its covenant.
// One per charged swap. The swap's total fee is toLaunch + toPayee; payee is zero unless a clause paid an account; volume is the gross pair-asset leg.
event FeeDrawn(bytes32 indexed poolId, address indexed trader, uint256 toLaunch, address indexed payee, uint256 toPayee, uint256 volume);
event RingOpened(bytes32 indexed poolId, address indexed coin, uint24 fee, bool quoteIsCurrency0);
event RulebookSet(bytes32 indexed poolId, address[] rules, bool matures, int24 maturityTick);
event Matured(bytes32 indexed poolId, int24 markTick);
event Redeemed(address indexed account, Currency indexed currency, address to, uint256 amount);
ring.rateOf(PoolId id) returns (uint24) // the coin's rate
ring.rulebookOf(PoolId id) returns (RuleEntry[]) // the covenant, in run order
ring.termsOf(PoolId id) returns (RingSettings) // coin, founder, release target, high-water tick
ring.owed(address account, uint256 currencyId) returns (uint256) // pending clause winnings
Routers that go straight to Uniswap v4 (the Universal Router, Uniswap's quoter) handle hooked pools natively; an aggregator has to integrate a hook before routing through it. So the site trades these coins through the Universal Router itself (the 2.1.2 build, the one Uniswap lists for Arc), with empty hookData.
The clauses
A coin names up to six modules at creation, forever. Each is a small contract the hook calls around every swap (and for a few, on every coin transfer): it can refuse the trade, add a fee, or keep score. For the clauses, the trader is the wallet that signed the transaction (tx.origin), whatever the router. The creator's first buy is marked as the creation, and clauses that would make a creation impossible let it through. No combination of clauses can keep a holder from selling forever: any clause that refuses sells is limited in time. A clause that takes an amount takes it in the pair asset's units; the wizard asks in dollars.
The owner allows modules once (ring.allowRule); a coin can only name allowed modules. The full reference (callbacks wants, enrol, preTrade, postTrade, onMove, params, limits) is in the repo's contracts/RULES.md, and each module's header documents its params exactly. The covenant maps the site's names to the module names.
Paying holders (payout mode 2)
vault.paymaster() returns (address) // zero until set; mode 2 is refused until then
vault.sharePot(address token) returns (uint256) // pair asset waiting for the coin's holders
vault.queuedRewards(address token) returns (uint256) // always zero: mode 2 never sells
vault.burnQueue(address token) returns (uint256) // pair asset held back by the price cap
// Paymaster only. Lengths must match; the total can't exceed the pot.
vault.payHolders(address token, address[] recipients, uint256[] amounts)
event SharePotFilled(address indexed token, uint256 coinSold, uint256 soldFor, uint256 added, uint256 pot);
event HoldersPaid(address indexed token, address indexed quote, address[] recipients, uint256[] amounts, uint256 total, uint256 pot);
Where the lock is
There is no lock contract and no locked LP token: Uniswap v4 has no LP tokens. A position is a storage slot keyed by (owner, tickLower, tickUpper, salt). The usual "locked liquidity" detectors look for LP tokens sent to a burn address or a locker, and here find nothing of the kind.
What holds instead is a property of the vault, checkable in its published source: no code path in the vault passes a negative liquidityDelta. Fee collection calls modifyLiquidity with a delta of exactly zero. No withdraw, no emergency exit, no owner function to add one.
Owner powers
The owner can set the depth floors, rate bounds, dock fee (capped at 100 USDC in code), the house, the burn desk, the paymaster (replace it, never remove it), the listed assets, the gates and their params, the modules new coins can pick, and the closed-day calendar for the Open hours clause. It cannot touch a coin's liquidity, change the founder share, rate or covenant of an existing coin, or redirect a founder's fees.
Ownership transfer is two-step everywhere: a handover that is never accepted leaves the current owner in place.
The owner should be a multisig behind a timelock. The contracts do not enforce that themselves.
None of this is audited.
