From 4c9ef12f0f87bcac73e44813b25a8f91859805e1 Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Thu, 18 Jun 2026 09:01:10 +0200 Subject: [PATCH 1/2] feat(amm): create TWAP price observations on behalf of the pool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `CreatePriceObservations` instruction that registers a TWAP price-observations account for a pool over a time window, via a chained call to the configured TWAP oracle program. The pool acts as the price source: the AMM authorizes it with its pool PDA seed so the oracle ties the feed to that pool. The feed's initial tick is read from the pool's authoritative `CurrentTickAccount` (validated against its pool-derived PDA) rather than being supplied by the caller, so the feed cannot be seeded at a forged price — mirroring what `RecordTick` does. The clock is verified to be the canonical 1-block LEZ clock, and creation is rejected if the observations account already exists. To support the chained call, `AmmConfig` and the `Initialize` instruction are extended with a `twap_oracle_program_id` that the instruction reads. --- Cargo.lock | 4 + artifacts/amm-idl.json | 49 ++ programs/amm/Cargo.toml | 2 + programs/amm/core/src/lib.rs | 39 +- programs/amm/methods/guest/Cargo.lock | 800 ++++++++++++++++-- programs/amm/methods/guest/src/bin/amm.rs | 42 +- programs/amm/src/create_price_observations.rs | 433 ++++++++++ programs/amm/src/initialize.rs | 46 +- programs/amm/src/lib.rs | 1 + programs/amm/src/tests.rs | 2 + programs/integration_tests/Cargo.toml | 2 + programs/integration_tests/tests/amm.rs | 161 ++++ 12 files changed, 1514 insertions(+), 67 deletions(-) create mode 100644 programs/amm/src/create_price_observations.rs diff --git a/Cargo.lock b/Cargo.lock index 96fdd84..1e269ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,8 +93,10 @@ name = "amm_program" version = "0.1.0" dependencies = [ "amm_core", + "clock_core", "nssa_core", "token_core", + "twap_oracle_core", ] [[package]] @@ -1988,6 +1990,8 @@ dependencies = [ "stablecoin_core", "token-methods", "token_core", + "twap-oracle-methods", + "twap_oracle_core", ] [[package]] diff --git a/artifacts/amm-idl.json b/artifacts/amm-idl.json index 8e27428..dfa00c0 100644 --- a/artifacts/amm-idl.json +++ b/artifacts/amm-idl.json @@ -16,6 +16,51 @@ { "name": "token_program_id", "type": "program_id" + }, + { + "name": "twap_oracle_program_id", + "type": "program_id" + } + ] + }, + { + "name": "create_price_observations", + "accounts": [ + { + "name": "config", + "writable": false, + "signer": false, + "init": false + }, + { + "name": "pool", + "writable": false, + "signer": false, + "init": false + }, + { + "name": "current_tick_account", + "writable": false, + "signer": false, + "init": false + }, + { + "name": "price_observations", + "writable": false, + "signer": false, + "init": false + }, + { + "name": "clock", + "writable": false, + "signer": false, + "init": false + } + ], + "args": [ + { + "name": "window_duration", + "type": "u64" } ] }, @@ -434,6 +479,10 @@ { "name": "token_program_id", "type": "program_id" + }, + { + "name": "twap_oracle_program_id", + "type": "program_id" } ] } diff --git a/programs/amm/Cargo.toml b/programs/amm/Cargo.toml index 28dce03..578f8ac 100644 --- a/programs/amm/Cargo.toml +++ b/programs/amm/Cargo.toml @@ -8,5 +8,7 @@ workspace = true [dependencies] nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3", features = ["host"] } +clock_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0-rc3" } amm_core = { path = "core" } token_core = { path = "../token/core" } +twap_oracle_core = { path = "../twap_oracle/core" } diff --git a/programs/amm/core/src/lib.rs b/programs/amm/core/src/lib.rs index 324e8ef..0f7e8b8 100644 --- a/programs/amm/core/src/lib.rs +++ b/programs/amm/core/src/lib.rs @@ -19,16 +19,45 @@ pub enum Instruction { /// Initializes the AMM Program by creating its singleton configuration account. /// /// The configuration account is a PDA derived from the constant `"CONFIG"` seed - /// (`compute_config_pda(self_program_id)`). It stores the Token Program ID that the AMM - /// uses for every chained call. The Program must be initialized via this instruction before - /// any pool can be created or interacted with — the other instructions read the Token - /// Program ID from this account and reject calls when it does not yet exist. + /// (`compute_config_pda(self_program_id)`). It stores the program IDs the AMM issues chained + /// calls to (the Token Program and the TWAP oracle program). The Program must be initialized + /// via this instruction before any pool can be created or interacted with — the other + /// instructions read these program IDs from this account and reject calls when it does not + /// yet exist. /// /// Required accounts: /// - AMM Config Account, uninitialized, derived as `compute_config_pda(self_program_id)` Initialize { /// Program ID of the Token Program the AMM will issue chained calls to. token_program_id: ProgramId, + /// Program ID of the TWAP oracle program the AMM will issue chained calls to. + twap_oracle_program_id: ProgramId, + }, + + /// Creates a TWAP price-observations account for a pool over a time window, on behalf of the + /// AMM, via a chained call to the configured TWAP oracle program. + /// + /// The pool acts as the price source: the AMM authorizes it (via its pool PDA seed) so the + /// oracle ties the observations account to this pool. The feed's initial tick is read from the + /// pool's [`CurrentTickAccount`](twap_oracle_core::CurrentTickAccount) — the authoritative + /// tick the AMM previously wrote — rather than being supplied by the caller, so the feed + /// cannot be seeded at a forged price. Rejects if the observations account already exists. + /// The clock must be the canonical 1-block LEZ clock. + /// + /// Required accounts: + /// - AMM Config Account (initialized) + /// - AMM Pool (initialized; acts as the price source) + /// - Current Tick Account, the pool's initialized TWAP PDA derived as + /// `compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id)`; supplies the + /// initial tick + /// - Price Observations Account, uninitialized TWAP PDA derived as + /// `compute_price_observations_pda(twap_oracle_program_id, pool.account_id, + /// window_duration)` + /// - Clock Account (the canonical 1-block LEZ clock) + CreatePriceObservations { + /// Duration of the TWAP window this feed serves, in milliseconds. Part of the + /// observations PDA seed, so each window gets a distinct account. + window_duration: u64, }, /// Initializes a new Pool (or re-initializes an existing zero-supply Pool). @@ -204,6 +233,8 @@ impl From<&PoolDefinition> for Data { pub struct AmmConfig { /// Program ID of the Token Program the AMM issues chained calls to. pub token_program_id: ProgramId, + /// Program ID of the TWAP oracle program the AMM issues chained calls to. + pub twap_oracle_program_id: ProgramId, } impl TryFrom<&Data> for AmmConfig { diff --git a/programs/amm/methods/guest/Cargo.lock b/programs/amm/methods/guest/Cargo.lock index 16a38c0..204c32d 100644 --- a/programs/amm/methods/guest/Cargo.lock +++ b/programs/amm/methods/guest/Cargo.lock @@ -29,6 +29,44 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "alloy-primitives" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4885c1409b6936c4898e646ef58baf6ec54edaf6d8179f79df805a7b85b7cf3e" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "foldhash 0.2.0", + "hashbrown 0.17.1", + "indexmap 2.14.0", + "itoa", + "k256", + "keccak-asm", + "paste", + "proptest", + "rand 0.9.4", + "rapidhash", + "ruint", + "rustc-hash", + "secp256k1", + "serde", + "sha3", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc90b1e703d3c03f4ff7f48e82dd0bc1c8211ab7d079cd836a06fcfeb06651cb" +dependencies = [ + "arrayvec", + "bytes", +] + [[package]] name = "amm-guest" version = "0.1.0" @@ -60,8 +98,10 @@ name = "amm_program" version = "0.1.0" dependencies = [ "amm_core", + "clock_core", "nssa_core", "token_core", + "twap_oracle_core", ] [[package]] @@ -86,9 +126,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" dependencies = [ "ark-ec", - "ark-ff", + "ark-ff 0.5.0", "ark-r1cs-std", - "ark-std", + "ark-std 0.5.0", ] [[package]] @@ -100,14 +140,14 @@ dependencies = [ "ahash", "ark-crypto-primitives-macros", "ark-ec", - "ark-ff", + "ark-ff 0.5.0", "ark-relations", - "ark-serialize", + "ark-serialize 0.5.0", "ark-snark", - "ark-std", + "ark-std 0.5.0", "blake2", "derivative", - "digest", + "digest 0.10.7", "fnv", "merlin", "sha2", @@ -131,10 +171,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" dependencies = [ "ahash", - "ark-ff", + "ark-ff 0.5.0", "ark-poly", - "ark-serialize", - "ark-std", + "ark-serialize 0.5.0", + "ark-std 0.5.0", "educe", "fnv", "hashbrown 0.15.5", @@ -145,18 +185,56 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ark-ff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" +dependencies = [ + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.4.1", + "zeroize", +] + [[package]] name = "ark-ff" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" dependencies = [ - "ark-ff-asm", - "ark-ff-macros", - "ark-serialize", - "ark-std", + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", "arrayvec", - "digest", + "digest 0.10.7", "educe", "itertools 0.13.0", "num-bigint", @@ -165,6 +243,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ark-ff-asm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "ark-ff-asm" version = "0.5.0" @@ -175,6 +273,31 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "ark-ff-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +dependencies = [ + "num-bigint", + "num-traits", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "ark-ff-macros" version = "0.5.0" @@ -196,11 +319,11 @@ checksum = "88f1d0f3a534bb54188b8dcc104307db6c56cdae574ddc3212aec0625740fc7e" dependencies = [ "ark-crypto-primitives", "ark-ec", - "ark-ff", + "ark-ff 0.5.0", "ark-poly", "ark-relations", - "ark-serialize", - "ark-std", + "ark-serialize 0.5.0", + "ark-std 0.5.0", ] [[package]] @@ -210,9 +333,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" dependencies = [ "ahash", - "ark-ff", - "ark-serialize", - "ark-std", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", "educe", "fnv", "hashbrown 0.15.5", @@ -225,9 +348,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "941551ef1df4c7a401de7068758db6503598e6f01850bdb2cfdb614a1f9dbea1" dependencies = [ "ark-ec", - "ark-ff", + "ark-ff 0.5.0", "ark-relations", - "ark-std", + "ark-std 0.5.0", "educe", "num-bigint", "num-integer", @@ -241,12 +364,33 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec46ddc93e7af44bcab5230937635b06fb5744464dd6a7e7b083e80ebd274384" dependencies = [ - "ark-ff", - "ark-std", + "ark-ff 0.5.0", + "ark-std 0.5.0", "tracing", "tracing-subscriber", ] +[[package]] +name = "ark-serialize" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" +dependencies = [ + "ark-std 0.3.0", + "digest 0.9.0", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint", +] + [[package]] name = "ark-serialize" version = "0.5.0" @@ -254,9 +398,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" dependencies = [ "ark-serialize-derive", - "ark-std", + "ark-std 0.5.0", "arrayvec", - "digest", + "digest 0.10.7", "num-bigint", ] @@ -277,10 +421,30 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d368e2848c2d4c129ce7679a7d0d2d612b6a274d3ea6a13bad4445d61b381b88" dependencies = [ - "ark-ff", + "ark-ff 0.5.0", "ark-relations", - "ark-serialize", - "ark-std", + "ark-serialize 0.5.0", + "ark-std 0.5.0", +] + +[[package]] +name = "ark-std" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.6", ] [[package]] @@ -311,6 +475,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -350,12 +525,37 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + [[package]] name = "bit-vec" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitcoin-io" +version = "0.1.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11301df0b06f22dea7bb1916403fdd88a371031e495c49b8f96931b28189e175" + +[[package]] +name = "bitcoin_hashes" +version = "0.14.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9901a56e133a1fc86eeb1113e2591f45f4682451ca893bff494d2f88918e3f" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -368,13 +568,25 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -453,6 +665,12 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + [[package]] name = "bytemuck" version = "1.25.0" @@ -523,7 +741,7 @@ checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" dependencies = [ "camino", "cargo-platform", - "semver", + "semver 1.0.28", "serde", "serde_json", "thiserror 2.0.18", @@ -585,6 +803,15 @@ dependencies = [ "inout", ] +[[package]] +name = "clock_core" +version = "0.1.0" +source = "git+https://github.com/logos-blockchain/logos-execution-zone.git?tag=v0.2.0-rc3#cf3639d8252040d13b3d4e933feb19b42c76e14a" +dependencies = [ + "borsh", + "nssa_core", +] + [[package]] name = "cobs" version = "0.3.0" @@ -594,12 +821,54 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "const-hex" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "proptest", + "serde_core", +] + [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -645,6 +914,12 @@ dependencies = [ "libc", ] +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -823,13 +1098,23 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ + "convert_case", "proc-macro2", "quote", - "rustc_version", + "rustc_version 0.4.1", "syn 2.0.117", "unicode-xid", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.10.7" @@ -842,6 +1127,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.2", +] + [[package]] name = "dirs" version = "6.0.0" @@ -910,7 +1205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", + "digest 0.10.7", "elliptic-curve", "rfc6979", "serdect", @@ -950,7 +1245,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", @@ -1020,12 +1315,44 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "fastrlp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "fastrlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + [[package]] name = "ff" version = "0.13.1" @@ -1042,6 +1369,18 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.6", + "rustc-hex", + "static_assertions", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1054,6 +1393,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.5.0" @@ -1090,6 +1435,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures-channel" version = "0.3.32" @@ -1226,7 +1577,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -1234,6 +1585,9 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] [[package]] name = "hashlink" @@ -1256,6 +1610,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + [[package]] name = "hex-literal" version = "0.4.1" @@ -1268,7 +1631,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1517,12 +1880,38 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "include_bytes_aligned" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee796ad498c8d9a1d68e477df8f754ed784ef875de1414ebdaf169f70a6a784" +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + [[package]] name = "indexmap" version = "1.9.3" @@ -1561,6 +1950,15 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -1621,6 +2019,41 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "keccak-asm" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5dc2c0d691cbf7595cde551ced329cca99c2387c2cbc97754c5d0cd045d3ee" +dependencies = [ + "digest 0.10.7", + "sha3-asm", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + [[package]] name = "lazy-regex" version = "3.6.0" @@ -1737,7 +2170,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" dependencies = [ "byteorder", - "keccak", + "keccak 0.1.6", "rand_core 0.6.4", "zeroize", ] @@ -1895,6 +2328,34 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "paste" version = "1.0.15" @@ -1916,6 +2377,16 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -1989,6 +2460,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint", +] + [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -2025,11 +2507,16 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ + "bit-set", + "bit-vec", "bitflags 2.11.1", "num-traits", "rand 0.9.4", "rand_chacha 0.9.0", "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", "unarray", ] @@ -2056,6 +2543,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quinn" version = "0.11.9" @@ -2132,12 +2625,19 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ + "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", ] @@ -2199,6 +2699,15 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rapidhash" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +dependencies = [ + "rustversion", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -2342,7 +2851,7 @@ dependencies = [ "risc0-zkp", "risc0-zkvm-platform", "ruint", - "semver", + "semver 1.0.28", "serde", "tracing", ] @@ -2364,7 +2873,7 @@ dependencies = [ "risc0-zkp", "risc0-zkvm-platform", "rzup", - "semver", + "semver 1.0.28", "serde", "serde_json", "stability", @@ -2439,9 +2948,9 @@ dependencies = [ "anyhow", "ark-bn254", "ark-ec", - "ark-ff", + "ark-ff 0.5.0", "ark-groth16", - "ark-serialize", + "ark-serialize 0.5.0", "bytemuck", "hex", "num-bigint", @@ -2473,7 +2982,7 @@ dependencies = [ "borsh", "bytemuck", "cfg-if", - "digest", + "digest 0.10.7", "hex", "hex-literal", "metal", @@ -2515,7 +3024,7 @@ dependencies = [ "risc0-zkvm-platform", "rrs-lib", "rzup", - "semver", + "semver 1.0.28", "serde", "sha2", "stability", @@ -2539,6 +3048,16 @@ dependencies = [ "stability", ] +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + [[package]] name = "rrs-lib" version = "0.1.0" @@ -2556,7 +3075,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid", - "digest", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", @@ -2571,14 +3090,27 @@ dependencies = [ [[package]] name = "ruint" -version = "1.18.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0298da754d1395046b0afdc2f20ee76d29a8ae310cd30ffa84ed42acba9cb12a" +checksum = "a68df0380e5c9d20ce49534f292a36a7514ae21350726efe1865bdb1fa91d278" dependencies = [ + "alloy-rlp", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "ark-ff 0.5.0", "borsh", + "bytes", + "fastrlp 0.3.1", + "fastrlp 0.4.0", + "num-bigint", + "num-integer", + "num-traits", + "parity-scale-codec", + "primitive-types", "proptest", "rand 0.8.6", "rand 0.9.4", + "rlp", "ruint-macro", "serde_core", "valuable", @@ -2597,13 +3129,28 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver", + "semver 1.0.28", ] [[package]] @@ -2660,6 +3207,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.23" @@ -2674,7 +3233,7 @@ checksum = "5d2aed296f203fa64bcb4b52069356dd86d6ec578593985b919b6995bee1f0ae" dependencies = [ "hex", "rsa", - "semver", + "semver 1.0.28", "serde", "serde_with", "sha2", @@ -2724,6 +3283,35 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" +dependencies = [ + "bitcoin_hashes", + "rand 0.9.4", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + [[package]] name = "semver" version = "1.0.28" @@ -2734,6 +3322,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "semver-parser" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +dependencies = [ + "pest", +] + [[package]] name = "serde" version = "1.0.228" @@ -2848,7 +3445,27 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", +] + +[[package]] +name = "sha3-asm" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6287fd675f713484342a89cbf0a386abef5f15919cfad607e5e1f19e1e15331" +dependencies = [ + "cc", + "cfg-if", ] [[package]] @@ -2863,7 +3480,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -2961,6 +3578,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -3036,6 +3659,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.27.0" @@ -3356,12 +3985,44 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "twap_oracle_core" +version = "0.1.0" +dependencies = [ + "alloy-primitives", + "borsh", + "nssa_core", + "risc0-zkvm", + "ruint", + "serde", + "spel-framework-macros", + "uniswap_v3_math", +] + [[package]] name = "typenum" version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + [[package]] name = "unarray" version = "0.1.4" @@ -3374,12 +4035,29 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "uniswap_v3_math" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e393498a831893ce69ed6e1d06615e400bd1e8f97e9fcd113324f2d610fe6d45" +dependencies = [ + "alloy-primitives", + "eyre", + "thiserror 2.0.18", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -3416,6 +4094,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "want" version = "0.3.1" @@ -3548,7 +4235,7 @@ dependencies = [ "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap 2.14.0", - "semver", + "semver 1.0.28", ] [[package]] @@ -3899,7 +4586,7 @@ dependencies = [ "id-arena", "indexmap 2.14.0", "log", - "semver", + "semver 1.0.28", "serde", "serde_derive", "serde_json", @@ -3913,6 +4600,15 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yaml-rust2" version = "0.10.4" diff --git a/programs/amm/methods/guest/src/bin/amm.rs b/programs/amm/methods/guest/src/bin/amm.rs index 2c62ae3..222de11 100644 --- a/programs/amm/methods/guest/src/bin/amm.rs +++ b/programs/amm/methods/guest/src/bin/amm.rs @@ -29,12 +29,50 @@ mod amm { ctx: ProgramContext, config: AccountWithMetadata, token_program_id: ProgramId, + twap_oracle_program_id: ProgramId, ) -> SpelResult { - let post_states = - amm_program::initialize::initialize(config, token_program_id, ctx.self_program_id); + let post_states = amm_program::initialize::initialize( + config, + token_program_id, + twap_oracle_program_id, + ctx.self_program_id, + ); Ok(spel_framework::SpelOutput::execute(post_states, vec![])) } + /// Creates a TWAP price-observations account for a pool over a time window, on behalf of the + /// AMM, via a chained call to the configured TWAP oracle program. + /// + /// Expected accounts: + /// 1. `config` — initialized AMM config account. + /// 2. `pool` — initialized AMM pool; acts as the (authorized) price source. + /// 3. `current_tick_account` — the pool's initialized TWAP current-tick PDA; supplies the + /// initial tick. + /// 4. `price_observations` — uninitialized TWAP PDA for `(pool, window_duration)`. + /// 5. `clock` — the canonical 1-block LEZ clock account. + #[instruction] + pub fn create_price_observations( + ctx: ProgramContext, + config: AccountWithMetadata, + pool: AccountWithMetadata, + current_tick_account: AccountWithMetadata, + price_observations: AccountWithMetadata, + clock: AccountWithMetadata, + window_duration: u64, + ) -> SpelResult { + let (post_states, chained_calls) = + amm_program::create_price_observations::create_price_observations( + config, + pool, + current_tick_account, + price_observations, + clock, + window_duration, + ctx.self_program_id, + ); + Ok(spel_framework::SpelOutput::execute(post_states, chained_calls)) + } + /// Initializes a new Pool (or re-initializes an existing zero-supply Pool). /// A fresh user LP holding must be explicitly authorized by the caller. #[expect( diff --git a/programs/amm/src/create_price_observations.rs b/programs/amm/src/create_price_observations.rs new file mode 100644 index 0000000..46b4dff --- /dev/null +++ b/programs/amm/src/create_price_observations.rs @@ -0,0 +1,433 @@ +use amm_core::{ + compute_config_pda, compute_pool_pda, compute_pool_pda_seed, AmmConfig, PoolDefinition, +}; +use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID; +use nssa_core::{ + account::{Account, AccountWithMetadata}, + program::{AccountPostState, ChainedCall, ProgramId}, +}; +use twap_oracle_core::{ + compute_current_tick_account_pda, compute_price_observations_pda, CurrentTickAccount, +}; + +/// Creates a TWAP price-observations account for `pool` over a time window, on behalf of the AMM. +/// +/// The pool acts as the price source: the AMM authorizes it via its pool PDA seed so the oracle +/// ties the observations account to this pool. The work itself is delegated to the configured +/// TWAP oracle program through a single chained call to its `CreatePriceObservations` instruction, +/// which claims and initialises the observations PDA. +/// +/// The initial tick is **not** caller-supplied: it is read from the pool's +/// [`CurrentTickAccount`] — the authoritative tick the AMM previously wrote via the oracle — so the +/// feed cannot be seeded at a forged price. This mirrors what `RecordTick` does, using the current +/// tick to seed the very first observation. +/// +/// The TWAP oracle program ID is read from the AMM config account (the initialization gate). The +/// clock must be the canonical 1-block LEZ system clock, and the observations account must not yet +/// exist — both are checked here so the call is rejected early with an AMM-level error, in +/// addition to the oracle's own checks. +/// +/// # Panics +/// Panics if: +/// - `config.account_id` does not match `compute_config_pda(amm_program_id)`, or the config is +/// uninitialized (the AMM Program has not been initialized). +/// - `clock.account_id` is not [`CLOCK_01_PROGRAM_ACCOUNT_ID`]. +/// - `pool.account` does not hold a valid [`PoolDefinition`], or `pool.account_id` does not match +/// its pool PDA. +/// - `current_tick_account.account_id` does not match the pool's current-tick PDA, or the account +/// does not hold a valid [`CurrentTickAccount`] (it has not been created yet). +/// - `price_observations.account_id` does not match the expected TWAP PDA for `(pool, +/// window_duration)`, or `price_observations.account` already exists. +pub fn create_price_observations( + config: AccountWithMetadata, + pool: AccountWithMetadata, + current_tick_account: AccountWithMetadata, + price_observations: AccountWithMetadata, + clock: AccountWithMetadata, + window_duration: u64, + amm_program_id: ProgramId, +) -> (Vec, Vec) { + // Config gate: validate the config PDA and read the TWAP oracle program ID from it. + assert_eq!( + config.account_id, + compute_config_pda(amm_program_id), + "Create price observations: AMM config Account ID does not match PDA" + ); + let twap_oracle_program_id = AmmConfig::try_from(&config.account.data) + .expect("Create price observations: AMM Program must be initialized before use") + .twap_oracle_program_id; + + // The clock must be the canonical 1-block LEZ system clock; otherwise a caller could seed the + // feed with a forged base timestamp. + assert_eq!( + clock.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID, + "Create price observations: clock account must be the canonical 1-block LEZ clock account" + ); + + // The pool is the price source. Verify it is a genuine AMM pool PDA so we only ever authorize + // a real pool as the source. + let pool_def = PoolDefinition::try_from(&pool.account.data) + .expect("Create price observations: AMM Program expects a valid Pool Definition Account"); + assert_eq!( + pool.account_id, + compute_pool_pda( + amm_program_id, + pool_def.definition_token_a_id, + pool_def.definition_token_b_id, + ), + "Create price observations: Pool Account ID does not match PDA" + ); + + // The initial tick comes from the pool's authoritative CurrentTickAccount, not from the + // caller. Verifying its PDA ties it to this exact pool, so the seed tick cannot be forged. + assert_eq!( + current_tick_account.account_id, + compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id), + "Create price observations: current tick Account ID does not match PDA" + ); + let initial_tick = CurrentTickAccount::try_from(¤t_tick_account.account.data) + .expect("Create price observations: AMM Program expects a valid CurrentTickAccount") + .tick; + + // Verify the observations account is the expected TWAP PDA for this (pool, window) pair and + // reject if it already exists. + assert_eq!( + price_observations.account_id, + compute_price_observations_pda(twap_oracle_program_id, pool.account_id, window_duration), + "Create price observations: price observations Account ID does not match PDA" + ); + assert_eq!( + price_observations.account, + Account::default(), + "Create price observations: price observations account already exists" + ); + + // Authorize the pool as the price source so the oracle ties the feed to this pool. The AMM + // proves control of the pool PDA via its seed. + let mut pool_price_source = pool.clone(); + pool_price_source.is_authorized = true; + + let chained_call = ChainedCall::new( + twap_oracle_program_id, + vec![price_observations.clone(), pool_price_source, clock.clone()], + &twap_oracle_core::Instruction::CreatePriceObservations { + initial_tick, + window_duration, + }, + ) + .with_pda_seeds(vec![compute_pool_pda_seed( + pool_def.definition_token_a_id, + pool_def.definition_token_b_id, + )]); + + let post_states = vec![ + AccountPostState::new(config.account.clone()), + AccountPostState::new(pool.account.clone()), + AccountPostState::new(current_tick_account.account.clone()), + AccountPostState::new(price_observations.account.clone()), + AccountPostState::new(clock.account.clone()), + ]; + + (post_states, vec![chained_call]) +} + +#[cfg(test)] +mod tests { + use amm_core::compute_pool_pda_seed; + use nssa_core::account::{Account, AccountId, Data, Nonce}; + use twap_oracle_core::compute_current_tick_account_pda; + + use super::*; + + const AMM_PROGRAM_ID: ProgramId = [42; 8]; + const TOKEN_PROGRAM_ID: ProgramId = [15; 8]; + const TWAP_ORACLE_PROGRAM_ID: ProgramId = [77; 8]; + /// 24-hour window in milliseconds. + const WINDOW_24H: u64 = 24 * 60 * 60 * 1_000; + /// The authoritative tick stored in the pool's `CurrentTickAccount`. + const CURRENT_TICK: i32 = -1_234; + + fn token_a_id() -> AccountId { + AccountId::new([3; 32]) + } + + fn token_b_id() -> AccountId { + AccountId::new([4; 32]) + } + + fn pool_id() -> AccountId { + compute_pool_pda(AMM_PROGRAM_ID, token_a_id(), token_b_id()) + } + + fn config_init() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: AMM_PROGRAM_ID, + balance: 0, + data: Data::from(&AmmConfig { + token_program_id: TOKEN_PROGRAM_ID, + twap_oracle_program_id: TWAP_ORACLE_PROGRAM_ID, + }), + nonce: Nonce(0), + }, + is_authorized: false, + account_id: compute_config_pda(AMM_PROGRAM_ID), + } + } + + fn pool() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: AMM_PROGRAM_ID, + balance: 0, + data: Data::from(&PoolDefinition { + definition_token_a_id: token_a_id(), + definition_token_b_id: token_b_id(), + vault_a_id: AccountId::new([5; 32]), + vault_b_id: AccountId::new([6; 32]), + liquidity_pool_id: AccountId::new([7; 32]), + liquidity_pool_supply: 5_000, + reserve_a: 5_000, + reserve_b: 2_500, + fees: amm_core::FEE_TIER_BPS_30, + }), + nonce: Nonce(0), + }, + is_authorized: false, + account_id: pool_id(), + } + } + + fn current_tick_account() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TWAP_ORACLE_PROGRAM_ID, + balance: 0, + data: Data::from(&CurrentTickAccount { + tick: CURRENT_TICK, + last_updated: 1_700_000_000_000, + }), + nonce: Nonce(0), + }, + is_authorized: false, + account_id: compute_current_tick_account_pda(TWAP_ORACLE_PROGRAM_ID, pool_id()), + } + } + + fn price_observations_uninit() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: compute_price_observations_pda( + TWAP_ORACLE_PROGRAM_ID, + pool_id(), + WINDOW_24H, + ), + } + } + + fn clock() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: CLOCK_01_PROGRAM_ACCOUNT_ID, + } + } + + fn call() -> (Vec, Vec) { + create_price_observations( + config_init(), + pool(), + current_tick_account(), + price_observations_uninit(), + clock(), + WINDOW_24H, + AMM_PROGRAM_ID, + ) + } + + // ── happy path ──────────────────────────────────────────────────────────── + + #[test] + fn returns_five_post_states_unchanged() { + let (post_states, _) = call(); + assert_eq!(post_states.len(), 5); + assert_eq!(*post_states[0].account(), config_init().account); + assert_eq!(*post_states[1].account(), pool().account); + assert_eq!(*post_states[2].account(), current_tick_account().account); + assert_eq!( + *post_states[3].account(), + price_observations_uninit().account + ); + assert_eq!(*post_states[4].account(), clock().account); + } + + #[test] + fn seeds_chained_call_with_tick_from_current_tick_account() { + let (_, chained_calls) = call(); + assert_eq!(chained_calls.len(), 1); + + // The chained call must carry the tick read from the CurrentTickAccount, not a + // caller-supplied value, and authorize the pool as the price source. + let mut pool_authorized = pool(); + pool_authorized.is_authorized = true; + let expected = ChainedCall::new( + TWAP_ORACLE_PROGRAM_ID, + vec![price_observations_uninit(), pool_authorized, clock()], + &twap_oracle_core::Instruction::CreatePriceObservations { + initial_tick: CURRENT_TICK, + window_duration: WINDOW_24H, + }, + ) + .with_pda_seeds(vec![compute_pool_pda_seed(token_a_id(), token_b_id())]); + + assert_eq!(chained_calls[0], expected); + } + + // ── precondition violations ─────────────────────────────────────────────── + + #[test] + #[should_panic(expected = "AMM config Account ID does not match PDA")] + fn wrong_config_pda_panics() { + let mut config = config_init(); + config.account_id = AccountId::new([0; 32]); + create_price_observations( + config, + pool(), + current_tick_account(), + price_observations_uninit(), + clock(), + WINDOW_24H, + AMM_PROGRAM_ID, + ); + } + + #[test] + #[should_panic(expected = "AMM Program must be initialized before use")] + fn uninitialized_config_panics() { + let config = AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: compute_config_pda(AMM_PROGRAM_ID), + }; + create_price_observations( + config, + pool(), + current_tick_account(), + price_observations_uninit(), + clock(), + WINDOW_24H, + AMM_PROGRAM_ID, + ); + } + + #[test] + #[should_panic(expected = "clock account must be the canonical 1-block LEZ clock account")] + fn non_canonical_clock_panics() { + let mut clock = clock(); + clock.account_id = AccountId::new([9; 32]); + create_price_observations( + config_init(), + pool(), + current_tick_account(), + price_observations_uninit(), + clock, + WINDOW_24H, + AMM_PROGRAM_ID, + ); + } + + #[test] + #[should_panic(expected = "Pool Account ID does not match PDA")] + fn forged_pool_account_panics() { + let mut pool = pool(); + pool.account_id = AccountId::new([8; 32]); + create_price_observations( + config_init(), + pool, + current_tick_account(), + price_observations_uninit(), + clock(), + WINDOW_24H, + AMM_PROGRAM_ID, + ); + } + + /// A caller cannot substitute a current-tick account they control to forge the seed tick: the + /// account ID must match the pool's current-tick PDA. + #[test] + #[should_panic(expected = "current tick Account ID does not match PDA")] + fn forged_current_tick_account_panics() { + let mut current_tick = current_tick_account(); + current_tick.account_id = AccountId::new([2; 32]); + create_price_observations( + config_init(), + pool(), + current_tick, + price_observations_uninit(), + clock(), + WINDOW_24H, + AMM_PROGRAM_ID, + ); + } + + #[test] + #[should_panic(expected = "AMM Program expects a valid CurrentTickAccount")] + fn uninitialized_current_tick_account_panics() { + let current_tick = AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: compute_current_tick_account_pda(TWAP_ORACLE_PROGRAM_ID, pool_id()), + }; + create_price_observations( + config_init(), + pool(), + current_tick, + price_observations_uninit(), + clock(), + WINDOW_24H, + AMM_PROGRAM_ID, + ); + } + + #[test] + #[should_panic(expected = "price observations Account ID does not match PDA")] + fn wrong_observations_pda_panics() { + let mut observations = price_observations_uninit(); + observations.account_id = AccountId::new([1; 32]); + create_price_observations( + config_init(), + pool(), + current_tick_account(), + observations, + clock(), + WINDOW_24H, + AMM_PROGRAM_ID, + ); + } + + #[test] + #[should_panic(expected = "price observations account already exists")] + fn already_existing_observations_panics() { + let mut observations = price_observations_uninit(); + observations.account.data = Data::try_from(vec![1u8; 8]).expect("fits in Data"); + create_price_observations( + config_init(), + pool(), + current_tick_account(), + observations, + clock(), + WINDOW_24H, + AMM_PROGRAM_ID, + ); + } + + #[test] + fn different_windows_produce_distinct_observation_pdas() { + let window_7d = 7 * 24 * 60 * 60 * 1_000u64; + assert_ne!( + compute_price_observations_pda(TWAP_ORACLE_PROGRAM_ID, pool_id(), WINDOW_24H), + compute_price_observations_pda(TWAP_ORACLE_PROGRAM_ID, pool_id(), window_7d), + ); + } +} diff --git a/programs/amm/src/initialize.rs b/programs/amm/src/initialize.rs index c3576c9..8bda771 100644 --- a/programs/amm/src/initialize.rs +++ b/programs/amm/src/initialize.rs @@ -7,9 +7,10 @@ use nssa_core::{ /// Initializes the AMM Program by creating its singleton configuration account. /// /// The config account is a PDA derived from the constant `"CONFIG"` seed -/// (`compute_config_pda(amm_program_id)`) and stores `token_program_id`, the Token Program the -/// AMM issues every chained call to. Its existence is the Program's "initialized" flag: the -/// chained-call instructions read the Token Program ID from it and reject calls until it exists. +/// (`compute_config_pda(amm_program_id)`) and stores the program IDs the AMM issues chained calls +/// to: `token_program_id` (the Token Program) and `twap_oracle_program_id` (the TWAP oracle). Its +/// existence is the Program's "initialized" flag: the chained-call instructions read these +/// program IDs from it and reject calls until it exists. /// /// # Panics /// Panics if: @@ -18,6 +19,7 @@ use nssa_core::{ pub fn initialize( config: AccountWithMetadata, token_program_id: ProgramId, + twap_oracle_program_id: ProgramId, amm_program_id: ProgramId, ) -> Vec { assert_eq!( @@ -32,7 +34,10 @@ pub fn initialize( ); let mut config_post = config.account.clone(); - config_post.data = Data::from(&AmmConfig { token_program_id }); + config_post.data = Data::from(&AmmConfig { + token_program_id, + twap_oracle_program_id, + }); vec![AccountPostState::new_claimed( config_post, @@ -49,6 +54,7 @@ mod tests { const AMM_PROGRAM_ID: ProgramId = [42; 8]; const TOKEN_PROGRAM_ID: ProgramId = [15; 8]; + const TWAP_ORACLE_PROGRAM_ID: ProgramId = [77; 8]; fn config_uninit() -> AccountWithMetadata { AccountWithMetadata { @@ -60,7 +66,12 @@ mod tests { #[test] fn returns_single_pda_claimed_post_state() { - let post_states = initialize(config_uninit(), TOKEN_PROGRAM_ID, AMM_PROGRAM_ID); + let post_states = initialize( + config_uninit(), + TOKEN_PROGRAM_ID, + TWAP_ORACLE_PROGRAM_ID, + AMM_PROGRAM_ID, + ); assert_eq!(post_states.len(), 1); assert_eq!( post_states[0].required_claim(), @@ -69,11 +80,17 @@ mod tests { } #[test] - fn stores_token_program_id() { - let post_states = initialize(config_uninit(), TOKEN_PROGRAM_ID, AMM_PROGRAM_ID); + fn stores_program_ids() { + let post_states = initialize( + config_uninit(), + TOKEN_PROGRAM_ID, + TWAP_ORACLE_PROGRAM_ID, + AMM_PROGRAM_ID, + ); let config = AmmConfig::try_from(&post_states[0].account().data) .expect("post state must contain a valid AmmConfig"); assert_eq!(config.token_program_id, TOKEN_PROGRAM_ID); + assert_eq!(config.twap_oracle_program_id, TWAP_ORACLE_PROGRAM_ID); } #[test] @@ -81,7 +98,12 @@ mod tests { fn wrong_config_account_id_panics() { let mut wrong = config_uninit(); wrong.account_id = AccountId::new([0; 32]); - initialize(wrong, TOKEN_PROGRAM_ID, AMM_PROGRAM_ID); + initialize( + wrong, + TOKEN_PROGRAM_ID, + TWAP_ORACLE_PROGRAM_ID, + AMM_PROGRAM_ID, + ); } #[test] @@ -90,8 +112,14 @@ mod tests { let mut initialized = config_uninit(); initialized.account.data = Data::from(&AmmConfig { token_program_id: TOKEN_PROGRAM_ID, + twap_oracle_program_id: TWAP_ORACLE_PROGRAM_ID, }); initialized.account.nonce = Nonce(0); - initialize(initialized, TOKEN_PROGRAM_ID, AMM_PROGRAM_ID); + initialize( + initialized, + TOKEN_PROGRAM_ID, + TWAP_ORACLE_PROGRAM_ID, + AMM_PROGRAM_ID, + ); } } diff --git a/programs/amm/src/lib.rs b/programs/amm/src/lib.rs index 3165998..1fe818a 100644 --- a/programs/amm/src/lib.rs +++ b/programs/amm/src/lib.rs @@ -3,6 +3,7 @@ pub use amm_core as core; pub mod add; +pub mod create_price_observations; pub mod initialize; pub mod new_definition; pub mod remove; diff --git a/programs/amm/src/tests.rs b/programs/amm/src/tests.rs index 25cd7b9..e14df3d 100644 --- a/programs/amm/src/tests.rs +++ b/programs/amm/src/tests.rs @@ -30,6 +30,7 @@ use crate::{ const TOKEN_PROGRAM_ID: ProgramId = [15; 8]; const AMM_PROGRAM_ID: ProgramId = [42; 8]; +const TWAP_ORACLE_PROGRAM_ID: ProgramId = [77; 8]; const MALICIOUS_TOKEN_PROGRAM_ID: ProgramId = [99; 8]; struct BalanceForTests; @@ -628,6 +629,7 @@ impl AccountWithMetadataForTests { balance: 0u128, data: Data::from(&AmmConfig { token_program_id: TOKEN_PROGRAM_ID, + twap_oracle_program_id: TWAP_ORACLE_PROGRAM_ID, }), nonce: Nonce(0), }, diff --git a/programs/integration_tests/Cargo.toml b/programs/integration_tests/Cargo.toml index f2374fd..2e96a38 100644 --- a/programs/integration_tests/Cargo.toml +++ b/programs/integration_tests/Cargo.toml @@ -13,7 +13,9 @@ amm_core = { workspace = true } token_core = { workspace = true } ata_core = { workspace = true } stablecoin_core = { workspace = true } +twap_oracle_core = { workspace = true } token-methods = { path = "../token/methods" } amm-methods = { path = "../amm/methods" } ata-methods = { path = "../ata/methods" } stablecoin-methods = { path = "../stablecoin/methods" } +twap-oracle-methods = { path = "../twap_oracle/methods" } diff --git a/programs/integration_tests/tests/amm.rs b/programs/integration_tests/tests/amm.rs index bd01402..3ddc2b1 100644 --- a/programs/integration_tests/tests/amm.rs +++ b/programs/integration_tests/tests/amm.rs @@ -11,6 +11,7 @@ use nssa::{ error::NssaError, program_deployment_transaction::{self, ProgramDeploymentTransaction}, public_transaction, PrivateKey, PublicKey, PublicTransaction, V03State, + CLOCK_01_PROGRAM_ACCOUNT_ID, }; use nssa_core::account::{Account, AccountId, Data, Nonce}; use token_core::{TokenDefinition, TokenHolding}; @@ -43,10 +44,29 @@ impl Ids { amm_methods::AMM_ID } + fn twap_oracle_program() -> nssa_core::program::ProgramId { + twap_oracle_methods::TWAP_ORACLE_ID + } + fn config() -> AccountId { amm_core::compute_config_pda(Self::amm_program()) } + fn price_observations(window_duration: u64) -> AccountId { + twap_oracle_core::compute_price_observations_pda( + Self::twap_oracle_program(), + Self::pool_definition(), + window_duration, + ) + } + + fn current_tick_account() -> AccountId { + twap_oracle_core::compute_current_tick_account_pda( + Self::twap_oracle_program(), + Self::pool_definition(), + ) + } + fn token_a_definition() -> AccountId { AccountId::new([3; 32]) } @@ -293,6 +313,21 @@ impl Accounts { balance: 0_u128, data: Data::from(&amm_core::AmmConfig { token_program_id: Ids::token_program(), + twap_oracle_program_id: Ids::twap_oracle_program(), + }), + nonce: Nonce(0), + } + } + + /// The pool's TWAP current-tick account, owned by the oracle program. Seeded directly into + /// state so the AMM has an authoritative tick to read when creating observations. + fn current_tick_account(tick: i32) -> Account { + Account { + program_owner: Ids::twap_oracle_program(), + balance: 0_u128, + data: Data::from(&twap_oracle_core::CurrentTickAccount { + tick, + last_updated: 1_700_000_000_000, }), nonce: Nonce(0), } @@ -924,6 +959,14 @@ fn deploy_programs(state: &mut V03State) { amm_message, )) .expect("amm program deployment must succeed"); + + let twap_message = + program_deployment_transaction::Message::new(twap_oracle_methods::TWAP_ORACLE_ELF.to_vec()); + state + .transition_from_program_deployment_transaction(&ProgramDeploymentTransaction::new( + twap_message, + )) + .expect("twap oracle program deployment must succeed"); } fn state_for_amm_tests() -> V03State { @@ -1180,6 +1223,7 @@ fn execute_remove_liquidity( fn execute_initialize(state: &mut V03State) { let instruction = amm_core::Instruction::Initialize { token_program_id: Ids::token_program(), + twap_oracle_program_id: Ids::twap_oracle_program(), }; let message = public_transaction::Message::try_new( @@ -1196,6 +1240,32 @@ fn execute_initialize(state: &mut V03State) { state.transition_from_public_transaction(&tx, 0, 0).unwrap(); } +#[cfg(test)] +fn execute_create_price_observations( + state: &mut V03State, + window_duration: u64, +) -> Result<(), NssaError> { + let instruction = amm_core::Instruction::CreatePriceObservations { window_duration }; + + let message = public_transaction::Message::try_new( + Ids::amm_program(), + vec![ + Ids::config(), + Ids::pool_definition(), + Ids::current_tick_account(), + Ids::price_observations(window_duration), + CLOCK_01_PROGRAM_ACCOUNT_ID, + ], + vec![], + instruction, + ) + .unwrap(); + + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx, 0, 0) +} + fn fungible_balance(account: &Account) -> u128 { let holding = TokenHolding::try_from(&account.data).expect("expected token holding"); let TokenHolding::Fungible { @@ -1247,6 +1317,97 @@ fn amm_initialize_creates_config_account() { assert_eq!(config.token_program_id, Ids::token_program()); } +#[test] +fn amm_creates_price_observations_on_twap_oracle() { + let mut state = state_for_amm_tests(); + let window_duration = 24 * 60 * 60 * 1_000u64; + let current_tick = 1_234_i32; + + // The pool already has an authoritative current-tick account written by the oracle. + state.force_insert_account( + Ids::current_tick_account(), + Accounts::current_tick_account(current_tick), + ); + + // The observations PDA does not exist before the AMM creates it. + assert_eq!( + state.get_account_by_id(Ids::price_observations(window_duration)), + Account::default() + ); + + execute_create_price_observations(&mut state, window_duration).unwrap(); + + // The observations account now exists, is owned by the TWAP oracle program, and is seeded + // with the pool as its price source and the tick read from the current-tick account. + let account = state.get_account_by_id(Ids::price_observations(window_duration)); + assert_ne!(account, Account::default()); + assert_eq!(account.program_owner, Ids::twap_oracle_program()); + + let feed = twap_oracle_core::PriceObservations::try_from(&account.data) + .expect("observations account must hold a valid PriceObservations"); + assert_eq!(feed.price_source_id, Ids::pool_definition()); + assert_eq!(feed.last_recorded_tick, current_tick); + assert_eq!(feed.write_index, 1); + assert_eq!(feed.total_entries, 1); + assert_eq!( + feed.entries.len(), + usize::try_from(twap_oracle_core::OBSERVATIONS_CAPACITY) + .expect("OBSERVATIONS_CAPACITY fits in usize") + ); + + // The AMM config and pool are left unchanged by the operation. + assert_eq!(state.get_account_by_id(Ids::config()), Accounts::config()); + assert_eq!( + state.get_account_by_id(Ids::pool_definition()), + Accounts::pool_definition_init() + ); +} + +#[test] +fn amm_create_price_observations_rejects_existing_account() { + let mut state = state_for_amm_tests(); + let window_duration = 24 * 60 * 60 * 1_000u64; + state.force_insert_account( + Ids::current_tick_account(), + Accounts::current_tick_account(1_234), + ); + + // First creation succeeds. + execute_create_price_observations(&mut state, window_duration).unwrap(); + let feed_after_first = twap_oracle_core::PriceObservations::try_from( + &state + .get_account_by_id(Ids::price_observations(window_duration)) + .data, + ) + .expect("observations account must hold a valid PriceObservations"); + + // A second creation for the same (pool, window) is rejected because the observations account + // already exists, and leaves the existing account intact. + let result = execute_create_price_observations(&mut state, window_duration); + assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_)))); + let feed_after_second = twap_oracle_core::PriceObservations::try_from( + &state + .get_account_by_id(Ids::price_observations(window_duration)) + .data, + ) + .expect("observations account must hold a valid PriceObservations"); + assert_eq!(feed_after_first, feed_after_second); +} + +#[test] +fn amm_create_price_observations_without_current_tick_account_fails() { + let mut state = state_for_amm_tests(); + let window_duration = 24 * 60 * 60 * 1_000u64; + + // No current-tick account was created, so there is no authoritative tick to seed from. + let result = execute_create_price_observations(&mut state, window_duration); + assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_)))); + assert_eq!( + state.get_account_by_id(Ids::price_observations(window_duration)), + Account::default() + ); +} + #[test] fn amm_remove_liquidity() { let mut state = state_for_amm_tests(); From ca59f7d7441b29ba34f4c0eedd25a52411223b81 Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:07:04 +0200 Subject: [PATCH 2/2] feat(amm): bootstrap pool TWAP current-tick account at pool creation Extend new_definition to also create the pool's TWAP current-tick account via a chained CreateCurrentTickAccount, so a pool and its price feed are born together. The opening tick is derived on-chain from the pool's own reserves (reserve_b / reserve_a as Q64.64), not caller-supplied, so it cannot be forged. The pool is passed in its post-claim state and authorized as the price source via its pool PDA seed. Add spot_price_q64_64 to amm_core (not the oracle): the reserves -> price mapping is the price source's concern; the oracle only converts price to a tick. --- Cargo.lock | 2 + artifacts/amm-idl.json | 12 +++ programs/amm/core/Cargo.toml | 5 ++ programs/amm/core/src/lib.rs | 71 ++++++++++++++++++ programs/amm/methods/guest/Cargo.lock | 2 + programs/amm/methods/guest/src/bin/amm.rs | 4 + programs/amm/src/new_definition.rs | 63 ++++++++++++++-- programs/amm/src/tests.rs | 90 +++++++++++++++++++++++ programs/integration_tests/tests/amm.rs | 67 ++++++++++------- 9 files changed, 281 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e269ca..c5efd1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,9 +80,11 @@ dependencies = [ name = "amm_core" version = "0.1.0" dependencies = [ + "alloy-primitives", "borsh", "nssa_core", "risc0-zkvm", + "ruint", "serde", "spel-framework-macros", "token_core", diff --git a/artifacts/amm-idl.json b/artifacts/amm-idl.json index dfa00c0..fdcb66d 100644 --- a/artifacts/amm-idl.json +++ b/artifacts/amm-idl.json @@ -120,6 +120,18 @@ "writable": false, "signer": false, "init": false + }, + { + "name": "current_tick_account", + "writable": false, + "signer": false, + "init": false + }, + { + "name": "clock", + "writable": false, + "signer": false, + "init": false } ], "args": [ diff --git a/programs/amm/core/Cargo.toml b/programs/amm/core/Cargo.toml index f45bcbd..5aad42c 100644 --- a/programs/amm/core/Cargo.toml +++ b/programs/amm/core/Cargo.toml @@ -13,3 +13,8 @@ token_core = { path = "../../token/core" } borsh = { version = "1.5", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } risc0-zkvm = { version = "=3.0.5", default-features = false } +alloy-primitives = { version = "1", default-features = false } +# Pin ruint (transitive via alloy-primitives) below 1.18, which raised its MSRV to rustc 1.90. +# The risc0 guest toolchain ships rustc 1.88, so 1.18+ fails the guest build. 1.17.0 (MSRV 1.85) +# is the newest compatible release. Remove this pin once the risc0 toolchain advances past 1.90. +ruint = { version = "=1.17.0", default-features = false } diff --git a/programs/amm/core/src/lib.rs b/programs/amm/core/src/lib.rs index 0f7e8b8..a4916c3 100644 --- a/programs/amm/core/src/lib.rs +++ b/programs/amm/core/src/lib.rs @@ -201,6 +201,35 @@ pub fn assert_supported_fee_tier(fees: u128) { ); } +/// Computes a `Q64.64` spot price (`reserve_quote` per `reserve_base`) from raw pool reserves. +/// +/// This is the constant-product AMM's spot price (`reserve_quote / reserve_base`) expressed as a +/// `Q64.64` fixed-point value: `(reserve_quote / reserve_base) * 2^64`. It is computed in 256-bit +/// precision and saturates at `u128::MAX` if the ratio exceeds the representable range. The TWAP +/// oracle consumes exactly this representation (it converts the `Q64.64` price to a tick), so the +/// AMM owns the reserves → price mapping and the oracle stays agnostic to how the price is formed. +/// +/// # Panics +/// Panics if `reserve_base` is zero. +#[must_use] +pub fn spot_price_q64_64(reserve_base: u128, reserve_quote: u128) -> u128 { + use alloy_primitives::U256; + + assert!( + reserve_base != 0, + "spot_price_q64_64: reserve_base must be non-zero" + ); + + let numerator = U256::from(reserve_quote) + .checked_shl(64) + .expect("reserve_quote < 2^128, so reserve_quote << 64 fits in U256"); + let price = numerator + .checked_div(U256::from(reserve_base)) + .expect("reserve_base is non-zero after the assertion above"); + + u128::try_from(price).unwrap_or(u128::MAX) +} + impl TryFrom<&Data> for PoolDefinition { type Error = std::io::Error; @@ -411,3 +440,45 @@ pub fn read_vault_fungible_balances( (vault_a_balance, vault_b_balance) } + +#[cfg(test)] +mod tests { + use super::*; + + /// `1.0` in Q64.64 is `2^64`. + const ONE_Q64_64: u128 = 1u128 << 64; + + #[test] + fn equal_reserves_map_to_unit_price() { + assert_eq!(spot_price_q64_64(1_000, 1_000), ONE_Q64_64); + } + + #[test] + fn spot_price_reflects_reserve_ratio() { + // reserve_quote / reserve_base = 2.0 -> 2 * 2^64. + assert_eq!(spot_price_q64_64(1_000, 2_000), ONE_Q64_64 * 2); + // reserve_quote / reserve_base = 0.5 -> 2^64 / 2. + assert_eq!(spot_price_q64_64(2_000, 1_000), ONE_Q64_64 / 2); + } + + #[test] + fn spot_price_saturates_instead_of_overflowing() { + // A huge quote-to-base ratio would exceed u128 in Q64.64; it must saturate, not panic. + assert_eq!(spot_price_q64_64(1, u128::MAX), u128::MAX); + } + + #[test] + fn spot_price_handles_large_reserves_without_intermediate_overflow() { + // reserve_quote >= 2^64 would overflow a naive `reserve_quote << 64` in u128; the U256 + // intermediate keeps it exact. Ratio here is 4.0. + let base = 1u128 << 64; + let quote = 1u128 << 66; + assert_eq!(spot_price_q64_64(base, quote), ONE_Q64_64 * 4); + } + + #[test] + #[should_panic(expected = "reserve_base must be non-zero")] + fn zero_reserve_base_panics() { + let _ = spot_price_q64_64(0, 1_000); + } +} diff --git a/programs/amm/methods/guest/Cargo.lock b/programs/amm/methods/guest/Cargo.lock index 204c32d..72563fe 100644 --- a/programs/amm/methods/guest/Cargo.lock +++ b/programs/amm/methods/guest/Cargo.lock @@ -85,9 +85,11 @@ dependencies = [ name = "amm_core" version = "0.1.0" dependencies = [ + "alloy-primitives", "borsh", "nssa_core", "risc0-zkvm", + "ruint", "serde", "spel-framework-macros", "token_core", diff --git a/programs/amm/methods/guest/src/bin/amm.rs b/programs/amm/methods/guest/src/bin/amm.rs index 222de11..48d5949 100644 --- a/programs/amm/methods/guest/src/bin/amm.rs +++ b/programs/amm/methods/guest/src/bin/amm.rs @@ -91,6 +91,8 @@ mod amm { user_holding_a: AccountWithMetadata, user_holding_b: AccountWithMetadata, user_holding_lp: AccountWithMetadata, + current_tick_account: AccountWithMetadata, + clock: AccountWithMetadata, token_a_amount: u128, token_b_amount: u128, fees: u128, @@ -106,6 +108,8 @@ mod amm { user_holding_a, user_holding_b, user_holding_lp, + current_tick_account, + clock, NonZeroU128::new(token_a_amount).expect("token_a_amount must be nonzero"), NonZeroU128::new(token_b_amount).expect("token_b_amount must be nonzero"), fees, diff --git a/programs/amm/src/new_definition.rs b/programs/amm/src/new_definition.rs index bf4b3b9..a78c221 100644 --- a/programs/amm/src/new_definition.rs +++ b/programs/amm/src/new_definition.rs @@ -4,13 +4,15 @@ use amm_core::{ assert_supported_fee_tier, compute_config_pda, compute_liquidity_token_pda, compute_liquidity_token_pda_seed, compute_lp_lock_holding_pda, compute_lp_lock_holding_pda_seed, compute_pool_pda, compute_pool_pda_seed, compute_vault_pda, - compute_vault_pda_seed, AmmConfig, PoolDefinition, MINIMUM_LIQUIDITY, + compute_vault_pda_seed, spot_price_q64_64, AmmConfig, PoolDefinition, MINIMUM_LIQUIDITY, }; +use clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID; use nssa_core::{ account::{Account, AccountWithMetadata, Data}, program::{AccountPostState, ChainedCall, Claim, ProgramId}, }; use token_core::TokenDefinition; +use twap_oracle_core::compute_current_tick_account_pda; #[expect( clippy::too_many_arguments, @@ -26,6 +28,8 @@ pub fn new_definition( user_holding_a: AccountWithMetadata, user_holding_b: AccountWithMetadata, user_holding_lp: AccountWithMetadata, + current_tick_account: AccountWithMetadata, + clock: AccountWithMetadata, token_a_amount: NonZeroU128, token_b_amount: NonZeroU128, fees: u128, @@ -45,9 +49,10 @@ pub fn new_definition( compute_config_pda(amm_program_id), "New definition: AMM config Account ID does not match PDA" ); - let token_program_id = AmmConfig::try_from(&config.account.data) - .expect("New definition: AMM Program must be initialized before use") - .token_program_id; + let config_data = AmmConfig::try_from(&config.account.data) + .expect("New definition: AMM Program must be initialized before use"); + let token_program_id = config_data.token_program_id; + let twap_oracle_program_id = config_data.twap_oracle_program_id; assert_eq!( user_holding_a.account.program_owner, token_program_id, @@ -100,6 +105,18 @@ pub fn new_definition( "Fresh user LP holding requires user authorization" ); + // The pool's TWAP current-tick account is created in the same transaction (a chained call to + // the oracle). Validate its PDA and that the clock is the canonical 1-block LEZ clock. + assert_eq!( + current_tick_account.account_id, + compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id), + "New definition: current tick Account ID does not match PDA" + ); + assert_eq!( + clock.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID, + "New definition: clock account must be the canonical 1-block LEZ clock account" + ); + // LP Token minting calculation let initial_lp = token_a_amount .get() @@ -115,7 +132,6 @@ pub fn new_definition( .expect("initial liquidity must exceed minimum liquidity after validation"); // Update pool account - let mut pool_post = pool.account.clone(); let pool_post_definition = PoolDefinition { definition_token_a_id, definition_token_b_id, @@ -128,9 +144,10 @@ pub fn new_definition( fees, }; - pool_post.data = Data::from(&pool_post_definition); + let mut pool_initialized = pool.account.clone(); + pool_initialized.data = Data::from(&pool_post_definition); let pool_post: AccountPostState = AccountPostState::new_claimed( - pool_post.clone(), + pool_initialized.clone(), Claim::Pda(compute_pool_pda_seed( definition_token_a_id, definition_token_b_id, @@ -202,11 +219,41 @@ pub fn new_definition( ) .with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]); + // Chain call to create the pool's TWAP current-tick account, with the pool as the price + // source. The oracle derives the tick from the opening spot price (reserve_b / reserve_a as a + // Q64.64 ratio), so the seed value is taken from the pool's own reserves, not the caller. + // + // The pool is claimed (and thus owned by this program) by this same instruction, so the + // chained call must present the pool in its post-claim state to match the accumulated state + // diff: the runtime sets the claimed pool's owner to this program, so we predict that here. + let initial_price = spot_price_q64_64(token_a_amount.get(), token_b_amount.get()); + let mut pool_price_source_account = pool_initialized; + pool_price_source_account.program_owner = amm_program_id; + let pool_price_source = AccountWithMetadata { + account: pool_price_source_account, + is_authorized: true, + account_id: pool.account_id, + }; + let call_create_current_tick = ChainedCall::new( + twap_oracle_program_id, + vec![ + current_tick_account.clone(), + pool_price_source, + clock.clone(), + ], + &twap_oracle_core::Instruction::CreateCurrentTickAccount { initial_price }, + ) + .with_pda_seeds(vec![compute_pool_pda_seed( + definition_token_a_id, + definition_token_b_id, + )]); + let chained_calls = vec![ call_token_lp_lock, call_token_lp_user, call_token_b, call_token_a, + call_create_current_tick, ]; let post_states = vec![ @@ -219,6 +266,8 @@ pub fn new_definition( AccountPostState::new(user_holding_a.account.clone()), AccountPostState::new(user_holding_b.account.clone()), AccountPostState::new(user_holding_lp.account.clone()), + AccountPostState::new(current_tick_account.account.clone()), + AccountPostState::new(clock.account.clone()), ]; (post_states, chained_calls) diff --git a/programs/amm/src/tests.rs b/programs/amm/src/tests.rs index e14df3d..cd640d6 100644 --- a/programs/amm/src/tests.rs +++ b/programs/amm/src/tests.rs @@ -565,6 +565,33 @@ impl ChainedCallForTests { IdForTests::pool_definition_id(), )]) } + + fn cc_new_definition_create_current_tick() -> ChainedCall { + // The pool is passed to the oracle in its post-claim state: owned by the AMM program and + // carrying the freshly written PoolDefinition, authorized as the price source. + let mut pool_price_source = AccountForTests::pool_definition_init(); + pool_price_source.account.program_owner = AMM_PROGRAM_ID; + pool_price_source.is_authorized = true; + + let initial_price = amm_core::spot_price_q64_64( + BalanceForTests::vault_a_reserve_init(), + BalanceForTests::vault_b_reserve_init(), + ); + + ChainedCall::new( + TWAP_ORACLE_PROGRAM_ID, + vec![ + AccountForTests::current_tick_account_uninit(), + pool_price_source, + AccountForTests::clock(), + ], + &twap_oracle_core::Instruction::CreateCurrentTickAccount { initial_price }, + ) + .with_pda_seeds(vec![compute_pool_pda_seed( + IdForTests::token_a_definition_id(), + IdForTests::token_b_definition_id(), + )]) + } } impl IdForTests { @@ -654,6 +681,27 @@ impl AccountWithMetadataForTests { config } + /// The pool's TWAP current-tick PDA, uninitialized (created by `new_definition`). + fn current_tick_account_uninit() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: twap_oracle_core::compute_current_tick_account_pda( + TWAP_ORACLE_PROGRAM_ID, + IdForTests::pool_definition_id(), + ), + } + } + + /// The canonical 1-block LEZ clock account. + fn clock() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: clock_core::CLOCK_01_PROGRAM_ACCOUNT_ID, + } + } + fn user_holding_a() -> AccountWithMetadata { AccountWithMetadata { account: Account { @@ -2048,6 +2096,8 @@ fn test_call_new_definition_with_zero_balance_1() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b(), AccountWithMetadataForTests::user_holding_lp_uninit(), + AccountWithMetadataForTests::current_tick_account_uninit(), + AccountWithMetadataForTests::clock(), NonZero::new(0).expect("Balances must be nonzero"), NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), BalanceForTests::fee_tier(), @@ -2068,6 +2118,8 @@ fn test_call_new_definition_with_zero_balance_2() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b(), AccountWithMetadataForTests::user_holding_lp_uninit(), + AccountWithMetadataForTests::current_tick_account_uninit(), + AccountWithMetadataForTests::clock(), NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), NonZero::new(0).expect("Balances must be nonzero"), BalanceForTests::fee_tier(), @@ -2088,6 +2140,8 @@ fn test_call_new_definition_same_token_definition() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_lp_uninit(), + AccountWithMetadataForTests::current_tick_account_uninit(), + AccountWithMetadataForTests::clock(), NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), BalanceForTests::fee_tier(), @@ -2108,6 +2162,8 @@ fn test_call_new_definition_wrong_liquidity_id() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b(), AccountWithMetadataForTests::user_holding_lp_uninit(), + AccountWithMetadataForTests::current_tick_account_uninit(), + AccountWithMetadataForTests::clock(), NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), BalanceForTests::fee_tier(), @@ -2128,6 +2184,8 @@ fn test_call_new_definition_wrong_lp_lock_holding_id() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b(), AccountWithMetadataForTests::user_holding_lp_uninit(), + AccountWithMetadataForTests::current_tick_account_uninit(), + AccountWithMetadataForTests::clock(), NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), BalanceForTests::fee_tier(), @@ -2148,6 +2206,8 @@ fn test_call_new_definition_wrong_pool_id() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b(), AccountWithMetadataForTests::user_holding_lp_uninit(), + AccountWithMetadataForTests::current_tick_account_uninit(), + AccountWithMetadataForTests::clock(), NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), BalanceForTests::fee_tier(), @@ -2168,6 +2228,8 @@ fn test_call_new_definition_wrong_vault_id_1() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b(), AccountWithMetadataForTests::user_holding_lp_uninit(), + AccountWithMetadataForTests::current_tick_account_uninit(), + AccountWithMetadataForTests::clock(), NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), BalanceForTests::fee_tier(), @@ -2188,6 +2250,8 @@ fn test_call_new_definition_wrong_vault_id_2() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b(), AccountWithMetadataForTests::user_holding_lp_uninit(), + AccountWithMetadataForTests::current_tick_account_uninit(), + AccountWithMetadataForTests::clock(), NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), BalanceForTests::fee_tier(), @@ -2209,6 +2273,8 @@ fn test_call_new_definition_rejects_initialized_pool() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b(), AccountWithMetadataForTests::user_holding_lp_uninit(), + AccountWithMetadataForTests::current_tick_account_uninit(), + AccountWithMetadataForTests::clock(), NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), BalanceForTests::fee_tier(), @@ -2230,6 +2296,8 @@ fn test_call_new_definition_initial_lp_too_small() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b(), AccountWithMetadataForTests::user_holding_lp_uninit(), + AccountWithMetadataForTests::current_tick_account_uninit(), + AccountWithMetadataForTests::clock(), NonZero::new(MINIMUM_LIQUIDITY).unwrap(), NonZero::new(MINIMUM_LIQUIDITY).unwrap(), BalanceForTests::fee_tier(), @@ -2249,6 +2317,8 @@ fn test_call_new_definition_chained_call_successful() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b(), AccountWithMetadataForTests::user_holding_lp_uninit(), + AccountWithMetadataForTests::current_tick_account_uninit(), + AccountWithMetadataForTests::clock(), NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), BalanceForTests::fee_tier(), @@ -2275,6 +2345,14 @@ fn test_call_new_definition_chained_call_successful() { assert!(chained_call_b == ChainedCallForTests::cc_new_definition_token_b()); assert!(chained_call_lp_lock == ChainedCallForTests::cc_new_definition_token_lp_lock()); assert!(chained_call_lp_user == ChainedCallForTests::cc_new_definition_token_lp_user()); + + // The fifth chained call creates the pool's TWAP current-tick account, seeding the tick from + // the opening reserves. + assert_eq!(chained_calls.len(), 5); + assert!(chained_calls[4] == ChainedCallForTests::cc_new_definition_create_current_tick()); + + // Two extra post-states (current-tick + clock) are echoed back unchanged. + assert_eq!(post_states.len(), 11); } #[should_panic(expected = "AccountId is not a token type for the pool")] @@ -2956,6 +3034,8 @@ fn test_new_definition_lp_asymmetric_amounts() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b(), AccountWithMetadataForTests::user_holding_lp_uninit(), + AccountWithMetadataForTests::current_tick_account_uninit(), + AccountWithMetadataForTests::clock(), NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), BalanceForTests::fee_tier(), @@ -2994,6 +3074,8 @@ fn test_new_definition_lp_symmetric_amounts() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b(), AccountWithMetadataForTests::user_holding_lp_uninit(), + AccountWithMetadataForTests::current_tick_account_uninit(), + AccountWithMetadataForTests::clock(), NonZero::new(token_a_amount).unwrap(), NonZero::new(token_b_amount).unwrap(), BalanceForTests::fee_tier(), @@ -3064,6 +3146,8 @@ fn test_minimum_liquidity_lock_and_remove_all_user_lp() { AccountForTests::user_holding_a(), AccountForTests::user_holding_b(), AccountForTests::user_holding_lp_uninit(), + AccountWithMetadataForTests::current_tick_account_uninit(), + AccountWithMetadataForTests::clock(), NonZero::new(token_a_amount).unwrap(), NonZero::new(token_b_amount).unwrap(), BalanceForTests::fee_tier(), @@ -3288,6 +3372,8 @@ fn new_definition_overflow_protection() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b(), AccountWithMetadataForTests::user_holding_lp_uninit(), + AccountWithMetadataForTests::current_tick_account_uninit(), + AccountWithMetadataForTests::clock(), NonZero::new(large_amount).unwrap(), NonZero::new(2).unwrap(), BalanceForTests::fee_tier(), @@ -3543,6 +3629,8 @@ fn test_new_definition_supports_all_fee_tiers() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b(), AccountWithMetadataForTests::user_holding_lp_uninit(), + AccountWithMetadataForTests::current_tick_account_uninit(), + AccountWithMetadataForTests::clock(), NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), fees, @@ -3568,6 +3656,8 @@ fn test_new_definition_rejects_unsupported_fee_tier() { AccountWithMetadataForTests::user_holding_a(), AccountWithMetadataForTests::user_holding_b(), AccountWithMetadataForTests::user_holding_lp_uninit(), + AccountWithMetadataForTests::current_tick_account_uninit(), + AccountWithMetadataForTests::clock(), NonZero::new(BalanceForTests::vault_a_reserve_init()).unwrap(), NonZero::new(BalanceForTests::vault_b_reserve_init()).unwrap(), 2, diff --git a/programs/integration_tests/tests/amm.rs b/programs/integration_tests/tests/amm.rs index 3ddc2b1..2f4acde 100644 --- a/programs/integration_tests/tests/amm.rs +++ b/programs/integration_tests/tests/amm.rs @@ -319,20 +319,6 @@ impl Accounts { } } - /// The pool's TWAP current-tick account, owned by the oracle program. Seeded directly into - /// state so the AMM has an authoritative tick to read when creating observations. - fn current_tick_account(tick: i32) -> Account { - Account { - program_owner: Ids::twap_oracle_program(), - balance: 0_u128, - data: Data::from(&twap_oracle_core::CurrentTickAccount { - tick, - last_updated: 1_700_000_000_000, - }), - nonce: Nonce(0), - } - } - fn user_a_holding() -> Account { Account { program_owner: Ids::token_program(), @@ -1046,6 +1032,8 @@ fn try_execute_new_definition( Ids::user_a(), Ids::user_b(), Ids::user_lp(), + Ids::current_tick_account(), + CLOCK_01_PROGRAM_ACCOUNT_ID, ], if authorize_user_lp { vec![ @@ -1266,6 +1254,18 @@ fn execute_create_price_observations( state.transition_from_public_transaction(&tx, 0, 0) } +/// Builds a state whose pool was created through `new_definition`, which also creates the pool's +/// TWAP current-tick account (seeded from the opening reserves). Used by the observation tests so +/// they consume the real current-tick account rather than a hand-inserted one. +#[cfg(test)] +fn state_with_pool_created_via_new_definition() -> V03State { + let mut state = state_for_amm_tests_with_new_def(); + state.force_insert_account(Ids::vault_a(), Accounts::vault_a_reinitializable()); + state.force_insert_account(Ids::vault_b(), Accounts::vault_b_reinitializable()); + execute_new_definition(&mut state, Balances::fee_tier()); + state +} + fn fungible_balance(account: &Account) -> u128 { let holding = TokenHolding::try_from(&account.data).expect("expected token holding"); let TokenHolding::Fungible { @@ -1319,15 +1319,15 @@ fn amm_initialize_creates_config_account() { #[test] fn amm_creates_price_observations_on_twap_oracle() { - let mut state = state_for_amm_tests(); + let mut state = state_with_pool_created_via_new_definition(); let window_duration = 24 * 60 * 60 * 1_000u64; - let current_tick = 1_234_i32; - // The pool already has an authoritative current-tick account written by the oracle. - state.force_insert_account( - Ids::current_tick_account(), - Accounts::current_tick_account(current_tick), - ); + // The current-tick account created during pool creation supplies the authoritative seed tick, + // derived from the opening reserves (reserve_b / reserve_a as a Q64.64 spot price). + let expected_tick = twap_oracle_core::price_to_tick(amm_core::spot_price_q64_64( + Balances::vault_a_init(), + Balances::vault_b_init(), + )); // The observations PDA does not exist before the AMM creates it. assert_eq!( @@ -1346,7 +1346,7 @@ fn amm_creates_price_observations_on_twap_oracle() { let feed = twap_oracle_core::PriceObservations::try_from(&account.data) .expect("observations account must hold a valid PriceObservations"); assert_eq!(feed.price_source_id, Ids::pool_definition()); - assert_eq!(feed.last_recorded_tick, current_tick); + assert_eq!(feed.last_recorded_tick, expected_tick); assert_eq!(feed.write_index, 1); assert_eq!(feed.total_entries, 1); assert_eq!( @@ -1359,18 +1359,14 @@ fn amm_creates_price_observations_on_twap_oracle() { assert_eq!(state.get_account_by_id(Ids::config()), Accounts::config()); assert_eq!( state.get_account_by_id(Ids::pool_definition()), - Accounts::pool_definition_init() + Accounts::pool_definition_new_init() ); } #[test] fn amm_create_price_observations_rejects_existing_account() { - let mut state = state_for_amm_tests(); + let mut state = state_with_pool_created_via_new_definition(); let window_duration = 24 * 60 * 60 * 1_000u64; - state.force_insert_account( - Ids::current_tick_account(), - Accounts::current_tick_account(1_234), - ); // First creation succeeds. execute_create_price_observations(&mut state, window_duration).unwrap(); @@ -1546,6 +1542,19 @@ fn amm_new_definition_uninitialized_pool() { state.get_account_by_id(Ids::user_lp()), Accounts::user_lp_holding_new_init() ); + + // Pool creation also created the pool's TWAP current-tick account (a chained call to the + // oracle), owned by the oracle program and seeded with the tick derived from the opening + // reserves (reserve_b / reserve_a as a Q64.64 spot price). + let current_tick = state.get_account_by_id(Ids::current_tick_account()); + assert_eq!(current_tick.program_owner, Ids::twap_oracle_program()); + let tick_account = twap_oracle_core::CurrentTickAccount::try_from(¤t_tick.data) + .expect("current tick account must hold a valid CurrentTickAccount"); + let expected_tick = twap_oracle_core::price_to_tick(amm_core::spot_price_q64_64( + Balances::vault_a_init(), + Balances::vault_b_init(), + )); + assert_eq!(tick_account.tick, expected_tick); } #[test] @@ -2101,6 +2110,8 @@ fn amm_new_definition_rejects_expired_deadline() { Ids::user_a(), Ids::user_b(), Ids::user_lp(), + Ids::current_tick_account(), + CLOCK_01_PROGRAM_ACCOUNT_ID, ], vec![ current_nonce(&state, Ids::user_a()),