-
Notifications
You must be signed in to change notification settings - Fork 315
feat: init Lazer Aptos contract #2381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 5 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
762cfef
feat: init aptos contract
tejasbadadare 4c3193f
fix: remove unnecessary check
tejasbadadare 01322a5
temp key
tejasbadadare 7c2283c
add tests
tejasbadadare 3078260
lints
tejasbadadare 333cd24
fix: typo
tejasbadadare 69f2946
fix: remove num_trusted_signers, remove max signers functionality, ad…
tejasbadadare 62d6905
fix: update readme, remove test_initialize
tejasbadadare f33ef01
ci: add aptos move fmt & lint hooks to precommit
tejasbadadare 6943287
ci: run lazer aptos tests in CI, add precommit deps
tejasbadadare d154842
ci: fix ci
tejasbadadare d2b734a
test: add test_verify_invalid_message_fails
tejasbadadare ac5f922
ci: fix precommit
tejasbadadare 60fc3d5
Merge branch 'main' of github.com:pyth-network/pyth-crosschain into t…
tejasbadadare c9ec44f
test: remove unnecessary assert
tejasbadadare d17ca91
fix: use dev-addresses
tejasbadadare 1d9fad0
feat: add AdminCapability
tejasbadadare 57059b7
feat: add verify_message_with_funder
tejasbadadare f1ffb21
docs: add docstrings
tejasbadadare 5259a52
fix: drop PendingAdminCapability after claiming
tejasbadadare File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| [package] | ||
| name = "pyth_lazer" | ||
| version = "0.1.0" | ||
| license = "UNLICENSED" | ||
|
|
||
| [dependencies.AptosFramework] | ||
| git = "https://github.com/aptos-labs/aptos-framework.git" | ||
| rev = "mainnet" | ||
| subdir = "aptos-framework" | ||
|
|
||
| [addresses] | ||
| pyth_lazer = "0x8731685005cfb169b4da4bbfab0c91c5ba59508bbd6d26990ee2be7225cb34d1" # Temporary key during development | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| ## Pyth Lazer Aptos Contract | ||
|
|
||
| This package is built using the Move language and Aptos framework. | ||
|
|
||
| `PythLazer` is an Aptos contract that allows consumers to easily verify Pyth Lazer updates for use on-chain. | ||
|
|
||
| ### Build, test, deploy | ||
|
|
||
| Install Aptos CLI and set it up: | ||
|
|
||
| ```shell | ||
tejasbadadare marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| $ brew install | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| $ aptos --version | ||
| $ aptos init --network devnet | ||
| ``` | ||
|
|
||
| Compile the contract and run tests: | ||
|
|
||
| ```shell | ||
| $ aptos move compile | ||
| $ aptos move test | ||
| ``` | ||
|
|
||
| Deploy to the network configured in your aptos profile: | ||
|
|
||
| ```shell | ||
| $ aptos move publish | ||
| ``` | ||
|
|
||
| Invoke deployed contract functions on-chain: | ||
|
|
||
| ```shell | ||
| aptos move run --function-id 'default::pyth_lazer::update_trusted_signer' --args 'hex:0x8731685005cfb169b4da4bbfab0c91c5ba59508bbd6d26990ee2be7225cb34d1' 'u64:9999999999' | ||
| ``` | ||
|
|
||
| ### Error Handling | ||
tejasbadadare marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| The contract uses the following error codes: | ||
|
|
||
| - ENO_PERMISSIONS (1): Caller lacks required permissions | ||
| - EINVALID_SIGNER (2): Invalid or expired signer | ||
| - ENO_SPACE (3): Maximum number of signers reached | ||
| - ENO_SUCH_PUBKEY (4): Attempting to remove non-existent signer | ||
| - EINVALID_SIGNATURE (5): Invalid Ed25519 signature | ||
| - EINSUFFICIENT_FEE (6): Insufficient fee provided | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| module pyth_lazer::pyth_lazer { | ||
| use std::vector; | ||
| use std::signer; | ||
| use aptos_framework::timestamp; | ||
| use aptos_framework::coin; | ||
| use aptos_framework::aptos_coin::AptosCoin; | ||
| use aptos_std::ed25519; | ||
|
|
||
| /// Error codes | ||
| const ENO_PERMISSIONS: u64 = 1; | ||
| const EINVALID_SIGNER: u64 = 2; | ||
| const ENO_SPACE: u64 = 3; | ||
| const ENO_SUCH_PUBKEY: u64 = 4; | ||
| const EINVALID_SIGNATURE: u64 = 5; | ||
| const EINSUFFICIENT_FEE: u64 = 6; | ||
|
|
||
| /// Constants | ||
| const MAX_NUM_TRUSTED_SIGNERS: u8 = 2; | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const ED25519_PUBLIC_KEY_LENGTH: u64 = 32; | ||
|
|
||
| /// Stores information about a trusted signer including their public key and expiration | ||
| struct TrustedSignerInfo has store, drop { | ||
| pubkey: vector<u8>, // Ed25519 public key (32 bytes) | ||
| expires_at: u64, // Unix timestamp | ||
| } | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /// Main storage for the Lazer contract | ||
| struct Storage has key { | ||
| top_authority: address, | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| treasury: address, | ||
| single_update_fee: u64, | ||
| num_trusted_signers: u8, | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| trusted_signers: vector<TrustedSignerInfo>, | ||
| } | ||
|
|
||
| /// Events | ||
| struct TrustedSignerUpdateEvent has drop, store { | ||
tejasbadadare marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| pubkey: vector<u8>, | ||
| expires_at: u64, | ||
| } | ||
|
|
||
| /// Initialize the Lazer contract with top authority and treasury | ||
| public entry fun initialize( | ||
| account: &signer, | ||
| top_authority: address, | ||
| treasury: address, | ||
| ) { | ||
| let storage = Storage { | ||
| top_authority, | ||
| treasury, | ||
| single_update_fee: 1, // Nominal fee | ||
| num_trusted_signers: 0, | ||
| trusted_signers: vector::empty(), | ||
| }; | ||
| move_to(account, storage); | ||
tejasbadadare marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /// Upsert a trusted signer's information or remove them | ||
| public entry fun update_trusted_signer( | ||
| account: &signer, | ||
| trusted_signer: vector<u8>, | ||
| expires_at: u64, | ||
| ) acquires Storage { | ||
| let storage = borrow_global_mut<Storage>(@pyth_lazer); | ||
| assert!(signer::address_of(account) == storage.top_authority, ENO_PERMISSIONS); | ||
| assert!(vector::length(&trusted_signer) == ED25519_PUBLIC_KEY_LENGTH, EINVALID_SIGNER); | ||
|
|
||
| let num_signers = storage.num_trusted_signers; | ||
| let i = 0; | ||
| let found = false; | ||
|
|
||
| while (i < num_signers) { | ||
| let signer_info = vector::borrow(&storage.trusted_signers, (i as u64)); | ||
| if (signer_info.pubkey == trusted_signer) { | ||
| found = true; | ||
| break | ||
| }; | ||
| i = i + 1; | ||
| }; | ||
|
|
||
| if (expires_at == 0) { | ||
| // Remove signer | ||
| assert!(found, ENO_SUCH_PUBKEY); | ||
| vector::remove(&mut storage.trusted_signers, (i as u64)); | ||
| storage.num_trusted_signers = storage.num_trusted_signers - 1; | ||
| } else if (found) { | ||
| // Update existing signer | ||
| let signer_info = vector::borrow_mut(&mut storage.trusted_signers, (i as u64)); | ||
| signer_info.expires_at = expires_at; | ||
| } else { | ||
| // Add new signer | ||
| assert!(storage.num_trusted_signers < MAX_NUM_TRUSTED_SIGNERS, ENO_SPACE); | ||
| vector::push_back(&mut storage.trusted_signers, TrustedSignerInfo { | ||
| pubkey: trusted_signer, | ||
| expires_at, | ||
| }); | ||
| storage.num_trusted_signers = storage.num_trusted_signers + 1; | ||
| }; | ||
| } | ||
|
|
||
| /// Verify a message signature and collect fee | ||
| public entry fun verify_message( | ||
| account: &signer, | ||
| message: vector<u8>, | ||
| signature: vector<u8>, | ||
| public_key: vector<u8>, | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ) acquires Storage { | ||
| let storage = borrow_global<Storage>(@pyth_lazer); | ||
|
|
||
| // Verify fee payment | ||
| assert!(coin::balance<AptosCoin>(signer::address_of(account)) >= storage.single_update_fee, EINSUFFICIENT_FEE); | ||
| coin::transfer<AptosCoin>(account, storage.treasury, storage.single_update_fee); | ||
|
|
||
| // Verify signer is trusted and not expired | ||
| let i = 0; | ||
| let valid = false; | ||
| while (i < storage.num_trusted_signers) { | ||
| let signer_info = vector::borrow(&storage.trusted_signers, (i as u64)); | ||
| if (signer_info.pubkey == public_key && signer_info.expires_at > timestamp::now_seconds()) { | ||
| valid = true; | ||
| break | ||
| }; | ||
| i = i + 1; | ||
| }; | ||
| assert!(valid, EINVALID_SIGNER); | ||
|
|
||
| // Verify signature | ||
| let sig = ed25519::new_signature_from_bytes(signature); | ||
| let pk = ed25519::new_unvalidated_public_key_from_bytes(public_key); | ||
| assert!(ed25519::signature_verify_strict(&sig, &pk, message), EINVALID_SIGNATURE); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| #[test_only] | ||
| module pyth_lazer::pyth_lazer_tests { | ||
| use std::signer; | ||
| use aptos_framework::account; | ||
| use aptos_framework::coin; | ||
| use aptos_framework::timestamp; | ||
| use aptos_framework::aptos_coin::AptosCoin; | ||
| use aptos_std::ed25519; | ||
| use pyth_lazer::pyth_lazer; | ||
|
|
||
| // Test accounts | ||
| const TOP_AUTHORITY: address = @0x3374049c3b46a907ff2fc6b62af51975fb9dc572b7e73eb1b255ed5edcd7cee0; | ||
| const TREASURY: address = @0x456; | ||
| const USER: address = @0x789; | ||
|
|
||
| // Test data | ||
| const TEST_PUBKEY: vector<u8> = x"3374049c3b46a907ff2fc6b62af51975fb9dc572b7e73eb1b255ed5edcd7cee0"; | ||
| const TEST_MESSAGE: vector<u8> = b"test message"; | ||
| const TEST_SIGNATURE: vector<u8> = x"20ebb15d70abc18abf636d77fa86a89e32596f90569b09e732b556bbc2f8afea07feff8d1beb18f7acd7ef1d3f914163fe03a3b4206f61f932e2d22a21278a01"; | ||
|
|
||
| #[test_only] | ||
| fun setup_aptos_coin(framework: &signer): coin::MintCapability<AptosCoin> { | ||
| let (burn_cap, freeze_cap, mint_cap) = coin::initialize<AptosCoin>( | ||
| framework, | ||
| std::string::utf8(b"Aptos Coin"), | ||
| std::string::utf8(b"APT"), | ||
| 8, | ||
| false, | ||
| ); | ||
| coin::destroy_burn_cap(burn_cap); | ||
| coin::destroy_freeze_cap(freeze_cap); | ||
| mint_cap | ||
| } | ||
|
|
||
| fun setup(): (signer, signer, signer) { | ||
| // Create test accounts | ||
| let framework = account::create_account_for_test(@aptos_framework); | ||
| let lazer_contract = account::create_account_for_test(@pyth_lazer); | ||
| let top_authority = account::create_account_for_test(TOP_AUTHORITY); | ||
| let treasury = account::create_account_for_test(TREASURY); | ||
| let user = account::create_account_for_test(USER); | ||
|
|
||
| // Setup AptosCoin and get mint capability | ||
| let mint_cap = setup_aptos_coin(&framework); | ||
|
|
||
| // Register accounts for AptosCoin | ||
| coin::register<AptosCoin>(&top_authority); | ||
| coin::register<AptosCoin>(&treasury); | ||
| coin::register<AptosCoin>(&user); | ||
|
|
||
| // Give user some coins for fees | ||
| let coins = coin::mint<AptosCoin>(1000, &mint_cap); | ||
| coin::deposit(signer::address_of(&user), coins); | ||
| coin::destroy_mint_cap(mint_cap); | ||
|
|
||
| // Initialize timestamp for expiration tests | ||
| timestamp::set_time_has_started_for_testing(&framework); | ||
|
|
||
| // Initialize contract | ||
| pyth_lazer::initialize(&lazer_contract, TOP_AUTHORITY, TREASURY); | ||
|
|
||
| (top_authority, treasury, user) | ||
| } | ||
|
|
||
| #[test] | ||
| fun test_initialize() { | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| let (_top_authority, _treasury, _) = setup(); | ||
| // Contract is already initialized in setup | ||
| } | ||
|
|
||
| #[test] | ||
| fun test_verify_message_success() { | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| let (top_authority, _treasury, user) = setup(); | ||
|
|
||
| // Add a valid signer | ||
| let expires_at = timestamp::now_seconds() + 1000; | ||
| pyth_lazer::update_trusted_signer(&top_authority, TEST_PUBKEY, expires_at); | ||
|
|
||
| // Create a valid ed25519 signature | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| let signature = ed25519::new_signature_from_bytes(TEST_SIGNATURE); | ||
| let pubkey = ed25519::new_unvalidated_public_key_from_bytes(TEST_PUBKEY); | ||
| assert!(ed25519::signature_verify_strict(&signature, &pubkey, TEST_MESSAGE), 0); | ||
|
|
||
| // This should succeed as we have a valid signer and sufficient fee | ||
| pyth_lazer::verify_message(&user, TEST_MESSAGE, TEST_SIGNATURE, TEST_PUBKEY); | ||
| } | ||
|
|
||
| #[test] | ||
| fun test_update_add_signer() { | ||
| let (top_authority, _treasury, _) = setup(); | ||
|
|
||
| // Add signer | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| let expires_at = timestamp::now_seconds() + 1000; | ||
| pyth_lazer::update_trusted_signer(&top_authority, TEST_PUBKEY, expires_at); | ||
|
|
||
| // Update signer | ||
| let new_expires_at = timestamp::now_seconds() + 2000; | ||
| pyth_lazer::update_trusted_signer(&top_authority, TEST_PUBKEY, new_expires_at); | ||
|
|
||
| // Remove signer | ||
| pyth_lazer::update_trusted_signer(&top_authority, TEST_PUBKEY, 0); | ||
| } | ||
|
|
||
| #[test] | ||
| #[expected_failure(abort_code = pyth_lazer::ENO_SPACE)] | ||
| fun test_max_signers() { | ||
| let (top_authority, _treasury, _) = setup(); | ||
|
|
||
| let expires_at = timestamp::now_seconds() + 1000; | ||
| let pubkey1 = x"1111111111111111111111111111111111111111111111111111111111111111"; | ||
| let pubkey2 = x"2222222222222222222222222222222222222222222222222222222222222222"; | ||
| let pubkey3 = x"3333333333333333333333333333333333333333333333333333333333333333"; | ||
|
|
||
| pyth_lazer::update_trusted_signer(&top_authority, pubkey1, expires_at); | ||
| pyth_lazer::update_trusted_signer(&top_authority, pubkey2, expires_at); | ||
| // This should fail as we already have 2 signers | ||
| pyth_lazer::update_trusted_signer(&top_authority, pubkey3, expires_at); | ||
| } | ||
|
|
||
| #[test] | ||
| #[expected_failure(abort_code = pyth_lazer::EINVALID_SIGNER)] | ||
| fun test_expired_signer() { | ||
| let (top_authority, _treasury, user) = setup(); | ||
|
|
||
| // Add signer that expires in 1000 seconds | ||
| let expires_at = timestamp::now_seconds() + 1000; | ||
| pyth_lazer::update_trusted_signer(&top_authority, TEST_PUBKEY, expires_at); | ||
|
|
||
| // Move time forward past expiration | ||
| timestamp::fast_forward_seconds(2000); | ||
|
|
||
| // This should fail as the signer is expired | ||
| pyth_lazer::verify_message(&user, TEST_MESSAGE, TEST_SIGNATURE, TEST_PUBKEY); | ||
| } | ||
|
|
||
| #[test] | ||
| #[expected_failure(abort_code = pyth_lazer::EINSUFFICIENT_FEE)] | ||
| fun test_insufficient_fee() { | ||
| let (_top_authority, _treasury, user) = setup(); | ||
|
|
||
| // Drain user's balance | ||
| let user_balance = coin::balance<AptosCoin>(signer::address_of(&user)); | ||
| coin::transfer<AptosCoin>(&user, TREASURY, user_balance); | ||
|
|
||
| // This should fail due to insufficient fee | ||
| pyth_lazer::verify_message(&user, TEST_MESSAGE, TEST_SIGNATURE, TEST_PUBKEY); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.