From 2c4ae463556c1a96c76b60ed4e812c9ff8fd1f31 Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Tue, 19 Aug 2025 14:17:09 -0700 Subject: [PATCH 01/17] feat(lazer/sui): specify version --- lazer/contracts/sui/Move.lock | 2 +- lazer/contracts/sui/Move.toml | 3 +-- lazer/contracts/sui/README.md | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lazer/contracts/sui/Move.lock b/lazer/contracts/sui/Move.lock index d08a0eb8d4..125f200e2f 100644 --- a/lazer/contracts/sui/Move.lock +++ b/lazer/contracts/sui/Move.lock @@ -2,7 +2,7 @@ [move] version = 3 -manifest_digest = "DD0B86B0E012F788977D2224EA46B39395FCF48AB7DAE200E70E6E12F9445868" +manifest_digest = "5B8FA4A1860DFE72BCB751FB8D867DA8D63CCFD7029F175B345B072433DFC568" deps_digest = "F9B494B64F0615AED0E98FC12A85B85ECD2BC5185C22D30E7F67786BB52E507C" dependencies = [ { id = "Bridge", name = "Bridge" }, diff --git a/lazer/contracts/sui/Move.toml b/lazer/contracts/sui/Move.toml index ae1617355c..b171bd4cc6 100644 --- a/lazer/contracts/sui/Move.toml +++ b/lazer/contracts/sui/Move.toml @@ -1,9 +1,8 @@ [package] name = "pyth_lazer" +version = "0.0.1" edition = "2024.beta" -[dependencies] - [addresses] pyth_lazer = "0x0" diff --git a/lazer/contracts/sui/README.md b/lazer/contracts/sui/README.md index 28c83546d0..7074925d47 100644 --- a/lazer/contracts/sui/README.md +++ b/lazer/contracts/sui/README.md @@ -2,7 +2,7 @@ `pyth_lazer` is a Sui package that allows consumers to easily parse and verify cryptographically signed price feed data from the Pyth Network's high-frequency Lazer protocol for use on-chain. -This package is built using the Move language and Sui framework. +This package is built using the Move language edition `2024.beta` and Sui framework `v1.53.2`. ### Build, test, deploy From 69682bc8726a81f40d0314bdd705f9d71c557613 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 21:19:54 +0000 Subject: [PATCH 02/17] feat(lazer-sui): add state module with trusted signer storage and tests Co-Authored-By: Tejas Badadare --- lazer/contracts/sui/sources/state.move | 170 +++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 lazer/contracts/sui/sources/state.move diff --git a/lazer/contracts/sui/sources/state.move b/lazer/contracts/sui/sources/state.move new file mode 100644 index 0000000000..ed465bedd5 --- /dev/null +++ b/lazer/contracts/sui/sources/state.move @@ -0,0 +1,170 @@ +module pyth_lazer::state; + +use std::vector; +use sui::object::{Self, UID}; +use sui::tx_context::{Self, TxContext}; + +friend pyth_lazer::pyth_lazer; + +const ED25519_PUBKEY_LEN: u64 = 32; +const E_INVALID_PUBKEY_LEN: u64 = 1; + +public struct TrustedSignerInfo has copy, drop, store { + public_key: vector, + expires_at: u64, +} + +public struct State has key, store { + id: UID, + trusted_signers: vector, +} + +public(friend) fun new(ctx: &mut TxContext): State { + State { + id: object::new(ctx), + trusted_signers: vector::empty(), + } +} + +public fun public_key(info: &TrustedSignerInfo): &vector { + &info.public_key +} + +public fun expires_at(info: &TrustedSignerInfo): u64 { + info.expires_at +} + +public fun get_trusted_signers(s: &State): &vector { + &s.trusted_signers +} + +public(package) fun update_trusted_signer(s: &mut State, pubkey: vector, expires_at: u64) { + assert!(vector::length(&pubkey) as u64 == ED25519_PUBKEY_LEN, E_INVALID_PUBKEY_LEN); + + let maybe_idx = find_signer_index(&s.trusted_signers, &pubkey); + if (expires_at == 0) { + if (option::is_some(&maybe_idx)) { + let idx = option::extract(&mut maybe_idx); + remove_by_index(&mut s.trusted_signers, idx) + } else { + option::destroy_none(maybe_idx) + } + return + }; + + if (option::is_some(&maybe_idx)) { + let idx = option::extract(&mut maybe_idx); + let info_ref = vector::borrow_mut(&mut s.trusted_signers, idx); + info_ref.expires_at = expires_at + } else { + option::destroy_none(maybe_idx); + vector::push_back( + &mut s.trusted_signers, + TrustedSignerInfo { public_key: pubkey, expires_at } + ) + } +} + +fun find_signer_index(signers: &vector, target: &vector): Option { + let len = vector::length(signers); + let mut i: u64 = 0; + while (i < (len as u64)) { + let info_ref = vector::borrow(signers, (i as u64)); + if (*public_key(info_ref) == *target) { + return option::some(i) + }; + i = i + 1 + }; + option::none() +} + +fun remove_by_index(v: &mut vector, idx: u64) { + let last_idx = (vector::length(v) as u64) - 1; + if (idx != last_idx) { + let last = vector::pop_back(v); + let slot_ref = vector::borrow_mut(v, idx); + *slot_ref = last + } else { + vector::pop_back(v) + } +} + +#[test_only] +public fun new_for_test(ctx: &mut TxContext): State { + State { + id: object::new(ctx), + trusted_signers: vector::empty(), + } +} + +#[test] +public fun test_add_new_signer() { + let mut ctx = tx_context::dummy(); + let mut s = new_for_test(&mut ctx); + + let pk = vector::from_array([ + 1,2,3,4,5,6,7,8, + 9,10,11,12,13,14,15,16, + 17,18,19,20,21,22,23,24, + 25,26,27,28,29,30,31,32 + ]); + let expiry: u64 = 123; + + update_trusted_signer(&mut s, pk, expiry); + + let signers_ref = get_trusted_signers(&s); + assert!(vector::length(signers_ref) == 1, 100); + let info = vector::borrow(signers_ref, 0); + assert!(expires_at(info) == 123, 101); + let got_pk = public_key(info); + assert!(vector::length(got_pk) == (ED25519_PUBKEY_LEN as u64), 102); +} + +#[test] +public fun test_update_existing_signer_expiry() { + let mut ctx = tx_context::dummy(); + let mut s = new_for_test(&mut ctx); + + let pk = vector::from_array([ + 42,42,42,42,42,42,42,42, + 42,42,42,42,42,42,42,42, + 42,42,42,42,42,42,42,42, + 42,42,42,42,42,42,42,42 + ]); + + update_trusted_signer(&mut s, vector::copy(&pk), 1000); + update_trusted_signer(&mut s, pk, 2000); + + let signers_ref = get_trusted_signers(&s); + assert!(vector::length(signers_ref) == 1, 110); + let info = vector::borrow(signers_ref, 0); + assert!(expires_at(info) == 2000, 111); +} + +#[test] +public fun test_remove_signer_by_zero_expiry() { + let mut ctx = tx_context::dummy(); + let mut s = new_for_test(&mut ctx); + + let pk = vector::from_array([ + 7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7 + ]); + + update_trusted_signer(&mut s, vector::copy(&pk), 999); + update_trusted_signer(&mut s, pk, 0); + + let signers_ref = get_trusted_signers(&s); + assert!(vector::length(signers_ref) == 0, 120); +} + +#[test, expected_failure(abort_code = E_INVALID_PUBKEY_LEN)] +public fun test_invalid_pubkey_length_rejected() { + let mut ctx = tx_context::dummy(); + let mut s = new_for_test(&mut ctx); + + let short_pk = vector::from_array([1,2,3]); // too short + update_trusted_signer(&mut s, short_pk, 1) +} From 738029364c71f00ae2f5e3f1fb03b857c1337722 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 21:30:52 +0000 Subject: [PATCH 03/17] fix Co-Authored-By: Tejas Badadare --- lazer/contracts/sui/sources/state.move | 60 +++++++++----------------- 1 file changed, 21 insertions(+), 39 deletions(-) diff --git a/lazer/contracts/sui/sources/state.move b/lazer/contracts/sui/sources/state.move index ed465bedd5..5a1c25f40a 100644 --- a/lazer/contracts/sui/sources/state.move +++ b/lazer/contracts/sui/sources/state.move @@ -1,10 +1,10 @@ module pyth_lazer::state; use std::vector; +use std::option::{Self, Option}; use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; -friend pyth_lazer::pyth_lazer; const ED25519_PUBKEY_LEN: u64 = 32; const E_INVALID_PUBKEY_LEN: u64 = 1; @@ -19,7 +19,7 @@ public struct State has key, store { trusted_signers: vector, } -public(friend) fun new(ctx: &mut TxContext): State { +public(package) fun new(ctx: &mut TxContext): State { State { id: object::new(ctx), trusted_signers: vector::empty(), @@ -45,10 +45,11 @@ public(package) fun update_trusted_signer(s: &mut State, pubkey: vector, exp if (expires_at == 0) { if (option::is_some(&maybe_idx)) { let idx = option::extract(&mut maybe_idx); - remove_by_index(&mut s.trusted_signers, idx) + // Remove by swapping with last (order not preserved), discard removed value + let _ = vector::swap_remove(&mut s.trusted_signers, idx); } else { option::destroy_none(maybe_idx) - } + }; return }; @@ -69,7 +70,7 @@ fun find_signer_index(signers: &vector, target: &vector): let len = vector::length(signers); let mut i: u64 = 0; while (i < (len as u64)) { - let info_ref = vector::borrow(signers, (i as u64)); + let info_ref = vector::borrow(signers, i); if (*public_key(info_ref) == *target) { return option::some(i) }; @@ -78,17 +79,6 @@ fun find_signer_index(signers: &vector, target: &vector): option::none() } -fun remove_by_index(v: &mut vector, idx: u64) { - let last_idx = (vector::length(v) as u64) - 1; - if (idx != last_idx) { - let last = vector::pop_back(v); - let slot_ref = vector::borrow_mut(v, idx); - *slot_ref = last - } else { - vector::pop_back(v) - } -} - #[test_only] public fun new_for_test(ctx: &mut TxContext): State { State { @@ -102,12 +92,8 @@ public fun test_add_new_signer() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); - let pk = vector::from_array([ - 1,2,3,4,5,6,7,8, - 9,10,11,12,13,14,15,16, - 17,18,19,20,21,22,23,24, - 25,26,27,28,29,30,31,32 - ]); + let pk = x"0102030405060708090a0b0c0d0e0f10 + 1112131415161718191a1b1c1d1e1f20"; let expiry: u64 = 123; update_trusted_signer(&mut s, pk, expiry); @@ -125,15 +111,13 @@ public fun test_update_existing_signer_expiry() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); - let pk = vector::from_array([ - 42,42,42,42,42,42,42,42, - 42,42,42,42,42,42,42,42, - 42,42,42,42,42,42,42,42, - 42,42,42,42,42,42,42,42 - ]); + let pk = x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a + 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"; - update_trusted_signer(&mut s, vector::copy(&pk), 1000); - update_trusted_signer(&mut s, pk, 2000); + update_trusted_signer(&mut s, x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a + 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", 1000); + update_trusted_signer(&mut s, x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a + 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", 2000); let signers_ref = get_trusted_signers(&s); assert!(vector::length(signers_ref) == 1, 110); @@ -146,15 +130,13 @@ public fun test_remove_signer_by_zero_expiry() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); - let pk = vector::from_array([ - 7,7,7,7,7,7,7,7, - 7,7,7,7,7,7,7,7, - 7,7,7,7,7,7,7,7, - 7,7,7,7,7,7,7,7 - ]); + let pk = x"07070707070707070707070707070707 + 07070707070707070707070707070707"; - update_trusted_signer(&mut s, vector::copy(&pk), 999); - update_trusted_signer(&mut s, pk, 0); + update_trusted_signer(&mut s, x"07070707070707070707070707070707 + 07070707070707070707070707070707", 999); + update_trusted_signer(&mut s, x"07070707070707070707070707070707 + 07070707070707070707070707070707", 0); let signers_ref = get_trusted_signers(&s); assert!(vector::length(signers_ref) == 0, 120); @@ -165,6 +147,6 @@ public fun test_invalid_pubkey_length_rejected() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); - let short_pk = vector::from_array([1,2,3]); // too short + let short_pk = x"010203"; update_trusted_signer(&mut s, short_pk, 1) } From f34b7fc8f17922e5ac71f0a8134792fbcfab563e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 21:34:25 +0000 Subject: [PATCH 04/17] fix(sui-lazer): finalize state module with trusted signer management and tests; verified with Sui CLI 1.53.2 Co-Authored-By: Tejas Badadare --- lazer/contracts/sui/sources/state.move | 37 +++++++++++++++----------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/lazer/contracts/sui/sources/state.move b/lazer/contracts/sui/sources/state.move index 5a1c25f40a..5ebc4759e3 100644 --- a/lazer/contracts/sui/sources/state.move +++ b/lazer/contracts/sui/sources/state.move @@ -41,7 +41,7 @@ public fun get_trusted_signers(s: &State): &vector { public(package) fun update_trusted_signer(s: &mut State, pubkey: vector, expires_at: u64) { assert!(vector::length(&pubkey) as u64 == ED25519_PUBKEY_LEN, E_INVALID_PUBKEY_LEN); - let maybe_idx = find_signer_index(&s.trusted_signers, &pubkey); + let mut maybe_idx = find_signer_index(&s.trusted_signers, &pubkey); if (expires_at == 0) { if (option::is_some(&maybe_idx)) { let idx = option::extract(&mut maybe_idx); @@ -92,8 +92,7 @@ public fun test_add_new_signer() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); - let pk = x"0102030405060708090a0b0c0d0e0f10 - 1112131415161718191a1b1c1d1e1f20"; + let pk = x"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"; let expiry: u64 = 123; update_trusted_signer(&mut s, pk, expiry); @@ -104,6 +103,9 @@ public fun test_add_new_signer() { assert!(expires_at(info) == 123, 101); let got_pk = public_key(info); assert!(vector::length(got_pk) == (ED25519_PUBKEY_LEN as u64), 102); + let State { id, trusted_signers } = s; + let _ = trusted_signers; + object::delete(id); } #[test] @@ -111,18 +113,18 @@ public fun test_update_existing_signer_expiry() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); - let pk = x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a - 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"; + let pk = x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"; - update_trusted_signer(&mut s, x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a - 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", 1000); - update_trusted_signer(&mut s, x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a - 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", 2000); + update_trusted_signer(&mut s, x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", 1000); + update_trusted_signer(&mut s, x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", 2000); let signers_ref = get_trusted_signers(&s); assert!(vector::length(signers_ref) == 1, 110); let info = vector::borrow(signers_ref, 0); assert!(expires_at(info) == 2000, 111); + let State { id, trusted_signers } = s; + let _ = trusted_signers; + object::delete(id); } #[test] @@ -130,16 +132,16 @@ public fun test_remove_signer_by_zero_expiry() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); - let pk = x"07070707070707070707070707070707 - 07070707070707070707070707070707"; + let pk = x"0707070707070707070707070707070707070707070707070707070707070707"; - update_trusted_signer(&mut s, x"07070707070707070707070707070707 - 07070707070707070707070707070707", 999); - update_trusted_signer(&mut s, x"07070707070707070707070707070707 - 07070707070707070707070707070707", 0); + update_trusted_signer(&mut s, x"0707070707070707070707070707070707070707070707070707070707070707", 999); + update_trusted_signer(&mut s, x"0707070707070707070707070707070707070707070707070707070707070707", 0); let signers_ref = get_trusted_signers(&s); assert!(vector::length(signers_ref) == 0, 120); + let State { id, trusted_signers } = s; + let _ = trusted_signers; + object::delete(id); } #[test, expected_failure(abort_code = E_INVALID_PUBKEY_LEN)] @@ -148,5 +150,8 @@ public fun test_invalid_pubkey_length_rejected() { let mut s = new_for_test(&mut ctx); let short_pk = x"010203"; - update_trusted_signer(&mut s, short_pk, 1) + update_trusted_signer(&mut s, short_pk, 1); + let State { id, trusted_signers } = s; + let _ = trusted_signers; + object::delete(id); } From d7bd862b3532f4a37d143af4d2dfcd246e096701 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 21:37:35 +0000 Subject: [PATCH 05/17] chore(sui-lazer): remove warnings in state module; clean build on Sui CLI 1.53.2 Co-Authored-By: Tejas Badadare --- lazer/contracts/sui/sources/state.move | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lazer/contracts/sui/sources/state.move b/lazer/contracts/sui/sources/state.move index 5ebc4759e3..45a8d84daf 100644 --- a/lazer/contracts/sui/sources/state.move +++ b/lazer/contracts/sui/sources/state.move @@ -1,9 +1,5 @@ module pyth_lazer::state; -use std::vector; -use std::option::{Self, Option}; -use sui::object::{Self, UID}; -use sui::tx_context::{Self, TxContext}; const ED25519_PUBKEY_LEN: u64 = 32; @@ -113,7 +109,6 @@ public fun test_update_existing_signer_expiry() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); - let pk = x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"; update_trusted_signer(&mut s, x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", 1000); update_trusted_signer(&mut s, x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", 2000); @@ -132,7 +127,6 @@ public fun test_remove_signer_by_zero_expiry() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); - let pk = x"0707070707070707070707070707070707070707070707070707070707070707"; update_trusted_signer(&mut s, x"0707070707070707070707070707070707070707070707070707070707070707", 999); update_trusted_signer(&mut s, x"0707070707070707070707070707070707070707070707070707070707070707", 0); From 48e070ddbaa7582a347570d3c6aa0dea4a9a3c8a Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Tue, 19 Aug 2025 15:04:22 -0700 Subject: [PATCH 06/17] feat(lazer/sui): add trusted signer state --- lazer/contracts/sui/sources/state.move | 65 ++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/lazer/contracts/sui/sources/state.move b/lazer/contracts/sui/sources/state.move index 45a8d84daf..adc50d9609 100644 --- a/lazer/contracts/sui/sources/state.move +++ b/lazer/contracts/sui/sources/state.move @@ -1,15 +1,22 @@ module pyth_lazer::state; - - const ED25519_PUBKEY_LEN: u64 = 32; const E_INVALID_PUBKEY_LEN: u64 = 1; +const E_SIGNER_NOT_FOUND: u64 = 2; + +/// A trusted signer is comprised of a pubkey and an expiry time. +/// A signer's signature should only be trusted up to timestamp `expires_at`. public struct TrustedSignerInfo has copy, drop, store { public_key: vector, expires_at: u64, } +/// Lazer State consists of the current set of trusted signers. +/// By verifying that a price update was signed by one of these public keys, +/// you can validate the authenticity of a Lazer price update. +/// +/// The trusted signers are subject to rotations and expiry. public struct State has key, store { id: UID, trusted_signers: vector, @@ -22,18 +29,25 @@ public(package) fun new(ctx: &mut TxContext): State { } } +/// Get the trusted signer's public key public fun public_key(info: &TrustedSignerInfo): &vector { &info.public_key } +/// Get the trusted signer's expiry timestamp public fun expires_at(info: &TrustedSignerInfo): u64 { info.expires_at } +/// Get the list of trusted signers public fun get_trusted_signers(s: &State): &vector { &s.trusted_signers } +/// Upsert a trusted signer's information or remove them. +/// - If the trusted signer pubkey already exists, the expires_at will be updated. +/// - If the expired_at is set to zero, the trusted signer will be removed. +/// - If the pubkey isn't found, it is added as a new trusted signer with the given expires_at. public(package) fun update_trusted_signer(s: &mut State, pubkey: vector, expires_at: u64) { assert!(vector::length(&pubkey) as u64 == ED25519_PUBKEY_LEN, E_INVALID_PUBKEY_LEN); @@ -44,7 +58,8 @@ public(package) fun update_trusted_signer(s: &mut State, pubkey: vector, exp // Remove by swapping with last (order not preserved), discard removed value let _ = vector::swap_remove(&mut s.trusted_signers, idx); } else { - option::destroy_none(maybe_idx) + option::destroy_none(maybe_idx); + abort E_SIGNER_NOT_FOUND }; return }; @@ -57,7 +72,7 @@ public(package) fun update_trusted_signer(s: &mut State, pubkey: vector, exp option::destroy_none(maybe_idx); vector::push_back( &mut s.trusted_signers, - TrustedSignerInfo { public_key: pubkey, expires_at } + TrustedSignerInfo { public_key: pubkey, expires_at }, ) } } @@ -109,9 +124,16 @@ public fun test_update_existing_signer_expiry() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); - - update_trusted_signer(&mut s, x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", 1000); - update_trusted_signer(&mut s, x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", 2000); + update_trusted_signer( + &mut s, + x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", + 1000, + ); + update_trusted_signer( + &mut s, + x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", + 2000, + ); let signers_ref = get_trusted_signers(&s); assert!(vector::length(signers_ref) == 1, 110); @@ -127,9 +149,16 @@ public fun test_remove_signer_by_zero_expiry() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); - - update_trusted_signer(&mut s, x"0707070707070707070707070707070707070707070707070707070707070707", 999); - update_trusted_signer(&mut s, x"0707070707070707070707070707070707070707070707070707070707070707", 0); + update_trusted_signer( + &mut s, + x"0707070707070707070707070707070707070707070707070707070707070707", + 999, + ); + update_trusted_signer( + &mut s, + x"0707070707070707070707070707070707070707070707070707070707070707", + 0, + ); let signers_ref = get_trusted_signers(&s); assert!(vector::length(signers_ref) == 0, 120); @@ -149,3 +178,19 @@ public fun test_invalid_pubkey_length_rejected() { let _ = trusted_signers; object::delete(id); } + +#[test, expected_failure(abort_code = E_SIGNER_NOT_FOUND)] +public fun test_remove_nonexistent_signer_fails() { + let mut ctx = tx_context::dummy(); + let mut s = new_for_test(&mut ctx); + + // Try to remove a signer that doesn't exist by setting expires_at to 0 + update_trusted_signer( + &mut s, + x"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + 0, + ); + let State { id, trusted_signers } = s; + let _ = trusted_signers; + object::delete(id); +} From 8ee64ba80822c6665f947a2d9d6a32e1de3a8c64 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 22:49:39 +0000 Subject: [PATCH 07/17] feat(lazer/sui): add package init and AdminCapability; share State and gate updates with admin cap Co-Authored-By: Tejas Badadare --- lazer/contracts/sui/sources/admin.move | 23 ++++++++++++++++++++ lazer/contracts/sui/sources/pyth_lazer.move | 11 ++++++++++ lazer/contracts/sui/sources/state.move | 24 ++++++++++++++++++--- 3 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 lazer/contracts/sui/sources/admin.move diff --git a/lazer/contracts/sui/sources/admin.move b/lazer/contracts/sui/sources/admin.move new file mode 100644 index 0000000000..0f2dbf3df1 --- /dev/null +++ b/lazer/contracts/sui/sources/admin.move @@ -0,0 +1,23 @@ +module pyth_lazer::admin; + +use sui::tx_context::TxContext; +use sui::object; + +public struct AdminCapability has key, store { + id: UID, +} + +public(package) fun mint(ctx: &mut TxContext): AdminCapability { + AdminCapability { id: object::new(ctx) } +} + +#[test_only] +public fun mint_for_test(ctx: &mut TxContext): AdminCapability { + AdminCapability { id: object::new(ctx) } +} + +#[test_only] +public fun destroy_for_test(cap: AdminCapability) { + let AdminCapability { id } = cap; + object::delete(id) +} diff --git a/lazer/contracts/sui/sources/pyth_lazer.move b/lazer/contracts/sui/sources/pyth_lazer.move index 0dabd24756..2a33ed0971 100644 --- a/lazer/contracts/sui/sources/pyth_lazer.move +++ b/lazer/contracts/sui/sources/pyth_lazer.move @@ -5,8 +5,19 @@ use pyth_lazer::i64::Self; use pyth_lazer::update::{Self, Update}; use pyth_lazer::feed::{Self, Feed}; use pyth_lazer::channel::Self; +use pyth_lazer::state; +use pyth_lazer::admin; use sui::bcs; use sui::ecdsa_k1::secp256k1_ecrecover; +use sui::transfer; +use sui::tx_context::{Self, TxContext}; + +fun init(ctx: &mut TxContext) { + let s = state::new(ctx); + transfer::public_share_object(s); + let cap = admin::mint(ctx); + transfer::public_transfer(cap, tx_context::sender(ctx)); +} const SECP256K1_SIG_LEN: u32 = 65; const UPDATE_MESSAGE_MAGIC: u32 = 1296547300; diff --git a/lazer/contracts/sui/sources/state.move b/lazer/contracts/sui/sources/state.move index adc50d9609..c0507d17fe 100644 --- a/lazer/contracts/sui/sources/state.move +++ b/lazer/contracts/sui/sources/state.move @@ -1,5 +1,8 @@ module pyth_lazer::state; +use pyth_lazer::admin::AdminCapability; +use pyth_lazer::admin; + const ED25519_PUBKEY_LEN: u64 = 32; const E_INVALID_PUBKEY_LEN: u64 = 1; const E_SIGNER_NOT_FOUND: u64 = 2; @@ -48,7 +51,7 @@ public fun get_trusted_signers(s: &State): &vector { /// - If the trusted signer pubkey already exists, the expires_at will be updated. /// - If the expired_at is set to zero, the trusted signer will be removed. /// - If the pubkey isn't found, it is added as a new trusted signer with the given expires_at. -public(package) fun update_trusted_signer(s: &mut State, pubkey: vector, expires_at: u64) { +public(package) fun update_trusted_signer(_admin: &AdminCapability, s: &mut State, pubkey: vector, expires_at: u64) { assert!(vector::length(&pubkey) as u64 == ED25519_PUBKEY_LEN, E_INVALID_PUBKEY_LEN); let mut maybe_idx = find_signer_index(&s.trusted_signers, &pubkey); @@ -102,11 +105,12 @@ public fun new_for_test(ctx: &mut TxContext): State { public fun test_add_new_signer() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); + let admin_cap = admin::mint_for_test(&mut ctx); let pk = x"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"; let expiry: u64 = 123; - update_trusted_signer(&mut s, pk, expiry); + update_trusted_signer(&admin_cap, &mut s, pk, expiry); let signers_ref = get_trusted_signers(&s); assert!(vector::length(signers_ref) == 1, 100); @@ -117,19 +121,23 @@ public fun test_add_new_signer() { let State { id, trusted_signers } = s; let _ = trusted_signers; object::delete(id); + admin::destroy_for_test(admin_cap); } #[test] public fun test_update_existing_signer_expiry() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); + let admin_cap = admin::mint_for_test(&mut ctx); update_trusted_signer( + &admin_cap, &mut s, x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", 1000, ); update_trusted_signer( + &admin_cap, &mut s, x"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", 2000, @@ -142,19 +150,23 @@ public fun test_update_existing_signer_expiry() { let State { id, trusted_signers } = s; let _ = trusted_signers; object::delete(id); + admin::destroy_for_test(admin_cap); } #[test] public fun test_remove_signer_by_zero_expiry() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); + let admin_cap = admin::mint_for_test(&mut ctx); update_trusted_signer( + &admin_cap, &mut s, x"0707070707070707070707070707070707070707070707070707070707070707", 999, ); update_trusted_signer( + &admin_cap, &mut s, x"0707070707070707070707070707070707070707070707070707070707070707", 0, @@ -165,27 +177,32 @@ public fun test_remove_signer_by_zero_expiry() { let State { id, trusted_signers } = s; let _ = trusted_signers; object::delete(id); + admin::destroy_for_test(admin_cap); } #[test, expected_failure(abort_code = E_INVALID_PUBKEY_LEN)] public fun test_invalid_pubkey_length_rejected() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); + let admin_cap = admin::mint_for_test(&mut ctx); let short_pk = x"010203"; - update_trusted_signer(&mut s, short_pk, 1); + update_trusted_signer(&admin_cap, &mut s, short_pk, 1); let State { id, trusted_signers } = s; let _ = trusted_signers; object::delete(id); + admin::destroy_for_test(admin_cap); } #[test, expected_failure(abort_code = E_SIGNER_NOT_FOUND)] public fun test_remove_nonexistent_signer_fails() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); + let admin_cap = admin::mint_for_test(&mut ctx); // Try to remove a signer that doesn't exist by setting expires_at to 0 update_trusted_signer( + &admin_cap, &mut s, x"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 0, @@ -193,4 +210,5 @@ public fun test_remove_nonexistent_signer_fails() { let State { id, trusted_signers } = s; let _ = trusted_signers; object::delete(id); + admin::destroy_for_test(admin_cap); } From a7436fa528e9216103c3805d45080e7099af453c Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Tue, 19 Aug 2025 15:50:53 -0700 Subject: [PATCH 08/17] comments --- lazer/contracts/sui/Move.toml | 2 +- lazer/contracts/sui/sources/state.move | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lazer/contracts/sui/Move.toml b/lazer/contracts/sui/Move.toml index b171bd4cc6..19ca0072da 100644 --- a/lazer/contracts/sui/Move.toml +++ b/lazer/contracts/sui/Move.toml @@ -1,6 +1,6 @@ [package] name = "pyth_lazer" -version = "0.0.1" +version = "0.0.0" edition = "2024.beta" [addresses] diff --git a/lazer/contracts/sui/sources/state.move b/lazer/contracts/sui/sources/state.move index adc50d9609..4247849615 100644 --- a/lazer/contracts/sui/sources/state.move +++ b/lazer/contracts/sui/sources/state.move @@ -1,17 +1,10 @@ module pyth_lazer::state; const ED25519_PUBKEY_LEN: u64 = 32; -const E_INVALID_PUBKEY_LEN: u64 = 1; -const E_SIGNER_NOT_FOUND: u64 = 2; +const EInvalidPubkeyLen: u64 = 1; +const ESignerNotFound: u64 = 2; -/// A trusted signer is comprised of a pubkey and an expiry time. -/// A signer's signature should only be trusted up to timestamp `expires_at`. -public struct TrustedSignerInfo has copy, drop, store { - public_key: vector, - expires_at: u64, -} - /// Lazer State consists of the current set of trusted signers. /// By verifying that a price update was signed by one of these public keys, /// you can validate the authenticity of a Lazer price update. @@ -22,6 +15,13 @@ public struct State has key, store { trusted_signers: vector, } +/// A trusted signer is comprised of a pubkey and an expiry time. +/// A signer's signature should only be trusted up to timestamp `expires_at`. +public struct TrustedSignerInfo has copy, drop, store { + public_key: vector, + expires_at: u64, +} + public(package) fun new(ctx: &mut TxContext): State { State { id: object::new(ctx), @@ -49,7 +49,7 @@ public fun get_trusted_signers(s: &State): &vector { /// - If the expired_at is set to zero, the trusted signer will be removed. /// - If the pubkey isn't found, it is added as a new trusted signer with the given expires_at. public(package) fun update_trusted_signer(s: &mut State, pubkey: vector, expires_at: u64) { - assert!(vector::length(&pubkey) as u64 == ED25519_PUBKEY_LEN, E_INVALID_PUBKEY_LEN); + assert!(vector::length(&pubkey) as u64 == ED25519_PUBKEY_LEN, EInvalidPubkeyLen); let mut maybe_idx = find_signer_index(&s.trusted_signers, &pubkey); if (expires_at == 0) { @@ -59,7 +59,7 @@ public(package) fun update_trusted_signer(s: &mut State, pubkey: vector, exp let _ = vector::swap_remove(&mut s.trusted_signers, idx); } else { option::destroy_none(maybe_idx); - abort E_SIGNER_NOT_FOUND + abort ESignerNotFound }; return }; @@ -167,7 +167,7 @@ public fun test_remove_signer_by_zero_expiry() { object::delete(id); } -#[test, expected_failure(abort_code = E_INVALID_PUBKEY_LEN)] +#[test, expected_failure(abort_code = EInvalidPubkeyLen)] public fun test_invalid_pubkey_length_rejected() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); @@ -179,7 +179,7 @@ public fun test_invalid_pubkey_length_rejected() { object::delete(id); } -#[test, expected_failure(abort_code = E_SIGNER_NOT_FOUND)] +#[test, expected_failure(abort_code = ESignerNotFound)] public fun test_remove_nonexistent_signer_fails() { let mut ctx = tx_context::dummy(); let mut s = new_for_test(&mut ctx); From e64eb21db3d2e6901e223e7a1b0fa8ac67ffee08 Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Tue, 19 Aug 2025 16:16:18 -0700 Subject: [PATCH 09/17] resolve warnings --- lazer/contracts/sui/sources/admin.move | 3 --- lazer/contracts/sui/sources/pyth_lazer.move | 22 ++++++++++----------- lazer/contracts/sui/sources/state.move | 4 ++-- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/lazer/contracts/sui/sources/admin.move b/lazer/contracts/sui/sources/admin.move index 0f2dbf3df1..1920e196c5 100644 --- a/lazer/contracts/sui/sources/admin.move +++ b/lazer/contracts/sui/sources/admin.move @@ -1,8 +1,5 @@ module pyth_lazer::admin; -use sui::tx_context::TxContext; -use sui::object; - public struct AdminCapability has key, store { id: UID, } diff --git a/lazer/contracts/sui/sources/pyth_lazer.move b/lazer/contracts/sui/sources/pyth_lazer.move index 2a33ed0971..9f6b889e12 100644 --- a/lazer/contracts/sui/sources/pyth_lazer.move +++ b/lazer/contracts/sui/sources/pyth_lazer.move @@ -9,15 +9,6 @@ use pyth_lazer::state; use pyth_lazer::admin; use sui::bcs; use sui::ecdsa_k1::secp256k1_ecrecover; -use sui::transfer; -use sui::tx_context::{Self, TxContext}; - -fun init(ctx: &mut TxContext) { - let s = state::new(ctx); - transfer::public_share_object(s); - let cap = admin::mint(ctx); - transfer::public_transfer(cap, tx_context::sender(ctx)); -} const SECP256K1_SIG_LEN: u32 = 65; const UPDATE_MESSAGE_MAGIC: u32 = 1296547300; @@ -25,12 +16,19 @@ const PAYLOAD_MAGIC: u32 = 2479346549; // TODO: -// initializer -// administration -> admin cap, upgrade cap, governance? -// storage module -> trusted signers, update fee?, treasury? // error handling // standalone verify signature function +/// Initializes the module. Called at publish time. +/// Creates and shares the singular State object. +/// Creates the singular AdminCapability and transfers it to the deployer. +fun init(ctx: &mut TxContext) { + let s = state::new(ctx); + transfer::public_share_object(s); + let cap = admin::mint(ctx); + transfer::public_transfer(cap, tx_context::sender(ctx)); +} + /// Parse the Lazer update message and validate the signature. /// /// The parsing logic is based on the Lazer rust protocol definition defined here: diff --git a/lazer/contracts/sui/sources/state.move b/lazer/contracts/sui/sources/state.move index 4e766a19e9..b4b9b2f5bf 100644 --- a/lazer/contracts/sui/sources/state.move +++ b/lazer/contracts/sui/sources/state.move @@ -47,11 +47,11 @@ public fun get_trusted_signers(s: &State): &vector { &s.trusted_signers } -/// Upsert a trusted signer's information or remove them. +/// Upsert a trusted signer's information or remove them. Can only be called by the AdminCapability holder. /// - If the trusted signer pubkey already exists, the expires_at will be updated. /// - If the expired_at is set to zero, the trusted signer will be removed. /// - If the pubkey isn't found, it is added as a new trusted signer with the given expires_at. -public(package) fun update_trusted_signer(_admin: &AdminCapability, s: &mut State, pubkey: vector, expires_at: u64) { +public(package) fun update_trusted_signer(_: &AdminCapability, s: &mut State, pubkey: vector, expires_at: u64) { assert!(vector::length(&pubkey) as u64 == ED25519_PUBKEY_LEN, EInvalidPubkeyLen); let mut maybe_idx = find_signer_index(&s.trusted_signers, &pubkey); From 187f0e9cb29a3f19fa34bd53da0f826f560271a7 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 23:34:21 +0000 Subject: [PATCH 10/17] feat(sui-lazer): add admin-gated entry to update trusted signer via AdminCapability Co-Authored-By: Tejas Badadare --- lazer/contracts/sui/sources/pyth_lazer.move | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lazer/contracts/sui/sources/pyth_lazer.move b/lazer/contracts/sui/sources/pyth_lazer.move index 9f6b889e12..c4e3c0f921 100644 --- a/lazer/contracts/sui/sources/pyth_lazer.move +++ b/lazer/contracts/sui/sources/pyth_lazer.move @@ -22,6 +22,17 @@ const PAYLOAD_MAGIC: u32 = 2479346549; /// Initializes the module. Called at publish time. /// Creates and shares the singular State object. /// Creates the singular AdminCapability and transfers it to the deployer. +public entry fun admin_update_trusted_signer( + cap: admin::AdminCapability, + s: &mut state::State, + pubkey: vector, + expires_at: u64, + ctx: &mut TxContext, +) { + state::update_trusted_signer(&cap, s, pubkey, expires_at); + transfer::public_transfer(cap, tx_context::sender(ctx)); +} + fun init(ctx: &mut TxContext) { let s = state::new(ctx); transfer::public_share_object(s); From ddad6d64bd40bbe10048d6e54b4ce7d076bba17d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 23:50:54 +0000 Subject: [PATCH 11/17] Revert "feat(sui-lazer): add admin-gated entry to update trusted signer via AdminCapability" This reverts commit 187f0e9cb29a3f19fa34bd53da0f826f560271a7. --- lazer/contracts/sui/sources/pyth_lazer.move | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lazer/contracts/sui/sources/pyth_lazer.move b/lazer/contracts/sui/sources/pyth_lazer.move index c4e3c0f921..9f6b889e12 100644 --- a/lazer/contracts/sui/sources/pyth_lazer.move +++ b/lazer/contracts/sui/sources/pyth_lazer.move @@ -22,17 +22,6 @@ const PAYLOAD_MAGIC: u32 = 2479346549; /// Initializes the module. Called at publish time. /// Creates and shares the singular State object. /// Creates the singular AdminCapability and transfers it to the deployer. -public entry fun admin_update_trusted_signer( - cap: admin::AdminCapability, - s: &mut state::State, - pubkey: vector, - expires_at: u64, - ctx: &mut TxContext, -) { - state::update_trusted_signer(&cap, s, pubkey, expires_at); - transfer::public_transfer(cap, tx_context::sender(ctx)); -} - fun init(ctx: &mut TxContext) { let s = state::new(ctx); transfer::public_share_object(s); From 0fc417ec139d1c155efccbd3496b354c8efb40c0 Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Tue, 19 Aug 2025 17:01:52 -0700 Subject: [PATCH 12/17] naming, make update_trusted_signer public --- lazer/contracts/sui/sources/admin.move | 14 +++++++------- lazer/contracts/sui/sources/state.move | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lazer/contracts/sui/sources/admin.move b/lazer/contracts/sui/sources/admin.move index 1920e196c5..a218dd5523 100644 --- a/lazer/contracts/sui/sources/admin.move +++ b/lazer/contracts/sui/sources/admin.move @@ -1,20 +1,20 @@ module pyth_lazer::admin; -public struct AdminCapability has key, store { +public struct AdminCap has key, store { id: UID, } -public(package) fun mint(ctx: &mut TxContext): AdminCapability { - AdminCapability { id: object::new(ctx) } +public(package) fun mint(ctx: &mut TxContext): AdminCap { + AdminCap { id: object::new(ctx) } } #[test_only] -public fun mint_for_test(ctx: &mut TxContext): AdminCapability { - AdminCapability { id: object::new(ctx) } +public fun mint_for_test(ctx: &mut TxContext): AdminCap { + AdminCap { id: object::new(ctx) } } #[test_only] -public fun destroy_for_test(cap: AdminCapability) { - let AdminCapability { id } = cap; +public fun destroy_for_test(cap: AdminCap) { + let AdminCap { id } = cap; object::delete(id) } diff --git a/lazer/contracts/sui/sources/state.move b/lazer/contracts/sui/sources/state.move index b4b9b2f5bf..a2c351efbb 100644 --- a/lazer/contracts/sui/sources/state.move +++ b/lazer/contracts/sui/sources/state.move @@ -1,6 +1,6 @@ module pyth_lazer::state; -use pyth_lazer::admin::AdminCapability; +use pyth_lazer::admin::AdminCap; use pyth_lazer::admin; const ED25519_PUBKEY_LEN: u64 = 32; @@ -47,11 +47,11 @@ public fun get_trusted_signers(s: &State): &vector { &s.trusted_signers } -/// Upsert a trusted signer's information or remove them. Can only be called by the AdminCapability holder. +/// Upsert a trusted signer's information or remove them. Can only be called by the AdminCap holder. /// - If the trusted signer pubkey already exists, the expires_at will be updated. /// - If the expired_at is set to zero, the trusted signer will be removed. /// - If the pubkey isn't found, it is added as a new trusted signer with the given expires_at. -public(package) fun update_trusted_signer(_: &AdminCapability, s: &mut State, pubkey: vector, expires_at: u64) { +public fun update_trusted_signer(_: &AdminCap, s: &mut State, pubkey: vector, expires_at: u64) { assert!(vector::length(&pubkey) as u64 == ED25519_PUBKEY_LEN, EInvalidPubkeyLen); let mut maybe_idx = find_signer_index(&s.trusted_signers, &pubkey); From da1a2a1361f002b2604e0ef9f7bd4aeb791e390f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 02:19:17 +0000 Subject: [PATCH 13/17] feat(sui-lazer): use OTW for AdminCap in admin::init; add OTW to pyth_lazer::init; share State; add OTW doc comments Co-Authored-By: Tejas Badadare --- lazer/contracts/sui/sources/admin.move | 15 +++++++++++++-- lazer/contracts/sui/sources/pyth_lazer.move | 17 ++++++++++++----- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/lazer/contracts/sui/sources/admin.move b/lazer/contracts/sui/sources/admin.move index a218dd5523..78b5312478 100644 --- a/lazer/contracts/sui/sources/admin.move +++ b/lazer/contracts/sui/sources/admin.move @@ -1,11 +1,22 @@ module pyth_lazer::admin; +use sui::tx_context::{Self, TxContext}; +use sui::object; +use sui::transfer; +use sui::types; + public struct AdminCap has key, store { id: UID, } -public(package) fun mint(ctx: &mut TxContext): AdminCap { - AdminCap { id: object::new(ctx) } +// One-Time Witness for the admin module. Constructed by the VM once at publish time. +// Docs: https://move-book.com/programmability/one-time-witness +public struct ADMIN has drop {} + +fun init(otw: ADMIN, ctx: &mut TxContext) { + assert!(types::is_one_time_witness(&otw), 1); + let cap = AdminCap { id: object::new(ctx) }; + transfer::public_transfer(cap, tx_context::sender(ctx)); } #[test_only] diff --git a/lazer/contracts/sui/sources/pyth_lazer.move b/lazer/contracts/sui/sources/pyth_lazer.move index 9f6b889e12..2fd7268e0f 100644 --- a/lazer/contracts/sui/sources/pyth_lazer.move +++ b/lazer/contracts/sui/sources/pyth_lazer.move @@ -10,6 +10,10 @@ use pyth_lazer::admin; use sui::bcs; use sui::ecdsa_k1::secp256k1_ecrecover; +use sui::transfer; +use sui::tx_context::{Self, TxContext}; +use sui::types; + const SECP256K1_SIG_LEN: u32 = 65; const UPDATE_MESSAGE_MAGIC: u32 = 1296547300; const PAYLOAD_MAGIC: u32 = 2479346549; @@ -19,14 +23,17 @@ const PAYLOAD_MAGIC: u32 = 2479346549; // error handling // standalone verify signature function -/// Initializes the module. Called at publish time. +// One-Time Witness for the pyth_lazer module. Constructed once by the VM at publish time. +// Docs: https://move-book.com/programmability/one-time-witness +public struct PYTH_LAZER has drop {} + +/// Initializes the module. Called at publish time. /// Creates and shares the singular State object. -/// Creates the singular AdminCapability and transfers it to the deployer. -fun init(ctx: &mut TxContext) { +/// AdminCap is created and transferred in admin::init via a One-Time Witness. +fun init(otw: PYTH_LAZER, ctx: &mut TxContext) { + assert!(types::is_one_time_witness(&otw), 1); let s = state::new(ctx); transfer::public_share_object(s); - let cap = admin::mint(ctx); - transfer::public_transfer(cap, tx_context::sender(ctx)); } /// Parse the Lazer update message and validate the signature. From 389f5d7412d3b56ced73ae6adc3934bb60a0ca37 Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Tue, 19 Aug 2025 19:47:53 -0700 Subject: [PATCH 14/17] lint --- lazer/contracts/sui/sources/admin.move | 9 +++------ lazer/contracts/sui/sources/pyth_lazer.move | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/lazer/contracts/sui/sources/admin.move b/lazer/contracts/sui/sources/admin.move index 78b5312478..ee65697f0a 100644 --- a/lazer/contracts/sui/sources/admin.move +++ b/lazer/contracts/sui/sources/admin.move @@ -1,16 +1,13 @@ module pyth_lazer::admin; - -use sui::tx_context::{Self, TxContext}; -use sui::object; -use sui::transfer; use sui::types; public struct AdminCap has key, store { id: UID, } -// One-Time Witness for the admin module. Constructed by the VM once at publish time. -// Docs: https://move-book.com/programmability/one-time-witness +/// The `ADMIN` resource serves as the one-time witness. +/// It has the `drop` ability, allowing it to be consumed immediately after use. +/// See: https://move-book.com/programmability/one-time-witness public struct ADMIN has drop {} fun init(otw: ADMIN, ctx: &mut TxContext) { diff --git a/lazer/contracts/sui/sources/pyth_lazer.move b/lazer/contracts/sui/sources/pyth_lazer.move index 2fd7268e0f..b3e5e38316 100644 --- a/lazer/contracts/sui/sources/pyth_lazer.move +++ b/lazer/contracts/sui/sources/pyth_lazer.move @@ -6,12 +6,8 @@ use pyth_lazer::update::{Self, Update}; use pyth_lazer::feed::{Self, Feed}; use pyth_lazer::channel::Self; use pyth_lazer::state; -use pyth_lazer::admin; use sui::bcs; use sui::ecdsa_k1::secp256k1_ecrecover; - -use sui::transfer; -use sui::tx_context::{Self, TxContext}; use sui::types; const SECP256K1_SIG_LEN: u32 = 65; @@ -23,8 +19,9 @@ const PAYLOAD_MAGIC: u32 = 2479346549; // error handling // standalone verify signature function -// One-Time Witness for the pyth_lazer module. Constructed once by the VM at publish time. -// Docs: https://move-book.com/programmability/one-time-witness +/// The `PYTH_LAZER` resource serves as the one-time witness. +/// It has the `drop` ability, allowing it to be consumed immediately after use. +/// See: https://move-book.com/programmability/one-time-witness public struct PYTH_LAZER has drop {} /// Initializes the module. Called at publish time. From f1b6fc4587ade1fdf47d802c5cccc2e6e2a49397 Mon Sep 17 00:00:00 2001 From: Tejas Badadare Date: Tue, 19 Aug 2025 19:51:40 -0700 Subject: [PATCH 15/17] doc --- lazer/contracts/sui/sources/admin.move | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lazer/contracts/sui/sources/admin.move b/lazer/contracts/sui/sources/admin.move index ee65697f0a..95f593bc6a 100644 --- a/lazer/contracts/sui/sources/admin.move +++ b/lazer/contracts/sui/sources/admin.move @@ -10,6 +10,10 @@ public struct AdminCap has key, store { /// See: https://move-book.com/programmability/one-time-witness public struct ADMIN has drop {} + +/// Initializes the module. Called at publish time. +/// Creates and transfers ownership of the singular AdminCap capability to the deployer. +/// Only the AdminCap owner can update the trusted signers. fun init(otw: ADMIN, ctx: &mut TxContext) { assert!(types::is_one_time_witness(&otw), 1); let cap = AdminCap { id: object::new(ctx) }; From b9a844dea980e19bd6cf81454a348f8f09c1ba5c Mon Sep 17 00:00:00 2001 From: Tejas Badadare <17058023+tejasbadadare@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:47:17 -0700 Subject: [PATCH 16/17] remove unneeded otw type check --- lazer/contracts/sui/sources/admin.move | 1 - lazer/contracts/sui/sources/pyth_lazer.move | 2 -- 2 files changed, 3 deletions(-) diff --git a/lazer/contracts/sui/sources/admin.move b/lazer/contracts/sui/sources/admin.move index 95f593bc6a..358c5b6213 100644 --- a/lazer/contracts/sui/sources/admin.move +++ b/lazer/contracts/sui/sources/admin.move @@ -15,7 +15,6 @@ public struct ADMIN has drop {} /// Creates and transfers ownership of the singular AdminCap capability to the deployer. /// Only the AdminCap owner can update the trusted signers. fun init(otw: ADMIN, ctx: &mut TxContext) { - assert!(types::is_one_time_witness(&otw), 1); let cap = AdminCap { id: object::new(ctx) }; transfer::public_transfer(cap, tx_context::sender(ctx)); } diff --git a/lazer/contracts/sui/sources/pyth_lazer.move b/lazer/contracts/sui/sources/pyth_lazer.move index b3e5e38316..01c1c51c60 100644 --- a/lazer/contracts/sui/sources/pyth_lazer.move +++ b/lazer/contracts/sui/sources/pyth_lazer.move @@ -8,7 +8,6 @@ use pyth_lazer::channel::Self; use pyth_lazer::state; use sui::bcs; use sui::ecdsa_k1::secp256k1_ecrecover; -use sui::types; const SECP256K1_SIG_LEN: u32 = 65; const UPDATE_MESSAGE_MAGIC: u32 = 1296547300; @@ -28,7 +27,6 @@ public struct PYTH_LAZER has drop {} /// Creates and shares the singular State object. /// AdminCap is created and transferred in admin::init via a One-Time Witness. fun init(otw: PYTH_LAZER, ctx: &mut TxContext) { - assert!(types::is_one_time_witness(&otw), 1); let s = state::new(ctx); transfer::public_share_object(s); } From 41c66dcb4e80a8dccbb74de0f74e4953c336310f Mon Sep 17 00:00:00 2001 From: Tejas Badadare <17058023+tejasbadadare@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:15:35 -0700 Subject: [PATCH 17/17] lint --- lazer/contracts/sui/sources/admin.move | 2 +- lazer/contracts/sui/sources/pyth_lazer.move | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lazer/contracts/sui/sources/admin.move b/lazer/contracts/sui/sources/admin.move index 358c5b6213..ab20d4492e 100644 --- a/lazer/contracts/sui/sources/admin.move +++ b/lazer/contracts/sui/sources/admin.move @@ -14,7 +14,7 @@ public struct ADMIN has drop {} /// Initializes the module. Called at publish time. /// Creates and transfers ownership of the singular AdminCap capability to the deployer. /// Only the AdminCap owner can update the trusted signers. -fun init(otw: ADMIN, ctx: &mut TxContext) { +fun init(_: ADMIN, ctx: &mut TxContext) { let cap = AdminCap { id: object::new(ctx) }; transfer::public_transfer(cap, tx_context::sender(ctx)); } diff --git a/lazer/contracts/sui/sources/pyth_lazer.move b/lazer/contracts/sui/sources/pyth_lazer.move index 01c1c51c60..102518d7a7 100644 --- a/lazer/contracts/sui/sources/pyth_lazer.move +++ b/lazer/contracts/sui/sources/pyth_lazer.move @@ -26,7 +26,7 @@ public struct PYTH_LAZER has drop {} /// Initializes the module. Called at publish time. /// Creates and shares the singular State object. /// AdminCap is created and transferred in admin::init via a One-Time Witness. -fun init(otw: PYTH_LAZER, ctx: &mut TxContext) { +fun init(_: PYTH_LAZER, ctx: &mut TxContext) { let s = state::new(ctx); transfer::public_share_object(s); }