Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
OscillonHookBasicTest:test_swap_WhenStableDropsTo089_UsesMaxFee() (gas: 278293)
OscillonFeePolicyTest:test_hybridFee_at20Bps_usesPiecewise() (gas: 1297)
OscillonFeePolicyTest:test_hybridFee_zeroDev_returnsOneBps() (gas: 343)
OscillonFeePolicyTest:test_piecewise_cappedAt50() (gas: 821)
OscillonFeePolicyTest:test_piecewise_zone1_deadBand() (gas: 423)
OscillonHookDepegFeeTest:test_chainlinkStale_FallsBackToTWAP() (gas: 224993)
OscillonHookDepegFeeTest:test_disagreementGuard_LargeMismatch_UsesConservative() (gas: 196146)
OscillonHookDepegFeeTest:test_exactOutput_RevertsDuringDepeg() (gas: 148129)
OscillonHookDepegFeeTest:test_swap_Drain20bps_AppliesHybridFee() (gas: 288519)
OscillonHookDepegFeeTest:test_swap_NoDepeg_AppliesBaseFee() (gas: 189905)
OscillonHookDepegFeeTest:test_swap_RestoreDirection_AppliesBaseFee() (gas: 215703)
OscillonHookDepegFeeTest:test_swap_SmallDepegBelowThreshold_AppliesBaseFee() (gas: 195743)
OscillonHookTwapTest:test_afterInitialize_seedsObservationZero() (gas: 16355)
OscillonHookTwapTest:test_afterSwap_appendsObservation() (gas: 243397)
OscillonHookTwapTest:test_afterSwap_dedupesSameSecondWrites() (gas: 322251)
OscillonHookTwapTest:test_observation_buildsHistoryOver30Min() (gas: 6568905)
OscillonHookTwapTest:test_observation_ringBufferWrapsAtCardinality() (gas: 16291403)
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ jobs:
- name: Run tests
run: forge test -vvv

- name: Check gas snapshots
run: forge snapshot --check
env:
FOUNDRY_PROFILE: ci

lint:
runs-on: ubuntu-latest
steps:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ docs/

# Dotenv file
.env

# Root deploy artifact (oscillon-ui/src/deployment.json is updated in-place by forge)
/deployment.json
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,20 @@ forge fmt
forge snapshot
```

## Known Limitations (Pre-Audit)

**Surplus accounting (indicative only)**

`surplusAccrued` and `protocolAccrued` track theoretical LP surplus from dynamic fees but are not connected to actual v4 fee settlement. These values are meaningful as indicators of mechanism activity but `collectProtocolFees` is not safe for production use without implementing proper fee skimming via `donate()` or `afterSwapReturnDelta`. This is a known P0 for mainnet — deferred for POC submission.

**Rounding direction**

Fee surplus math uses `mulDivDown` throughout. This is conservative (slightly under-charges). A production deployment would implement `mulDivUp` on surplus accrual per the rounding policy documented in the audit report.

**K parameter liquidity threshold**

`THIN_POOL_LIQUIDITY` comparison uses incorrect units (USDC atoms vs AMM liquidity units). Fixed by using `K_STANDARD=45` universally in this version.

## MVP Limitations

- Static thresholds and fee tiers (not governance-tunable yet)
Expand Down
6 changes: 5 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ solc_version = '0.8.26'
evm_versoin = 'cancun'
optimizer_runs = 800
via_ir = false


fs_permissions = [
{ access = "read-write", path = "./deployment.json" },
{ access = "read-write", path = "./oscillon-ui/src/deployment.json" },
]

[profile.ci]
ffi = false
Expand Down
265 changes: 265 additions & 0 deletions oscillon-ui/src/deployment.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
/**
* Single source of truth for contract addresses in the frontend.
* `deployment.json` is written automatically by `script/DeployOscillon.s.sol`
* as a chain-keyed registry: `{ "31337": { ... }, "421614": { ... } }`.
*/

import deploymentJson from './deployment.json'

export interface Deployment {
chainId: number
chainName: string
deployedAt: string
deployer: `0x${string}`
poolManager: `0x${string}`
swapRouter: `0x${string}`
liquidityRouter: `0x${string}`
oscillonHook: `0x${string}`
usdc: `0x${string}`
usdt: `0x${string}`
chainlinkFeed0: `0x${string}`
chainlinkFeed1: `0x${string}`
chainlinkAdapter0: `0x${string}`
chainlinkAdapter1: `0x${string}`
oracle0: `0x${string}`
oracle1: `0x${string}`
poolCurrency0: `0x${string}`
poolCurrency1: `0x${string}`
poolFee: number
poolTickSpacing: number
poolHooks: `0x${string}`
poolId: `0x${string}`
}

/** Chain-keyed deployments written by `DeployOscillon.s.sol`. */
export type DeploymentRegistry = Record<string, Deployment>

export const deployments = deploymentJson as DeploymentRegistry

export const DEFAULT_CHAIN_ID = 31337

const EMPTY_DEPLOYMENT: Deployment = {
chainId: 0,
chainName: '',
deployedAt: '0',
deployer: '0x0000000000000000000000000000000000000000',
poolManager: '0x0000000000000000000000000000000000000000',
swapRouter: '0x0000000000000000000000000000000000000000',
liquidityRouter: '0x0000000000000000000000000000000000000000',
oscillonHook: '0x0000000000000000000000000000000000000000',
usdc: '0x0000000000000000000000000000000000000000',
usdt: '0x0000000000000000000000000000000000000000',
chainlinkFeed0: '0x0000000000000000000000000000000000000000',
chainlinkFeed1: '0x0000000000000000000000000000000000000000',
chainlinkAdapter0: '0x0000000000000000000000000000000000000000',
chainlinkAdapter1: '0x0000000000000000000000000000000000000000',
oracle0: '0x0000000000000000000000000000000000000000',
oracle1: '0x0000000000000000000000000000000000000000',
poolCurrency0: '0x0000000000000000000000000000000000000000',
poolCurrency1: '0x0000000000000000000000000000000000000000',
poolFee: 8388608,
poolTickSpacing: 1,
poolHooks: '0x0000000000000000000000000000000000000000',
poolId: '0x0000000000000000000000000000000000000000000000000000000000000000',
}

export function getDeployment(chainId: number = DEFAULT_CHAIN_ID): Deployment {
return deployments[String(chainId)] ?? EMPTY_DEPLOYMENT
}

/** @deprecated Prefer `getDeployment(chainId)` when the wallet chain is known. */
export const deployment = getDeployment(DEFAULT_CHAIN_ID)

export function getHookAddress(chainId: number = DEFAULT_CHAIN_ID) {
return getDeployment(chainId).oscillonHook
}

export function getPoolKey(chainId: number = DEFAULT_CHAIN_ID) {
const d = getDeployment(chainId)
return {
currency0: d.poolCurrency0,
currency1: d.poolCurrency1,
fee: d.poolFee,
tickSpacing: d.poolTickSpacing,
hooks: d.poolHooks,
} as const
}

export const HOOK_ADDRESS = deployment.oscillonHook
export const PM_ADDRESS = deployment.poolManager
export const SWAP_ROUTER_ADDRESS = deployment.swapRouter
export const LIQUIDITY_ROUTER_ADDRESS = deployment.liquidityRouter
export const USDC_ADDRESS = deployment.usdc
export const USDT_ADDRESS = deployment.usdt
export const POOL_ID = deployment.poolId

export const POOL_KEY = getPoolKey(DEFAULT_CHAIN_ID)

export const HOOK_ABI = [
{
name: 'getPoolState',
type: 'function',
stateMutability: 'view',
inputs: [
{
name: 'key',
type: 'tuple',
components: [
{ name: 'currency0', type: 'address' },
{ name: 'currency1', type: 'address' },
{ name: 'fee', type: 'uint24' },
{ name: 'tickSpacing', type: 'int24' },
{ name: 'hooks', type: 'address' },
],
},
],
outputs: [
{ name: 'registered', type: 'bool' },
{ name: 'depegBps', type: 'uint256' },
{ name: 'pegBelow', type: 'bool' },
{ name: 'inRestoreWindow', type: 'bool' },
{ name: 'surplusAccrued', type: 'uint256' },
{ name: 'protocolAccrued', type: 'uint256' },
{ name: 'usingFallback', type: 'bool' },
],
},
{
name: 'getPoolConfig',
type: 'function',
stateMutability: 'view',
inputs: [
{
name: 'key',
type: 'tuple',
components: [
{ name: 'currency0', type: 'address' },
{ name: 'currency1', type: 'address' },
{ name: 'fee', type: 'uint24' },
{ name: 'tickSpacing', type: 'int24' },
{ name: 'hooks', type: 'address' },
],
},
],
outputs: [
{
name: '',
type: 'tuple',
components: [
{ name: 'registered', type: 'bool' },
{ name: 'token0', type: 'address' },
{ name: 'token1', type: 'address' },
{ name: 'maxDepegSwap0', type: 'uint256' },
{ name: 'maxDepegSwap1', type: 'uint256' },
{ name: 'lastHighDepegAt', type: 'uint256' },
{ name: 'surplusAccrued', type: 'uint256' },
{ name: 'protocolAccrued', type: 'uint256' },
],
},
],
},
{
name: 'DepegDetected',
type: 'event',
inputs: [
{ name: 'poolId', type: 'bytes32', indexed: true },
{ name: 'depegBps', type: 'uint256', indexed: false },
{ name: 'feeApplied', type: 'uint24', indexed: false },
{ name: 'swapSize', type: 'uint256', indexed: false },
{ name: 'isDrain', type: 'bool', indexed: false },
{ name: 'usingFallback', type: 'bool', indexed: false },
],
},
{
name: 'PoolRegistered',
type: 'event',
inputs: [
{ name: 'poolId', type: 'bytes32', indexed: true },
{ name: 'token0', type: 'address', indexed: false },
{ name: 'token1', type: 'address', indexed: false },
{ name: 'chainlink0', type: 'address', indexed: false },
{ name: 'chainlink1', type: 'address', indexed: false },
],
},
] as const

export const ERC20_ABI = [
{
name: 'approve',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
outputs: [{ name: '', type: 'bool' }],
},
{
name: 'balanceOf',
type: 'function',
stateMutability: 'view',
inputs: [{ name: 'account', type: 'address' }],
outputs: [{ name: '', type: 'uint256' }],
},
{
name: 'allowance',
type: 'function',
stateMutability: 'view',
inputs: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
],
outputs: [{ name: '', type: 'uint256' }],
},
] as const

export const ANVIL_CHAIN = {
id: 31337,
name: 'Anvil',
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrls: { default: { http: ['http://127.0.0.1:8545'] } },
} as const

export const ARBITRUM_SEPOLIA_CHAIN = {
id: 421614,
name: 'Arbitrum Sepolia',
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrls: { default: { http: ['https://sepolia-rollup.arbitrum.io/rpc'] } },
blockExplorers: {
default: { name: 'Arbiscan', url: 'https://sepolia.arbiscan.io' },
},
} as const

export function getChainConfig(chainId: number = DEFAULT_CHAIN_ID) {
switch (chainId) {
case 31337:
return ANVIL_CHAIN
case 421614:
return ARBITRUM_SEPOLIA_CHAIN
default:
return ANVIL_CHAIN
}
}

export function isDeployed(chainId: number = DEFAULT_CHAIN_ID): boolean {
const d = getDeployment(chainId)
return d.chainId !== 0 && d.oscillonHook !== '0x0000000000000000000000000000000000000000'
}

export function formatDepeg(depegBps: bigint): string {
const bps = Number(depegBps)
if (bps === 0) return 'At parity ($1.00)'
if (bps < 7) return `${bps} bps (micro-depeg)`
if (bps < 30) return `${bps} bps (active depeg)`
return `${bps} bps (SEVERE depeg)`
}

export function formatFee(feePips: number): string {
return `${(feePips / 100).toFixed(2)} bps`
}

export function formatUSD(amount: bigint, decimals = 6): string {
return `$${(Number(amount) / 10 ** decimals).toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`
}
26 changes: 26 additions & 0 deletions oscillon-ui/src/deployment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"31337": {
"chainId": 31337,
"chainName": "anvil",
"chainlinkAdapter0": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
"chainlinkAdapter1": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6",
"chainlinkFeed0": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707",
"chainlinkFeed1": "0x0165878A594ca255338adfa4d48449f69242Eb8F",
"deployedAt": "1781073714",
"deployer": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"liquidityRouter": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
"oracle0": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6",
"oracle1": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
"oscillonHook": "0xF3F624114f4987e11007330a4368D4300d5d10C0",
"poolCurrency0": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0",
"poolCurrency1": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
"poolFee": 8388608,
"poolHooks": "0xF3F624114f4987e11007330a4368D4300d5d10C0",
"poolId": "0x5290c603cc9d194e576d34937f591e5266bcb70a4b27e4c4ef72878dbe16c008",
"poolManager": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
"poolTickSpacing": 1,
"swapRouter": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
"usdc": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
"usdt": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0"
}
}
Loading