[sui 7/x] - contract upgrades, version control (#762)

* state getters and setters, change Move.toml dependency to sui/integration_v2

* finish state.move

* add new line to pyth

* use deployer cap pattern for state module

* sui pyth

* update price feeds, dynamic object fields, Sui object PriceInfoObject

* register price info object with pyth state after creation

* sui governance

* some newlines

* error codes

* update and comment

* unit tests for pyth.move, add UpgradeCap to Pyth State (will be used for contract upgrades)

* updates

* test_get_update_fee test passes

* fix test_get_update_fee and test_update_price_feeds_corrupt_vaa

* test_update_price_feeds_invalid_data_source

* test_create_and_update_price_feeds

* test_create_and_update_price_feeds_success and test_create_and_update_price_feeds_price_info_object_not_found_failure

* test_update_cache

* update

* test_update_cache_old_update

* update_price_feeds_if_fresh

* comment

* contract upgrades start

* contract upgradeability

* update clock stuff

* edits

* use clone of sui/integration_v2 for stability

* make contract_upgrade::execute a public(friend) fun, remove clock arg

* E_INCORRECT_IDENTIFIER_LENGTH

* comment and edit

* add a single comment
This commit is contained in:
optke3 2023-05-03 12:29:29 -04:00 committed by GitHub
parent 079828f8ac
commit b609b17fdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 800 additions and 104 deletions

View File

@ -5,12 +5,12 @@ version = "0.0.1"
[dependencies.Sui] [dependencies.Sui]
git = "https://github.com/MystenLabs/sui.git" git = "https://github.com/MystenLabs/sui.git"
subdir = "crates/sui-framework/packages/sui-framework" subdir = "crates/sui-framework/packages/sui-framework"
rev = "ddfc3fa0768a38286787319603a5458a9ff91cc1" rev = "a63f425b9999c7fdfe483598720a9effc0acdc9e"
[dependencies.Wormhole] [dependencies.Wormhole]
git = "https://github.com/wormhole-foundation/wormhole.git" git = "https://github.com/wormhole-foundation/wormhole.git"
subdir = "sui/wormhole" subdir = "sui/wormhole"
rev = "sui/integration_v2" rev = "sui/integration_v2_stable"
[addresses] [addresses]
pyth = "0x250" pyth = "0x250"

View File

@ -165,14 +165,11 @@ module pyth::batch_price_attestation {
fun test_deserialize_batch_price_attestation_invalid_magic() { fun test_deserialize_batch_price_attestation_invalid_magic() {
use sui::test_scenario::{Self, take_shared, return_shared, ctx}; use sui::test_scenario::{Self, take_shared, return_shared, ctx};
let test = test_scenario::begin(@0x1234); let test = test_scenario::begin(@0x1234);
clock::create_for_testing(ctx(&mut test)); let test_clock = clock::create_for_testing(ctx(&mut test));
test_scenario::next_tx(&mut test, @0x1234);
let test_clock = take_shared<Clock>(&test);
// A batch price attestation with a magic number of 0x50325749 // A batch price attestation with a magic number of 0x50325749
let bytes = x"5032574900030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001"; let bytes = x"5032574900030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001";
let _ = destroy(deserialize(bytes, &test_clock)); let _ = destroy(deserialize(bytes, &test_clock));
return_shared(test_clock); clock::destroy_for_testing(test_clock);
test_scenario::end(test); test_scenario::end(test);
} }
@ -181,9 +178,8 @@ module pyth::batch_price_attestation {
use sui::test_scenario::{Self, take_shared, return_shared, ctx}; use sui::test_scenario::{Self, take_shared, return_shared, ctx};
// Set the arrival time // Set the arrival time
let test = test_scenario::begin(@0x1234); let test = test_scenario::begin(@0x1234);
clock::create_for_testing(ctx(&mut test)); let test_clock = clock::create_for_testing(ctx(&mut test));
test_scenario::next_tx(&mut test, @0x1234); test_scenario::next_tx(&mut test, @0x1234);
let test_clock = take_shared<Clock>(&test);
let arrival_time_in_seconds = clock::timestamp_ms(&test_clock) / 1000; let arrival_time_in_seconds = clock::timestamp_ms(&test_clock) / 1000;
// let arrival_time = tx_context::epoch(ctx(&mut test)); // let arrival_time = tx_context::epoch(ctx(&mut test));
@ -244,7 +240,7 @@ module pyth::batch_price_attestation {
assert!(&expected == &deserialized, 1); assert!(&expected == &deserialized, 1);
destroy(expected); destroy(expected);
destroy(deserialized); destroy(deserialized);
return_shared(test_clock); clock::destroy_for_testing(test_clock);
test_scenario::end(test); test_scenario::end(test);
} }
} }

View File

@ -1,12 +1,101 @@
module pyth::contract_upgrade { // SPDX-License-Identifier: Apache 2
use pyth::state::{State};
use wormhole::state::{State as WormState}; /// Note: This module is based on the upgrade_contract module
/// from the Sui Wormhole package:
/// https://github.com/wormhole-foundation/wormhole/blob/sui/integration_v2/sui/wormhole/sources/governance/upgrade_contract.move
/// This module implements handling a governance VAA to enact upgrading the
/// Pyth contract to a new build. The procedure to upgrade this contract
/// requires a Programmable Transaction, which includes the following procedure:
/// 1. Load new build.
/// 2. Authorize upgrade.
/// 3. Upgrade.
/// 4. Commit upgrade.
module pyth::contract_upgrade {
use sui::event::{Self};
use sui::object::{Self, ID};
use sui::package::{UpgradeReceipt, UpgradeTicket};
use pyth::state::{Self, State};
use wormhole::bytes32::{Self, Bytes32};
use wormhole::cursor::{Self};
friend pyth::governance; friend pyth::governance;
/// Payload should be the bytes digest of the new contract. /// Digest is all zeros.
public(friend) fun execute(_worm_state: &WormState, _pyth_state: &State, _payload: vector<u8>){ const E_DIGEST_ZERO_BYTES: u64 = 0;
// TODO /// Specific governance payload ID (action) to complete upgrading the
/// contract.
const ACTION_UPGRADE_CONTRACT: u8 = 1;
// Event reflecting package upgrade.
struct ContractUpgraded has drop, copy {
old_contract: ID,
new_contract: ID
}
struct UpgradeContract {
digest: Bytes32
}
/// Redeem governance VAA to issue an `UpgradeTicket` for the upgrade given
/// a contract upgrade VAA. This governance message is only relevant for Sui
/// because a contract upgrade is only relevant to one particular network
/// (in this case Sui), whose build digest is encoded in this message.
///
/// NOTE: This method is guarded by a minimum build version check. This
/// method could break backward compatibility on an upgrade.
public(friend) fun execute(
pyth_state: &mut State,
payload: vector<u8>,
): UpgradeTicket {
// Proceed with processing new implementation version.
handle_upgrade_contract(pyth_state, payload)
}
fun handle_upgrade_contract(
pyth_state: &mut State,
payload: vector<u8>
): UpgradeTicket {
let UpgradeContract { digest } = deserialize(payload);
state::authorize_upgrade(pyth_state, digest)
}
/// Finalize the upgrade that ran to produce the given `receipt`. This
/// method invokes `state::commit_upgrade` which interacts with
/// `sui::package`.
public fun commit_upgrade(
self: &mut State,
receipt: UpgradeReceipt,
) {
let latest_package_id = state::commit_upgrade(self, receipt);
// Emit an event reflecting package ID change.
event::emit(
ContractUpgraded {
old_contract: object::id_from_address(@pyth),
new_contract: latest_package_id
}
);
}
fun deserialize(payload: vector<u8>): UpgradeContract {
let cur = cursor::new(payload);
// This amount cannot be greater than max u64.
let digest = bytes32::take_bytes(&mut cur);
assert!(bytes32::is_nonzero(&digest), E_DIGEST_ZERO_BYTES);
cursor::destroy_empty(cur);
UpgradeContract { digest }
}
#[test_only]
public fun action(): u8 {
ACTION_UPGRADE_CONTRACT
} }
} }

View File

@ -1,5 +1,7 @@
module pyth::governance { module pyth::governance {
use sui::clock::{Clock}; use sui::clock::{Clock};
use sui::package::{UpgradeTicket};
use sui::tx_context::{TxContext};
use pyth::data_source::{Self}; use pyth::data_source::{Self};
use pyth::governance_instruction; use pyth::governance_instruction;
@ -8,6 +10,7 @@ module pyth::governance {
use pyth::set_governance_data_source; use pyth::set_governance_data_source;
use pyth::set_data_sources; use pyth::set_data_sources;
use pyth::set_stale_price_threshold; use pyth::set_stale_price_threshold;
use pyth::transfer_fee;
use pyth::state::{State}; use pyth::state::{State};
use pyth::set_update_fee; use pyth::set_update_fee;
use pyth::state; use pyth::state;
@ -18,23 +21,47 @@ module pyth::governance {
const E_GOVERNANCE_CONTRACT_UPGRADE_CHAIN_ID_ZERO: u64 = 0; const E_GOVERNANCE_CONTRACT_UPGRADE_CHAIN_ID_ZERO: u64 = 0;
const E_INVALID_GOVERNANCE_ACTION: u64 = 1; const E_INVALID_GOVERNANCE_ACTION: u64 = 1;
const E_INVALID_GOVERNANCE_DATA_SOURCE: u64 = 2; const E_INVALID_GOVERNANCE_DATA_SOURCE: u64 = 2;
const E_INVALID_GOVERNANCE_SEQUENCE_NUMBER: u64 = 3; const E_MUST_USE_EXECUTE_CONTRACT_UPGRADE_GOVERNANCE_INSTRUCTION_CALLSITE: u64 = 3;
const E_GOVERNANCE_ACTION_MUST_BE_CONTRACT_UPGRADE: u64 = 4;
public entry fun execute_governance_instruction( /// Rather than having execute_governance_instruction handle the contract
/// upgrade governance instruction, we have a separate function that processes
/// contract upgrade instructions, because doing contract upgrades is a
/// multi-step process, and the first step of doing a contract upgrade
/// yields a return value, namely the upgrade ticket, which is non-droppable.
public fun execute_contract_upgrade_governance_instruction(
pyth_state : &mut State, pyth_state : &mut State,
worm_state: &WormState, worm_state: &WormState,
vaa_bytes: vector<u8>, vaa_bytes: vector<u8>,
clock: &Clock clock: &Clock
) { ): UpgradeTicket {
let parsed_vaa = parse_and_verify_governance_vaa(pyth_state, worm_state, vaa_bytes, clock); let parsed_vaa = parse_and_verify_and_replay_protect_governance_vaa(pyth_state, worm_state, vaa_bytes, clock);
let instruction = governance_instruction::from_byte_vec(vaa::take_payload(parsed_vaa)); let instruction = governance_instruction::from_byte_vec(vaa::take_payload(parsed_vaa));
// Dispatch the instruction to the appropiate handler
let action = governance_instruction::get_action(&instruction); let action = governance_instruction::get_action(&instruction);
if (action == governance_action::new_contract_upgrade()) { assert!(action == governance_action::new_contract_upgrade(),
E_GOVERNANCE_ACTION_MUST_BE_CONTRACT_UPGRADE);
assert!(governance_instruction::get_target_chain_id(&instruction) != 0, assert!(governance_instruction::get_target_chain_id(&instruction) != 0,
E_GOVERNANCE_CONTRACT_UPGRADE_CHAIN_ID_ZERO); E_GOVERNANCE_CONTRACT_UPGRADE_CHAIN_ID_ZERO);
contract_upgrade::execute(worm_state, pyth_state, governance_instruction::destroy(instruction)); contract_upgrade::execute(pyth_state, governance_instruction::destroy(instruction))
}
/// Execute a governance instruction.
public entry fun execute_governance_instruction(
pyth_state : &mut State,
worm_state: &WormState,
vaa_bytes: vector<u8>,
clock: &Clock,
ctx: &mut TxContext
) {
let parsed_vaa = parse_and_verify_and_replay_protect_governance_vaa(pyth_state, worm_state, vaa_bytes, clock);
let instruction = governance_instruction::from_byte_vec(vaa::take_payload(parsed_vaa));
// Get the governance action.
let action = governance_instruction::get_action(&instruction);
// Dispatch the instruction to the appropiate handler.
if (action == governance_action::new_contract_upgrade()) {
abort(E_MUST_USE_EXECUTE_CONTRACT_UPGRADE_GOVERNANCE_INSTRUCTION_CALLSITE)
} else if (action == governance_action::new_set_governance_data_source()) { } else if (action == governance_action::new_set_governance_data_source()) {
set_governance_data_source::execute(pyth_state, governance_instruction::destroy(instruction)); set_governance_data_source::execute(pyth_state, governance_instruction::destroy(instruction));
} else if (action == governance_action::new_set_data_sources()) { } else if (action == governance_action::new_set_data_sources()) {
@ -43,13 +70,15 @@ module pyth::governance {
set_update_fee::execute(pyth_state, governance_instruction::destroy(instruction)); set_update_fee::execute(pyth_state, governance_instruction::destroy(instruction));
} else if (action == governance_action::new_set_stale_price_threshold()) { } else if (action == governance_action::new_set_stale_price_threshold()) {
set_stale_price_threshold::execute(pyth_state, governance_instruction::destroy(instruction)); set_stale_price_threshold::execute(pyth_state, governance_instruction::destroy(instruction));
} else if (action == governance_action::new_transfer_fee()) {
transfer_fee::execute(pyth_state, governance_instruction::destroy(instruction), ctx);
} else { } else {
governance_instruction::destroy(instruction); governance_instruction::destroy(instruction);
assert!(false, E_INVALID_GOVERNANCE_ACTION); assert!(false, E_INVALID_GOVERNANCE_ACTION);
} }
} }
fun parse_and_verify_governance_vaa( fun parse_and_verify_and_replay_protect_governance_vaa(
pyth_state: &mut State, pyth_state: &mut State,
worm_state: &WormState, worm_state: &WormState,
bytes: vector<u8>, bytes: vector<u8>,
@ -66,11 +95,8 @@ module pyth::governance {
vaa::emitter_address(&parsed_vaa))), vaa::emitter_address(&parsed_vaa))),
E_INVALID_GOVERNANCE_DATA_SOURCE); E_INVALID_GOVERNANCE_DATA_SOURCE);
// Check that the sequence number is greater than the last executed governance VAA // Prevent replay attacks by consuming the VAA digest (adding it to a set)
let sequence = vaa::sequence(&parsed_vaa); state::consume_vaa(pyth_state, vaa::digest(&parsed_vaa));
assert!(sequence > state::get_last_executed_governance_sequence(pyth_state), E_INVALID_GOVERNANCE_SEQUENCE_NUMBER);
state::set_last_executed_governance_sequence(pyth_state, sequence);
parsed_vaa parsed_vaa
} }
} }

View File

@ -6,6 +6,7 @@ module pyth::governance_action {
const SET_DATA_SOURCES: u8 = 2; const SET_DATA_SOURCES: u8 = 2;
const SET_UPDATE_FEE: u8 = 3; const SET_UPDATE_FEE: u8 = 3;
const SET_STALE_PRICE_THRESHOLD: u8 = 4; const SET_STALE_PRICE_THRESHOLD: u8 = 4;
const TRANSFER_FEE: u8 = 5;
const E_INVALID_GOVERNANCE_ACTION: u64 = 5; const E_INVALID_GOVERNANCE_ACTION: u64 = 5;
@ -37,4 +38,8 @@ module pyth::governance_action {
public fun new_set_stale_price_threshold(): GovernanceAction { public fun new_set_stale_price_threshold(): GovernanceAction {
GovernanceAction { value: SET_STALE_PRICE_THRESHOLD } GovernanceAction { value: SET_STALE_PRICE_THRESHOLD }
} }
public fun new_transfer_fee(): GovernanceAction {
GovernanceAction { value: TRANSFER_FEE }
}
} }

View File

@ -8,19 +8,22 @@ module pyth::set_data_sources {
use pyth::deserialize; use pyth::deserialize;
use pyth::data_source::{Self, DataSource}; use pyth::data_source::{Self, DataSource};
use pyth::state::{Self, State}; use pyth::state::{Self, State};
use pyth::version_control::{SetDataSources};
friend pyth::governance; friend pyth::governance;
struct SetDataSources { struct DataSources {
sources: vector<DataSource>, sources: vector<DataSource>,
} }
public(friend) fun execute(state: &mut State, payload: vector<u8>) { public(friend) fun execute(state: &mut State, payload: vector<u8>) {
let SetDataSources { sources } = from_byte_vec(payload); state::check_minimum_requirement<SetDataSources>(state);
let DataSources { sources } = from_byte_vec(payload);
state::set_data_sources(state, sources); state::set_data_sources(state, sources);
} }
fun from_byte_vec(bytes: vector<u8>): SetDataSources { fun from_byte_vec(bytes: vector<u8>): DataSources {
let cursor = cursor::new(bytes); let cursor = cursor::new(bytes);
let data_sources_count = deserialize::deserialize_u8(&mut cursor); let data_sources_count = deserialize::deserialize_u8(&mut cursor);
@ -37,7 +40,7 @@ module pyth::set_data_sources {
cursor::destroy_empty(cursor); cursor::destroy_empty(cursor);
SetDataSources { DataSources {
sources sources
} }
} }

View File

@ -2,33 +2,35 @@ module pyth::set_governance_data_source {
use pyth::deserialize; use pyth::deserialize;
use pyth::data_source; use pyth::data_source;
use pyth::state::{Self, State}; use pyth::state::{Self, State};
use pyth::version_control::SetGovernanceDataSource;
use wormhole::cursor; use wormhole::cursor;
use wormhole::external_address::{Self, ExternalAddress}; use wormhole::external_address::{Self, ExternalAddress};
use wormhole::bytes32::{Self}; use wormhole::bytes32::{Self};
//use wormhole::state::{Self}
friend pyth::governance; friend pyth::governance;
struct SetGovernanceDataSource { struct GovernanceDataSource {
emitter_chain_id: u64, emitter_chain_id: u64,
emitter_address: ExternalAddress, emitter_address: ExternalAddress,
initial_sequence: u64, initial_sequence: u64,
} }
public(friend) fun execute(pyth_state: &mut State, payload: vector<u8>) { public(friend) fun execute(pyth_state: &mut State, payload: vector<u8>) {
let SetGovernanceDataSource { emitter_chain_id, emitter_address, initial_sequence } = from_byte_vec(payload); state::check_minimum_requirement<SetGovernanceDataSource>(pyth_state);
// TODO - What is GovernanceDataSource initial_sequence used for?
let GovernanceDataSource { emitter_chain_id, emitter_address, initial_sequence: _initial_sequence } = from_byte_vec(payload);
state::set_governance_data_source(pyth_state, data_source::new(emitter_chain_id, emitter_address)); state::set_governance_data_source(pyth_state, data_source::new(emitter_chain_id, emitter_address));
state::set_last_executed_governance_sequence(pyth_state, initial_sequence);
} }
fun from_byte_vec(bytes: vector<u8>): SetGovernanceDataSource { fun from_byte_vec(bytes: vector<u8>): GovernanceDataSource {
let cursor = cursor::new(bytes); let cursor = cursor::new(bytes);
let emitter_chain_id = deserialize::deserialize_u16(&mut cursor); let emitter_chain_id = deserialize::deserialize_u16(&mut cursor);
let emitter_address = external_address::new(bytes32::from_bytes(deserialize::deserialize_vector(&mut cursor, 32))); let emitter_address = external_address::new(bytes32::from_bytes(deserialize::deserialize_vector(&mut cursor, 32)));
let initial_sequence = deserialize::deserialize_u64(&mut cursor); let initial_sequence = deserialize::deserialize_u64(&mut cursor);
cursor::destroy_empty(cursor); cursor::destroy_empty(cursor);
SetGovernanceDataSource { GovernanceDataSource {
emitter_chain_id: (emitter_chain_id as u64), emitter_chain_id: (emitter_chain_id as u64),
emitter_address, emitter_address,
initial_sequence initial_sequence

View File

@ -2,23 +2,26 @@ module pyth::set_stale_price_threshold {
use wormhole::cursor; use wormhole::cursor;
use pyth::deserialize; use pyth::deserialize;
use pyth::state::{Self, State}; use pyth::state::{Self, State};
use pyth::version_control::SetStalePriceThreshold;
friend pyth::governance; friend pyth::governance;
struct SetStalePriceThreshold { struct StalePriceThreshold {
threshold: u64, threshold: u64,
} }
public(friend) fun execute(state: &mut State, payload: vector<u8>) { public(friend) fun execute(state: &mut State, payload: vector<u8>) {
let SetStalePriceThreshold { threshold } = from_byte_vec(payload); state::check_minimum_requirement<SetStalePriceThreshold>(state);
let StalePriceThreshold { threshold } = from_byte_vec(payload);
state::set_stale_price_threshold_secs(state, threshold); state::set_stale_price_threshold_secs(state, threshold);
} }
fun from_byte_vec(bytes: vector<u8>): SetStalePriceThreshold { fun from_byte_vec(bytes: vector<u8>): StalePriceThreshold {
let cursor = cursor::new(bytes); let cursor = cursor::new(bytes);
let threshold = deserialize::deserialize_u64(&mut cursor); let threshold = deserialize::deserialize_u64(&mut cursor);
cursor::destroy_empty(cursor); cursor::destroy_empty(cursor);
SetStalePriceThreshold { StalePriceThreshold {
threshold threshold
} }
} }

View File

@ -3,6 +3,7 @@ module pyth::set_update_fee {
use pyth::deserialize; use pyth::deserialize;
use pyth::state::{Self, State}; use pyth::state::{Self, State};
use pyth::version_control::SetUpdateFee;
use wormhole::cursor; use wormhole::cursor;
@ -12,24 +13,26 @@ module pyth::set_update_fee {
const MAX_U64: u128 = (1 << 64) - 1; const MAX_U64: u128 = (1 << 64) - 1;
const E_EXPONENT_DOES_NOT_FIT_IN_U8: u64 = 0; const E_EXPONENT_DOES_NOT_FIT_IN_U8: u64 = 0;
struct SetUpdateFee { struct UpdateFee {
mantissa: u64, mantissa: u64,
exponent: u64, exponent: u64,
} }
public(friend) fun execute(pyth_state: &mut State, payload: vector<u8>) { public(friend) fun execute(pyth_state: &mut State, payload: vector<u8>) {
let SetUpdateFee { mantissa, exponent } = from_byte_vec(payload); state::check_minimum_requirement<SetUpdateFee>(pyth_state);
let UpdateFee { mantissa, exponent } = from_byte_vec(payload);
assert!(exponent <= 255, E_EXPONENT_DOES_NOT_FIT_IN_U8); assert!(exponent <= 255, E_EXPONENT_DOES_NOT_FIT_IN_U8);
let fee = apply_exponent(mantissa, (exponent as u8)); let fee = apply_exponent(mantissa, (exponent as u8));
state::set_base_update_fee(pyth_state, fee); state::set_base_update_fee(pyth_state, fee);
} }
fun from_byte_vec(bytes: vector<u8>): SetUpdateFee { fun from_byte_vec(bytes: vector<u8>): UpdateFee {
let cursor = cursor::new(bytes); let cursor = cursor::new(bytes);
let mantissa = deserialize::deserialize_u64(&mut cursor); let mantissa = deserialize::deserialize_u64(&mut cursor);
let exponent = deserialize::deserialize_u64(&mut cursor); let exponent = deserialize::deserialize_u64(&mut cursor);
cursor::destroy_empty(cursor); cursor::destroy_empty(cursor);
SetUpdateFee { UpdateFee {
mantissa, mantissa,
exponent, exponent,
} }

View File

@ -0,0 +1,51 @@
module pyth::transfer_fee {
use sui::transfer::Self;
use sui::coin::Self;
use sui::tx_context::TxContext;
use wormhole::cursor;
use wormhole::external_address::{Self};
use wormhole::bytes32::{Self};
use pyth::state::{Self, State};
use pyth::version_control::{TransferFee};
friend pyth::governance;
struct PythFee {
amount: u64,
recipient: address
}
public(friend) fun execute(state: &mut State, payload: vector<u8>, ctx: &mut TxContext) {
state::check_minimum_requirement<TransferFee>(state);
let PythFee { amount, recipient } = from_byte_vec(payload);
transfer::public_transfer(
coin::from_balance(
state::withdraw_fee(state, amount),
ctx
),
recipient
);
}
fun from_byte_vec(payload: vector<u8>): PythFee {
let cur = cursor::new(payload);
// This amount cannot be greater than max u64.
let amount = bytes32::to_u64_be(bytes32::take_bytes(&mut cur));
// Recipient must be non-zero address.
let recipient = external_address::take_nonzero(&mut cur);
cursor::destroy_empty(cur);
PythFee {
amount: (amount as u64),
recipient: external_address::to_address(recipient)
}
}
}

View File

@ -0,0 +1,69 @@
// SPDX-License-Identifier: Apache 2
/// Note: This module is largely taken from the Sui Wormhole package:
/// https://github.com/wormhole-foundation/wormhole/blob/sui/integration_v2/sui/wormhole/sources/migrate.move
/// This module implements an entry method intended to be called after an
/// upgrade has been commited. The purpose is to add one-off migration logic
/// that would alter pyth `State`.
///
/// Included in migration is the ability to ensure that breaking changes for
/// any of pyth's methods by enforcing the current build version as their
/// required minimum version.
module pyth::migrate {
use pyth::state::{Self, State};
// This import is only used when `state::require_current_version` is used.
// use pyth::version_control::{Self as control};
/// Upgrade procedure is not complete (most likely due to an upgrade not
/// being initialized since upgrades can only be performed via programmable
/// transaction).
const E_CANNOT_MIGRATE: u64 = 0;
/// Execute migration logic. See `pyth::migrate` description for more
/// info.
public entry fun migrate(pyth_state: &mut State) {
// pyth `State` only allows one to call `migrate` after the upgrade
// procedure completed.
assert!(state::can_migrate(pyth_state), E_CANNOT_MIGRATE);
////////////////////////////////////////////////////////////////////////
//
// If there are any methods that require the current build, we need to
// explicity require them here.
//
// Calls to `require_current_version` are commented out for convenience.
//
////////////////////////////////////////////////////////////////////////
// state::require_current_version<control::SetDataSources>(pyth_state);
// state::require_current_version<control::SetGovernanceDataSource>(pyth_state);
// state::require_current_version<control::SetStalePriceThreshold>(pyth_state);
// state::require_current_version<control::SetUpdateFee>(pyth_state);
// state::require_current_version<control::TransferFee>(pyth_state);
// state::require_current_version<control::UpdatePriceFeeds>(pyth_state);
// state::require_current_version<control::CreatePriceFeeds>(pyth_state);
////////////////////////////////////////////////////////////////////////
//
// NOTE: Put any one-off migration logic here.
//
// Most upgrades likely won't need to do anything, in which case the
// rest of this function's body may be empty. Make sure to delete it
// after the migration has gone through successfully.
//
// WARNING: The migration does *not* proceed atomically with the
// upgrade (as they are done in separate transactions).
// If the nature of your migration absolutely requires the migration to
// happen before certain other functionality is available, then guard
// that functionality with the `assert!` from above.
//
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
// Ensure that `migrate` cannot be called again.
state::disable_migration(pyth_state);
}
}

View File

@ -1,16 +1,15 @@
module pyth::price_identifier { module pyth::price_identifier {
use std::vector; use std::vector;
//use pyth::error;
const IDENTIFIER_BYTES_LENGTH: u64 = 32; const IDENTIFIER_BYTES_LENGTH: u64 = 32;
const E_INCORRECT_IDENTIFIER_LENGTH: u64 = 0;
struct PriceIdentifier has copy, drop, store { struct PriceIdentifier has copy, drop, store {
bytes: vector<u8>, bytes: vector<u8>,
} }
public fun from_byte_vec(bytes: vector<u8>): PriceIdentifier { public fun from_byte_vec(bytes: vector<u8>): PriceIdentifier {
assert!(vector::length(&bytes) == IDENTIFIER_BYTES_LENGTH, 0); //error::incorrect_identifier_length() assert!(vector::length(&bytes) == IDENTIFIER_BYTES_LENGTH, E_INCORRECT_IDENTIFIER_LENGTH);
PriceIdentifier { PriceIdentifier {
bytes: bytes bytes: bytes
} }

View File

@ -15,6 +15,7 @@ module pyth::pyth {
use pyth::price_feed::{Self}; use pyth::price_feed::{Self};
use pyth::price::{Self, Price}; use pyth::price::{Self, Price};
use pyth::price_identifier::{PriceIdentifier}; use pyth::price_identifier::{PriceIdentifier};
use pyth::version_control::{UpdatePriceFeeds, CreatePriceFeeds};
use wormhole::external_address::{Self}; use wormhole::external_address::{Self};
use wormhole::vaa::{Self}; use wormhole::vaa::{Self};
@ -93,6 +94,8 @@ module pyth::pyth {
clock: &Clock, clock: &Clock,
ctx: &mut TxContext ctx: &mut TxContext
){ ){
// Version control.
state::check_minimum_requirement<CreatePriceFeeds>(pyth_state);
while (!vector::is_empty(&vaas)) { while (!vector::is_empty(&vaas)) {
let vaa = vector::pop_back(&mut vaas); let vaa = vector::pop_back(&mut vaas);
@ -137,8 +140,8 @@ module pyth::pyth {
}; };
} }
/// Update PriceInfo objects and corresponding price feeds with the /// Update Pyth Price Info objects (containing price feeds) with the
/// data in the given VAAs. /// price data in the given VAAs.
/// ///
/// The vaas argument is a vector of VAAs encoded as bytes. /// The vaas argument is a vector of VAAs encoded as bytes.
/// ///
@ -158,6 +161,8 @@ module pyth::pyth {
fee: Coin<SUI>, fee: Coin<SUI>,
clock: &Clock clock: &Clock
){ ){
// Version control.
state::check_minimum_requirement<UpdatePriceFeeds>(pyth_state);
// Charge the message update fee // Charge the message update fee
assert!(get_total_update_fee(pyth_state, &vaas) <= coin::value(&fee), E_INSUFFICIENT_FEE); assert!(get_total_update_fee(pyth_state, &vaas) <= coin::value(&fee), E_INSUFFICIENT_FEE);
@ -176,9 +181,11 @@ module pyth::pyth {
}; };
} }
/// Precondition: A Sui object of type PriceInfoObject must exist for each update /// Make sure that a Sui object of type PriceInfoObject exists for each update
/// encoded in the worm_vaa (batch_attestation_vaa). These should be passed in /// encoded in the worm_vaa (batch_attestation_vaa). These should be passed in
/// via the price_info_objects argument. /// via the price_info_objects argument. If for any price feed update, a
/// a PriceInfoObject with a matching price identifier is not found, the update_cache
/// function will revert, causing this function to revert.
fun update_price_feed_from_single_vaa( fun update_price_feed_from_single_vaa(
worm_state: &WormState, worm_state: &WormState,
pyth_state: &PythState, pyth_state: &PythState,
@ -216,10 +223,9 @@ module pyth::pyth {
let update = vector::pop_back(&mut updates); let update = vector::pop_back(&mut updates);
let i = 0; let i = 0;
let found = false; let found = false;
// Find PriceInfoObjects corresponding to the current update (PriceInfo). // Note - Would it be worth it to construct an in-memory hash-map to make look-ups faster?
// TODO - Construct an in-memory table to make look-ups faster? // This loop might be expensive if there are a large number of price_info_objects
// This loop might be expensive if there are a large number // passed in.
// of updates and/or price_info_objects we are updating.
while (i < vector::length<PriceInfoObject>(price_info_objects) && found == false){ while (i < vector::length<PriceInfoObject>(price_info_objects) && found == false){
// Check if the current price info object corresponds to the price feed that // Check if the current price info object corresponds to the price feed that
// the update is meant for. // the update is meant for.
@ -386,10 +392,10 @@ module pyth::pyth_tests{
use sui::sui::SUI; use sui::sui::SUI;
use sui::coin::{Self, Coin}; use sui::coin::{Self, Coin};
use sui::clock::{Self, Clock};
use sui::test_scenario::{Self, Scenario, ctx, take_shared, return_shared}; use sui::test_scenario::{Self, Scenario, ctx, take_shared, return_shared};
use sui::package::Self; use sui::package::Self;
use sui::object::{Self, ID}; use sui::object::{Self, ID};
use sui::clock::{Self, Clock};
use pyth::state::{Self, State as PythState}; use pyth::state::{Self, State as PythState};
use pyth::price_identifier::{Self}; use pyth::price_identifier::{Self};
@ -426,7 +432,7 @@ module pyth::pyth_tests{
data_sources: vector<DataSource>, data_sources: vector<DataSource>,
base_update_fee: u64, base_update_fee: u64,
to_mint: u64 to_mint: u64
): (Scenario, Coin<SUI>) { ): (Scenario, Coin<SUI>, Clock) {
let scenario = test_scenario::begin(DEPLOYER); let scenario = test_scenario::begin(DEPLOYER);
@ -470,9 +476,6 @@ module pyth::pyth_tests{
test_scenario::ctx(&mut scenario) test_scenario::ctx(&mut scenario)
); );
// Create and share a global clock object for timekeeping.
clock::create_for_testing(ctx(&mut scenario));
// Initialize Pyth state. // Initialize Pyth state.
let pyth_upgrade_cap= let pyth_upgrade_cap=
package::test_publish( package::test_publish(
@ -498,7 +501,8 @@ module pyth::pyth_tests{
); );
let coins = coin::mint_for_testing<SUI>(to_mint, ctx(&mut scenario)); let coins = coin::mint_for_testing<SUI>(to_mint, ctx(&mut scenario));
(scenario, coins) let clock = clock::create_for_testing(ctx(&mut scenario));
(scenario, coins, clock)
} }
#[test_only] #[test_only]
@ -571,7 +575,7 @@ module pyth::pyth_tests{
#[test] #[test]
fun test_get_update_fee() { fun test_get_update_fee() {
let single_update_fee = 50; let single_update_fee = 50;
let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", vector[], single_update_fee, 0); let (scenario, test_coins, _clock) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", vector[], single_update_fee, 0);
test_scenario::next_tx(&mut scenario, DEPLOYER, ); test_scenario::next_tx(&mut scenario, DEPLOYER, );
let pyth_state = take_shared<PythState>(&scenario); let pyth_state = take_shared<PythState>(&scenario);
// Pass in a single VAA // Pass in a single VAA
@ -590,17 +594,17 @@ module pyth::pyth_tests{
return_shared(pyth_state); return_shared(pyth_state);
coin::burn_for_testing<SUI>(test_coins); coin::burn_for_testing<SUI>(test_coins);
clock::destroy_for_testing(_clock);
test_scenario::end(scenario); test_scenario::end(scenario);
} }
#[test] #[test]
#[expected_failure(abort_code = wormhole::vaa::E_WRONG_VERSION)] #[expected_failure(abort_code = wormhole::vaa::E_WRONG_VERSION)]
fun test_create_price_feeds_corrupt_vaa() { fun test_create_price_feeds_corrupt_vaa() {
let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", vector[], 50, 0); let (scenario, test_coins, clock) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", vector[], 50, 0);
test_scenario::next_tx(&mut scenario, DEPLOYER); test_scenario::next_tx(&mut scenario, DEPLOYER);
let pyth_state = take_shared<PythState>(&scenario); let pyth_state = take_shared<PythState>(&scenario);
let worm_state = take_shared<WormState>(&scenario); let worm_state = take_shared<WormState>(&scenario);
let clock = take_shared<Clock>(&scenario);
// Pass in a corrupt VAA, which should fail deseriaizing // Pass in a corrupt VAA, which should fail deseriaizing
let corrupt_vaa = x"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"; let corrupt_vaa = x"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1";
@ -616,7 +620,7 @@ module pyth::pyth_tests{
return_shared(pyth_state); return_shared(pyth_state);
return_shared(worm_state); return_shared(worm_state);
return_shared(clock); clock::destroy_for_testing(clock);
coin::burn_for_testing<SUI>(test_coins); coin::burn_for_testing<SUI>(test_coins);
test_scenario::end(scenario); test_scenario::end(scenario);
} }
@ -634,12 +638,11 @@ module pyth::pyth_tests{
) )
]; ];
let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, 50, 0); let (scenario, test_coins, clock) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, 50, 0);
test_scenario::next_tx(&mut scenario, DEPLOYER); test_scenario::next_tx(&mut scenario, DEPLOYER);
let pyth_state = take_shared<PythState>(&scenario); let pyth_state = take_shared<PythState>(&scenario);
let worm_state = take_shared<WormState>(&scenario); let worm_state = take_shared<WormState>(&scenario);
let clock = take_shared<Clock>(&scenario);
pyth::create_price_feeds( pyth::create_price_feeds(
&mut worm_state, &mut worm_state,
@ -651,7 +654,7 @@ module pyth::pyth_tests{
return_shared(pyth_state); return_shared(pyth_state);
return_shared(worm_state); return_shared(worm_state);
return_shared(clock); clock::destroy_for_testing(clock);
coin::burn_for_testing<SUI>(test_coins); coin::burn_for_testing<SUI>(test_coins);
test_scenario::end(scenario); test_scenario::end(scenario);
} }
@ -675,12 +678,11 @@ module pyth::pyth_tests{
let base_update_fee = 50; let base_update_fee = 50;
let coins_to_mint = 5000; let coins_to_mint = 5000;
let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); let (scenario, test_coins, clock) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint);
test_scenario::next_tx(&mut scenario, DEPLOYER); test_scenario::next_tx(&mut scenario, DEPLOYER);
let pyth_state = take_shared<PythState>(&scenario); let pyth_state = take_shared<PythState>(&scenario);
let worm_state = take_shared<WormState>(&scenario); let worm_state = take_shared<WormState>(&scenario);
let clock = take_shared<Clock>(&scenario);
pyth::create_price_feeds( pyth::create_price_feeds(
&mut worm_state, &mut worm_state,
@ -730,7 +732,7 @@ module pyth::pyth_tests{
return_shared(price_info_object_3); return_shared(price_info_object_3);
return_shared(price_info_object_4); return_shared(price_info_object_4);
return_shared(clock); clock::destroy_for_testing(clock);
test_scenario::end(scenario); test_scenario::end(scenario);
} }
@ -741,12 +743,11 @@ module pyth::pyth_tests{
let base_update_fee = 50; let base_update_fee = 50;
let coins_to_mint = 5000; let coins_to_mint = 5000;
let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); let (scenario, test_coins, clock) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint);
test_scenario::next_tx(&mut scenario, DEPLOYER); test_scenario::next_tx(&mut scenario, DEPLOYER);
let pyth_state = take_shared<PythState>(&scenario); let pyth_state = take_shared<PythState>(&scenario);
let worm_state = take_shared<WormState>(&scenario); let worm_state = take_shared<WormState>(&scenario);
let clock = take_shared<Clock>(&scenario);
pyth::create_price_feeds( pyth::create_price_feeds(
&mut worm_state, &mut worm_state,
@ -805,12 +806,11 @@ module pyth::pyth_tests{
let base_update_fee = 50; let base_update_fee = 50;
let coins_to_mint = 5; let coins_to_mint = 5;
let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); let (scenario, test_coins, clock) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint);
test_scenario::next_tx(&mut scenario, DEPLOYER); test_scenario::next_tx(&mut scenario, DEPLOYER);
let pyth_state = take_shared<PythState>(&scenario); let pyth_state = take_shared<PythState>(&scenario);
let worm_state = take_shared<WormState>(&scenario); let worm_state = take_shared<WormState>(&scenario);
let clock = take_shared<Clock>(&scenario);
pyth::create_price_feeds( pyth::create_price_feeds(
&mut worm_state, &mut worm_state,
@ -849,13 +849,13 @@ module pyth::pyth_tests{
let base_update_fee = 50; let base_update_fee = 50;
let coins_to_mint = 5000; let coins_to_mint = 5000;
let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); let (scenario, test_coins, clock) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint);
test_scenario::next_tx(&mut scenario, DEPLOYER); test_scenario::next_tx(&mut scenario, DEPLOYER);
let pyth_state = take_shared<PythState>(&scenario); let pyth_state = take_shared<PythState>(&scenario);
let worm_state = take_shared<WormState>(&scenario); let worm_state = take_shared<WormState>(&scenario);
let clock = take_shared<Clock>(&scenario);
// Update cache is called by create_price_feeds.
pyth::create_price_feeds( pyth::create_price_feeds(
&mut worm_state, &mut worm_state,
&mut pyth_state, &mut pyth_state,
@ -898,7 +898,7 @@ module pyth::pyth_tests{
return_shared(price_info_object_4); return_shared(price_info_object_4);
coin::burn_for_testing<SUI>(test_coins); coin::burn_for_testing<SUI>(test_coins);
return_shared(clock); clock::destroy_for_testing(clock);
test_scenario::end(scenario); test_scenario::end(scenario);
} }
@ -908,12 +908,11 @@ module pyth::pyth_tests{
let base_update_fee = 50; let base_update_fee = 50;
let coins_to_mint = 5000; let coins_to_mint = 5000;
let (scenario, test_coins) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint); let (scenario, test_coins, clock) = setup_test(500, 23, x"5d1f252d5de865279b00c84bce362774c2804294ed53299bc4a0389a5defef92", data_sources, base_update_fee, coins_to_mint);
test_scenario::next_tx(&mut scenario, DEPLOYER); test_scenario::next_tx(&mut scenario, DEPLOYER);
let pyth_state = take_shared<PythState>(&scenario); let pyth_state = take_shared<PythState>(&scenario);
let worm_state = take_shared<WormState>(&scenario); let worm_state = take_shared<WormState>(&scenario);
let clock = take_shared<Clock>(&scenario);
pyth::create_price_feeds( pyth::create_price_feeds(
&mut worm_state, &mut worm_state,
@ -965,7 +964,6 @@ module pyth::pyth_tests{
vector::destroy_empty(price_info_object_vec); vector::destroy_empty(price_info_object_vec);
// Confirm that the current price and ema price didn't change
let current_price_info = price_info::get_price_info_from_price_info_object(&price_info_object_1); let current_price_info = price_info::get_price_info_from_price_info_object(&price_info_object_1);
let current_price_feed = price_info::get_price_feed(&current_price_info); let current_price_feed = price_info::get_price_feed(&current_price_info);
let current_price = price_feed::get_price(current_price_feed); let current_price = price_feed::get_price(current_price_feed);
@ -1007,7 +1005,7 @@ module pyth::pyth_tests{
vector::destroy_empty(price_info_object_vec); vector::destroy_empty(price_info_object_vec);
// Confirm that the Pyth cached price got updated to fresh_price // Confirm that the Pyth cached price got updated to fresh_price.
let current_price_info = price_info::get_price_info_from_price_info_object(&price_info_object_1); let current_price_info = price_info::get_price_info_from_price_info_object(&price_info_object_1);
let current_price_feed = price_info::get_price_feed(&current_price_info); let current_price_feed = price_info::get_price_feed(&current_price_info);
let current_price = price_feed::get_price(current_price_feed); let current_price = price_feed::get_price(current_price_feed);
@ -1024,7 +1022,7 @@ module pyth::pyth_tests{
return_shared(price_info_object_4); return_shared(price_info_object_4);
coin::burn_for_testing<SUI>(test_coins); coin::burn_for_testing<SUI>(test_coins);
return_shared(clock); clock::destroy_for_testing(clock);
test_scenario::end(scenario); test_scenario::end(scenario);
} }
} }

View File

@ -0,0 +1,241 @@
// SPDX-License-Identifier: Apache 2
/// Note: This module is based on the required_version module
/// from the Sui Wormhole package:
/// https://github.com/wormhole-foundation/wormhole/blob/sui/integration_v2/sui/wormhole/sources/resources/required_version.move
/// This module implements a mechanism for version control. While keeping track
/// of the latest version of a package build, `RequiredVersion` manages the
/// minimum required version number for any method in that package. For any
/// upgrade where a particular method can have backward compatibility, the
/// minimum version would not have to change (because the method should work the
/// same way with the previous version or current version).
///
/// If there happens to be a breaking change for a particular method, this
/// module can force that the method's minimum requirement be the latest build.
/// If a previous build were used, the method would abort if a check is in place
/// with `RequiredVersion`.
///
/// There is no magic behind the way ths module works. `RequiredVersion` is
/// intended to live in a package's shared object that gets passed into its
/// methods (e.g. pyth's `State` object).
module pyth::required_version {
use sui::dynamic_field::{Self as field};
use sui::object::{Self, UID};
use sui::package::{Self, UpgradeCap};
use sui::tx_context::{TxContext};
/// Build version passed does not meet method's minimum required version.
const E_OUTDATED_VERSION: u64 = 0;
/// Container to keep track of latest build version. Dynamic fields are
/// associated with its `id`.
struct RequiredVersion has store {
id: UID,
latest_version: u64
}
struct Key<phantom MethodType> has store, drop, copy {}
/// Create new `RequiredVersion` with a configured starting version.
public fun new(version: u64, ctx: &mut TxContext): RequiredVersion {
RequiredVersion {
id: object::new(ctx),
latest_version: version
}
}
/// Retrieve latest build version.
public fun current(self: &RequiredVersion): u64 {
self.latest_version
}
/// Add specific method handling via custom `MethodType`. At the time a
/// method is added, the minimum build version associated with this method
/// by default is the latest version.
public fun add<MethodType>(self: &mut RequiredVersion) {
field::add(&mut self.id, Key<MethodType> {}, self.latest_version)
}
/// This method will abort if the version for a particular `MethodType` is
/// not up-to-date with the version of the current build.
///
/// For example, if the minimum requirement for `foobar` module (with an
/// appropriately named `MethodType` like `FooBar`) is `1` and the current
/// implementation version is `2`, this method will succeed because the
/// build meets the minimum required version of `1` in order for `foobar` to
/// work. So if someone were to use an older build like version `1`, this
/// method will succeed.
///
/// But if `check_minimum_requirement` were invoked for `foobar` when the
/// minimum requirement is `2` and the current build is only version `1`,
/// then this method will abort because the build does not meet the minimum
/// version requirement for `foobar`.
///
/// This method also assumes that the `MethodType` being checked for is
/// already a dynamic field (using `add`) during initialization.
public fun check_minimum_requirement<MethodType>(
self: &RequiredVersion,
build_version: u64
) {
assert!(
build_version >= minimum_for<MethodType>(self),
E_OUTDATED_VERSION
);
}
/// At `commit_upgrade`, use this method to update the tracker's knowledge
/// of the latest upgrade (build) version, which is obtained from the
/// `UpgradeCap` in `sui::package`.
public fun update_latest(
self: &mut RequiredVersion,
upgrade_cap: &UpgradeCap
) {
self.latest_version = package::version(upgrade_cap);
}
/// Once the global version is updated via `commit_upgrade` and there is a
/// particular method that has a breaking change, use this method to uptick
/// that method's minimum required version to the latest.
public fun require_current_version<MethodType>(self: &mut RequiredVersion) {
let min_version = field::borrow_mut(&mut self.id, Key<MethodType> {});
*min_version = self.latest_version;
}
/// Retrieve the minimum required version for a particular method (via
/// `MethodType`).
public fun minimum_for<MethodType>(self: &RequiredVersion): u64 {
*field::borrow(&self.id, Key<MethodType> {})
}
#[test_only]
public fun set_required_version<MethodType>(
self: &mut RequiredVersion,
version: u64
) {
*field::borrow_mut(&mut self.id, Key<MethodType> {}) = version;
}
#[test_only]
public fun destroy(req: RequiredVersion) {
let RequiredVersion { id, latest_version: _} = req;
object::delete(id);
}
}
#[test_only]
module pyth::required_version_test {
use sui::hash::{keccak256};
use sui::object::{Self};
use sui::package::{Self};
use sui::tx_context::{Self};
use pyth::required_version::{Self};
struct SomeMethod {}
struct AnotherMethod {}
#[test]
public fun test_check_minimum_requirement() {
let ctx = &mut tx_context::dummy();
let version = 1;
let req = required_version::new(version, ctx);
assert!(required_version::current(&req) == version, 0);
required_version::add<SomeMethod>(&mut req);
assert!(required_version::minimum_for<SomeMethod>(&req) == version, 0);
// Should not abort here.
required_version::check_minimum_requirement<SomeMethod>(&req, version);
// And should not abort if the version is anything greater than the
// current.
let new_version = version + 1;
required_version::check_minimum_requirement<SomeMethod>(
&req,
new_version
);
// Uptick based on new upgrade.
let upgrade_cap = package::test_publish(
object::id_from_address(@pyth),
ctx
);
let digest = keccak256(&x"DEADBEEF");
let policy = package::upgrade_policy(&upgrade_cap);
let upgrade_ticket =
package::authorize_upgrade(&mut upgrade_cap, policy, digest);
let upgrade_receipt = package::test_upgrade(upgrade_ticket);
package::commit_upgrade(&mut upgrade_cap, upgrade_receipt);
assert!(package::version(&upgrade_cap) == new_version, 0);
// Update to the latest version.
required_version::update_latest(&mut req, &upgrade_cap);
assert!(required_version::current(&req) == new_version, 0);
// Should still not abort here.
required_version::check_minimum_requirement<SomeMethod>(
&req,
new_version
);
// Require new version for `SomeMethod` and show that
// `check_minimum_requirement` still succeeds.
required_version::require_current_version<SomeMethod>(&mut req);
assert!(
required_version::minimum_for<SomeMethod>(&req) == new_version,
0
);
required_version::check_minimum_requirement<SomeMethod>(
&req,
new_version
);
// If another method gets added to the mix, it should automatically meet
// the minimum requirement because its version will be the latest.
required_version::add<AnotherMethod>(&mut req);
assert!(
required_version::minimum_for<AnotherMethod>(&req) == new_version,
0
);
required_version::check_minimum_requirement<SomeMethod>(
&req,
new_version
);
// Clean up.
package::make_immutable(upgrade_cap);
required_version::destroy(req);
}
#[test]
#[expected_failure(abort_code = required_version::E_OUTDATED_VERSION)]
public fun test_cannot_check_minimum_requirement_with_outdated_version() {
let ctx = &mut tx_context::dummy();
let version = 1;
let req = required_version::new(version, ctx);
assert!(required_version::current(&req) == version, 0);
required_version::add<SomeMethod>(&mut req);
// Should not abort here.
required_version::check_minimum_requirement<SomeMethod>(&req, version);
// Uptick minimum requirement and fail at `check_minimum_requirement`.
let new_version = 10;
required_version::set_required_version<SomeMethod>(
&mut req,
new_version
);
let old_version = new_version - 1;
required_version::check_minimum_requirement<SomeMethod>(
&req,
old_version
);
// Clean up.
required_version::destroy(req);
}
}

View File

@ -3,11 +3,21 @@ module pyth::state {
use sui::object::{Self, UID, ID}; use sui::object::{Self, UID, ID};
use sui::transfer::{Self}; use sui::transfer::{Self};
use sui::tx_context::{Self, TxContext}; use sui::tx_context::{Self, TxContext};
use sui::package::{Self, UpgradeCap}; use sui::dynamic_field::{Self as field};
use sui::package::{Self, UpgradeCap, UpgradeReceipt, UpgradeTicket};
use sui::balance::{Balance};
use sui::sui::SUI;
use pyth::data_source::{Self, DataSource}; use pyth::data_source::{Self, DataSource};
use pyth::price_info::{Self}; use pyth::price_info::{Self};
use pyth::price_identifier::{PriceIdentifier}; use pyth::price_identifier::{PriceIdentifier};
use pyth::required_version::{Self, RequiredVersion};
use pyth::version_control::{Self as control};
use wormhole::setup::{assert_package_upgrade_cap};
use wormhole::consumed_vaas::{Self, ConsumedVAAs};
use wormhole::bytes32::{Self, Bytes32};
use wormhole::fee_collector::{Self, FeeCollector};
use wormhole::setup::{assert_package_upgrade_cap}; use wormhole::setup::{assert_package_upgrade_cap};
@ -19,6 +29,13 @@ module pyth::state {
friend pyth::set_data_sources; friend pyth::set_data_sources;
friend pyth::governance; friend pyth::governance;
friend pyth::set_governance_data_source; friend pyth::set_governance_data_source;
friend pyth::migrate;
friend pyth::contract_upgrade;
friend pyth::transfer_fee;
const E_BUILD_VERSION_MISMATCH: u64 = 0;
const E_INVALID_BUILD_VERSION: u64 = 1;
const E_VAA_ALREADY_CONSUMED: u64 = 2;
/// Capability for creating a bridge state object, granted to sender when this /// Capability for creating a bridge state object, granted to sender when this
/// module is deployed /// module is deployed
@ -26,13 +43,27 @@ module pyth::state {
id: UID id: UID
} }
/// Used as key for dynamic field reflecting whether `migrate` can be
/// called.
///
/// See `migrate` module for more info.
struct MigrationControl has store, drop, copy {}
struct State has key { struct State has key {
id: UID, id: UID,
governance_data_source: DataSource, governance_data_source: DataSource,
last_executed_governance_sequence: u64,
stale_price_threshold: u64, stale_price_threshold: u64,
base_update_fee: u64, base_update_fee: u64,
upgrade_cap: UpgradeCap consumed_vaas: ConsumedVAAs,
// Upgrade capability.
upgrade_cap: UpgradeCap,
// Fee collector.
fee_collector: FeeCollector,
/// Contract build version tracker.
required_version: RequiredVersion
} }
fun init(ctx: &mut TxContext) { fun init(ctx: &mut TxContext) {
@ -56,7 +87,7 @@ module pyth::state {
); );
} }
// Initialization /// Initialization
public(friend) fun init_and_share_state( public(friend) fun init_and_share_state(
deployer: DeployerCap, deployer: DeployerCap,
upgrade_cap: UpgradeCap, upgrade_cap: UpgradeCap,
@ -66,9 +97,9 @@ module pyth::state {
sources: vector<DataSource>, sources: vector<DataSource>,
ctx: &mut TxContext ctx: &mut TxContext
) { ) {
// TODO - version control // Only init and share state once (in the initial deployment).
// let version = wormhole::version_control::version(); let version = wormhole::version_control::version();
//assert!(version == 1, E_INVALID_BUILD_VERSION); assert!(version == 1, E_INVALID_BUILD_VERSION);
let DeployerCap { id } = deployer; let DeployerCap { id } = deployer;
object::delete(id); object::delete(id);
@ -81,6 +112,8 @@ module pyth::state {
let uid = object::new(ctx); let uid = object::new(ctx);
field::add(&mut uid, MigrationControl {}, false);
// Create a set that contains all registered data sources and // Create a set that contains all registered data sources and
// attach it to uid as a dynamic field to minimize the // attach it to uid as a dynamic field to minimize the
// size of State. // size of State.
@ -97,19 +130,131 @@ module pyth::state {
data_source::add(&mut uid, vector::pop_back(&mut sources)); data_source::add(&mut uid, vector::pop_back(&mut sources));
}; };
let consumed_vaas = consumed_vaas::new(ctx);
let required_version = required_version::new(control::version(), ctx);
required_version::add<control::SetDataSources>(&mut required_version);
required_version::add<control::SetGovernanceDataSource>(&mut required_version);
required_version::add<control::SetStalePriceThreshold>(&mut required_version);
required_version::add<control::SetUpdateFee>(&mut required_version);
required_version::add<control::TransferFee>(&mut required_version);
required_version::add<control::UpdatePriceFeeds>(&mut required_version);
required_version::add<control::CreatePriceFeeds>(&mut required_version);
// Share state so that is a shared Sui object. // Share state so that is a shared Sui object.
transfer::share_object( transfer::share_object(
State { State {
id: uid, id: uid,
upgrade_cap, upgrade_cap,
governance_data_source, governance_data_source,
last_executed_governance_sequence: 0,
stale_price_threshold, stale_price_threshold,
base_update_fee, base_update_fee,
consumed_vaas,
fee_collector: fee_collector::new(base_update_fee),
required_version
} }
); );
} }
/// Retrieve current build version of latest upgrade.
public fun current_version(self: &State): u64 {
required_version::current(&self.required_version)
}
/// Issue an `UpgradeTicket` for the upgrade.
public(friend) fun authorize_upgrade(
self: &mut State,
implementation_digest: Bytes32
): UpgradeTicket {
let policy = package::upgrade_policy(&self.upgrade_cap);
// TODO: grab package ID from `UpgradeCap` and store it
// in a dynamic field. This will be the old ID after the upgrade.
// Both IDs will be emitted in a `ContractUpgraded` event.
package::authorize_upgrade(
&mut self.upgrade_cap,
policy,
bytes32::to_bytes(implementation_digest),
)
}
/// Finalize the upgrade that ran to produce the given `receipt`.
public(friend) fun commit_upgrade(
self: &mut State,
receipt: UpgradeReceipt
): ID {
// Uptick the upgrade cap version number using this receipt.
package::commit_upgrade(&mut self.upgrade_cap, receipt);
// Check that the upticked hard-coded version version agrees with the
// upticked version number.
assert!(
package::version(&self.upgrade_cap) == control::version() + 1,
E_BUILD_VERSION_MISMATCH
);
// Update global version.
required_version::update_latest(
&mut self.required_version,
&self.upgrade_cap
);
// Enable `migrate` to be called after commiting the upgrade.
//
// A separate method is required because `state` is a dependency of
// `migrate`. This method warehouses state modifications required
// for the new implementation plus enabling any methods required to be
// gated by the current implementation version. In most cases `migrate`
// is a no-op. But it still must be called in order to reset the
// migration control to `false`.
//
// See `migrate` module for more info.
enable_migration(self);
package::upgrade_package(&self.upgrade_cap)
}
/// Enforce a particular method to use the current build version as its
/// minimum required version. This method ensures that a method is not
/// backwards compatible with older builds.
public(friend) fun require_current_version<ControlType>(self: &mut State) {
required_version::require_current_version<ControlType>(
&mut self.required_version,
)
}
/// Check whether a particular method meets the minimum build version for
/// the latest Wormhole implementation.
public(friend) fun check_minimum_requirement<ControlType>(self: &State) {
required_version::check_minimum_requirement<ControlType>(
&self.required_version,
control::version()
)
}
// Upgrade and migration-related functionality
/// Check whether `migrate` can be called.
///
/// See `wormhole::migrate` module for more info.
public fun can_migrate(self: &State): bool {
*field::borrow(&self.id, MigrationControl {})
}
/// Allow `migrate` to be called after upgrade.
///
/// See `wormhole::migrate` module for more info.
public(friend) fun enable_migration(self: &mut State) {
*field::borrow_mut(&mut self.id, MigrationControl {}) = true;
}
/// Disallow `migrate` to be called.
///
/// See `wormhole::migrate` module for more info.
public(friend) fun disable_migration(self: &mut State) {
*field::borrow_mut(&mut self.id, MigrationControl {}) = false;
}
// Accessors // Accessors
public fun get_stale_price_threshold_secs(s: &State): u64 { public fun get_stale_price_threshold_secs(s: &State): u64 {
s.stale_price_threshold s.stale_price_threshold
@ -127,15 +272,35 @@ module pyth::state {
s.governance_data_source == source s.governance_data_source == source
} }
public fun get_last_executed_governance_sequence(s: &State): u64 {
s.last_executed_governance_sequence
}
public fun price_feed_object_exists(s: &State, p: PriceIdentifier): bool { public fun price_feed_object_exists(s: &State, p: PriceIdentifier): bool {
price_info::contains(&s.id, p) price_info::contains(&s.id, p)
} }
// Setters // Mutators and Setters
/// Withdraw collected fees when governance action to transfer fees to a
/// particular recipient.
///
/// See `pyth::transfer_fee` for more info.
public(friend) fun withdraw_fee(
self: &mut State,
amount: u64
): Balance<SUI> {
fee_collector::withdraw_balance(&mut self.fee_collector, amount)
}
public(friend) fun deposit_fee(self: &mut State, fee: Balance<SUI>) {
fee_collector::deposit_balance(&mut self.fee_collector, fee);
}
public(friend) fun set_fee_collector_fee(self: &mut State, amount: u64) {
fee_collector::change_fee(&mut self.fee_collector, amount);
}
public(friend) fun consume_vaa(state: &mut State, vaa_digest: Bytes32){
consumed_vaas::consume(&mut state.consumed_vaas, vaa_digest);
}
public(friend) fun set_data_sources(s: &mut State, new_sources: vector<DataSource>) { public(friend) fun set_data_sources(s: &mut State, new_sources: vector<DataSource>) {
// Empty the existing table of data sources registered in state. // Empty the existing table of data sources registered in state.
data_source::empty(&mut s.id); data_source::empty(&mut s.id);
@ -149,10 +314,6 @@ module pyth::state {
price_info::add(&mut s.id, price_identifier, id); price_info::add(&mut s.id, price_identifier, id);
} }
public(friend) fun set_last_executed_governance_sequence(s: &mut State, sequence: u64) {
s.last_executed_governance_sequence = sequence;
}
public(friend) fun set_governance_data_source(s: &mut State, source: DataSource) { public(friend) fun set_governance_data_source(s: &mut State, source: DataSource) {
s. governance_data_source = source; s. governance_data_source = source;
} }

View File

@ -0,0 +1,50 @@
// SPDX-License-Identifier: Apache 2
/// Note: This module is based on the version_control module in
/// the Sui Wormhole package:
/// https://github.com/wormhole-foundation/wormhole/blob/sui/integration_v2/sui/wormhole/sources/version_control.move
/// This module implements dynamic field keys as empty structs. These keys with
/// `RequiredVersion` are used to determine minimum build requirements for
/// particular Pyth methods and breaking backward compatibility for these
/// methods if an upgrade requires the latest upgrade version for its
/// functionality.
///
/// See `pyth::required_version` and `pyth::state` for more info.
module pyth::version_control {
/// This value tracks the current Pyth contract version. We are
/// placing this constant value at the top, which goes against Move style
/// guides so that we bring special attention to changing this value when
/// a new implementation is built for a contract upgrade.
const CURRENT_BUILD_VERSION: u64 = 1;
/// Key used to check minimum version requirement for `set_data_sources`
struct SetDataSources {}
/// Key used to check minimum version requirement for `set_governance_data_source`
struct SetGovernanceDataSource {}
/// Key used to check minimum version requirement for `set_stale_price_threshold`
struct SetStalePriceThreshold {}
/// Key used to check minimum version requirement for `set_update_fee`
struct SetUpdateFee {}
/// Key used to check minimum version requirement for `transfer_fee`
struct TransferFee {}
/// Key used to check minimum version requirement for `update_price_feeds`
struct UpdatePriceFeeds {}
/// Key used to check minimum version requirement for `create_price_feeds`
struct CreatePriceFeeds {}
//=======================================================================
/// Return const value `CURRENT_BUILD_VERSION` for this particular build.
/// This value is used to determine whether this implementation meets
/// minimum requirements for various Pyth methods required by `State`.
public fun version(): u64 {
CURRENT_BUILD_VERSION
}
}