// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title Shillwood v1.1 (no upstream) — the Unrugable launcher ported to Robinhood Chain (4663, Uniswap V4)
///
/// ═══ v1.1 — NO UPSTREAM (founder ruling 2026-07-20) ═══
/// The reactor's 10%-of-paired-fees upstream leg to the prime is REMOVED
/// ENTIRELY. Rationale: the network already gets its cut structurally — every
/// launch's GST and FTP walls force buy-demand into the charity vaults; no
/// skim needed on top. 100% of paired fees now route to buyback/deepen.
/// The leg is DELETED (not zeroed): no fuel constant, no probe, no fuel()
/// call, no zero-amount no-ops. upstreamReactor is still accepted + stored at
/// initialize (identity / invite-chain bookkeeping / ABI parity) but is
/// NEVER called. Everything else is behavior-identical to the LIVE v1
/// (factory 0xbc275E1B91d03716846A7a83513f1E47929dEF46, impl
/// 0xFc3A7EeB3eCE87358A2950F3b96eCc4908132348 — which keep old behavior for
/// their existing immutable launches).
///
///
/// PORT OF (verified on-chain 2026-07-14 via probes against the LIVE Base factory
/// 0x90297A8a1F9A7E35bbC9DF8C35Aa7F3FFBe9BDb2 — which is MycoPadV7.sol
/// `contract Unrugable` — and its reactor impl 0x891587AD… which is
/// SporeReactorV4.sol; NOTE: the live factory is NOT source-verified on BaseScan,
/// so identity was pinned by ABI-surface + version-marker probes + the live
/// clone's storage, all matching the local sources exactly):
///
/// Unrugable V7 behavior replicated, RETUNED to a THREE-WALL charity split
/// (founder-approved 2026-07-14 — supersedes the Base 70/30 Money/Meme split):
/// - launch(name, symbol, customUpstream): free, no seed
/// - 1,000,000,000 fixed supply LaunchToken (no owner, no mint, immutable)
/// - $10,000 market cap start price on EVERY wall
/// - THREE equal one-sided TOKEN sell walls, ⅓ / ⅓ / ⅓ of supply:
/// 1. TOKEN / ETH (native, V4 currency address(0)) — the buy-in door;
/// start price = the SAME $10K MC, expressed in ETH by reading the
/// LIVE ETH/USDG V4 pool (no hardcoded ETH price) [D13]
/// 2. TOKEN / Trees (GST, Grow Some Trees, ~$1 pegged) — fixed $10K MC
/// 3. TOKEN / Food (FTP, ~$1 pegged) — fixed $10K MC
/// ALL single-sided: the launcher deposits ONLY TOKEN; buyers bring the
/// ETH / GST / FTP. The launcher provides ZERO ETH / GST / FTP.
/// - the OLD MfT (Meme) wall is REMOVED. MfT is now fed by the charity
/// tokens' yield (GST/FTP harvest → meme reactor), not by a launch wall.
/// `meme` is kept as an immutable + reactor field for ABI parity only.
/// - STAGED for the Base gas cap ([D14], "stage don't monolith"): launch()
/// mints only the TWO ERC20 walls (GST + FTP) in-tx — the SAME mint count
/// as the old design — and reserves the ETH ⅓ in the factory. The ETH
/// wall is minted in its OWN tx via mintEthWall(reactor) and locked into
/// the same reactor. Fork tests DO NOT catch the 16.5M gas cap; never put
/// more mint work in the launch tx than the old two-wall design did.
/// - a per-launch reactor CLONE (EIP-1167) receives ALL THREE LP positions
/// and has NO code path to ever release them or decrease liquidity — the
/// LP is locked by the ABSENCE of code, exactly the Base guarantee
/// - reactor doctrine (SporeReactorV4): every 2h, permissionless execute():
/// collect fees (ALL 3 pools) -> core-token fees 50% BURN / 50% to the
/// launcher -> for ERC20 walls: 100% of paired fees to buyback/deepen
/// (v1.1: the old 10% upstream skim to the prime is REMOVED — founder
/// ruling 2026-07-20) -> half buys the core token back -> deposit both sides as
/// deeper liquidity -> burn core dust. For the NATIVE (ETH) wall the core
/// side is burned/paid identically and the ETH side is TAKEN into the
/// (locked) reactor; native buyback/LP are skipped (no fragile
/// native-swap path) — fees still route to the reactor, value stays
/// locked, and a future impl can compound the native side [D13].
/// - factory owner keys are ADD-ONLY (addPool helpers / rescue of factory-
/// held dust); no path to user liquidity
///
/// VENUE LAYER SWAPPED (Base Uniswap V3 -> RH Uniswap V4) — ALL DEVIATIONS FLAGGED:
/// [D1] V3 factory.createPool+initialize -> V4 PoolManager.initialize(poolKey)
/// (singleton; poolKey = {currency0,currency1,fee,tickSpacing,hooks}).
/// V4 needs a tickSpacing in the key: 200 (matches Base's TICK_SPACE for
/// the 1% fee tier — same geometry).
/// [D2] V3 NonfungiblePositionManager.mint(desired amounts) -> V4
/// PositionManager.modifyLiquidities(MINT_POSITION + SETTLE_PAIR).
/// V4 takes a LIQUIDITY value, not desired amounts, so the factory
/// computes liquidity on-chain (canonical TickMath/LiquidityAmounts,
/// integer math) and decrements until V4's round-UP owed amount fits the
/// supply share (same guard as lib-rh-v4.cjs, proven on RH).
/// [D3] ERC20 approvals to the V3 PM -> Permit2 two-step approvals (RH V4
/// periphery pulls via Permit2). Exact amounts, short expiry, revoked
/// after every op. Never MaxUint.
/// [D4] V3 SwapRouter02.exactInputSingle(sqrtPriceLimitX96 ±3%) -> V4
/// UniversalRouter V4_SWAP exact-in-single with amountOutMinimum derived
/// ON-CHAIN from the pool's LIVE sqrtPrice × (1 - 3%). Behavior note:
/// V3's price-limit PARTIALLY FILLS to the limit; V4's minOut is
/// all-or-revert, so a too-thin pool SKIPS the cycle (fees roll forward)
/// instead of partially filling. Funds are never at risk either way.
/// [D5] V3 collect() -> V4 DECREASE_LIQUIDITY(0) + TAKE_PAIR (the V4 fee-
/// collect idiom). Liquidity argument is HARDCODED 0 — this call can
/// never withdraw liquidity.
/// [D6] V3 increaseLiquidity(desired amounts) -> V4 INCREASE_LIQUIDITY with
/// on-chain liquidity sizing from live price + position range, shaved
/// 0.05% so V4's round-up can never exceed held balances (RH lesson
/// 2026-07-12). If nothing fits geometrically the add is skipped with an
/// event and amounts roll to the next cycle (V3's desired-amount API did
/// this implicitly). The add closes deltas with CLOSE_CURRENCY (settle-
/// or-take) because a V4 increase NETS pending fees into the deltas —
/// after the same-cycle buyback pays fees to this very position, a thin
/// side can net a CREDIT and plain SETTLE_PAIR would revert
/// DeltaNotNegative (found on the RH fork 2026-07-14).
/// [D7] Base Money.registerV3Position (Aave yield hook) DROPPED — the RH FTP
/// vault has no position-registration surface (its yield is Morpho
/// harvest() with a fixed 3-way split; the meme-reactor leg already
/// receives FTP flow). This was a best-effort try/catch on Base.
/// [D8] BURN sink: Base uses 0xfd780B0a… (a 45-byte sink contract on Base
/// that does not exist on RH). Shillwood burns to 0x…dEaD — the burn
/// address the live RH Meme Reactor prime already uses. Both are
/// irrecoverable.
/// [D9] REMOVED in v1.1 (was: upstream probe + 10% fuel). upstreamReactor is
/// still STORED (identity / invite-chain bookkeeping / ABI parity) but
/// the reactor NEVER calls it — no probe, no fuel(), no skim.
/// [D10] execute() keeps Base's ARGLESS permissionless signature; the 3%
/// slippage bound is derived on-chain per pool from the live price (no
/// hardcoded prices, no keeper-supplied numbers).
/// [D11] Per-pool gas floor in execute() (require gasleft() > 2M): RH-proven
/// guard — eth_estimateGas otherwise finds the all-pools-skipped path
/// and an under-gassed call silently no-ops. Base has no equivalent
/// because its swap cannot OOG-mask the same way. Fail-loud > silent.
/// [D12] EXTENSIBILITY (coordinator, v1-behavior-neutral): the launcher fee
/// leg is a recipients/bps SPLIT (validated to sum to 10000). v1 passes
/// [launcher],[10000] — byte-for-byte the Base payout. A future v2
/// factory can wire launcher/charity/NFT splits into the SAME reactor
/// impl. Additional LP pairs/causes attach via the staged, permissioned
/// addLaunchLP()/addPoolToReactor() path (never more mint work inside
/// the launch tx), each with the same lock-forever guarantee.
/// [D13] NATIVE (ETH) WALL — the buy-in door. Its start price is the SAME $10K
/// MC as the ERC20 walls, converted to ETH by reading the LIVE ETH/USDG
/// V4 pool (currency0=address(0), currency1=USDG, fee/spacing wired in),
/// usdgPerEth = (sqrtP^2 * 1e18) >> 192 (the exact buy-rh-usdg.cjs read),
/// then ethMC = TARGET_MC_MONEY * 1e18 / usdgPerEth. This REPURPOSES the
/// old Money/Meme price-derivation (which the removed meme wall used) and
/// reverts loud if the ETH/USDG pool is unreadable — NEVER a hardcoded
/// ETH price. The wall is single-sided TOKEN below spot (token=currency1,
/// ETH=currency0), so the launcher owes ZERO ETH at mint (amount0Max=0).
/// In the reactor, the native wall's TOKEN-side fees are burned/paid by
/// the same doctrine; its ETH-side fees are TAKEN into the locked reactor
/// (receive() accepts them); native buyback/LP are SKIPPED — the
/// UR/Permit2 add path is ERC20-shaped and a
/// native-value swap in the reactor is untested surface we won't ship
/// under a "correctness dominates" mandate. Fees still route to the
/// reactor; value stays locked forever; a future impl can compound it.
/// [D14] STAGE, DON'T MONOLITH (Base ~16.5M per-tx gas cap; fork tests DON'T
/// catch it). launch() mints only the TWO ERC20 walls in-tx — identical
/// mint count to the old design — and reserves the ETH ⅓ supply in the
/// factory (pendingEthSupply[reactor]). mintEthWall(reactor) mints the
/// native wall in a SEPARATE tx (its own gas budget) and locks it. The
/// reserved supply can only become the ETH wall (or be rescued by the
/// pre-existing owner-only rescue — NOT a new privilege); it can never
/// touch locked LP.
///
/// Chain constants this factory is meant to be deployed with (RH 4663 —
/// verified on-chain 2026-07-14, from this session's rh-v4-addresses.json):
/// PoolManager 0x8366a39CC670B4001A1121B8F6A443A643e40951
/// PositionManager 0x58daec3116aae6D93017bAAea7749052E8a04fA7
/// UniversalRouter 0x53BF6B0684Ec7eF91e1387Da3D1a1769bC5A6F77 (canonical)
/// Permit2 0x000000000022D473030F116dDEE9F6B43aC78BA3
/// Money (FTP/Food) 0x873739aeD7b49f005965377b5645914b1D78Ccd3 (6 dec)
/// Trees (GST) 0x95eD511Dbdd7b52795e1F515314bE8d888Ea4F3F (6 dec)
/// Meme (MfT twin) 0x6ae576608725677Bf8D05EA7796849E6F8F57608 (18 dec, parity only)
/// USDG 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 (6 dec)
/// upstream prime 0xd51125e200689bf07A9b36A6c12fE440bb92dd4D (Meme Reactor)
/// ETH/USDG ref pool key: fee 500, tickSpacing 10, hooks 0 (currency0=ETH=
/// address(0), currency1=USDG — the DEEP live pool, verified initialized
/// on-chain 2026-07-14, ~$1872/ETH; the same pool buy-rh-usdg.cjs uses)
// ═══════════════════════════════════════════════════════════════════════════
// Minimal interfaces (verified against RH V4 periphery used all session)
// ═══════════════════════════════════════════════════════════════════════════
interface IERC20 {
function approve(address spender, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
/// @dev V4 PoolKey. currency0 < currency1 (sorted). Native = address(0).
struct PoolKey {
address currency0;
address currency1;
uint24 fee;
int24 tickSpacing;
address hooks;
}
/// @dev V4Router.ExactInputSingleParams — MUST be abi.encode()'d as ONE struct
/// value (dynamic hookData ⇒ leading offset word). Field-encoding makes a
/// different layout and the router empty-reverts (RH lesson 2026-07-12).
struct ExactInputSingleParams {
PoolKey poolKey;
bool zeroForOne;
uint128 amountIn;
uint128 amountOutMinimum;
bytes hookData;
}
interface IV4PoolManager {
function initialize(PoolKey calldata key, uint160 sqrtPriceX96) external returns (int24 tick);
function extsload(bytes32 slot) external view returns (bytes32);
}
interface IV4PositionManager {
function modifyLiquidities(bytes calldata unlockData, uint256 deadline) external payable;
function getPositionLiquidity(uint256 tokenId) external view returns (uint128);
function getPoolAndPositionInfo(uint256 tokenId) external view returns (PoolKey memory, uint256);
function ownerOf(uint256 tokenId) external view returns (address);
function nextTokenId() external view returns (uint256);
function transferFrom(address from, address to, uint256 tokenId) external;
function safeTransferFrom(address from, address to, uint256 tokenId) external;
}
interface IUniversalRouter {
function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) external payable;
}
interface IPermit2 {
function approve(address token, address spender, uint160 amount, uint48 expiration) external;
}
/// @dev The upstream aggregator (RH Meme Reactor prime). [v1.1] kept ONLY as
/// the storage type of the inert upstreamReactor field — never called.
interface IV4Upstream {
function fuel(address currency, uint256 amount) external;
function acceptsCurrency(address currency) external view returns (bool);
}
interface IShillwoodReactor {
function initialize(
address _token, address _mft,
address _pm, address _router, address _permit2, address _poolManager,
address _upstreamReactor,
address[] calldata _feeRecipients, uint16[] calldata _feeBps
) external;
function addPool(uint256 tokenId) external;
function transferAdmin(address newAdmin) external;
}
// ═══════════════════════════════════════════════════════════════════════════
// V4 action opcodes (verified vs Uniswap/v4-periphery Actions.sol, and used
// live on RH by this session's seed scripts + reactors)
// ═══════════════════════════════════════════════════════════════════════════
library V4Actions {
uint8 internal constant INCREASE_LIQUIDITY = 0x00;
uint8 internal constant DECREASE_LIQUIDITY = 0x01;
uint8 internal constant MINT_POSITION = 0x02;
uint8 internal constant SETTLE_PAIR = 0x0d;
uint8 internal constant TAKE_PAIR = 0x11;
uint8 internal constant CLOSE_CURRENCY = 0x12; // settle-or-take, sign-agnostic
uint8 internal constant UR_V4_SWAP = 0x10; // UniversalRouter command
uint8 internal constant SWAP_EXACT_IN_SINGLE = 0x06; // V4Router sub-action
uint8 internal constant SETTLE_ALL = 0x0c;
uint8 internal constant TAKE_ALL = 0x0f;
}
// ═══════════════════════════════════════════════════════════════════════════
// Canonical Uniswap integer math (verbatim from V4ReactorSuite.sol, itself
// ported verbatim from Uniswap v4-core / v3-periphery). Float is never used.
// ═══════════════════════════════════════════════════════════════════════════
library TickMath {
int24 internal constant MIN_TICK = -887272;
int24 internal constant MAX_TICK = 887272;
function getSqrtPriceAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) {
unchecked {
uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick));
require(absTick <= uint256(int256(MAX_TICK)), "T");
uint256 price = absTick & 0x1 != 0 ? 0xfffcb933bd6fad37aa2d162d1a594001 : 0x100000000000000000000000000000000;
if (absTick & 0x2 != 0) price = (price * 0xfff97272373d413259a46990580e213a) >> 128;
if (absTick & 0x4 != 0) price = (price * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128;
if (absTick & 0x8 != 0) price = (price * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128;
if (absTick & 0x10 != 0) price = (price * 0xffcb9843d60f6159c9db58835c926644) >> 128;
if (absTick & 0x20 != 0) price = (price * 0xff973b41fa98c081472e6896dfb254c0) >> 128;
if (absTick & 0x40 != 0) price = (price * 0xff2ea16466c96a3843ec78b326b52861) >> 128;
if (absTick & 0x80 != 0) price = (price * 0xfe5dee046a99a2a811c461f1969c3053) >> 128;
if (absTick & 0x100 != 0) price = (price * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128;
if (absTick & 0x200 != 0) price = (price * 0xf987a7253ac413176f2b074cf7815e54) >> 128;
if (absTick & 0x400 != 0) price = (price * 0xf3392b0822b70005940c7a398e4b70f3) >> 128;
if (absTick & 0x800 != 0) price = (price * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128;
if (absTick & 0x1000 != 0) price = (price * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128;
if (absTick & 0x2000 != 0) price = (price * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128;
if (absTick & 0x4000 != 0) price = (price * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128;
if (absTick & 0x8000 != 0) price = (price * 0x31be135f97d08fd981231505542fcfa6) >> 128;
if (absTick & 0x10000 != 0) price = (price * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128;
if (absTick & 0x20000 != 0) price = (price * 0x5d6af8dedb81196699c329225ee604) >> 128;
if (absTick & 0x40000 != 0) price = (price * 0x2216e584f5fa1ea926041bedfe98) >> 128;
if (absTick & 0x80000 != 0) price = (price * 0x48a170391f7dc42444e8fa2) >> 128;
if (tick > 0) price = type(uint256).max / price;
sqrtPriceX96 = uint160((price >> 32) + (price % (1 << 32) == 0 ? 0 : 1));
}
}
}
library FullMath {
function mulDiv(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) {
unchecked {
uint256 prod0; uint256 prod1;
assembly { let mm := mulmod(a, b, not(0)) prod0 := mul(a, b) prod1 := sub(sub(mm, prod0), lt(mm, prod0)) }
if (prod1 == 0) { require(denominator > 0); assembly { result := div(prod0, denominator) } return result; }
require(denominator > prod1);
uint256 remainder;
assembly { remainder := mulmod(a, b, denominator) prod1 := sub(prod1, gt(remainder, prod0)) prod0 := sub(prod0, remainder) }
uint256 twos = denominator & (~denominator + 1);
assembly { denominator := div(denominator, twos) prod0 := div(prod0, twos) twos := add(div(sub(0, twos), twos), 1) }
prod0 |= prod1 * twos;
uint256 inv = (3 * denominator) ^ 2;
inv *= 2 - denominator * inv;
inv *= 2 - denominator * inv;
inv *= 2 - denominator * inv;
inv *= 2 - denominator * inv;
inv *= 2 - denominator * inv;
inv *= 2 - denominator * inv;
result = prod0 * inv;
}
}
}
library LiquidityAmounts {
function getLiquidityForAmount0(uint160 sqrtA, uint160 sqrtB, uint256 amount0) internal pure returns (uint128) {
if (sqrtA > sqrtB) (sqrtA, sqrtB) = (sqrtB, sqrtA);
uint256 intermediate = FullMath.mulDiv(uint256(sqrtA), uint256(sqrtB), 0x1000000000000000000000000);
return _toU128(FullMath.mulDiv(amount0, intermediate, uint256(sqrtB) - uint256(sqrtA)));
}
function getLiquidityForAmount1(uint160 sqrtA, uint160 sqrtB, uint256 amount1) internal pure returns (uint128) {
if (sqrtA > sqrtB) (sqrtA, sqrtB) = (sqrtB, sqrtA);
return _toU128(FullMath.mulDiv(amount1, 0x1000000000000000000000000, uint256(sqrtB) - uint256(sqrtA)));
}
function getLiquidityForAmounts(uint160 sqrtCur, uint160 sqrtA, uint160 sqrtB, uint256 amount0, uint256 amount1)
internal pure returns (uint128 liquidity)
{
if (sqrtA > sqrtB) (sqrtA, sqrtB) = (sqrtB, sqrtA);
if (sqrtCur <= sqrtA) {
liquidity = getLiquidityForAmount0(sqrtA, sqrtB, amount0);
} else if (sqrtCur < sqrtB) {
uint128 l0 = getLiquidityForAmount0(sqrtCur, sqrtB, amount0);
uint128 l1 = getLiquidityForAmount1(sqrtA, sqrtCur, amount1);
liquidity = l0 < l1 ? l0 : l1;
} else {
liquidity = getLiquidityForAmount1(sqrtA, sqrtB, amount1);
}
}
/// @dev amount0 OWED for liquidity L over [spL,spU], rounded UP exactly like
/// V4 SqrtPriceMath.getAmount0Delta (the mint's pull side). [D2]
function amount0ForLiquidityUp(uint128 L, uint160 spL, uint160 spU) internal pure returns (uint256) {
if (spL > spU) (spL, spU) = (spU, spL);
uint256 numerator1 = uint256(L) << 96;
uint256 numerator2 = uint256(spU) - uint256(spL);
uint256 step1 = FullMath.mulDiv(numerator1, numerator2, uint256(spU));
if (mulmod(numerator1, numerator2, uint256(spU)) != 0) step1 += 1;
uint256 out = step1 / uint256(spL);
if (step1 % uint256(spL) != 0) out += 1;
return out;
}
/// @dev amount1 OWED for liquidity L over [spL,spU], rounded UP (V4 style). [D2]
function amount1ForLiquidityUp(uint128 L, uint160 spL, uint160 spU) internal pure returns (uint256) {
if (spL > spU) (spL, spU) = (spU, spL);
uint256 num = uint256(L) * (uint256(spU) - uint256(spL));
return (num + (1 << 96) - 1) >> 96;
}
function _toU128(uint256 x) private pure returns (uint128 y) { require((y = uint128(x)) == x, "L128"); }
}
// ═══════════════════════════════════════════════════════════════════════════
// LaunchToken — byte-identical port of the Base LaunchToken (no owner, no
// mint, no burn, fully immutable at birth). Only the metadata base differs
// (Shillwood namespace).
// ═══════════════════════════════════════════════════════════════════════════
contract LaunchToken {
string public name;
string public symbol;
uint8 public constant decimals = 18;
uint256 public totalSupply;
string private _baseURI;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(string memory _name, string memory _symbol, uint256 _supply, address _recipient, string memory baseURI_) {
require(_supply > 0, "zero supply");
require(_recipient != address(0), "zero recipient");
name = _name;
symbol = _symbol;
totalSupply = _supply;
_baseURI = baseURI_;
balanceOf[_recipient] = _supply;
emit Transfer(address(0), _recipient, _supply);
}
/// @notice EIP-7572 contract-level metadata for aggregators
function contractURI() external view returns (string memory) {
return string.concat(_baseURI, _toHexString(address(this)));
}
function _toHexString(address addr) internal pure returns (string memory) {
bytes memory s = new bytes(42);
s[0] = "0";
s[1] = "x";
bytes memory hex16 = "0123456789abcdef";
uint160 v = uint160(addr);
for (uint256 i = 41; i > 1; i--) {
s[i] = hex16[v & 0xf];
v >>= 4;
}
return string(s);
}
function transfer(address to, uint256 amount) external returns (bool) {
return _transfer(msg.sender, to, amount);
}
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
uint256 current = allowance[from][msg.sender];
if (current != type(uint256).max) {
require(current >= amount, "allowance exceeded");
unchecked { allowance[from][msg.sender] = current - amount; }
}
return _transfer(from, to, amount);
}
function _transfer(address from, address to, uint256 amount) internal returns (bool) {
require(from != address(0) && to != address(0), "zero address");
require(balanceOf[from] >= amount, "exceeds balance");
unchecked {
balanceOf[from] -= amount;
balanceOf[to] += amount;
}
emit Transfer(from, to, amount);
return true;
}
}
// ═══════════════════════════════════════════════════════════════════════════
// ShillwoodReactor — SporeReactorV4 ported to Uniswap V4.
//
// THE LOCK: this contract has NO withdrawPosition, NO BURN_POSITION, NO NFT
// transfer-out, and its only DECREASE_LIQUIDITY call hardcodes liquidity=0
// (fee collection). Once a position NFT is inside, no code path can ever
// release it or its principal. Admin surface is add-only:
// addPool / disablePool / enablePool / setPaused / transferAdmin.
// ═══════════════════════════════════════════════════════════════════════════
contract ShillwoodReactor {
/// @notice v1.1 (no upstream): the 10% paired-fee upstream leg is removed —
/// 100% of paired fees route to buyback/deepen. Founder ruling 2026-07-20.
string public constant VERSION = "shillwood-reactor-v1.1-no-upstream";
/// @dev [D8] RH ecosystem burn sink (the live Meme Reactor prime burns here).
address public constant BURN = 0x000000000000000000000000000000000000dEaD;
uint256 public constant COOLDOWN = 2 hours;
uint256 public constant MAX_POOLS = 20;
uint256 public constant MIN_FUEL = 1000;
uint256 public constant MIN_PROCESS = 1000;
uint256 private constant LAUNCHER_BPS = 5000; // 50% of core fees to fee split
uint256 public constant MAX_PRICE_IMPACT_BPS = 300; // 3% (Base parity) [D4]
/// @dev [D11] gas floor per pool: refuse to start a pool without enough gas
/// to complete its grind, so under-gassed calls REVERT visibly instead
/// of skipping every pool "successfully".
uint256 private constant MIN_GAS_PER_POOL = 2_000_000;
/// @dev V4 UR/PM check deadlines strictly; a short forward buffer is required
/// (bare block.timestamp empty-reverted on RH 2026-07-12).
uint256 private constant DEADLINE_BUFFER = 15 minutes;
/// @dev V4 StateLibrary: pools mapping at PoolManager storage slot 6.
uint256 private constant POOLS_SLOT = 6;
/// @dev [D6] shave the computed liquidity 0.05% so V4's round-UP owed
/// amounts can never exceed our amountMax (RH lesson 2026-07-12).
uint256 private constant LIQ_SHAVE_BPS = 5;
// ── wiring (set once at initialize) ────────────────────────────────────
address public token; // core token (the launched token)
address public mft; // Meme (MfT twin) — kept for ABI parity
IV4PositionManager public pm;
IUniversalRouter public router;
IPermit2 public permit2;
IV4PoolManager public poolManager; // extsload live-price reads
IV4Upstream public upstreamReactor; // [v1.1] stored for identity/parity ONLY — never called
/// @notice first fee recipient — the launcher. Kept as its own getter for
/// Base ABI parity (tools/UI read launcher()).
address public launcher;
/// @dev [D12] fee split for the 50% "launcher share" of core fees.
address[] public feeRecipients;
uint16[] public feeBps; // sums to 10000
bool public initialized;
uint256 public lastExecute;
bool public paused;
struct Pool {
uint256 tokenId;
PoolKey key;
address xToken; // the paired ("other side") token
bool tokenIsCurrency0;
bool disabled;
int24 tickLower;
int24 tickUpper;
}
Pool[] public pools;
mapping(uint256 => bool) public registeredTokenId;
mapping(address => bool) public hasXToken;
address public admin;
address public pendingAdmin;
uint256 private _locked = 1;
modifier nonReentrant() { require(_locked == 1, "reentrant"); _locked = 2; _; _locked = 1; }
modifier onlyAdmin() { require(msg.sender == admin, "not admin"); _; }
modifier onlySelf() { require(msg.sender == address(this), "internal only"); _; }
event Executed(uint256 burned, uint256 paid, uint256 deposited, uint256 fueled, uint256 timestamp, address caller);
event LauncherPaid(address indexed launcher, uint256 amount);
event FeePaid(address indexed recipient, uint256 amount);
event PoolAdded(uint256 indexed tokenId, address xToken, bool tokenIsCurrency0);
event PoolDisabled(uint256 indexed poolIndex, uint256 tokenId);
event PoolEnabled(uint256 indexed poolIndex, uint256 tokenId);
event PoolSkipped(uint256 indexed poolIndex, uint256 tokenId);
event LiquidityDeposited(uint256 indexed poolIndex, uint256 tokenAmount, uint256 xAmount);
event DepositSkipped(uint256 indexed poolIndex, uint256 tokenAmount, uint256 xAmount);
event Fueled(uint256 indexed poolIndex, address xToken, uint256 xIn, uint256 tokenDeposited, uint256 xDeposited);
event DustBurned(uint256 amount);
event AdminTransferStarted(address indexed current, address indexed pending);
event AdminTransferred(address indexed previous, address indexed newAdmin);
event Paused(bool status);
// ═══════════════════════════════════════════════════════════════════════
// Initialize (EIP-1167 clone target)
// ═══════════════════════════════════════════════════════════════════════
function initialize(
address _token,
address _mft,
address _pm,
address _router,
address _permit2,
address _poolManager,
address _upstreamReactor,
address[] calldata _feeRecipients,
uint16[] calldata _feeBps
) external {
require(!initialized, "already initialized");
initialized = true;
require(_token != address(0), "zero token");
require(_mft != address(0), "zero mft");
require(_pm != address(0), "zero pm");
require(_router != address(0), "zero router");
require(_permit2 != address(0), "zero permit2");
require(_poolManager != address(0), "zero poolManager");
require(_upstreamReactor != address(0), "zero upstream");
require(_token != _mft, "token cannot be mft");
require(_feeRecipients.length > 0 && _feeRecipients.length == _feeBps.length, "bad fee split");
uint256 sum;
for (uint256 i; i < _feeRecipients.length; ++i) {
require(_feeRecipients[i] != address(0), "zero fee recipient");
sum += _feeBps[i];
feeRecipients.push(_feeRecipients[i]);
feeBps.push(_feeBps[i]);
}
require(sum == 10000, "fee bps != 10000");
token = _token;
mft = _mft;
pm = IV4PositionManager(_pm);
router = IUniversalRouter(_router);
permit2 = IPermit2(_permit2);
poolManager = IV4PoolManager(_poolManager);
upstreamReactor = IV4Upstream(_upstreamReactor);
launcher = _feeRecipients[0];
admin = msg.sender;
_locked = 1;
}
// ═══════════════════════════════════════════════════════════════════════
// Pool management (admin = the factory; add-only)
// ═══════════════════════════════════════════════════════════════════════
function addPool(uint256 tokenId) external onlyAdmin {
require(pools.length < MAX_POOLS, "max pools reached");
require(!registeredTokenId[tokenId], "already registered");
require(pm.ownerOf(tokenId) == address(this), "NFT not owned by reactor");
(PoolKey memory key, uint256 info) = pm.getPoolAndPositionInfo(tokenId);
bool is0 = (key.currency0 == token);
bool is1 = (key.currency1 == token);
require(is0 || is1, "token not in pair");
address xToken = is0 ? key.currency1 : key.currency0;
require(xToken != token, "xToken cannot be native token");
// pool must be live (initialized) — V4 analogue of Base's getPool check
require(_liveSqrtPriceX96(key) != 0, "pool not found");
// v4-periphery PositionInfo layout: [poolId 200b | tickUpper 24b | tickLower 24b | hasSub 8b]
int24 tickLower = int24(uint24(info >> 8));
int24 tickUpper = int24(uint24(info >> 32));
pools.push(Pool({
tokenId: tokenId,
key: key,
xToken: xToken,
tokenIsCurrency0: is0,
disabled: false,
tickLower: tickLower,
tickUpper: tickUpper
}));
registeredTokenId[tokenId] = true;
hasXToken[xToken] = true;
emit PoolAdded(tokenId, xToken, is0);
}
function disablePool(uint256 poolIndex) external onlyAdmin {
require(poolIndex < pools.length, "invalid index");
require(!pools[poolIndex].disabled, "already disabled");
pools[poolIndex].disabled = true;
emit PoolDisabled(poolIndex, pools[poolIndex].tokenId);
}
function enablePool(uint256 poolIndex) external onlyAdmin {
require(poolIndex < pools.length, "invalid index");
require(pools[poolIndex].disabled, "already enabled");
pools[poolIndex].disabled = false;
emit PoolEnabled(poolIndex, pools[poolIndex].tokenId);
}
function transferAdmin(address newAdmin) external onlyAdmin {
require(newAdmin != address(0), "zero address");
pendingAdmin = newAdmin;
emit AdminTransferStarted(admin, newAdmin);
}
function acceptAdmin() external {
require(msg.sender == pendingAdmin, "not pending admin");
emit AdminTransferred(admin, pendingAdmin);
admin = pendingAdmin;
pendingAdmin = address(0);
}
function renounceAdmin() external onlyAdmin {
emit AdminTransferred(admin, address(0));
admin = address(0);
pendingAdmin = address(0);
}
function setPaused(bool _paused) external onlyAdmin {
paused = _paused;
emit Paused(_paused);
}
// ═══════════════════════════════════════════════════════════════════════
// Fuel intake (public — community deepening, Base parity)
// ═══════════════════════════════════════════════════════════════════════
/// @notice Deposit token and/or paired token straight into a pool position.
function depositLiquidity(uint256 poolIndex, uint256 tokenAmount, uint256 xAmount) external nonReentrant {
require(poolIndex < pools.length, "invalid index");
require(!pools[poolIndex].disabled, "pool disabled");
Pool memory pool = pools[poolIndex];
if (tokenAmount > 0) _safeTransferFrom(token, msg.sender, address(this), tokenAmount);
if (xAmount > 0) _safeTransferFrom(pool.xToken, msg.sender, address(this), xAmount);
uint256 a0 = pool.tokenIsCurrency0 ? tokenAmount : xAmount;
uint256 a1 = pool.tokenIsCurrency0 ? xAmount : tokenAmount;
uint128 liq = _liquidityFor(pool, a0, a1);
require(liq > 0, "zero liquidity");
_increase(pool, liq, a0, a1);
emit LiquidityDeposited(poolIndex, tokenAmount, xAmount);
}
/// @notice Push paired token in: half buys core at the live price (3% bound),
/// both sides deposited, core dust burned. Base fuel() parity.
function fuel(address xToken, uint256 amount) external nonReentrant {
require(amount >= MIN_FUEL, "below minimum fuel");
uint256 poolIndex = _findPool(xToken);
require(poolIndex < type(uint256).max, "no pool for token");
Pool memory pool = pools[poolIndex];
require(!pool.disabled, "pool disabled");
_safeTransferFrom(xToken, msg.sender, address(this), amount);
uint256 halfX = amount / 2;
uint256 xForLP = amount - halfX;
uint256 tokenAmount = _buyCore(pool, halfX, _minCoreOut(pool, halfX)); // [D4]
uint256 a0 = pool.tokenIsCurrency0 ? tokenAmount : xForLP;
uint256 a1 = pool.tokenIsCurrency0 ? xForLP : tokenAmount;
uint128 liq = _liquidityFor(pool, a0, a1);
if (liq > 0) {
_increase(pool, liq, a0, a1);
} else {
emit DepositSkipped(poolIndex, tokenAmount, xForLP); // [D6]
}
_burnDust();
emit Fueled(poolIndex, xToken, amount, tokenAmount, xForLP);
}
// ═══════════════════════════════════════════════════════════════════════
// Execute — permissionless after cooldown (argless, Base parity) [D10]
// ═══════════════════════════════════════════════════════════════════════
function execute() external nonReentrant {
require(!paused, "paused");
require(block.timestamp >= lastExecute + COOLDOWN, "cooldown");
lastExecute = block.timestamp;
uint256 totalBurned;
uint256 totalPaid;
uint256 totalDeposited;
uint256 totalFueled;
uint256 len = pools.length;
for (uint256 i; i < len; ++i) {
if (pools[i].disabled) {
emit PoolSkipped(i, pools[i].tokenId);
continue;
}
require(gasleft() > MIN_GAS_PER_POOL, "insufficient gas"); // [D11]
try this.processPool(i) returns (uint256 burned, uint256 paid, uint256 bought, uint256 fueled) {
totalBurned += burned;
totalPaid += paid;
totalDeposited += bought;
totalFueled += fueled;
} catch {
emit PoolSkipped(i, pools[i].tokenId);
}
}
emit Executed(totalBurned, totalPaid, totalDeposited, totalFueled, block.timestamp, msg.sender);
}
/// @notice One pool cycle — the exact SporeReactorV4 doctrine on V4 rails.
function processPool(uint256 poolIndex) external onlySelf returns (uint256 burned, uint256 paid, uint256 bought, uint256 fueled) {
Pool memory pool = pools[poolIndex];
// 1. Collect fees (V4 idiom: DECREASE_LIQUIDITY(0) + TAKE_PAIR) [D5]
_collectFees(pool);
// 2. Split core-token fees — 50% burn, 50% to the fee split (launcher)
uint256 tokenBal = IERC20(token).balanceOf(address(this));
if (tokenBal > 0) {
uint256 toLauncher = tokenBal * LAUNCHER_BPS / 10000;
uint256 toBurn = tokenBal - toLauncher;
if (toBurn > 0) {
_safeTransfer(token, BURN, toBurn);
burned = toBurn;
}
if (toLauncher > 0) {
paid = _payFeeSplit(toLauncher); // [D12] v1: single recipient = launcher
}
}
// 2b. [D13] NATIVE (ETH) wall: the core side is burned/paid above exactly
// like any wall. _collectFees already TOOK the ETH-side fees into this
// (locked, receive()-enabled) reactor. Native buyback/LP are
// intentionally SKIPPED — the UR/Permit2 add
// path is ERC20-shaped; a native-value swap here is untested surface
// we won't ship. Fees have routed to the reactor; the ETH stays locked
// forever. Return here; no ERC20 xToken to process.
if (pool.xToken == address(0)) return (burned, paid, 0, 0);
// 3. Total paired-token balance
uint256 xBal = IERC20(pool.xToken).balanceOf(address(this));
if (xBal < MIN_PROCESS) return (burned, paid, 0, 0);
// 4. [v1.1 — LEG REMOVED] The 10%-of-paired-fees upstream fuel() to the
// prime is CUT ENTIRELY (founder ruling 2026-07-20): the network gets
// its cut structurally — every launch's GST and FTP walls force
// buy-demand into the charity vaults. The leg is DELETED, not zeroed:
// no probe, no approve, no zero-amount fuel() no-op. `fueled` stays in
// the return (always 0) so execute()'s aggregation and the Executed
// event keep their exact v1 shape.
// 5. Paired fees: half buy / half LP — 100% to buyback/deepen
uint256 xForBuy = xBal / 2;
uint256 xForLP = xBal - xForBuy;
// 6. Buy core with half, bounded to the live price minus 3% [D4]
bought = _buyCore(pool, xForBuy, _minCoreOut(pool, xForBuy));
// 7. Deposit bought core + remaining paired as deeper liquidity [D6]
if (bought > 0 && xForLP > 0) {
uint256 a0 = pool.tokenIsCurrency0 ? bought : xForLP;
uint256 a1 = pool.tokenIsCurrency0 ? xForLP : bought;
uint128 liq = _liquidityFor(pool, a0, a1);
if (liq > 0) {
_increase(pool, liq, a0, a1);
} else {
emit DepositSkipped(poolIndex, bought, xForLP);
}
}
// 8. Burn any core dust from geometry mismatch
uint256 dust = IERC20(token).balanceOf(address(this));
if (dust > 0) {
_safeTransfer(token, BURN, dust);
burned += dust;
}
}
// ═══════════════════════════════════════════════════════════════════════
// V4 primitives
// ═══════════════════════════════════════════════════════════════════════
/// @dev [D5] Collect owed fees to this contract. liquidity is HARDCODED 0 —
/// this is the only DECREASE_LIQUIDITY in the contract and it cannot
/// remove principal.
function _collectFees(Pool memory p) internal {
bytes memory actions = abi.encodePacked(
uint8(V4Actions.DECREASE_LIQUIDITY),
uint8(V4Actions.TAKE_PAIR)
);
bytes[] memory params = new bytes[](2);
params[0] = abi.encode(p.tokenId, uint128(0), uint256(0), uint256(0), bytes(""));
params[1] = abi.encode(p.key.currency0, p.key.currency1, address(this));
pm.modifyLiquidities(abi.encode(actions, params), block.timestamp + DEADLINE_BUFFER);
}
/// @dev [D6] Increase this position by `liq`, paying at most a0/a1.
/// Uses CLOSE_CURRENCY (settle-or-take) instead of SETTLE_PAIR: a V4
/// increase NETS the position's PENDING FEES into the deltas, so after
/// a same-cycle buyback swap (which pays fees to this very position) a
/// thin side's delta can net POSITIVE (credit > owed) and SETTLE_PAIR
/// would revert DeltaNotNegative. CLOSE_CURRENCY pays debt or takes
/// credit either way — any taken credit lands back on the reactor and
/// is burned/compounded next cycle. (Found on the RH fork 2026-07-14.)
function _increase(Pool memory p, uint128 liq, uint256 a0, uint256 a1) internal {
address c0 = p.key.currency0;
address c1 = p.key.currency1;
if (a0 > 0) _permit2Approve(c0, address(pm), a0);
if (a1 > 0) _permit2Approve(c1, address(pm), a1);
bytes memory actions = abi.encodePacked(
uint8(V4Actions.INCREASE_LIQUIDITY),
uint8(V4Actions.CLOSE_CURRENCY),
uint8(V4Actions.CLOSE_CURRENCY)
);
bytes[] memory params = new bytes[](3);
params[0] = abi.encode(p.tokenId, liq, uint128(a0), uint128(a1), bytes(""));
params[1] = abi.encode(c0);
params[2] = abi.encode(c1);
pm.modifyLiquidities(abi.encode(actions, params), block.timestamp + DEADLINE_BUFFER);
if (a0 > 0) _permit2Revoke(c0, address(pm));
if (a1 > 0) _permit2Revoke(c1, address(pm));
}
/// @dev Liquidity mintable for (a0, a1) at the LIVE price inside this
/// position's range, shaved 0.05% for V4's round-up. [D6]
function _liquidityFor(Pool memory p, uint256 a0, uint256 a1) internal view returns (uint128) {
uint160 sqrtCur = _liveSqrtPriceX96(p.key);
require(sqrtCur != 0, "pool uninitialized");
uint128 liqRaw = LiquidityAmounts.getLiquidityForAmounts(
sqrtCur,
TickMath.getSqrtPriceAtTick(p.tickLower),
TickMath.getSqrtPriceAtTick(p.tickUpper),
a0, a1
);
return uint128((uint256(liqRaw) * (10000 - LIQ_SHAVE_BPS)) / 10000);
}
/// @dev [D4] live-price-derived minOut for a paired->core exact-in swap:
/// expected out at the CURRENT pool price × (1 - 3%). No hardcoded
/// prices; reverts loud if the pool is unreadable.
function _minCoreOut(Pool memory p, uint256 pairedIn) internal view returns (uint256) {
uint160 sp = _liveSqrtPriceX96(p.key);
require(sp != 0, "pool uninitialized");
uint256 expOut;
if (p.tokenIsCurrency0) {
// paired = currency1 -> core out = in / price = in * 2^192 / sp^2
expOut = FullMath.mulDiv(
FullMath.mulDiv(pairedIn, 1 << 96, uint256(sp)),
1 << 96,
uint256(sp)
);
} else {
// paired = currency0 -> core out = in * price = in * sp^2 / 2^192
expOut = FullMath.mulDiv(
FullMath.mulDiv(pairedIn, uint256(sp), 1 << 96),
uint256(sp),
1 << 96
);
}
require(expOut > 0, "expected out zero");
uint256 minOut = expOut * (10000 - MAX_PRICE_IMPACT_BPS) / 10000;
require(minOut > 0, "minOut zero");
return minOut;
}
/// @dev Buy core with `pairedIn` via the canonical Universal Router.
/// ExactInputSingleParams MUST be encoded as one struct value.
function _buyCore(Pool memory p, uint256 pairedIn, uint256 minOut) internal returns (uint256 coreOut) {
_permit2Approve(p.xToken, address(router), pairedIn);
bool zeroForOne = !p.tokenIsCurrency0; // paired -> core
bytes memory swapActions = abi.encodePacked(
uint8(V4Actions.SWAP_EXACT_IN_SINGLE),
uint8(V4Actions.SETTLE_ALL),
uint8(V4Actions.TAKE_ALL)
);
bytes[] memory swapParams = new bytes[](3);
swapParams[0] = abi.encode(ExactInputSingleParams({
poolKey: p.key,
zeroForOne: zeroForOne,
amountIn: uint128(pairedIn),
amountOutMinimum: uint128(minOut),
hookData: bytes("")
}));
swapParams[1] = abi.encode(p.xToken, pairedIn); // SETTLE_ALL(currency, maxAmount)
swapParams[2] = abi.encode(token, minOut); // TAKE_ALL(currency, minAmount)
bytes memory commands = abi.encodePacked(uint8(V4Actions.UR_V4_SWAP));
bytes[] memory inputs = new bytes[](1);
inputs[0] = abi.encode(swapActions, swapParams);
uint256 before = IERC20(token).balanceOf(address(this));
router.execute(commands, inputs, block.timestamp + DEADLINE_BUFFER);
coreOut = IERC20(token).balanceOf(address(this)) - before;
_permit2Revoke(p.xToken, address(router));
require(coreOut >= minOut, "buyback under minOut");
}
/// @dev live slot0 sqrtPrice via the PoolManager StateLibrary path.
function _liveSqrtPriceX96(PoolKey memory key) internal view returns (uint160) {
bytes32 poolId = keccak256(abi.encode(key));
bytes32 stateSlot = keccak256(abi.encode(poolId, POOLS_SLOT));
return uint160(uint256(poolManager.extsload(stateSlot)));
}
// ═══════════════════════════════════════════════════════════════════════
// Internals
// ═══════════════════════════════════════════════════════════════════════
/// @dev [D12] pay the launcher share across the fee split. Last recipient
/// gets the remainder so nothing is stranded to rounding.
function _payFeeSplit(uint256 amount) internal returns (uint256 total) {
uint256 n = feeRecipients.length;
uint256 left = amount;
for (uint256 i; i < n; ++i) {
uint256 share = i == n - 1 ? left : amount * feeBps[i] / 10000;
if (share > 0) {
_safeTransfer(token, feeRecipients[i], share);
left -= share;
emit FeePaid(feeRecipients[i], share);
if (i == 0) emit LauncherPaid(feeRecipients[0], share); // Base event parity
}
}
return amount - left;
}
/// @dev Permit2 two-step exact approval (ERC20 -> Permit2 once, Permit2 ->
/// spender exact + 20-min expiry). [D3]
function _permit2Approve(address _token, address spender, uint256 amount) internal {
require(amount <= type(uint160).max, "amount overflow");
(bool ok, bytes memory d) = _token.staticcall(
abi.encodeWithSignature("allowance(address,address)", address(this), address(permit2))
);
require(ok && d.length >= 32, "allowance read failed");
if (abi.decode(d, (uint256)) < amount) {
_safeApprove(_token, address(permit2), amount);
}
permit2.approve(_token, spender, uint160(amount), uint48(block.timestamp + 20 minutes));
}
function _permit2Revoke(address _token, address spender) internal {
permit2.approve(_token, spender, 0, 0);
}
function _safeTransfer(address _token, address to, uint256 amount) internal {
(bool success, bytes memory data) = _token.call(
abi.encodeWithSelector(IERC20.transfer.selector, to, amount)
);
require(success && (data.length == 0 || abi.decode(data, (bool))), "transfer failed");
}
function _safeTransferFrom(address _token, address from, address to, uint256 amount) internal {
(bool success, bytes memory data) = _token.call(
abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, amount)
);
require(success && (data.length == 0 || abi.decode(data, (bool))), "transferFrom failed");
}
function _safeApprove(address _token, address spender, uint256 amount) internal {
(bool success, bytes memory data) = _token.call(
abi.encodeWithSelector(IERC20.approve.selector, spender, 0)
);
require(success && (data.length == 0 || abi.decode(data, (bool))), "approve reset failed");
if (amount > 0) {
(success, data) = _token.call(
abi.encodeWithSelector(IERC20.approve.selector, spender, amount)
);
require(success && (data.length == 0 || abi.decode(data, (bool))), "approve failed");
}
}
/// @dev [D13] native (address(0)) walls are never fuelable/deposit-able via
/// the ERC20-shaped fuel()/depositLiquidity() path — exclude them so
/// canFuel()/acceptsCurrency() answer honestly (fuel would revert loud
/// on a native transferFrom otherwise).
function _findPool(address xToken) internal view returns (uint256) {
if (xToken == address(0)) return type(uint256).max;
uint256 len = pools.length;
for (uint256 i; i < len; ++i) {
if (pools[i].xToken == xToken && !pools[i].disabled) return i;
}
return type(uint256).max;
}
function _burnDust() internal {
uint256 dust = IERC20(token).balanceOf(address(this));
if (dust > 0) {
_safeTransfer(token, BURN, dust);
emit DustBurned(dust);
}
}
// ═══════════════════════════════════════════════════════════════════════
// Views + receiver
// ═══════════════════════════════════════════════════════════════════════
function poolCount() external view returns (uint256) { return pools.length; }
function activePoolCount() external view returns (uint256) {
uint256 count;
uint256 len = pools.length;
for (uint256 i; i < len; ++i) {
if (!pools[i].disabled) count++;
}
return count;
}
function timeUntilExecute() external view returns (uint256) {
if (block.timestamp >= lastExecute + COOLDOWN) return 0;
return (lastExecute + COOLDOWN) - block.timestamp;
}
function canFuel(address xToken) external view returns (bool) {
return _findPool(xToken) < type(uint256).max;
}
/// @notice Prime-ABI alias of canFuel. v1.1 reactors never PUSH fees
/// upstream, but this reactor still ACCEPTS inbound fuel() (v1
/// reactors/other callers may target it), so both probes stay honest.
function acceptsCurrency(address xToken) external view returns (bool) {
return _findPool(xToken) < type(uint256).max;
}
function feeRecipientCount() external view returns (uint256) { return feeRecipients.length; }
function dustBalance(address _token) external view returns (uint256) {
return IERC20(_token).balanceOf(address(this));
}
function onERC721Received(address, address from, uint256, bytes calldata) external view returns (bytes4) {
require(from == admin, "only admin can send NFTs");
return this.onERC721Received.selector;
}
/// @dev [D13] accept native ETH so the ETH wall's fee-collect (TAKE_PAIR on
/// currency0 == address(0)) can deliver ETH-side fees INTO the reactor.
/// The reactor has NO code path that ever sends ETH out — collected ETH
/// is locked here forever, exactly like the LP principal. A plain
/// receiver only (no logic) → cannot be a reentrancy vector.
receive() external payable {}
}
// ═══════════════════════════════════════════════════════════════════════════
// Shillwood — the launch factory (Unrugable V7 on V4 rails)
// ═══════════════════════════════════════════════════════════════════════════
contract Shillwood {
/// @notice v1.1 (no upstream): reactors cut the 10% paired-fee upstream leg —
/// 100% of paired fees route to buyback/deepen. Founder ruling 2026-07-20.
string public constant VERSION = "shillwood-v1.1-no-upstream";
// ── Immutables ────────────────────────────────────────────────────────
address public immutable meme; // MfT twin (18 dec) — parity only, NO wall
address public immutable money; // FTP / Food (6 dec)
address public immutable trees; // GST / Grow Some Trees (6 dec)
address public immutable poolManager; // V4 singleton
address public immutable positionManager; // V4 PositionManager
address public immutable swapRouter; // canonical Universal Router
address public immutable permit2;
address public immutable reactorImpl; // ShillwoodReactor implementation
address public immutable upstreamReactor; // RH Meme Reactor prime
uint24 public immutable ethUsdgFee; // [D13] ETH/USDG ref pool fee
int24 public immutable ethUsdgTickSpacing; // [D13] ETH/USDG ref pool spacing
/// @dev [D13] ETH/USDG live-price ref pool. ETH = native = address(0), so it
/// always sorts as currency0; USDG is currency1. USDG is the $-anchor:
/// usdgPerEth (6-dec) doubles as the USD price of ETH (USDG ~ $1).
address public constant USDG = 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168;
address public owner;
// ── Constants ─────────────────────────────────────────────────────────
string constant METADATA_BASE = "https://tasern.quest/api/shillwood/metadata/";
uint256 constant TOTAL_SUPPLY = 1_000_000_000e18; // 1B tokens
uint256 constant TARGET_MC_MONEY = 10_000_000_000; // $10K in 6-dec ($-pegged)
uint24 constant FEE_TIER = 10000; // 1% fee tier (the walls)
int24 constant TICK_SPACE = 200;
int24 constant TICK_MIN = -887200;
int24 constant TICK_MAX = 887200;
// ── Registry (Base parity) ────────────────────────────────────────────
mapping(address => bool) public isReactor;
mapping(address => address) public reactorOf;
mapping(address => address) public launcherOf;
/// @dev [D14] reverse of reactorOf, so the staged mintEthWall() can find the
/// launched token from its reactor.
mapping(address => address) public tokenOfReactor;
/// @dev [D14] TOKEN ⅓ reserved (held in the factory) for each reactor's ETH
/// wall, minted later by mintEthWall(). Zeroed once the wall is minted.
mapping(address => uint256) public pendingEthSupply;
/// @dev [D14] inverse of reactorOf (token -> its reactor), so rescue() can
/// look up a launched token's reserved-ETH-third and refuse to sweep it.
/// A launched token's ⅓ can ONLY ever become the locked ETH wall.
mapping(address => address) public reactorOfToken;
struct Launch {
address token;
address reactor;
address launcher;
uint256 timestamp;
}
Launch[] public launches;
event TokenLaunched(
address indexed token, address reactor,
address indexed launcher, string name, string symbol
);
event EthWallAdded(address indexed reactor, address indexed token, uint256 tokenId, uint256 tokenAmount);
// ═══════════════════════════════════════════════════════════════════════
// Constructor
// ═══════════════════════════════════════════════════════════════════════
constructor(
address _meme,
address _money,
address _trees,
address _poolManager,
address _positionManager,
address _router,
address _permit2,
address _reactorImpl,
address _upstreamReactor,
uint24 _ethUsdgFee,
int24 _ethUsdgTickSpacing
) {
require(_meme != address(0) && _money != address(0) && _trees != address(0)
&& _poolManager != address(0)
&& _positionManager != address(0) && _router != address(0) && _permit2 != address(0)
&& _reactorImpl != address(0) && _upstreamReactor != address(0), "zero address");
require(_trees != _money && _trees != _meme, "trees dup");
meme = _meme;
money = _money;
trees = _trees;
poolManager = _poolManager;
positionManager = _positionManager;
swapRouter = _router;
permit2 = _permit2;
reactorImpl = _reactorImpl;
upstreamReactor = _upstreamReactor;
ethUsdgFee = _ethUsdgFee;
ethUsdgTickSpacing = _ethUsdgTickSpacing;
owner = msg.sender;
}
// ═══════════════════════════════════════════════════════════════════════
// Launch — single transaction, no seed required (Base parity)
// ═══════════════════════════════════════════════════════════════════════
function launch(
string calldata _name,
string calldata _symbol,
address _customUpstream
) external returns (address tokenAddr, address reactorAddr) {
// 1. Mint 1B tokens to this contract
tokenAddr = address(new LaunchToken(_name, _symbol, TOTAL_SUPPLY, address(this), METADATA_BASE));
// Equal thirds. The ETH ⅓ absorbs the rounding remainder so nothing is
// left unassigned; it is reserved in the factory for mintEthWall(). [D14]
uint256 thirdSupply = TOTAL_SUPPLY / 3;
uint256 ethSupply = TOTAL_SUPPLY - 2 * thirdSupply;
// 2. [D14] Mint ONLY the two ERC20 walls in-tx — the same mint count as
// the old (Money+Meme) design — so the launch tx never exceeds the
// gas the old two-wall launch used. Both at the fixed $10K MC (GST and
// FTP are ~$1-pegged). The native ETH wall is staged (step 8).
// 3. TOKEN/Trees (GST) sell wall (⅓) at the $10K MC price
uint256 treesWallId = _createWall(tokenAddr, trees, thirdSupply, TARGET_MC_MONEY);
// 4. TOKEN/Food (FTP) sell wall (⅓) at the $10K MC price
uint256 moneyWallId = _createWall(tokenAddr, money, thirdSupply, TARGET_MC_MONEY);
// (Base 2b — Money yield registration — DROPPED on RH, see [D7])
// (Base meme wall — REMOVED; MfT fed via charity-token yield now, see header)
// 5. Upstream reactor (default prime, or a validated custom contract).
// [v1.1] recorded in the reactor for identity/invite bookkeeping ONLY —
// the reactor never sends it fees (upstream leg removed).
address upstream = upstreamReactor;
if (_customUpstream != address(0)) {
uint256 sz;
assembly { sz := extcodesize(_customUpstream) }
if (sz > 0) upstream = _customUpstream;
}
// 6. Deploy the per-launch reactor, hand it BOTH ERC20 positions — THE LOCK
reactorAddr = _cloneReactor();
{
// [D12] v1 fee split = 100% to the launcher (byte-for-byte Base payout)
address[] memory recips = new address[](1);
uint16[] memory bps = new uint16[](1);
recips[0] = msg.sender;
bps[0] = 10000;
IShillwoodReactor(reactorAddr).initialize(
tokenAddr, meme,
positionManager, swapRouter, permit2, poolManager,
upstream, recips, bps
);
}
_transferNFT(treesWallId, reactorAddr);
IShillwoodReactor(reactorAddr).addPool(treesWallId);
_transferNFT(moneyWallId, reactorAddr);
IShillwoodReactor(reactorAddr).addPool(moneyWallId);
// 7. Registry (before the reserve/refund so mintEthWall's lookups are set)
isReactor[reactorAddr] = true;
reactorOf[tokenAddr] = reactorAddr;
tokenOfReactor[reactorAddr] = tokenAddr;
reactorOfToken[tokenAddr] = reactorAddr; // [D14] inverse for rescue() guard
launcherOf[reactorAddr] = msg.sender;
launches.push(Launch({
token: tokenAddr, reactor: reactorAddr,
launcher: msg.sender, timestamp: block.timestamp
}));
// 8. [D14] Reserve the ETH ⅓ in the factory for the staged native wall,
// then refund ONLY the rounding dust from the two mints. The factory
// now holds exactly `ethSupply` earmarked for this reactor.
pendingEthSupply[reactorAddr] = ethSupply;
uint256 held = IERC20(tokenAddr).balanceOf(address(this));
if (held > ethSupply) IERC20(tokenAddr).transfer(msg.sender, held - ethSupply);
emit TokenLaunched(tokenAddr, reactorAddr, msg.sender, _name, _symbol);
}
// ═══════════════════════════════════════════════════════════════════════
// Staged native (ETH) wall — SEPARATE tx (gas cap), same lock guarantee [D14]
// ═══════════════════════════════════════════════════════════════════════
/// @notice Mint the TOKEN/ETH one-sided sell wall from the ⅓ reserved at
/// launch and lock it into the reactor forever. Permissioned to the
/// owner or the launcher (the add-only doctrine). Single-sided: the
/// factory deposits ONLY TOKEN (below spot) — ZERO ETH. Priced at the
/// same $10K MC as the other two walls, in ETH read LIVE from the
/// ETH/USDG pool [D13]. Runs in its own tx so its mint gas is a fresh
/// budget — never crammed into launch() (Base ~16.5M cap).
function mintEthWall(address reactor) external returns (uint256 positionId) {
require(msg.sender == owner || msg.sender == launcherOf[reactor], "not authorized");
uint256 amt = pendingEthSupply[reactor];
require(amt > 0, "no pending eth wall");
address tokenAddr = tokenOfReactor[reactor];
require(tokenAddr != address(0), "unknown reactor");
pendingEthSupply[reactor] = 0; // effects before interactions
positionId = _createEthWall(tokenAddr, amt);
_transferNFT(positionId, reactor);
IShillwoodReactor(reactor).addPool(positionId);
// refund this wall's own rounding leftover to the launcher
_refundDust(tokenAddr, launcherOf[reactor]);
emit EthWallAdded(reactor, tokenAddr, positionId, amt);
}
// ═══════════════════════════════════════════════════════════════════════
// Internal — one-sided sell wall on V4 [D1][D2][D3]
// ═══════════════════════════════════════════════════════════════════════
/// @dev Create + initialize the TOKEN/quote V4 pool at the target price and
/// mint the one-sided TOKEN wall from just above spot to max range.
/// quoteTotal = the quote-token value of the WHOLE supply (the MC).
function _createWall(
address tokenAddr,
address quote,
uint256 tokenAmount,
uint256 quoteTotal
) internal returns (uint256 positionId) {
bool tokenIs0 = tokenAddr < quote;
// identical price math to Base: sqrt(amount1/amount0) in Q96
uint160 sqrtPrice = tokenIs0
? _calcSqrtPrice(TOTAL_SUPPLY, quoteTotal)
: _calcSqrtPrice(quoteTotal, TOTAL_SUPPLY);
PoolKey memory key = PoolKey({
currency0: tokenIs0 ? tokenAddr : quote,
currency1: tokenIs0 ? quote : tokenAddr,
fee: FEE_TIER,
tickSpacing: TICK_SPACE,
hooks: address(0)
});
// [D1] V4 initialize returns the tick directly (no slot0 re-read needed)
int24 currentTick = IV4PoolManager(poolManager).initialize(key, sqrtPrice);
int24 baseTick = (currentTick / TICK_SPACE) * TICK_SPACE;
if (baseTick > currentTick) baseTick -= TICK_SPACE;
int24 tickLower;
int24 tickUpper;
if (tokenIs0) {
tickLower = baseTick + TICK_SPACE;
tickUpper = TICK_MAX;
} else {
tickLower = TICK_MIN;
tickUpper = baseTick;
}
positionId = _mintWall(key, tokenAddr, tokenIs0, tickLower, tickUpper, tokenAmount);
}
/// @dev [D13] ETH wall: express the $10K MC in ETH by reading the LIVE
/// ETH/USDG V4 pool, then build the one-sided wall identically to the
/// $-pegged walls. ETH = native = address(0) = currency0; USDG =
/// currency1. Reverts loud if the pool is unreadable — NEVER hardcoded.
function _createEthWall(address tokenAddr, uint256 tokenAmount) internal returns (uint256 positionId) {
// live ETH/USDG sqrtPrice from the PoolManager (extsload / StateLibrary)
PoolKey memory refKey = PoolKey({
currency0: address(0), // ETH (native) always sorts first
currency1: USDG,
fee: ethUsdgFee,
tickSpacing: ethUsdgTickSpacing,
hooks: address(0)
});
bytes32 poolId = keccak256(abi.encode(refKey));
bytes32 stateSlot = keccak256(abi.encode(poolId, uint256(6)));
uint256 sqrtP = uint256(uint160(uint256(IV4PoolManager(poolManager).extsload(stateSlot))));
require(sqrtP != 0, "no ETH/USDG pool");
// usdgPerEth (6-dec) = (sqrtP^2 * 1e18) >> 192 (exact buy-rh-usdg.cjs read).
// ETH(18) = currency0, USDG(6) = currency1, so this is USDG-per-1-ETH in
// 6-dec — and since USDG ~ $1 it is the USD price of ETH. Split into two
// FullMath.mulDiv steps so sqrt^2 uses the 512-bit intermediate and can
// never overflow a bare uint256 (sqrtP can be up to ~2^160).
uint256 priceX96 = FullMath.mulDiv(sqrtP, sqrtP, 1 << 96); // sqrtP^2 / 2^96
uint256 usdgPerEth = FullMath.mulDiv(priceX96, 1e18, 1 << 96); // * 1e18 / 2^96
require(usdgPerEth > 0, "eth price zero");
// ethMC (wei) = $10K expressed in ETH = TARGET_MC(6-dec USDG) * 1e18 / usdgPerEth(6-dec).
// The 6-dec units cancel; result is the ETH (wei) value of the WHOLE supply.
uint256 ethMC = FullMath.mulDiv(TARGET_MC_MONEY, 1e18, usdgPerEth);
require(ethMC > 0, "eth MC zero");
return _createWall(tokenAddr, address(0), tokenAmount, ethMC);
}
/// @dev [D2][D3] V4 wall mint: on-chain liquidity sizing with the round-up
/// guard, Permit2 exact approvals, MINT_POSITION + SETTLE_PAIR.
function _mintWall(
PoolKey memory key,
address tokenAddr,
bool tokenIs0,
int24 tickLower,
int24 tickUpper,
uint256 tokenAmount
) internal returns (uint256 positionId) {
uint160 spL = TickMath.getSqrtPriceAtTick(tickLower);
uint160 spU = TickMath.getSqrtPriceAtTick(tickUpper);
// floor liquidity from the one-sided amount, then DECREMENT until the
// V4 round-UP owed amount fits inside tokenAmount (lib-rh-v4 pattern,
// proven on every RH wall this session).
uint128 liq;
if (tokenIs0) {
liq = LiquidityAmounts.getLiquidityForAmount0(spL, spU, tokenAmount);
while (liq > 0 && LiquidityAmounts.amount0ForLiquidityUp(liq, spL, spU) > tokenAmount) liq--;
} else {
liq = LiquidityAmounts.getLiquidityForAmount1(spL, spU, tokenAmount);
while (liq > 0 && LiquidityAmounts.amount1ForLiquidityUp(liq, spL, spU) > tokenAmount) liq--;
}
require(liq > 0, "zero wall liquidity");
// Permit2 exact approval for the token side only; the quote side owes 0.
IERC20(tokenAddr).approve(permit2, tokenAmount);
IPermit2(permit2).approve(tokenAddr, positionManager, uint160(tokenAmount), uint48(block.timestamp + 20 minutes));
positionId = IV4PositionManager(positionManager).nextTokenId();
bytes memory actions = abi.encodePacked(
uint8(V4Actions.MINT_POSITION),
uint8(V4Actions.SETTLE_PAIR)
);
bytes[] memory params = new bytes[](2);
params[0] = abi.encode(
key, tickLower, tickUpper, uint256(liq),
tokenIs0 ? uint128(tokenAmount) : uint128(0), // amount0Max
tokenIs0 ? uint128(0) : uint128(tokenAmount), // amount1Max
address(this), bytes("")
);
params[1] = abi.encode(key.currency0, key.currency1);
IV4PositionManager(positionManager).modifyLiquidities(
abi.encode(actions, params), block.timestamp + 15 minutes
);
// approval hygiene: no standing allowances [D3]
IPermit2(permit2).approve(tokenAddr, positionManager, 0, 0);
IERC20(tokenAddr).approve(permit2, 0);
require(IV4PositionManager(positionManager).ownerOf(positionId) == address(this), "wall mint failed");
}
// ═══════════════════════════════════════════════════════════════════════
// Math (verbatim Base; _convertViaPool removed with the meme wall — the
// ETH/USDG conversion in _createEthWall uses FullMath.mulDiv instead)
// ═══════════════════════════════════════════════════════════════════════
function _calcSqrtPrice(uint256 amount0, uint256 amount1) internal pure returns (uint160) {
require(amount0 > 0 && amount1 > 0, "zero amount");
uint256 s1 = _sqrt(amount1);
uint256 s0 = _sqrt(amount0);
uint256 result = (s1 << 96) / s0;
require(result > 0 && result <= type(uint160).max, "sqrt overflow");
return uint160(result);
}
function _sqrt(uint256 x) internal pure returns (uint256 y) {
if (x == 0) return 0;
y = x;
uint256 z = (x + 1) / 2;
while (z < y) { y = z; z = (x / z + z) / 2; }
}
// ═══════════════════════════════════════════════════════════════════════
// Helpers (verbatim Base, V4 NFT surface)
// ═══════════════════════════════════════════════════════════════════════
function _cloneReactor() internal returns (address instance) {
address impl = reactorImpl;
assembly {
let ptr := mload(0x40)
mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
mstore(add(ptr, 0x14), shl(0x60, impl))
mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
instance := create(0, ptr, 0x37)
}
require(instance != address(0), "clone failed");
}
function _transferNFT(uint256 tokenId, address to) internal {
IV4PositionManager(positionManager).safeTransferFrom(address(this), to, tokenId);
}
function _refundDust(address tokenAddr, address to) internal {
uint256 bal = IERC20(tokenAddr).balanceOf(address(this));
if (bal > 0) IERC20(tokenAddr).transfer(to, bal);
}
function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
return this.onERC721Received.selector;
}
// ═══════════════════════════════════════════════════════════════════════
// Admin (add-only; verbatim Base surface + the staged addLaunchLP hook)
// ═══════════════════════════════════════════════════════════════════════
/// @notice Pull a position NFT from the caller and lock it into `reactor`
/// forever. THE staged path for additional pairs/causes [D12]:
/// mint the extra LP in its own tx, then lock it here.
function addPoolToReactor(address reactor, uint256 tokenId) external {
require(msg.sender == owner || msg.sender == launcherOf[reactor], "not authorized");
IV4PositionManager(positionManager).transferFrom(msg.sender, address(this), tokenId);
_transferNFT(tokenId, reactor);
IShillwoodReactor(reactor).addPool(tokenId);
}
/// @notice Same as addPoolToReactor — named hook for the v2 customization
/// layer (multi-asset pairs / multi-cause). Staged, permissioned,
/// same lock-forever guarantee.
function addLaunchLP(address reactor, uint256 tokenId) external {
require(msg.sender == owner || msg.sender == launcherOf[reactor], "not authorized");
IV4PositionManager(positionManager).transferFrom(msg.sender, address(this), tokenId);
_transferNFT(tokenId, reactor);
IShillwoodReactor(reactor).addPool(tokenId);
}
/// @notice Lock a factory-held position NFT into a reactor.
function addPoolFromHolding(address reactor, uint256 tokenId) external {
require(msg.sender == owner || msg.sender == launcherOf[reactor], "not authorized");
_transferNFT(tokenId, reactor);
IShillwoodReactor(reactor).addPool(tokenId);
}
function transferReactorAdmin(address reactor, address newAdmin) external {
require(msg.sender == owner, "not owner");
IShillwoodReactor(reactor).transferAdmin(newAdmin);
}
function transferOwnership(address newOwner) external {
require(msg.sender == owner, "not owner");
owner = newOwner;
}
/// @dev [D14] Owner-only sweep of factory-held dust/external tokens. The ETH
/// ⅓ reserved for a launched token's staged wall (pendingEthSupply, held
/// here between launch() and mintEthWall()) is UN-rescuable: only the
/// balance ABOVE the pending reserve can leave, so that reserved third can
/// ONLY ever become the locked ETH wall. External/dust tokens have no
/// reactor (reactorOfToken == 0 → pending == 0) and are still fully
/// swept. Once mintEthWall() zeroes the pending, the token is fully
/// rescuable again. No new privilege — still owner-only, sweep-only.
function rescue(address _token) external {
require(msg.sender == owner, "not owner");
uint256 bal = IERC20(_token).balanceOf(address(this));
uint256 pending = pendingEthSupply[reactorOfToken[_token]];
if (bal > pending) IERC20(_token).transfer(owner, bal - pending);
}
// ═══════════════════════════════════════════════════════════════════════
// Views
// ═══════════════════════════════════════════════════════════════════════
function launchCount() external view returns (uint256) {
return launches.length;
}
}[
{
"type": "constructor",
"inputs": [
{
"name": "_name",
"type": "string",
"internalType": "string"
},
{
"name": "_symbol",
"type": "string",
"internalType": "string"
},
{
"name": "_supply",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "_recipient",
"type": "address",
"internalType": "address"
},
{
"name": "baseURI_",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "nonpayable"
},
{
"name": "Approval",
"type": "event",
"inputs": [
{
"name": "owner",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "spender",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "value",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "Transfer",
"type": "event",
"inputs": [
{
"name": "from",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "to",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "value",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "allowance",
"type": "function",
"inputs": [
{
"name": "",
"type": "address",
"internalType": "address"
},
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "approve",
"type": "function",
"inputs": [
{
"name": "spender",
"type": "address",
"internalType": "address"
},
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "nonpayable"
},
{
"name": "balanceOf",
"type": "function",
"inputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "contractURI",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "view"
},
{
"name": "decimals",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint8",
"internalType": "uint8"
}
],
"stateMutability": "view"
},
{
"name": "name",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "view"
},
{
"name": "symbol",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "view"
},
{
"name": "totalSupply",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "transfer",
"type": "function",
"inputs": [
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "nonpayable"
},
{
"name": "transferFrom",
"type": "function",
"inputs": [
{
"name": "from",
"type": "address",
"internalType": "address"
},
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "nonpayable"
}
]0x6080604052600436101561001257600080fd5b60003560e01c806306fdde0314610578578063095ea7b3146104fe57806318160ddd146104e057806323b872dd14610410578063313ce567146103f457806370a08231146103ba57806395d89b41146102de578063a9059cbb146102ac578063dd62ed3e1461025b5763e8a3d4851461008a57600080fd5b34610256576000366003190112610256576040516100a960608261063a565b602a81526020810190604036833780511561024057603082538051600110156102405760786021820153604080516100e1828261063a565b601081526f181899199a1a9b1b9c1cb0b131b232b360811b60208201526029305b600182116101e75750505080519160009060035461011f81610600565b90600181169081156101c35750600114610169575b508161015b9261014c61016597879451938491610672565b0103601f19810184528361063a565b5191829182610695565b0390f35b90915060036000527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b6000905b8282106101ad575050830160200190610165610134565b6001816020925483858a01015201910190610196565b60ff1916602080880191909152821515909202860190910192506101659050610134565b6001600160f81b03196101fd600f8316856107f6565b511660001a61020c83876107f6565b5360041c60016001609c1b031690801561022a576000190190610102565b634e487b7160e01b600052601160045260246000fd5b634e487b7160e01b600052603260045260246000fd5b600080fd5b34610256576040366003190112610256576102746106c1565b61027c6106d7565b6001600160a01b039182166000908152600560209081526040808320949093168252928352819020549051908152f35b346102565760403660031901126102565760206102d46102ca6106c1565b60243590336106ed565b6040519015158152f35b34610256576000366003190112610256576040516001546000908161030282610600565b80855291600181169081156103935750600114610336575b6101658461032a8186038261063a565b60405191829182610695565b600181527fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6939250905b8082106103795750909150810160200161032a8261031a565b919260018160209254838588010152019101909291610360565b60ff191660208087019190915292151560051b8501909201925061032a915083905061031a565b34610256576020366003190112610256576001600160a01b036103db6106c1565b1660005260046020526020604060002054604051908152f35b3461025657600036600319011261025657602060405160128152f35b34610256576060366003190112610256576104296106c1565b6104316106d7565b6001600160a01b0382166000818152600560209081526040808320338452909152902054604435939160018201610470575b60206102d48686866106ed565b929093918285106104a6576000938452600560209081526040808620338752825290942094839003909455909290918290610463565b60405162461bcd60e51b8152602060048201526012602482015271185b1b1bddd85b98d948195e18d95959195960721b6044820152606490fd5b34610256576000366003190112610256576020600254604051908152f35b34610256576040366003190112610256576105176106c1565b3360008181526005602090815260408083206001600160a01b03909516808452948252918290206024359081905591519182527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591a3602060405160018152f35b3461025657600036600319011261025657604051600080548161059a82610600565b808552916001811690811561039357506001146105c1576101658461032a8186038261063a565b80805260208120939250905b8082106105e65750909150810160200161032a8261031a565b9192600181602092548385880101520191019092916105cd565b90600182811c92168015610630575b602083101461061a57565b634e487b7160e01b600052602260045260246000fd5b91607f169161060f565b90601f8019910116810190811067ffffffffffffffff82111761065c57604052565b634e487b7160e01b600052604160045260246000fd5b60005b8381106106855750506000910152565b8181015183820152602001610675565b604091602082526106b58151809281602086015260208686019101610672565b601f01601f1916010190565b600435906001600160a01b038216820361025657565b602435906001600160a01b038216820361025657565b6001600160a01b031690811515806107e4575b156107b05781600052600460205282604060002054106107795760207fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef918360005260048252604060002085815403905560018060a01b03169384600052600482526040600020818154019055604051908152a3600190565b60405162461bcd60e51b815260206004820152600f60248201526e657863656564732062616c616e636560881b6044820152606490fd5b60405162461bcd60e51b815260206004820152600c60248201526b7a65726f206164647265737360a01b6044820152606490fd5b506001600160a01b0381161515610700565b90815181101561024057016020019056fea2646970667358221220574b93ed3ef8028b28b56015b667c8e0e453f58fce708d86558ca7f74e58cb4c64736f6c63430008230033
| Token | Symbol | Balance | Price | Value |
|---|---|---|---|---|
| no token holdings | ||||
| Type | Age | Block | Details |
|---|---|---|---|
| no cross-chain L1↔L2 transactions for this address | |||
| Transaction Hash | Method | Block | Age | From | To | Type | Item | ||
|---|---|---|---|---|---|---|---|---|---|
| no NFT transfers for this address yet | |||||||||
| Transaction Hash | Method | Block | Age | From | To | Amount | Token | ||
|---|---|---|---|---|---|---|---|---|---|
| no token transfers for this address yet | |||||||||
| Txn Hash | Age | Event | Topics / Data |
|---|---|---|---|
| 0x8f5145…fcc9fd | 28 days agoMon, 20 Jul 2026 20:22:41 UTC | Transfer | [0] 0x000000000000…f3dcce70 [1] 0x000000000000…d6e0ac10 data: 0x000000000000000000…00001e69 |
| 0x8f5145…fcc9fd | 28 days agoMon, 20 Jul 2026 20:22:41 UTC | Approval | [0] 0x000000000000…f3dcce70 [1] 0x000000000000…3ac78ba3 data: 0x000000000000000000…00000000 |
| 0x8f5145…fcc9fd | 28 days agoMon, 20 Jul 2026 20:22:41 UTC | Transfer | [0] 0x000000000000…f3dcce70 [1] 0x000000000000…43e40951 data: 0x000000000000000000…4d5536ed |
| 0x8f5145…fcc9fd | 28 days agoMon, 20 Jul 2026 20:22:41 UTC | Approval | [0] 0x000000000000…f3dcce70 [1] 0x000000000000…3ac78ba3 data: 0x000000000000000000…4d555556 |
| 0xf7b99a…cf1610 | 28 days agoMon, 20 Jul 2026 20:22:36 UTC | Transfer | [0] 0x000000000000…f3dcce70 [1] 0x000000000000…d6e0ac10 data: 0x000000000000000000…1cfba2c8 |
| 0xf7b99a…cf1610 | 28 days agoMon, 20 Jul 2026 20:22:36 UTC | Approval | [0] 0x000000000000…f3dcce70 [1] 0x000000000000…3ac78ba3 data: 0x000000000000000000…00000000 |
| 0xf7b99a…cf1610 | 28 days agoMon, 20 Jul 2026 20:22:36 UTC | Transfer | [0] 0x000000000000…f3dcce70 [1] 0x000000000000…43e40951 data: 0x000000000000000000…3ed783f1 |
| 0xf7b99a…cf1610 | 28 days agoMon, 20 Jul 2026 20:22:36 UTC | Approval | [0] 0x000000000000…f3dcce70 [1] 0x000000000000…3ac78ba3 data: 0x000000000000000000…4d555555 |
| 0xf7b99a…cf1610 | 28 days agoMon, 20 Jul 2026 20:22:36 UTC | Approval | [0] 0x000000000000…f3dcce70 [1] 0x000000000000…3ac78ba3 data: 0x000000000000000000…00000000 |
| 0xf7b99a…cf1610 | 28 days agoMon, 20 Jul 2026 20:22:36 UTC | Transfer | [0] 0x000000000000…f3dcce70 [1] 0x000000000000…43e40951 data: 0x000000000000000000…3ed783f1 |
| 0xf7b99a…cf1610 | 28 days agoMon, 20 Jul 2026 20:22:36 UTC | Approval | [0] 0x000000000000…f3dcce70 [1] 0x000000000000…3ac78ba3 data: 0x000000000000000000…4d555555 |
| 0xf7b99a…cf1610 | 28 days agoMon, 20 Jul 2026 20:22:36 UTC | Transfer | [0] 0x000000000000…00000000 [1] 0x000000000000…f3dcce70 data: 0x000000000000000000…e8000000 |
| Transaction Hash | Method ? | Block | Age | From | To | Amount | Txn Fee ? | ||
|---|---|---|---|---|---|---|---|---|---|
| No direct transactions — this address is only ever reached via internal calls (common for a contract only invoked through a router or proxy). View Internal Transactions → | |||||||||
| Block | Age | Parent Transaction Hash | Type | Method | From | To | Value | |
|---|---|---|---|---|---|---|---|---|
| 14,985,048 | 28 days agoMon, 20 Jul 2026 20:22:36 UTC | 0xf7b99a…cf1610 | CREATE | launch | 0xca80…ce70 | IN | 0x1622…b323 | 0 ETH |