Skip to content

Commit 1bad16e

Browse files
authored
fix(target_chains/fuel): address audit (#1799)
* PN-02: Absolute difference function allows price updates from the future * PN-01: Guardians are assumed trusted and no input checks are done * refactor magic number
1 parent 63e528b commit 1bad16e

File tree

4 files changed

+92
-41
lines changed

4 files changed

+92
-41
lines changed

target_chains/fuel/contracts/pyth-contract/src/main.sw

Lines changed: 85 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ use std::{
1414
keccak256,
1515
sha256,
1616
},
17+
revert::revert,
1718
storage::{
1819
storage_map::StorageMap,
1920
storage_vec::*,
2021
},
21-
revert::revert,
2222
};
2323

2424
use pyth_interface::{
@@ -31,19 +31,33 @@ use pyth_interface::{
3131
update_type::UpdateType,
3232
wormhole_light::*,
3333
},
34-
errors::{PythError, WormholeError},
35-
events::{ConstructedEvent, NewGuardianSetEvent, UpdatedPriceFeedsEvent, ContractUpgradedEvent, GovernanceDataSourceSetEvent, DataSourcesSetEvent, FeeSetEvent, ValidPeriodSetEvent},
34+
errors::{
35+
PythError,
36+
WormholeError,
37+
},
38+
events::{
39+
ConstructedEvent,
40+
ContractUpgradedEvent,
41+
DataSourcesSetEvent,
42+
FeeSetEvent,
43+
GovernanceDataSourceSetEvent,
44+
NewGuardianSetEvent,
45+
UpdatedPriceFeedsEvent,
46+
ValidPeriodSetEvent,
47+
},
3648
pyth_merkle_proof::validate_proof,
37-
utils::{difference, total_fee},
3849
PythCore,
3950
PythInfo,
4051
PythInit,
52+
utils::total_fee,
4153
WormholeGuardians,
4254
};
4355

4456
use sway_libs::ownership::*;
4557
use standards::src5::{SRC5, State};
4658

59+
const GUARDIAN_SET_EXPIRATION_TIME_SECONDS: u64 = 86400; // 24 hours in seconds
60+
4761
configurable {
4862
DEPLOYER: Identity = Identity::Address(Address::from(ZERO_B256)),
4963
}
@@ -140,7 +154,7 @@ impl PythCore for Contract {
140154

141155
let mut output_price_feeds: Vec<PriceFeed> = Vec::with_capacity(target_price_feed_ids.len());
142156
let mut i = 0;
143-
while i < update_data.len() {
157+
while i < update_data.len() {
144158
let data = update_data.get(i).unwrap();
145159

146160
match UpdateType::determine_type(data) {
@@ -229,7 +243,7 @@ impl PythCore for Contract {
229243

230244
require(
231245
target_price_feed_ids
232-
.len() == output_price_feeds
246+
.len() == output_price_feeds
233247
.len(),
234248
PythError::PriceFeedNotFoundWithinRange,
235249
);
@@ -270,13 +284,13 @@ impl PythCore for Contract {
270284
) {
271285
require(
272286
price_feed_ids
273-
.len() == publish_times
287+
.len() == publish_times
274288
.len(),
275289
PythError::LengthOfPriceFeedIdsAndPublishTimesMustMatch,
276290
);
277291

278292
let mut i = 0;
279-
while i < price_feed_ids.len() {
293+
while i < price_feed_ids.len() {
280294
if latest_publish_time(price_feed_ids.get(i).unwrap()) < publish_times.get(i).unwrap()
281295
{
282296
update_price_feeds(update_data);
@@ -297,9 +311,9 @@ impl PythCore for Contract {
297311
#[storage(read)]
298312
fn ema_price_no_older_than(time_period: u64, price_feed_id: PriceFeedId) -> Price {
299313
let price = ema_price_unsafe(price_feed_id);
300-
314+
let current_time = timestamp();
301315
require(
302-
difference(timestamp(), price.publish_time) <= time_period,
316+
current_time - price.publish_time <= time_period,
303317
PythError::OutdatedPrice,
304318
);
305319

@@ -317,8 +331,9 @@ fn ema_price_unsafe(price_feed_id: PriceFeedId) -> Price {
317331
#[storage(read)]
318332
fn price_no_older_than(time_period: u64, price_feed_id: PriceFeedId) -> Price {
319333
let price = price_unsafe(price_feed_id);
334+
let current_time = timestamp();
320335
require(
321-
difference(timestamp(), price.publish_time) <= time_period,
336+
current_time - price.publish_time <= time_period,
322337
PythError::OutdatedPrice,
323338
);
324339

@@ -337,7 +352,7 @@ fn price_unsafe(price_feed_id: PriceFeedId) -> Price {
337352
fn update_fee(update_data: Vec<Bytes>) -> u64 {
338353
let mut total_number_of_updates = 0;
339354
let mut i = 0;
340-
while i < update_data.len() {
355+
while i < update_data.len() {
341356
let data = update_data.get(i).unwrap();
342357

343358
match UpdateType::determine_type(data) {
@@ -368,7 +383,7 @@ fn update_price_feeds(update_data: Vec<Bytes>) {
368383

369384
// let mut updated_price_feeds: Vec<PriceFeedId> = Vec::new(); // TODO: requires append for Vec
370385
let mut i = 0;
371-
while i < update_data.len() {
386+
while i < update_data.len() {
372387
let data = update_data.get(i).unwrap();
373388

374389
match UpdateType::determine_type(data) {
@@ -473,7 +488,7 @@ impl PythInit for Contract {
473488
// This function ensures that the sender is the owner. https://github.com/FuelLabs/sway-libs/blob/8045a19e3297599750abdf6300c11e9927a29d40/libs/src/ownership.sw#L59-L65
474489
only_owner();
475490

476-
require(data_sources.len() > 0, PythError::InvalidDataSourcesLength);
491+
require(data_sources.len() > 0, PythError::InvalidDataSourcesLength);
477492

478493
let mut i = 0;
479494
while i < data_sources.len() {
@@ -483,7 +498,9 @@ impl PythInit for Contract {
483498

484499
i += 1;
485500
}
486-
storage.latest_price_feed.write(StorageMap::<PriceFeedId, PriceFeed> {});
501+
storage
502+
.latest_price_feed
503+
.write(StorageMap::<PriceFeedId, PriceFeed> {});
487504

488505
storage
489506
.valid_time_period_seconds
@@ -493,7 +510,11 @@ impl PythInit for Contract {
493510
let guardian_length: u8 = wormhole_guardian_set_addresses.len().try_as_u8().unwrap();
494511
let mut new_guardian_set = StorageGuardianSet::new(
495512
0,
496-
StorageKey::<StorageVec<b256>>::new(sha256(("guardian_set_keys", wormhole_guardian_set_index)), 0, ZERO_B256),
513+
StorageKey::<StorageVec<b256>>::new(
514+
sha256(("guardian_set_keys", wormhole_guardian_set_index)),
515+
0,
516+
ZERO_B256,
517+
),
497518
);
498519
let mut i: u8 = 0;
499520
while i < guardian_length {
@@ -502,17 +523,27 @@ impl PythInit for Contract {
502523
i += 1;
503524
}
504525

505-
storage.wormhole_guardian_set_index.write(wormhole_guardian_set_index);
506-
storage.wormhole_guardian_sets.insert(wormhole_guardian_set_index, new_guardian_set);
526+
storage
527+
.wormhole_guardian_set_index
528+
.write(wormhole_guardian_set_index);
529+
storage
530+
.wormhole_guardian_sets
531+
.insert(wormhole_guardian_set_index, new_guardian_set);
507532

508533
storage.governance_data_source.write(governance_data_source);
509-
storage.wormhole_governance_data_source.write(wormhole_governance_data_source);
534+
storage
535+
.wormhole_governance_data_source
536+
.write(wormhole_governance_data_source);
510537
storage.governance_data_source_index.write(0);
511-
storage.wormhole_consumed_governance_actions.write(StorageMap::<b256, bool> {});
538+
storage
539+
.wormhole_consumed_governance_actions
540+
.write(StorageMap::<b256, bool> {});
512541
storage.chain_id.write(chain_id);
513542
storage.last_executed_governance_sequence.write(0);
514543

515-
storage.current_implementation.write(Identity::Address(Address::from(ZERO_B256)));
544+
storage
545+
.current_implementation
546+
.write(Identity::Address(Address::from(ZERO_B256)));
516547

517548
// This function revokes ownership of the current owner and disallows any new owners. https://github.com/FuelLabs/sway-libs/blob/8045a19e3297599750abdf6300c11e9927a29d40/libs/src/ownership.sw#L89-L99
518549
renounce_ownership();
@@ -670,7 +701,7 @@ fn submit_new_guardian_set(encoded_vm: Bytes) {
670701
let current_guardian_set = storage.wormhole_guardian_sets.get(current_guardian_set_index).try_read();
671702
if current_guardian_set.is_some() {
672703
let mut current_guardian_set = current_guardian_set.unwrap();
673-
current_guardian_set.expiration_time = timestamp() + 86400;
704+
current_guardian_set.expiration_time = timestamp() + GUARDIAN_SET_EXPIRATION_TIME_SECONDS;
674705
storage
675706
.wormhole_guardian_sets
676707
.insert(current_guardian_set_index, current_guardian_set);
@@ -691,27 +722,41 @@ fn submit_new_guardian_set(encoded_vm: Bytes) {
691722

692723
/// Transfer the governance data source to a new value with sanity checks to ensure the new governance data source can manage the contract.
693724
#[storage(read, write)]
694-
fn authorize_governance_data_source_transfer(payload: AuthorizeGovernanceDataSourceTransferPayload) {
725+
fn authorize_governance_data_source_transfer(
726+
payload: AuthorizeGovernanceDataSourceTransferPayload,
727+
) {
695728
let old_governance_data_source = governance_data_source();
696729

697730
// Parse and verify the VAA contained in the payload to ensure it's valid and can manage the contract
698731
let vm: WormholeVM = WormholeVM::parse_and_verify_wormhole_vm(
699732
current_guardian_set_index(),
700-
payload.claim_vaa,
701-
storage.wormhole_guardian_sets,
733+
payload
734+
.claim_vaa,
735+
storage
736+
.wormhole_guardian_sets,
702737
);
703738

704739
let gi = GovernanceInstruction::parse_governance_instruction(vm.payload);
705-
require(gi.target_chain_id == chain_id() || gi.target_chain_id == 0, PythError::InvalidGovernanceTarget);
740+
require(
741+
gi.target_chain_id == chain_id() || gi.target_chain_id == 0,
742+
PythError::InvalidGovernanceTarget,
743+
);
706744

707-
require(match gi.action {
708-
GovernanceAction::RequestGovernanceDataSourceTransfer => true,
709-
_ => false,
710-
}, PythError::InvalidGovernanceMessage);
745+
require(
746+
match gi.action {
747+
GovernanceAction::RequestGovernanceDataSourceTransfer => true,
748+
_ => false,
749+
},
750+
PythError::InvalidGovernanceMessage,
751+
);
711752

712753
let claim_payload = GovernanceInstruction::parse_request_governance_data_source_transfer_payload(gi.payload);
713754

714-
require(governance_data_source_index() < claim_payload.governance_data_source_index, PythError::OldGovernanceMessage);
755+
require(
756+
governance_data_source_index() < claim_payload
757+
.governance_data_source_index,
758+
PythError::OldGovernanceMessage,
759+
);
715760

716761
set_governance_data_source_index(claim_payload.governance_data_source_index);
717762

@@ -737,7 +782,7 @@ fn set_data_sources(payload: SetDataSourcesPayload) {
737782
let old_data_sources = storage.valid_data_sources.load_vec();
738783

739784
let mut i = 0;
740-
while i < old_data_sources.len() {
785+
while i < old_data_sources.len() {
741786
let data_source = old_data_sources.get(i).unwrap();
742787
storage.is_valid_data_source.insert(data_source, false);
743788
i += 1;
@@ -748,7 +793,7 @@ fn set_data_sources(payload: SetDataSourcesPayload) {
748793

749794
i = 0;
750795
// Add new data sources from the payload and mark them as valid
751-
while i < payload.data_sources.len() {
796+
while i < payload.data_sources.len() {
752797
let data_source = payload.data_sources.get(i).unwrap();
753798
storage.valid_data_sources.push(data_source);
754799
storage.is_valid_data_source.insert(data_source, true);
@@ -777,7 +822,9 @@ fn set_fee(payload: SetFeePayload) {
777822
#[storage(read, write)]
778823
fn set_valid_period(payload: SetValidPeriodPayload) {
779824
let old_valid_period = storage.valid_time_period_seconds.read();
780-
storage.valid_time_period_seconds.write(payload.new_valid_period);
825+
storage
826+
.valid_time_period_seconds
827+
.write(payload.new_valid_period);
781828

782829
log(ValidPeriodSetEvent {
783830
old_valid_period,
@@ -809,7 +856,10 @@ impl PythGovernance for Contract {
809856
// Log so that the GovernanceInstruction struct will show up in the ABI and can be used in the tests
810857
log(gi);
811858

812-
require(gi.target_chain_id == chain_id() || gi.target_chain_id == 0, PythError::InvalidGovernanceTarget);
859+
require(
860+
gi.target_chain_id == chain_id() || gi.target_chain_id == 0,
861+
PythError::InvalidGovernanceTarget,
862+
);
813863

814864
match gi.action {
815865
GovernanceAction::UpgradeContract => {
@@ -853,7 +903,6 @@ impl PythGovernance for Contract {
853903
}
854904
}
855905

856-
857906
#[storage(read, write)]
858907
fn verify_governance_vm(encoded_vm: Bytes) -> WormholeVM {
859908
let vm: WormholeVM = WormholeVM::parse_and_verify_wormhole_vm(

target_chains/fuel/contracts/pyth-interface/src/data_structures/price.sw

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
library;
22

3-
use std::bytes::Bytes;
3+
use std::{bytes::Bytes, block::timestamp};
44

55
use ::errors::PythError;
66
use ::utils::absolute_of_exponent;
@@ -175,6 +175,11 @@ impl PriceFeed {
175175
//convert publish_time from UNIX to TAI64
176176
publish_time += TAI64_DIFFERENCE;
177177

178+
require(
179+
publish_time <= timestamp(),
180+
PythError::FuturePriceNotAllowed,
181+
);
182+
178183
PriceFeed::new(
179184
Price::new(ema_confidence, exponent, ema_price, publish_time),
180185
price_feed_id,

target_chains/fuel/contracts/pyth-interface/src/errors.sw

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ library;
22

33
pub enum PythError {
44
FeesCanOnlyBePaidInTheBaseAsset: (),
5+
FuturePriceNotAllowed: (),
56
GuardianSetNotFound: (),
67
IncorrectMessageType: (),
78
InsufficientFee: (),

target_chains/fuel/contracts/pyth-interface/src/utils.sw

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
library;
22

3-
pub fn difference(x: u64, y: u64) -> u64 {
4-
if x > y { x - y } else { y - x }
5-
}
6-
73
pub fn absolute_of_exponent(exponent: u32) -> u32 {
84
if exponent == 0u32 {
95
exponent

0 commit comments

Comments
 (0)