Skip to content
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

feat(lazer): add Aptos implementation #2380

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions lazer/contracts/aptos/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "pyth_lazer"
version = "0.1.0"
license = "UNLICENSED"

[dependencies]
AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework/", rev = "mainnet" }

[addresses]
pyth_lazer = "0x123" # Using TOP_AUTHORITY address for testing

# For more details on Move.toml configuration, see:
# https://move-language.github.io/move/packages.html
34 changes: 34 additions & 0 deletions lazer/contracts/aptos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
## Pyth Lazer Aptos Contract

This package is built using the Move language and Aptos framework.

`PythLazer` is an Aptos on-chain contract that keeps track of trusted signers of Pyth Lazer payloads. It allows consumers to easily check validity of Pyth Lazer signatures while enabling key rotation.

### Key Features
- Ed25519 signature verification using Aptos standard library
- Support for up to 2 trusted signers
- Fee collection in Aptos native token
- Signer expiration management

### Build and Test

```shell
$ aptos move compile
$ aptos move test
```

### Implementation Details
- Uses Ed25519 signature verification from Aptos standard library
- Maintains compatibility with Solana/EVM implementations
- Follows Move best practices for resource management
- Collects 1 wei fee per verification in Aptos native token
- Supports maximum of 2 trusted signers (matching Solana implementation)

### 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
140 changes: 140 additions & 0 deletions lazer/contracts/aptos/sources/pyth_lazer.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
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, // Fee in Aptos native token (1 wei)
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, // 1 wei in Aptos native token
num_trusted_signers: 0,
trusted_signers: vector::empty(),
};
move_to(account, storage);
}

/// Update 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);
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);
}
}
131 changes: 131 additions & 0 deletions lazer/contracts/aptos/tests/pyth_lazer_tests.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#[test_only]
module pyth_lazer::pyth_lazer_tests {
use std::signer;
use std::string;
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 = @0x123;
const TREASURY: address = @0x456;
const USER: address = @0x789;

// Test data
const TEST_PUBKEY: vector<u8> = x"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
const TEST_MESSAGE: vector<u8> = x"deadbeef";
const TEST_SIGNATURE: vector<u8> = x"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";

#[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 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(&top_authority, 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_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