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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions apps/hermes/server/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions lazer/contracts/aptos/Move.toml
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
45 changes: 45 additions & 0 deletions lazer/contracts/aptos/README.md
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
$ brew install
$ 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

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
132 changes: 132 additions & 0 deletions lazer/contracts/aptos/sources/pyth_lazer.move
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;
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
}

/// Main storage for the Lazer contract
struct Storage has key {
top_authority: address,
treasury: address,
single_update_fee: u64,
num_trusted_signers: u8,
trusted_signers: vector<TrustedSignerInfo>,
}

/// Events
struct TrustedSignerUpdateEvent has drop, store {
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);
}

/// 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>,
) 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);
}
}
148 changes: 148 additions & 0 deletions lazer/contracts/aptos/tests/pyth_lazer_tests.move
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() {
let (_top_authority, _treasury, _) = setup();
// Contract is already initialized in setup
}

#[test]
fun test_verify_message_success() {
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
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
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);
}
}
Loading