Skip to content

Commit 762cfef

Browse files
committed
feat: init aptos contract
1 parent ee557d2 commit 762cfef

File tree

5 files changed

+323
-2
lines changed

5 files changed

+323
-2
lines changed

apps/hermes/server/Cargo.lock

Lines changed: 16 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lazer/contracts/aptos/Move.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "pyth_lazer"
3+
version = "0.1.0"
4+
license = "UNLICENSED"
5+
6+
[dependencies]
7+
AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework/", rev = "mainnet" }
8+
9+
[addresses]
10+
pyth_lazer = "0x123" # Using TOP_AUTHORITY address for testing
11+
12+
# For more details on Move.toml configuration, see:
13+
# https://move-language.github.io/move/packages.html

lazer/contracts/aptos/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
## Pyth Lazer Aptos Contract
2+
3+
This package is built using the Move language and Aptos framework.
4+
5+
`PythLazer` is an Aptos on-chain contract that allows consumers to easily verify Pyth Lazer updates for use on-chain.
6+
7+
### Build and Test
8+
9+
```shell
10+
$ aptos move compile
11+
$ aptos move test
12+
```
13+
14+
### Error Handling
15+
16+
The contract uses the following error codes:
17+
18+
- ENO_PERMISSIONS (1): Caller lacks required permissions
19+
- EINVALID_SIGNER (2): Invalid or expired signer
20+
- ENO_SPACE (3): Maximum number of signers reached
21+
- ENO_SUCH_PUBKEY (4): Attempting to remove non-existent signer
22+
- EINVALID_SIGNATURE (5): Invalid Ed25519 signature
23+
- EINSUFFICIENT_FEE (6): Insufficient fee provided
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
module pyth_lazer::pyth_lazer {
2+
use std::vector;
3+
use std::signer;
4+
use aptos_framework::timestamp;
5+
use aptos_framework::coin;
6+
use aptos_framework::aptos_coin::AptosCoin;
7+
use aptos_std::ed25519;
8+
9+
/// Error codes
10+
const ENO_PERMISSIONS: u64 = 1;
11+
const EINVALID_SIGNER: u64 = 2;
12+
const ENO_SPACE: u64 = 3;
13+
const ENO_SUCH_PUBKEY: u64 = 4;
14+
const EINVALID_SIGNATURE: u64 = 5;
15+
const EINSUFFICIENT_FEE: u64 = 6;
16+
17+
/// Constants
18+
const MAX_NUM_TRUSTED_SIGNERS: u8 = 5;
19+
const ED25519_PUBLIC_KEY_LENGTH: u64 = 32;
20+
21+
/// Stores information about a trusted signer including their public key and expiration
22+
struct TrustedSignerInfo has store, drop {
23+
pubkey: vector<u8>, // Ed25519 public key (32 bytes)
24+
expires_at: u64, // Unix timestamp
25+
}
26+
27+
/// Main storage for the Lazer contract
28+
struct Storage has key {
29+
top_authority: address,
30+
treasury: address,
31+
single_update_fee: u64, // Fee in APT token (1 wei)
32+
num_trusted_signers: u8,
33+
trusted_signers: vector<TrustedSignerInfo>,
34+
}
35+
36+
/// Events
37+
struct TrustedSignerUpdateEvent has drop, store {
38+
pubkey: vector<u8>,
39+
expires_at: u64,
40+
}
41+
42+
/// Initialize the Lazer contract with top authority and treasury
43+
public entry fun initialize(
44+
account: &signer,
45+
top_authority: address,
46+
treasury: address,
47+
) {
48+
let storage = Storage {
49+
top_authority,
50+
treasury,
51+
single_update_fee: 1, // 1 wei in Aptos native token
52+
num_trusted_signers: 0,
53+
trusted_signers: vector::empty(),
54+
};
55+
move_to(account, storage);
56+
}
57+
58+
/// Update a trusted signer's information or remove them
59+
public entry fun update_trusted_signer(
60+
account: &signer,
61+
trusted_signer: vector<u8>,
62+
expires_at: u64,
63+
) acquires Storage {
64+
let storage = borrow_global_mut<Storage>(@pyth_lazer);
65+
assert!(signer::address_of(account) == storage.top_authority, ENO_PERMISSIONS);
66+
assert!(vector::length(&trusted_signer) == ED25519_PUBLIC_KEY_LENGTH, EINVALID_SIGNER);
67+
68+
let num_signers = storage.num_trusted_signers;
69+
let i = 0;
70+
let found = false;
71+
72+
while (i < num_signers) {
73+
let signer_info = vector::borrow(&storage.trusted_signers, (i as u64));
74+
if (signer_info.pubkey == trusted_signer) {
75+
found = true;
76+
break
77+
};
78+
i = i + 1;
79+
};
80+
81+
if (expires_at == 0) {
82+
// Remove signer
83+
assert!(found, ENO_SUCH_PUBKEY);
84+
vector::remove(&mut storage.trusted_signers, (i as u64));
85+
storage.num_trusted_signers = storage.num_trusted_signers - 1;
86+
} else if (found) {
87+
// Update existing signer
88+
let signer_info = vector::borrow_mut(&mut storage.trusted_signers, (i as u64));
89+
signer_info.expires_at = expires_at;
90+
} else {
91+
// Add new signer
92+
assert!(storage.num_trusted_signers < MAX_NUM_TRUSTED_SIGNERS, ENO_SPACE);
93+
vector::push_back(&mut storage.trusted_signers, TrustedSignerInfo {
94+
pubkey: trusted_signer,
95+
expires_at,
96+
});
97+
storage.num_trusted_signers = storage.num_trusted_signers + 1;
98+
};
99+
}
100+
101+
/// Verify a message signature and collect fee
102+
public entry fun verify_message(
103+
account: &signer,
104+
message: vector<u8>,
105+
signature: vector<u8>,
106+
public_key: vector<u8>,
107+
) acquires Storage {
108+
let storage = borrow_global<Storage>(@pyth_lazer);
109+
110+
// Verify fee payment
111+
assert!(coin::balance<AptosCoin>(signer::address_of(account)) >= storage.single_update_fee, EINSUFFICIENT_FEE);
112+
coin::transfer<AptosCoin>(account, storage.treasury, storage.single_update_fee);
113+
114+
// Verify signer is trusted and not expired
115+
let i = 0;
116+
let valid = false;
117+
while (i < storage.num_trusted_signers) {
118+
let signer_info = vector::borrow(&storage.trusted_signers, (i as u64));
119+
if (signer_info.pubkey == public_key && signer_info.expires_at > timestamp::now_seconds()) {
120+
valid = true;
121+
break
122+
};
123+
i = i + 1;
124+
};
125+
assert!(valid, EINVALID_SIGNER);
126+
127+
// Verify signature
128+
let sig = ed25519::new_signature_from_bytes(signature);
129+
let pk = ed25519::new_unvalidated_public_key_from_bytes(public_key);
130+
assert!(ed25519::signature_verify_strict(&sig, &pk, message), EINVALID_SIGNATURE);
131+
let signer_info = vector::borrow(&storage.trusted_signers, (i as u64));
132+
if (signer_info.pubkey == public_key && signer_info.expires_at > timestamp::now_seconds()) {
133+
valid = true;
134+
break
135+
};
136+
i = i + 1;
137+
};
138+
assert!(valid, EINVALID_SIGNER);
139+
}
140+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#[test_only]
2+
module pyth_lazer::pyth_lazer_tests {
3+
use std::signer;
4+
use std::string;
5+
use aptos_framework::account;
6+
use aptos_framework::coin;
7+
use aptos_framework::timestamp;
8+
use aptos_framework::aptos_coin::AptosCoin;
9+
use aptos_std::ed25519;
10+
use pyth_lazer::pyth_lazer;
11+
12+
// Test accounts
13+
const TOP_AUTHORITY: address = @0x123;
14+
const TREASURY: address = @0x456;
15+
const USER: address = @0x789;
16+
17+
// Test data
18+
const TEST_PUBKEY: vector<u8> = x"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
19+
const TEST_MESSAGE: vector<u8> = x"deadbeef";
20+
const TEST_SIGNATURE: vector<u8> = x"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
21+
22+
#[test_only]
23+
fun setup_aptos_coin(framework: &signer): coin::MintCapability<AptosCoin> {
24+
let (burn_cap, freeze_cap, mint_cap) = coin::initialize<AptosCoin>(
25+
framework,
26+
std::string::utf8(b"Aptos Coin"),
27+
std::string::utf8(b"APT"),
28+
8,
29+
false,
30+
);
31+
coin::destroy_burn_cap(burn_cap);
32+
coin::destroy_freeze_cap(freeze_cap);
33+
mint_cap
34+
}
35+
36+
fun setup(): (signer, signer, signer) {
37+
// Create test accounts
38+
let framework = account::create_account_for_test(@aptos_framework);
39+
let top_authority = account::create_account_for_test(TOP_AUTHORITY);
40+
let treasury = account::create_account_for_test(TREASURY);
41+
let user = account::create_account_for_test(USER);
42+
43+
// Setup AptosCoin and get mint capability
44+
let mint_cap = setup_aptos_coin(&framework);
45+
46+
// Register accounts for AptosCoin
47+
coin::register<AptosCoin>(&top_authority);
48+
coin::register<AptosCoin>(&treasury);
49+
coin::register<AptosCoin>(&user);
50+
51+
// Give user some coins for fees
52+
let coins = coin::mint<AptosCoin>(1000, &mint_cap);
53+
coin::deposit(signer::address_of(&user), coins);
54+
coin::destroy_mint_cap(mint_cap);
55+
56+
// Initialize timestamp for expiration tests
57+
timestamp::set_time_has_started_for_testing(&framework);
58+
59+
// Initialize contract
60+
pyth_lazer::initialize(&top_authority, TOP_AUTHORITY, TREASURY);
61+
62+
(top_authority, treasury, user)
63+
}
64+
65+
#[test]
66+
fun test_initialize() {
67+
let (top_authority, _treasury, _) = setup();
68+
// Contract is already initialized in setup
69+
}
70+
71+
#[test]
72+
fun test_update_add_signer() {
73+
let (top_authority, _treasury, _) = setup();
74+
75+
// Add signer
76+
let expires_at = timestamp::now_seconds() + 1000;
77+
pyth_lazer::update_trusted_signer(&top_authority, TEST_PUBKEY, expires_at);
78+
79+
// Update signer
80+
let new_expires_at = timestamp::now_seconds() + 2000;
81+
pyth_lazer::update_trusted_signer(&top_authority, TEST_PUBKEY, new_expires_at);
82+
83+
// Remove signer
84+
pyth_lazer::update_trusted_signer(&top_authority, TEST_PUBKEY, 0);
85+
}
86+
87+
#[test]
88+
#[expected_failure(abort_code = pyth_lazer::ENO_SPACE)]
89+
fun test_max_signers() {
90+
let (top_authority, _treasury, _) = setup();
91+
92+
let expires_at = timestamp::now_seconds() + 1000;
93+
let pubkey1 = x"1111111111111111111111111111111111111111111111111111111111111111";
94+
let pubkey2 = x"2222222222222222222222222222222222222222222222222222222222222222";
95+
let pubkey3 = x"3333333333333333333333333333333333333333333333333333333333333333";
96+
97+
pyth_lazer::update_trusted_signer(&top_authority, pubkey1, expires_at);
98+
pyth_lazer::update_trusted_signer(&top_authority, pubkey2, expires_at);
99+
// This should fail as we already have 2 signers
100+
pyth_lazer::update_trusted_signer(&top_authority, pubkey3, expires_at);
101+
}
102+
103+
#[test]
104+
#[expected_failure(abort_code = pyth_lazer::EINVALID_SIGNER)]
105+
fun test_expired_signer() {
106+
let (top_authority, _treasury, user) = setup();
107+
108+
// Add signer that expires in 1000 seconds
109+
let expires_at = timestamp::now_seconds() + 1000;
110+
pyth_lazer::update_trusted_signer(&top_authority, TEST_PUBKEY, expires_at);
111+
112+
// Move time forward past expiration
113+
timestamp::fast_forward_seconds(2000);
114+
115+
// This should fail as the signer is expired
116+
pyth_lazer::verify_message(&user, TEST_MESSAGE, TEST_SIGNATURE, TEST_PUBKEY);
117+
}
118+
119+
#[test]
120+
#[expected_failure(abort_code = pyth_lazer::EINSUFFICIENT_FEE)]
121+
fun test_insufficient_fee() {
122+
let (top_authority, _treasury, user) = setup();
123+
124+
// Drain user's balance
125+
let user_balance = coin::balance<AptosCoin>(signer::address_of(&user));
126+
coin::transfer<AptosCoin>(&user, TREASURY, user_balance);
127+
128+
// This should fail due to insufficient fee
129+
pyth_lazer::verify_message(&user, TEST_MESSAGE, TEST_SIGNATURE, TEST_PUBKEY);
130+
}
131+
}

0 commit comments

Comments
 (0)