Skip to content

Commit b609b17

Browse files
authored
[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
1 parent 079828f commit b609b17

16 files changed

+799
-103
lines changed

target_chains/sui/contracts/Move.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ version = "0.0.1"
55
[dependencies.Sui]
66
git = "https://github.com/MystenLabs/sui.git"
77
subdir = "crates/sui-framework/packages/sui-framework"
8-
rev = "ddfc3fa0768a38286787319603a5458a9ff91cc1"
8+
rev = "a63f425b9999c7fdfe483598720a9effc0acdc9e"
99

1010
[dependencies.Wormhole]
1111
git = "https://github.com/wormhole-foundation/wormhole.git"
1212
subdir = "sui/wormhole"
13-
rev = "sui/integration_v2"
13+
rev = "sui/integration_v2_stable"
1414

1515
[addresses]
1616
pyth = "0x250"

target_chains/sui/contracts/sources/batch_price_attestation.move

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -165,14 +165,11 @@ module pyth::batch_price_attestation {
165165
fun test_deserialize_batch_price_attestation_invalid_magic() {
166166
use sui::test_scenario::{Self, take_shared, return_shared, ctx};
167167
let test = test_scenario::begin(@0x1234);
168-
clock::create_for_testing(ctx(&mut test));
169-
test_scenario::next_tx(&mut test, @0x1234);
170-
let test_clock = take_shared<Clock>(&test);
171-
168+
let test_clock = clock::create_for_testing(ctx(&mut test));
172169
// A batch price attestation with a magic number of 0x50325749
173170
let bytes = x"5032574900030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001";
174171
let _ = destroy(deserialize(bytes, &test_clock));
175-
return_shared(test_clock);
172+
clock::destroy_for_testing(test_clock);
176173
test_scenario::end(test);
177174
}
178175

@@ -181,9 +178,8 @@ module pyth::batch_price_attestation {
181178
use sui::test_scenario::{Self, take_shared, return_shared, ctx};
182179
// Set the arrival time
183180
let test = test_scenario::begin(@0x1234);
184-
clock::create_for_testing(ctx(&mut test));
181+
let test_clock = clock::create_for_testing(ctx(&mut test));
185182
test_scenario::next_tx(&mut test, @0x1234);
186-
let test_clock = take_shared<Clock>(&test);
187183
let arrival_time_in_seconds = clock::timestamp_ms(&test_clock) / 1000;
188184

189185
// let arrival_time = tx_context::epoch(ctx(&mut test));
@@ -244,7 +240,7 @@ module pyth::batch_price_attestation {
244240
assert!(&expected == &deserialized, 1);
245241
destroy(expected);
246242
destroy(deserialized);
247-
return_shared(test_clock);
243+
clock::destroy_for_testing(test_clock);
248244
test_scenario::end(test);
249245
}
250246
}
Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,101 @@
1+
// SPDX-License-Identifier: Apache 2
2+
3+
/// Note: This module is based on the upgrade_contract module
4+
/// from the Sui Wormhole package:
5+
/// https://github.com/wormhole-foundation/wormhole/blob/sui/integration_v2/sui/wormhole/sources/governance/upgrade_contract.move
6+
7+
/// This module implements handling a governance VAA to enact upgrading the
8+
/// Pyth contract to a new build. The procedure to upgrade this contract
9+
/// requires a Programmable Transaction, which includes the following procedure:
10+
/// 1. Load new build.
11+
/// 2. Authorize upgrade.
12+
/// 3. Upgrade.
13+
/// 4. Commit upgrade.
114
module pyth::contract_upgrade {
2-
use pyth::state::{State};
15+
use sui::event::{Self};
16+
use sui::object::{Self, ID};
17+
use sui::package::{UpgradeReceipt, UpgradeTicket};
18+
19+
use pyth::state::{Self, State};
320

4-
use wormhole::state::{State as WormState};
21+
use wormhole::bytes32::{Self, Bytes32};
22+
use wormhole::cursor::{Self};
523

624
friend pyth::governance;
725

8-
/// Payload should be the bytes digest of the new contract.
9-
public(friend) fun execute(_worm_state: &WormState, _pyth_state: &State, _payload: vector<u8>){
10-
// TODO
26+
/// Digest is all zeros.
27+
const E_DIGEST_ZERO_BYTES: u64 = 0;
28+
/// Specific governance payload ID (action) to complete upgrading the
29+
/// contract.
30+
const ACTION_UPGRADE_CONTRACT: u8 = 1;
31+
32+
// Event reflecting package upgrade.
33+
struct ContractUpgraded has drop, copy {
34+
old_contract: ID,
35+
new_contract: ID
36+
}
37+
38+
struct UpgradeContract {
39+
digest: Bytes32
40+
}
41+
42+
/// Redeem governance VAA to issue an `UpgradeTicket` for the upgrade given
43+
/// a contract upgrade VAA. This governance message is only relevant for Sui
44+
/// because a contract upgrade is only relevant to one particular network
45+
/// (in this case Sui), whose build digest is encoded in this message.
46+
///
47+
/// NOTE: This method is guarded by a minimum build version check. This
48+
/// method could break backward compatibility on an upgrade.
49+
public(friend) fun execute(
50+
pyth_state: &mut State,
51+
payload: vector<u8>,
52+
): UpgradeTicket {
53+
// Proceed with processing new implementation version.
54+
handle_upgrade_contract(pyth_state, payload)
55+
}
56+
57+
fun handle_upgrade_contract(
58+
pyth_state: &mut State,
59+
payload: vector<u8>
60+
): UpgradeTicket {
61+
62+
let UpgradeContract { digest } = deserialize(payload);
63+
64+
state::authorize_upgrade(pyth_state, digest)
65+
}
66+
67+
/// Finalize the upgrade that ran to produce the given `receipt`. This
68+
/// method invokes `state::commit_upgrade` which interacts with
69+
/// `sui::package`.
70+
public fun commit_upgrade(
71+
self: &mut State,
72+
receipt: UpgradeReceipt,
73+
) {
74+
let latest_package_id = state::commit_upgrade(self, receipt);
75+
76+
// Emit an event reflecting package ID change.
77+
event::emit(
78+
ContractUpgraded {
79+
old_contract: object::id_from_address(@pyth),
80+
new_contract: latest_package_id
81+
}
82+
);
83+
}
84+
85+
fun deserialize(payload: vector<u8>): UpgradeContract {
86+
let cur = cursor::new(payload);
87+
88+
// This amount cannot be greater than max u64.
89+
let digest = bytes32::take_bytes(&mut cur);
90+
assert!(bytes32::is_nonzero(&digest), E_DIGEST_ZERO_BYTES);
91+
92+
cursor::destroy_empty(cur);
93+
94+
UpgradeContract { digest }
95+
}
96+
97+
#[test_only]
98+
public fun action(): u8 {
99+
ACTION_UPGRADE_CONTRACT
11100
}
12101
}

target_chains/sui/contracts/sources/governance/governance.move

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module pyth::governance {
22
use sui::clock::{Clock};
3+
use sui::package::{UpgradeTicket};
4+
use sui::tx_context::{TxContext};
35

46
use pyth::data_source::{Self};
57
use pyth::governance_instruction;
@@ -8,6 +10,7 @@ module pyth::governance {
810
use pyth::set_governance_data_source;
911
use pyth::set_data_sources;
1012
use pyth::set_stale_price_threshold;
13+
use pyth::transfer_fee;
1114
use pyth::state::{State};
1215
use pyth::set_update_fee;
1316
use pyth::state;
@@ -18,23 +21,47 @@ module pyth::governance {
1821
const E_GOVERNANCE_CONTRACT_UPGRADE_CHAIN_ID_ZERO: u64 = 0;
1922
const E_INVALID_GOVERNANCE_ACTION: u64 = 1;
2023
const E_INVALID_GOVERNANCE_DATA_SOURCE: u64 = 2;
21-
const E_INVALID_GOVERNANCE_SEQUENCE_NUMBER: u64 = 3;
24+
const E_MUST_USE_EXECUTE_CONTRACT_UPGRADE_GOVERNANCE_INSTRUCTION_CALLSITE: u64 = 3;
25+
const E_GOVERNANCE_ACTION_MUST_BE_CONTRACT_UPGRADE: u64 = 4;
2226

23-
public entry fun execute_governance_instruction(
27+
/// Rather than having execute_governance_instruction handle the contract
28+
/// upgrade governance instruction, we have a separate function that processes
29+
/// contract upgrade instructions, because doing contract upgrades is a
30+
/// multi-step process, and the first step of doing a contract upgrade
31+
/// yields a return value, namely the upgrade ticket, which is non-droppable.
32+
public fun execute_contract_upgrade_governance_instruction(
2433
pyth_state : &mut State,
2534
worm_state: &WormState,
2635
vaa_bytes: vector<u8>,
2736
clock: &Clock
37+
): UpgradeTicket {
38+
let parsed_vaa = parse_and_verify_and_replay_protect_governance_vaa(pyth_state, worm_state, vaa_bytes, clock);
39+
let instruction = governance_instruction::from_byte_vec(vaa::take_payload(parsed_vaa));
40+
let action = governance_instruction::get_action(&instruction);
41+
assert!(action == governance_action::new_contract_upgrade(),
42+
E_GOVERNANCE_ACTION_MUST_BE_CONTRACT_UPGRADE);
43+
assert!(governance_instruction::get_target_chain_id(&instruction) != 0,
44+
E_GOVERNANCE_CONTRACT_UPGRADE_CHAIN_ID_ZERO);
45+
contract_upgrade::execute(pyth_state, governance_instruction::destroy(instruction))
46+
}
47+
48+
/// Execute a governance instruction.
49+
public entry fun execute_governance_instruction(
50+
pyth_state : &mut State,
51+
worm_state: &WormState,
52+
vaa_bytes: vector<u8>,
53+
clock: &Clock,
54+
ctx: &mut TxContext
2855
) {
29-
let parsed_vaa = parse_and_verify_governance_vaa(pyth_state, worm_state, vaa_bytes, clock);
56+
let parsed_vaa = parse_and_verify_and_replay_protect_governance_vaa(pyth_state, worm_state, vaa_bytes, clock);
3057
let instruction = governance_instruction::from_byte_vec(vaa::take_payload(parsed_vaa));
3158

32-
// Dispatch the instruction to the appropiate handler
59+
// Get the governance action.
3360
let action = governance_instruction::get_action(&instruction);
61+
62+
// Dispatch the instruction to the appropiate handler.
3463
if (action == governance_action::new_contract_upgrade()) {
35-
assert!(governance_instruction::get_target_chain_id(&instruction) != 0,
36-
E_GOVERNANCE_CONTRACT_UPGRADE_CHAIN_ID_ZERO);
37-
contract_upgrade::execute(worm_state, pyth_state, governance_instruction::destroy(instruction));
64+
abort(E_MUST_USE_EXECUTE_CONTRACT_UPGRADE_GOVERNANCE_INSTRUCTION_CALLSITE)
3865
} else if (action == governance_action::new_set_governance_data_source()) {
3966
set_governance_data_source::execute(pyth_state, governance_instruction::destroy(instruction));
4067
} else if (action == governance_action::new_set_data_sources()) {
@@ -43,13 +70,15 @@ module pyth::governance {
4370
set_update_fee::execute(pyth_state, governance_instruction::destroy(instruction));
4471
} else if (action == governance_action::new_set_stale_price_threshold()) {
4572
set_stale_price_threshold::execute(pyth_state, governance_instruction::destroy(instruction));
73+
} else if (action == governance_action::new_transfer_fee()) {
74+
transfer_fee::execute(pyth_state, governance_instruction::destroy(instruction), ctx);
4675
} else {
4776
governance_instruction::destroy(instruction);
4877
assert!(false, E_INVALID_GOVERNANCE_ACTION);
4978
}
5079
}
5180

52-
fun parse_and_verify_governance_vaa(
81+
fun parse_and_verify_and_replay_protect_governance_vaa(
5382
pyth_state: &mut State,
5483
worm_state: &WormState,
5584
bytes: vector<u8>,
@@ -66,11 +95,8 @@ module pyth::governance {
6695
vaa::emitter_address(&parsed_vaa))),
6796
E_INVALID_GOVERNANCE_DATA_SOURCE);
6897

69-
// Check that the sequence number is greater than the last executed governance VAA
70-
let sequence = vaa::sequence(&parsed_vaa);
71-
assert!(sequence > state::get_last_executed_governance_sequence(pyth_state), E_INVALID_GOVERNANCE_SEQUENCE_NUMBER);
72-
state::set_last_executed_governance_sequence(pyth_state, sequence);
73-
98+
// Prevent replay attacks by consuming the VAA digest (adding it to a set)
99+
state::consume_vaa(pyth_state, vaa::digest(&parsed_vaa));
74100
parsed_vaa
75101
}
76102
}

target_chains/sui/contracts/sources/governance/governance_action.move

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module pyth::governance_action {
66
const SET_DATA_SOURCES: u8 = 2;
77
const SET_UPDATE_FEE: u8 = 3;
88
const SET_STALE_PRICE_THRESHOLD: u8 = 4;
9+
const TRANSFER_FEE: u8 = 5;
910

1011
const E_INVALID_GOVERNANCE_ACTION: u64 = 5;
1112

@@ -37,4 +38,8 @@ module pyth::governance_action {
3738
public fun new_set_stale_price_threshold(): GovernanceAction {
3839
GovernanceAction { value: SET_STALE_PRICE_THRESHOLD }
3940
}
41+
42+
public fun new_transfer_fee(): GovernanceAction {
43+
GovernanceAction { value: TRANSFER_FEE }
44+
}
4045
}

target_chains/sui/contracts/sources/governance/set_data_sources.move

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,22 @@ module pyth::set_data_sources {
88
use pyth::deserialize;
99
use pyth::data_source::{Self, DataSource};
1010
use pyth::state::{Self, State};
11+
use pyth::version_control::{SetDataSources};
1112

1213
friend pyth::governance;
1314

14-
struct SetDataSources {
15+
struct DataSources {
1516
sources: vector<DataSource>,
1617
}
1718

1819
public(friend) fun execute(state: &mut State, payload: vector<u8>) {
19-
let SetDataSources { sources } = from_byte_vec(payload);
20+
state::check_minimum_requirement<SetDataSources>(state);
21+
22+
let DataSources { sources } = from_byte_vec(payload);
2023
state::set_data_sources(state, sources);
2124
}
2225

23-
fun from_byte_vec(bytes: vector<u8>): SetDataSources {
26+
fun from_byte_vec(bytes: vector<u8>): DataSources {
2427
let cursor = cursor::new(bytes);
2528
let data_sources_count = deserialize::deserialize_u8(&mut cursor);
2629

@@ -37,7 +40,7 @@ module pyth::set_data_sources {
3740

3841
cursor::destroy_empty(cursor);
3942

40-
SetDataSources {
43+
DataSources {
4144
sources
4245
}
4346
}

target_chains/sui/contracts/sources/governance/set_governance_data_source.move

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,35 @@ module pyth::set_governance_data_source {
22
use pyth::deserialize;
33
use pyth::data_source;
44
use pyth::state::{Self, State};
5+
use pyth::version_control::SetGovernanceDataSource;
56

67
use wormhole::cursor;
78
use wormhole::external_address::{Self, ExternalAddress};
89
use wormhole::bytes32::{Self};
9-
//use wormhole::state::{Self}
1010

1111
friend pyth::governance;
1212

13-
struct SetGovernanceDataSource {
13+
struct GovernanceDataSource {
1414
emitter_chain_id: u64,
1515
emitter_address: ExternalAddress,
1616
initial_sequence: u64,
1717
}
1818

1919
public(friend) fun execute(pyth_state: &mut State, payload: vector<u8>) {
20-
let SetGovernanceDataSource { emitter_chain_id, emitter_address, initial_sequence } = from_byte_vec(payload);
20+
state::check_minimum_requirement<SetGovernanceDataSource>(pyth_state);
21+
22+
// TODO - What is GovernanceDataSource initial_sequence used for?
23+
let GovernanceDataSource { emitter_chain_id, emitter_address, initial_sequence: _initial_sequence } = from_byte_vec(payload);
2124
state::set_governance_data_source(pyth_state, data_source::new(emitter_chain_id, emitter_address));
22-
state::set_last_executed_governance_sequence(pyth_state, initial_sequence);
2325
}
2426

25-
fun from_byte_vec(bytes: vector<u8>): SetGovernanceDataSource {
27+
fun from_byte_vec(bytes: vector<u8>): GovernanceDataSource {
2628
let cursor = cursor::new(bytes);
2729
let emitter_chain_id = deserialize::deserialize_u16(&mut cursor);
2830
let emitter_address = external_address::new(bytes32::from_bytes(deserialize::deserialize_vector(&mut cursor, 32)));
2931
let initial_sequence = deserialize::deserialize_u64(&mut cursor);
3032
cursor::destroy_empty(cursor);
31-
SetGovernanceDataSource {
33+
GovernanceDataSource {
3234
emitter_chain_id: (emitter_chain_id as u64),
3335
emitter_address,
3436
initial_sequence

target_chains/sui/contracts/sources/governance/set_stale_price_threshold.move

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,26 @@ module pyth::set_stale_price_threshold {
22
use wormhole::cursor;
33
use pyth::deserialize;
44
use pyth::state::{Self, State};
5+
use pyth::version_control::SetStalePriceThreshold;
56

67
friend pyth::governance;
78

8-
struct SetStalePriceThreshold {
9+
struct StalePriceThreshold {
910
threshold: u64,
1011
}
1112

1213
public(friend) fun execute(state: &mut State, payload: vector<u8>) {
13-
let SetStalePriceThreshold { threshold } = from_byte_vec(payload);
14+
state::check_minimum_requirement<SetStalePriceThreshold>(state);
15+
16+
let StalePriceThreshold { threshold } = from_byte_vec(payload);
1417
state::set_stale_price_threshold_secs(state, threshold);
1518
}
1619

17-
fun from_byte_vec(bytes: vector<u8>): SetStalePriceThreshold {
20+
fun from_byte_vec(bytes: vector<u8>): StalePriceThreshold {
1821
let cursor = cursor::new(bytes);
1922
let threshold = deserialize::deserialize_u64(&mut cursor);
2023
cursor::destroy_empty(cursor);
21-
SetStalePriceThreshold {
24+
StalePriceThreshold {
2225
threshold
2326
}
2427
}

0 commit comments

Comments
 (0)