-
Notifications
You must be signed in to change notification settings - Fork 287
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
Changes from all commits
762cfef
4c3193f
01322a5
7c2283c
3078260
333cd24
69f2946
62d6905
f33ef01
6943287
d154842
d2b734a
ac5f922
60fc3d5
c9ec44f
d17ca91
1d9fad0
57059b7
f1ffb21
5259a52
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
on: | ||
pull_request: | ||
paths: | ||
- lazer/contracts/aptos/** | ||
push: | ||
branches: | ||
- main | ||
|
||
name: Lazer Aptos Contract | ||
|
||
jobs: | ||
aptos-tests: | ||
name: Aptos tests | ||
runs-on: ubuntu-latest | ||
defaults: | ||
run: | ||
working-directory: lazer/contracts/aptos/ | ||
steps: | ||
- uses: actions/checkout@v3 | ||
|
||
- name: Download CLI | ||
run: wget https://github.com/aptos-labs/aptos-core/releases/download/aptos-cli-v6.1.1/aptos-cli-6.1.1-Ubuntu-22.04-x86_64.zip | ||
|
||
- name: Unzip CLI | ||
run: unzip aptos-cli-6.1.1-Ubuntu-22.04-x86_64.zip | ||
|
||
- name: Run tests | ||
run: ./aptos move test |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,6 @@ on: | |
env: | ||
PYTHON_VERSION: "3.11" | ||
POETRY_VERSION: "1.4.2" | ||
|
||
jobs: | ||
pre-commit: | ||
runs-on: ubuntu-latest | ||
|
@@ -56,4 +55,13 @@ jobs: | |
with: | ||
path: ~/.cache/pypoetry | ||
key: poetry-cache-${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}-${{ env.POETRY_VERSION }} | ||
# Install Aptos CLI for Lazer contract formatting and linting | ||
- name: Download Aptos CLI | ||
run: wget https://github.com/aptos-labs/aptos-core/releases/download/aptos-cli-v6.1.1/aptos-cli-6.1.1-Ubuntu-22.04-x86_64.zip | ||
- name: Install Aptos CLI | ||
run: | | ||
unzip aptos-cli-6.1.1-Ubuntu-22.04-x86_64.zip | ||
sudo mv aptos /usr/local/bin/ | ||
chmod +x /usr/local/bin/aptos | ||
aptos update movefmt | ||
- uses: pre-commit/[email protected] |
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-framework.git", subdir = "aptos-framework", rev = "mainnet" } | ||
|
||
[addresses] | ||
pyth_lazer = "_" | ||
|
||
[dev-addresses] | ||
pyth_lazer = "0x8731685005cfb169b4da4bbfab0c91c5ba59508bbd6d26990ee2be7225cb34d1" |
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 | ||
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
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_SUCH_PUBKEY: u64 = 4; | ||
const EINVALID_SIGNATURE: u64 = 5; | ||
const EINSUFFICIENT_FEE: u64 = 6; | ||
|
||
/// Constants | ||
const ED25519_PUBLIC_KEY_LENGTH: u64 = 32; | ||
|
||
/// Admin capability - holder of this resource can perform admin actions, such as key rotations | ||
struct AdminCapability has key, store {} | ||
|
||
/// Stores the admin capability until it's claimed | ||
struct PendingAdminCapability has key, drop { | ||
admin: address | ||
} | ||
|
||
/// Stores information about a trusted signer including their public key and expiration | ||
struct TrustedSignerInfo has store, drop, copy { | ||
pubkey: vector<u8>, // Ed25519 public key (32 bytes) | ||
expires_at: u64 // Unix timestamp | ||
} | ||
|
||
/// Main storage for the Lazer contract | ||
struct Storage has key { | ||
treasury: address, | ||
single_update_fee: u64, | ||
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. One-time operation. | ||
public entry fun initialize( | ||
account: &signer, admin: address, treasury: address | ||
) { | ||
// Initialize must be called by the contract account | ||
assert!(signer::address_of(account) == @pyth_lazer, ENO_PERMISSIONS); | ||
let storage = Storage { | ||
treasury, | ||
single_update_fee: 1, // Nominal fee | ||
trusted_signers: vector::empty() | ||
}; | ||
|
||
// Store the pending admin capability | ||
move_to(account, PendingAdminCapability { admin }); | ||
|
||
// Can only be called once. If storage already exists in @pyth_lazer, | ||
// this operation will fail (one-time initialization). | ||
move_to(account, storage); | ||
tejasbadadare marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/// Allows the designated admin to claim their capability | ||
public entry fun claim_admin_capability(account: &signer) acquires PendingAdminCapability { | ||
let pending = borrow_global<PendingAdminCapability>(@pyth_lazer); | ||
assert!(signer::address_of(account) == pending.admin, ENO_PERMISSIONS); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need to invalidate the admin capability right? otherwise it seems we can mint infinite capabilities. more for my edification: could the @pyth get admincapability itself in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
multiple claim attempts by the But, on second thought, leaving it in global state is kinda confusing, and may invite people to try to use it. I'll destroy it after it's been claimed
and get rid of PendingAdminCapability? yeah i think that would work, but i was following the wormhole deployer's pattern of a pending capability. i'll dig into this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I looked it up and it seems that in Aptos a non-contract cannot pass it's owned objects to the functions and therefore i don't think they can transfer an object. I'm not sure but i saw this pattern a lot that there's an entrypoint for signer everywhere and then objects are collected (like what you do now) which partially defeats the purpose of this language construct 🤷 SUI handles it properly though. |
||
|
||
// Create and move the admin capability to the claiming account | ||
move_to(account, AdminCapability {}); | ||
|
||
// Clean up the pending admin capability | ||
let PendingAdminCapability { admin: _ } = | ||
move_from<PendingAdminCapability>(@pyth_lazer); | ||
} | ||
|
||
/// Verify a message signature and collect fee. | ||
/// | ||
/// This is a convenience wrapper around verify_message(), which allows you to verify an update | ||
/// using an entry function. If possible, it is recommended to use update_price_feeds() instead, | ||
/// which avoids the need to pass a signer account. update_price_feeds_with_funder() should only | ||
/// be used when you need to call an entry function. | ||
public entry fun verify_message_with_funder( | ||
account: &signer, | ||
message: vector<u8>, | ||
signature: vector<u8>, | ||
trusted_signer: 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 | ||
); | ||
let fee = coin::withdraw<AptosCoin>(account, storage.single_update_fee); | ||
verify_message(message, signature, trusted_signer, fee); | ||
} | ||
|
||
/// Verify a message signature with provided fee | ||
/// The provided `fee` must contain enough coins to pay a single update fee, which | ||
/// can be queried by calling calling get_update_fee(). | ||
public fun verify_message( | ||
message: vector<u8>, | ||
signature: vector<u8>, | ||
trusted_signer: vector<u8>, | ||
fee: coin::Coin<AptosCoin> | ||
) acquires Storage { | ||
let storage = borrow_global<Storage>(@pyth_lazer); | ||
|
||
// Verify fee amount | ||
assert!(coin::value(&fee) >= storage.single_update_fee, EINSUFFICIENT_FEE); | ||
|
||
// Transfer fee to treasury | ||
coin::deposit(storage.treasury, fee); | ||
|
||
// Verify signer is trusted and not expired | ||
let i = 0; | ||
let valid = false; | ||
while (i < storage.trusted_signers.length()) { | ||
let signer_info = vector::borrow(&storage.trusted_signers, (i as u64)); | ||
if (&signer_info.pubkey == &trusted_signer | ||
&& 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(trusted_signer); | ||
assert!( | ||
ed25519::signature_verify_strict(&sig, &pk, message), | ||
EINVALID_SIGNATURE | ||
); | ||
} | ||
|
||
/// 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 { | ||
// Verify admin capability | ||
assert!( | ||
exists<AdminCapability>(signer::address_of(account)), | ||
ENO_PERMISSIONS | ||
); | ||
|
||
assert!( | ||
vector::length(&trusted_signer) == ED25519_PUBLIC_KEY_LENGTH, | ||
EINVALID_SIGNER | ||
); | ||
|
||
let storage = borrow_global_mut<Storage>(@pyth_lazer); | ||
let num_signers = storage.trusted_signers.length(); | ||
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)); | ||
} 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 | ||
vector::push_back( | ||
&mut storage.trusted_signers, | ||
TrustedSignerInfo { pubkey: trusted_signer, expires_at } | ||
); | ||
}; | ||
} | ||
|
||
/// Returns the list of trusted signers | ||
public fun get_trusted_signers(): vector<TrustedSignerInfo> acquires Storage { | ||
let storage = borrow_global<Storage>(@pyth_lazer); | ||
storage.trusted_signers | ||
} | ||
|
||
/// Returns the fee required to verify a message | ||
public fun get_update_fee(): u64 acquires Storage { | ||
let storage = borrow_global<Storage>(@pyth_lazer); | ||
storage.single_update_fee | ||
} | ||
|
||
/// Signer pubkey getter | ||
public fun get_signer_pubkey(info: &TrustedSignerInfo): vector<u8> { | ||
info.pubkey | ||
} | ||
|
||
/// Signer expiry getter | ||
public fun get_signer_expires_at(info: &TrustedSignerInfo): u64 { | ||
info.expires_at | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.