diff --git a/app/vibenet/demos/validity/ValidityDemo.tsx b/app/vibenet/demos/validity/ValidityDemo.tsx index 9acbb5b6..5d969825 100644 --- a/app/vibenet/demos/validity/ValidityDemo.tsx +++ b/app/vibenet/demos/validity/ValidityDemo.tsx @@ -24,7 +24,8 @@ import { ValidityJson } from './components/ValidityJson'; import { amountInForVibe, amountOutAtLimit, - encodeHelperSwap, + encodeHelperSwapExactIn, + encodeHelperSwapExactOut, fillQuoteFromPairLogs, fillQuoteFromSwapReceipt, getReserves, @@ -59,7 +60,6 @@ import { clampToCondition, formatTokenAmount, quoteWad, - swapOuts, tokenInFor, USDV_DECIMALS, USDV_SYMBOL, @@ -683,32 +683,48 @@ function ValidityDemoInner() { const side: Side = draft.side; const tokenIn = tokenInFor(state.deployment, side === 'sell'); try { - const amountIn = amountInForVibe(TRADE_VIBE, side, k, draft.priceWad); - if (amountIn === 0n) throw new Error('Swap size is too small.'); - const inventory = await tokenBalance(publicClient, tokenIn, acct.address); - if (inventory < amountIn) { - throw new Error( - side === 'sell' - ? `Need ${formatTokenAmount(TRADE_VIBE)} ${VIBE_SYMBOL} to sell.` - : `Need ${formatTokenAmount(amountIn, USDV_DECIMALS)} ${USDV_SYMBOL} to buy ${formatTokenAmount(TRADE_VIBE)} ${VIBE_SYMBOL}.`, - ); + // Both sides trade exactly TRADE_VIBE VIBE, filled at the on-chain price + // when the predicate lets the tx in. Sell = exact-in (put in 100 VIBE, + // take >= the limit USDV); buy = exact-out (take exactly 100 VIBE, spend + // <= the limit USDV). The helper computes the variable leg on-chain, so + // neither leaves a surplus in the pool. + let call: ReturnType; + if (side === 'sell') { + const amountIn = TRADE_VIBE; + const inventory = await tokenBalance(publicClient, tokenIn, acct.address); + if (inventory < amountIn) { + throw new Error(`Need ${formatTokenAmount(TRADE_VIBE)} ${VIBE_SYMBOL} to sell.`); + } + const outAtLimit = amountOutAtLimit(amountIn, 'sell', k, draft.priceWad); + const minOut = outAtLimit > 1n ? outAtLimit - 1n : outAtLimit; + if (minOut === 0n) throw new Error('Swap size is too small.'); + call = encodeHelperSwapExactIn({ + helper: state.deployment.helper, + tokenIn, + pair: state.deployment.pair, + amountIn, + minOut, + }); + } else { + // Cap the input at the limit-priced USDV, plus a hair (0.1% + 1 wei) so + // integer rounding at the limit edge can't trip MAX_IN. + const limitIn = amountInForVibe(TRADE_VIBE, 'buy', k, draft.priceWad); + if (limitIn === 0n) throw new Error('Swap size is too small.'); + const maxIn = limitIn + limitIn / 1000n + 1n; + const inventory = await tokenBalance(publicClient, tokenIn, acct.address); + if (inventory < maxIn) { + throw new Error( + `Need ${formatTokenAmount(maxIn, USDV_DECIMALS)} ${USDV_SYMBOL} to buy ${formatTokenAmount(TRADE_VIBE)} ${VIBE_SYMBOL}.`, + ); + } + call = encodeHelperSwapExactOut({ + helper: state.deployment.helper, + tokenIn, + pair: state.deployment.pair, + amountOut: TRADE_VIBE, + maxIn, + }); } - const outExact = amountOutAtLimit(amountIn, side, k, draft.priceWad); - const out = outExact > 1n ? outExact - 1n : outExact; - if (out === 0n) throw new Error('Swap size is too small.'); - const { amount0Out, amount1Out } = swapOuts({ - vibeToken0, - sellVibe: side === 'sell', - amountOut: out, - }); - const call = encodeHelperSwap({ - helper: state.deployment.helper, - tokenIn, - pair: state.deployment.pair, - amountIn, - amount0Out, - amount1Out, - }); const seconds = submitMode === 'concurrent' ? clampNoncelessExpiry(expirySeconds) diff --git a/app/vibenet/demos/validity/lib/amm.ts b/app/vibenet/demos/validity/lib/amm.ts index 1121c4c5..9b6e1986 100644 --- a/app/vibenet/demos/validity/lib/amm.ts +++ b/app/vibenet/demos/validity/lib/amm.ts @@ -214,20 +214,48 @@ export function encodeApprove(token: Address, spender: Address): { to: Address; }; } -export function encodeHelperSwap(args: { +/** + * Exact-in swap through the 0-fee SwapHelper (used for sells): commit `amountIn`, + * receive the full on-chain-computed output (>= `minOut`). The output is derived + * from live reserves in the same tx, so nothing is left in the pool and there is + * no `UniswapV2: K` revert from a stale quote. + */ +export function encodeHelperSwapExactIn(args: { helper: Address; tokenIn: Address; pair: Address; amountIn: bigint; - amount0Out: bigint; - amount1Out: bigint; + minOut: bigint; +}): { to: Address; data: Hex } { + return { + to: args.helper, + data: encodeFunctionData({ + abi: helperAbi, + functionName: 'swapExactIn', + args: [args.tokenIn, args.pair, args.amountIn, args.minOut], + }), + }; +} + +/** + * Exact-out swap through the 0-fee SwapHelper (used for buys): receive exactly + * `amountOut`, spending the on-chain-computed input (<= `maxIn`). The helper + * pulls only the input required at live reserves, so a buy of exactly N VIBE + * never leaves a surplus in the pool. + */ +export function encodeHelperSwapExactOut(args: { + helper: Address; + tokenIn: Address; + pair: Address; + amountOut: bigint; + maxIn: bigint; }): { to: Address; data: Hex } { return { to: args.helper, data: encodeFunctionData({ abi: helperAbi, - functionName: 'swap', - args: [args.tokenIn, args.pair, args.amountIn, args.amount0Out, args.amount1Out], + functionName: 'swapExactOut', + args: [args.tokenIn, args.pair, args.amountOut, args.maxIn], }), }; } diff --git a/app/vibenet/demos/validity/lib/artifacts/SwapHelper.json b/app/vibenet/demos/validity/lib/artifacts/SwapHelper.json index ce1fcf6d..5f0066c1 100644 --- a/app/vibenet/demos/validity/lib/artifacts/SwapHelper.json +++ b/app/vibenet/demos/validity/lib/artifacts/SwapHelper.json @@ -1 +1,73 @@ -{"abi":[{"type":"function","name":"swap","inputs":[{"name":"tokenIn","type":"address","internalType":"address"},{"name":"pair","type":"address","internalType":"address"},{"name":"amountIn","type":"uint256","internalType":"uint256"},{"name":"amount0Out","type":"uint256","internalType":"uint256"},{"name":"amount1Out","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"}],"bytecode":"0x608060405234801561000f575f80fd5b506102298061001d5f395ff3fe608060405234801561000f575f80fd5b5060043610610029575f3560e01c80637a950f991461002d575b5f80fd5b61004061003b366004610184565b610042565b005b6040516323b872dd60e01b81523360048201526001600160a01b038581166024830152604482018590528616906323b872dd906064016020604051808303815f875af1158015610094573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906100b891906101cd565b6100f35760405162461bcd60e51b81526020600482015260086024820152672a2920a729a322a960c11b604482015260640160405180910390fd5b60405163022c0d9f60e01b81526004810183905260248101829052336044820152608060648201525f60848201526001600160a01b0385169063022c0d9f9060a4015f604051808303815f87803b15801561014c575f80fd5b505af115801561015e573d5f803e3d5ffd5b505050505050505050565b80356001600160a01b038116811461017f575f80fd5b919050565b5f805f805f60a08688031215610198575f80fd5b6101a186610169565b94506101af60208701610169565b94979496505050506040830135926060810135926080909101359150565b5f602082840312156101dd575f80fd5b815180151581146101ec575f80fd5b939250505056fea2646970667358221220543bb5afcba8324e91fa3066ed0aabc3ba64dc9364a70aec6466d23a776947e264736f6c63430008140033"} \ No newline at end of file +{ + "abi": [ + { + "type": "function", + "name": "swapExactIn", + "inputs": [ + { + "name": "tokenIn", + "type": "address", + "internalType": "address" + }, + { + "name": "pair", + "type": "address", + "internalType": "address" + }, + { + "name": "amountIn", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "minOut", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "out", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "swapExactOut", + "inputs": [ + { + "name": "tokenIn", + "type": "address", + "internalType": "address" + }, + { + "name": "pair", + "type": "address", + "internalType": "address" + }, + { + "name": "amountOut", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "maxIn", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "amountIn", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + } + ], + "bytecode": "0x6080604052348015600e575f80fd5b506107468061001c5f395ff3fe608060405234801561000f575f80fd5b5060043610610034575f3560e01c806310b8462314610038578063faf725671461005d575b5f80fd5b61004b61004636600461055d565b610070565b60405190815260200160405180910390f35b61004b61006b36600461055d565b61025c565b5f805f8061007e8888610423565b9194509250905061008f86846105b4565b61009983886105cd565b6100a391906105e4565b9350848410156100f05760405162461bcd60e51b815260206004820152601360248201527214ddd85c12195b1c195c8e8813525397d3d555606a1b60448201526064015b60405180910390fd5b6040516323b872dd60e01b81523360048201526001600160a01b038881166024830152604482018890528916906323b872dd906064016020604051808303815f875af1158015610142573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906101669190610603565b6101ae5760405162461bcd60e51b81526020600482015260196024820152785377617048656c7065723a205452414e534645525f46524f4d60381b60448201526064016100e7565b5f80826101bc57855f6101bf565b5f865b90925090506001600160a01b03891663022c0d9f8383335f5b6040519080825280601f01601f191660200182016040528015610202576020820181803683370190505b506040518563ffffffff1660e01b81526004016102229493929190610629565b5f604051808303815f87803b158015610239575f80fd5b505af115801561024b573d5f803e3d5ffd5b505050505050505050949350505050565b5f805f8061026a8888610423565b9250925092508186106102ca5760405162461bcd60e51b815260206004820152602260248201527f5377617048656c7065723a20494e53554646494349454e545f4c495155494449604482015261545960f01b60648201526084016100e7565b6102d4868361067b565b6102de87856105cd565b6102e891906105e4565b6102f39060016105b4565b93508484111561033a5760405162461bcd60e51b815260206004820152601260248201527129bbb0b82432b63832b91d1026a0ac2fa4a760711b60448201526064016100e7565b6040516323b872dd60e01b81523360048201526001600160a01b038881166024830152604482018690528916906323b872dd906064016020604051808303815f875af115801561038c573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906103b09190610603565b6103f85760405162461bcd60e51b81526020600482015260196024820152785377617048656c7065723a205452414e534645525f46524f4d60381b60448201526064016100e7565b5f808261040657875f6101bf565b505f9050866001600160a01b03891663022c0d9f838333826101d8565b5f805f805f856001600160a01b0316630902f1ac6040518163ffffffff1660e01b8152600401606060405180830381865afa158015610464573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061048891906106a9565b5091509150856001600160a01b0316630dfe16816040518163ffffffff1660e01b8152600401602060405180830381865afa1580156104c9573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906104ed91906106f5565b6001600160a01b0316876001600160a01b03161492508261052157806001600160701b0316826001600160701b0316610536565b816001600160701b0316816001600160701b03165b9098909750929550919350505050565b6001600160a01b038116811461055a575f80fd5b50565b5f805f8060808587031215610570575f80fd5b843561057b81610546565b9350602085013561058b81610546565b93969395505050506040820135916060013590565b634e487b7160e01b5f52601160045260245ffd5b808201808211156105c7576105c76105a0565b92915050565b80820281158282048414176105c7576105c76105a0565b5f826105fe57634e487b7160e01b5f52601260045260245ffd5b500490565b5f60208284031215610613575f80fd5b81518015158114610622575f80fd5b9392505050565b84815283602082015260018060a01b0383166040820152608060608201525f8251806080840152806020850160a085015e5f60a0828501015260a0601f19601f83011684010191505095945050505050565b818103818111156105c7576105c76105a0565b80516001600160701b03811681146106a4575f80fd5b919050565b5f805f606084860312156106bb575f80fd5b6106c48461068e565b92506106d26020850161068e565b9150604084015163ffffffff811681146106ea575f80fd5b809150509250925092565b5f60208284031215610705575f80fd5b81516106228161054656fea264697066735822122033045389fa8bd7949133dfb70f345f11e6379452902dc70bf9596232f4bbe8bc64736f6c634300081a0033" +} \ No newline at end of file diff --git a/app/vibenet/demos/validity/lib/constants.ts b/app/vibenet/demos/validity/lib/constants.ts index 185b56b3..2821b72a 100644 --- a/app/vibenet/demos/validity/lib/constants.ts +++ b/app/vibenet/demos/validity/lib/constants.ts @@ -1,12 +1,9 @@ -import { parseAbi, type Abi, type Hex } from 'viem'; +import { parseAbi } from 'viem'; -import helperArtifact from './artifacts/SwapHelper.json'; -import factoryArtifact from './artifacts/UniswapV2Factory.json'; -import minterArtifact from './artifacts/ValidityOpenMinter.json'; - -function with0x(value: string): Hex { - return (value.startsWith('0x') ? value : `0x${value}`) as Hex; -} +// Minimal ABI fragments — only the functions the client calls. The singleton +// addresses are hardcoded (see singleton.ts), so we no longer import the +// compiled artifacts (or their creation bytecode) here; the bytecode is +// vendored in base/vibenet's setup bytecode manifests for deployment. export const erc20Abi = parseAbi([ 'function name() view returns (string)', @@ -19,8 +16,10 @@ export const erc20Abi = parseAbi([ 'function mint(address,uint256)', ]); -export const factoryAbi = factoryArtifact.abi as Abi; -export const factoryBytecode = with0x(factoryArtifact.bytecode); +export const factoryAbi = parseAbi([ + 'function allPairsLength() view returns (uint256)', + 'function allPairs(uint256) view returns (address)', +]); export const pairAbi = parseAbi([ 'function token0() view returns (address)', @@ -30,15 +29,22 @@ export const pairAbi = parseAbi([ 'function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes data)', ]); -export const helperAbi = helperArtifact.abi as Abi; -export const helperBytecode = with0x(helperArtifact.bytecode); +// SwapHelper: 0-fee, computes the variable leg on-chain. exact-in for sells +// (spend amountIn, receive >= minOut); exact-out for buys (receive exactly +// amountOut, spend <= maxIn). See singleton.ts. +export const helperAbi = parseAbi([ + 'function swapExactIn(address tokenIn, address pair, uint256 amountIn, uint256 minOut) returns (uint256)', + 'function swapExactOut(address tokenIn, address pair, uint256 amountOut, uint256 maxIn) returns (uint256)', +]); -export const minterAbi = minterArtifact.abi as Abi; -export const minterBytecode = with0x(minterArtifact.bytecode); +// ValidityOpenMinter: relay holding VIBE's B20 MINT_ROLE. +export const minterAbi = parseAbi([ + 'function mint(address token, address to, uint256 amount)', +]); export const CANDLES_PATH = '/api/vibenet/validity/candles'; -export const STORAGE_KEY = 'vibenet.validity.v6'; +export const STORAGE_KEY = 'vibenet.validity.v7'; export const WAD = 10n ** 18n; export const USDV_DECIMALS = 6; diff --git a/app/vibenet/demos/validity/lib/singleton.test.ts b/app/vibenet/demos/validity/lib/singleton.test.ts deleted file mode 100644 index 685c4c14..00000000 --- a/app/vibenet/demos/validity/lib/singleton.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { predictSingleton, SINGLETON_SALTS, singletonSalt } from './singleton'; - -describe('predictSingleton', () => { - it('is stable across calls and distinct per contract', () => { - const first = predictSingleton(); - expect(predictSingleton()).toEqual(first); - const addrs = [first.minter, first.factory, first.helper]; - expect(new Set(addrs.map((addr) => addr.toLowerCase())).size).toBe(3); - for (const addr of addrs) { - expect(addr).toMatch(/^0x[0-9a-fA-F]{40}$/); - } - }); -}); - -describe('singletonSalt', () => { - it('keeps one salt per label', () => { - expect(singletonSalt('vibe')).toBe(SINGLETON_SALTS.vibe); - expect(new Set(Object.values(SINGLETON_SALTS)).size).toBe(4); - }); -}); diff --git a/app/vibenet/demos/validity/lib/singleton.ts b/app/vibenet/demos/validity/lib/singleton.ts index 48846401..71b9d4e0 100644 --- a/app/vibenet/demos/validity/lib/singleton.ts +++ b/app/vibenet/demos/validity/lib/singleton.ts @@ -1,73 +1,21 @@ -import { - encodeDeployData, - getContractAddress, - keccak256, - toBytes, - zeroAddress, - type Address, - type Hex, - type PublicClient, -} from 'viem'; +import { getContractAddress, keccak256, toBytes, type Address, type Hex, type PublicClient } from 'viem'; import { B20_FACTORY, factoryAbi as b20FactoryAbi } from '../../b20/lib/protocol'; import { vibenetApi } from '../../../library/client'; -import { - factoryAbi, - factoryBytecode, - helperAbi, - helperBytecode, - minterAbi, - minterBytecode, - pairAbi, -} from './constants'; +import { factoryAbi, pairAbi } from './constants'; import type { Deployment } from './types'; /** * Arachnid deterministic-deployment proxy. Already live on Vibenet; used here - * only to re-derive the shared singleton CREATE2 addresses so the demo can - * discover the pool the central actor system deployed. Address is - * CREATE(nickSigner, nonce=0). + * to derive CREATE2 addresses for contracts (like the conditional withdrawal + * demo) that the client itself deploys. Address is CREATE(nickSigner, nonce=0). */ export const CREATE2_DEPLOYER = '0x4e59b44847b379578588920cA78FbF26c0B4956C' as Address; -/** Factory `feeToSetter` is fixed so the CREATE2 address does not depend on who deploys. */ -export const FACTORY_FEE_TO_SETTER = zeroAddress; - export function singletonSalt(label: string): Hex { return keccak256(toBytes(`vibenet.validity.${label}.v1`)); } -export const SINGLETON_SALTS = { - vibe: singletonSalt('vibe'), - minter: singletonSalt('minter'), - factory: singletonSalt('factory'), - helper: singletonSalt('helper'), -} as const; - -export type PredictedSingleton = Pick; - -export function singletonInitCodes(): { - minter: Hex; - factory: Hex; - helper: Hex; -} { - return { - minter: encodeDeployData({ - abi: minterAbi, - bytecode: minterBytecode, - }), - factory: encodeDeployData({ - abi: factoryAbi, - bytecode: factoryBytecode, - args: [FACTORY_FEE_TO_SETTER], - }), - helper: encodeDeployData({ - abi: helperAbi, - bytecode: helperBytecode, - }), - }; -} - export function create2Address(salt: Hex, initCode: Hex): Address { return getContractAddress({ bytecode: initCode, @@ -77,15 +25,19 @@ export function create2Address(salt: Hex, initCode: Hex): Address { }); } -/** CREATE2 addresses for the Uni factory, helper, and VIBE minter. USDV is the faucet token. */ -export function predictSingleton(): PredictedSingleton { - const init = singletonInitCodes(); - return { - minter: create2Address(SINGLETON_SALTS.minter, init.minter), - factory: create2Address(SINGLETON_SALTS.factory, init.factory), - helper: create2Address(SINGLETON_SALTS.helper, init.helper), - }; -} +/** + * Deterministic v3 singleton addresses for the Uni factory, swap helper, and + * VIBE minter — CREATE2 via the deployer above with salt + * keccak256("vibenet.validity.