Skip to content

Commit 85dba51

Browse files
authored
feat(target_chains/starknet): support two fee tokens (#1644)
* feat(target_chains/starknet): support two fee tokens * fix(target_chains/starknet): use second fee if first fee balance is insufficient * refactor(target_chains/starknet): rename fee_token_address1 and single_update_fee1
1 parent 2f18d96 commit 85dba51

File tree

5 files changed

+318
-65
lines changed

5 files changed

+318
-65
lines changed

target_chains/starknet/contracts/deploy/local_deploy

+2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ pyth_address=$(starkli deploy "${pyth_hash}" \
6565
"${wormhole_address}" \
6666
"${fee_token_address}" \
6767
1000 0 `# fee amount` \
68+
"${fee_token_address}" `# there is only one pre-deployed token on katana so we use it again` \
69+
1000 0 `# fee amount` \
6870
1 `# num_data_sources` \
6971
26 `# emitter_chain_id` \
7072
58051393581875729396504816158954007153 299086580781278228892874716333010393740 `# emitter_address` \

target_chains/starknet/contracts/src/pyth.cairo

+80-31
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub use pyth::{
1111
};
1212
pub use errors::{
1313
GetPriceUnsafeError, GovernanceActionError, UpdatePriceFeedsError, GetPriceNoOlderThanError,
14-
UpdatePriceFeedsIfNecessaryError, ParsePriceFeedsError,
14+
UpdatePriceFeedsIfNecessaryError, ParsePriceFeedsError, GetSingleUpdateFeeError,
1515
};
1616
pub use interface::{
1717
IPyth, IPythDispatcher, IPythDispatcherTrait, DataSource, Price, PriceFeedPublishTime, PriceFeed
@@ -36,7 +36,7 @@ mod pyth {
3636
use super::{
3737
DataSource, UpdatePriceFeedsError, GovernanceActionError, Price, GetPriceUnsafeError,
3838
IPythDispatcher, IPythDispatcherTrait, PriceFeedPublishTime, GetPriceNoOlderThanError,
39-
UpdatePriceFeedsIfNecessaryError, PriceFeed, ParsePriceFeedsError,
39+
UpdatePriceFeedsIfNecessaryError, PriceFeed, ParsePriceFeedsError, GetSingleUpdateFeeError,
4040
};
4141
use super::governance;
4242
use super::governance::GovernancePayload;
@@ -97,8 +97,10 @@ mod pyth {
9797
#[storage]
9898
struct Storage {
9999
wormhole_address: ContractAddress,
100-
fee_token_address: ContractAddress,
101-
single_update_fee: u256,
100+
fee_token_address1: ContractAddress,
101+
fee_token_address2: ContractAddress,
102+
single_update_fee1: u256,
103+
single_update_fee2: u256,
102104
data_sources: LegacyMap<usize, DataSource>,
103105
num_data_sources: usize,
104106
// For fast validation.
@@ -115,29 +117,36 @@ mod pyth {
115117
///
116118
/// `wormhole_address` is the address of the deployed Wormhole contract implemented in the `wormhole` module.
117119
///
118-
/// `fee_token_address` is the address of the ERC20 token used to pay fees to Pyth
120+
/// `fee_token_address1` is the address of the ERC20 token used to pay fees to Pyth
119121
/// for price updates. There is no native token on Starknet so an ERC20 contract has to be used.
120122
/// On Katana, an ETH fee contract is pre-deployed. On Starknet testnet, ETH and STRK fee tokens are
121123
/// available. Any other ERC20-compatible token can also be used.
122124
/// In a Starknet Forge testing environment, a fee contract must be deployed manually.
123125
///
124-
/// `single_update_fee` is the number of tokens of `fee_token_address` charged for a single price update.
126+
/// `single_update_fee1` is the number of tokens of `fee_token_address1` charged for a single price update.
127+
///
128+
/// `fee_token_address2` and `single_update_fee2` specify the secondary fee contract and fee rate
129+
/// that can be used instead of the main fee token.
125130
///
126131
/// `data_sources` is the list of Wormhole data sources accepted by this contract.
127132
#[constructor]
128133
fn constructor(
129134
ref self: ContractState,
130135
wormhole_address: ContractAddress,
131-
fee_token_address: ContractAddress,
132-
single_update_fee: u256,
136+
fee_token_address1: ContractAddress,
137+
single_update_fee1: u256,
138+
fee_token_address2: ContractAddress,
139+
single_update_fee2: u256,
133140
data_sources: Array<DataSource>,
134141
governance_emitter_chain_id: u16,
135142
governance_emitter_address: u256,
136143
governance_initial_sequence: u64,
137144
) {
138145
self.wormhole_address.write(wormhole_address);
139-
self.fee_token_address.write(fee_token_address);
140-
self.single_update_fee.write(single_update_fee);
146+
self.fee_token_address1.write(fee_token_address1);
147+
self.single_update_fee1.write(single_update_fee1);
148+
self.fee_token_address2.write(fee_token_address2);
149+
self.single_update_fee2.write(single_update_fee2);
141150
self.write_data_sources(@data_sources);
142151
self
143152
.governance_data_source
@@ -253,13 +262,21 @@ mod pyth {
253262
self.update_price_feeds_internal(data, array![], 0, 0, false);
254263
}
255264

256-
fn get_update_fee(self: @ContractState, data: ByteArray) -> u256 {
265+
fn get_update_fee(self: @ContractState, data: ByteArray, token: ContractAddress) -> u256 {
266+
let single_update_fee = if token == self.fee_token_address1.read() {
267+
self.single_update_fee1.read()
268+
} else if token == self.fee_token_address2.read() {
269+
self.single_update_fee2.read()
270+
} else {
271+
panic_with_felt252(GetSingleUpdateFeeError::UnsupportedToken.into())
272+
};
273+
257274
let mut reader = ReaderImpl::new(data);
258275
read_and_verify_header(ref reader);
259276
let wormhole_proof_size = reader.read_u16();
260277
reader.skip(wormhole_proof_size.into());
261278
let num_updates = reader.read_u8();
262-
self.get_total_fee(num_updates)
279+
single_update_fee * num_updates.into()
263280
}
264281

265282
fn update_price_feeds_if_necessary(
@@ -314,12 +331,18 @@ mod pyth {
314331
self.wormhole_address.read()
315332
}
316333

317-
fn fee_token_address(self: @ContractState) -> ContractAddress {
318-
self.fee_token_address.read()
334+
fn fee_token_addresses(self: @ContractState) -> Array<ContractAddress> {
335+
array![self.fee_token_address1.read(), self.fee_token_address2.read()]
319336
}
320337

321-
fn get_single_update_fee(self: @ContractState) -> u256 {
322-
self.single_update_fee.read()
338+
fn get_single_update_fee(self: @ContractState, token: ContractAddress) -> u256 {
339+
if token == self.fee_token_address1.read() {
340+
self.single_update_fee1.read()
341+
} else if token == self.fee_token_address2.read() {
342+
self.single_update_fee2.read()
343+
} else {
344+
panic_with_felt252(GetSingleUpdateFeeError::UnsupportedToken.into())
345+
}
323346
}
324347

325348
fn valid_data_sources(self: @ContractState) -> Array<DataSource> {
@@ -370,8 +393,8 @@ mod pyth {
370393
match instruction.payload {
371394
GovernancePayload::SetFee(payload) => {
372395
let new_fee = apply_decimal_expo(payload.value, payload.expo);
373-
let old_fee = self.single_update_fee.read();
374-
self.single_update_fee.write(new_fee);
396+
let old_fee = self.single_update_fee1.read();
397+
self.single_update_fee1.write(new_fee);
375398
let event = FeeSet { old_fee, new_fee };
376399
self.emit(event);
377400
},
@@ -468,10 +491,6 @@ mod pyth {
468491
}
469492
}
470493

471-
fn get_total_fee(self: @ContractState, num_updates: u8) -> u256 {
472-
self.single_update_fee.read() * num_updates.into()
473-
}
474-
475494
fn verify_governance_vm(ref self: ContractState, vm: @VerifiedVM) {
476495
let governance_data_source = self.governance_data_source.read();
477496
if governance_data_source.emitter_chain_id != *vm.emitter_chain_id {
@@ -611,18 +630,27 @@ mod pyth {
611630
let root_digest = parse_wormhole_proof(vm.payload);
612631

613632
let num_updates = reader.read_u8();
614-
let total_fee = self.get_total_fee(num_updates);
615-
let fee_contract = IERC20CamelDispatcher {
616-
contract_address: self.fee_token_address.read()
617-
};
618633
let execution_info = get_execution_info().unbox();
619634
let caller = execution_info.caller_address;
620635
let contract = execution_info.contract_address;
621-
if fee_contract.allowance(caller, contract) < total_fee {
622-
panic_with_felt252(UpdatePriceFeedsError::InsufficientFeeAllowance.into());
623-
}
624-
if !fee_contract.transferFrom(caller, contract, total_fee) {
625-
panic_with_felt252(UpdatePriceFeedsError::InsufficientFeeAllowance.into());
636+
let fee1_transfered = transfer_fee(
637+
num_updates,
638+
caller,
639+
contract,
640+
self.fee_token_address1.read(),
641+
self.single_update_fee1.read(),
642+
);
643+
if !fee1_transfered {
644+
let fee2_transfered = transfer_fee(
645+
num_updates,
646+
caller,
647+
contract,
648+
self.fee_token_address2.read(),
649+
self.single_update_fee2.read(),
650+
);
651+
if !fee2_transfered {
652+
panic_with_felt252(UpdatePriceFeedsError::InsufficientFeeAllowance.into());
653+
}
626654
}
627655

628656
let mut i = 0;
@@ -708,4 +736,25 @@ mod pyth {
708736
Option::Some(i)
709737
}
710738
}
739+
740+
fn transfer_fee(
741+
num_updates: u8,
742+
caller: ContractAddress,
743+
contract: ContractAddress,
744+
fee_token: ContractAddress,
745+
single_update_fee1: u256,
746+
) -> bool {
747+
let total_fee = single_update_fee1 * num_updates.into();
748+
let fee_contract = IERC20CamelDispatcher { contract_address: fee_token };
749+
if fee_contract.allowance(caller, contract) < total_fee {
750+
return false;
751+
}
752+
if fee_contract.balanceOf(caller) < total_fee {
753+
return false;
754+
}
755+
if !fee_contract.transferFrom(caller, contract, total_fee) {
756+
return false;
757+
}
758+
true
759+
}
711760
}

target_chains/starknet/contracts/src/pyth/errors.cairo

+13
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,16 @@ impl ParsePriceFeedsErrorIntoFelt252 of Into<ParsePriceFeedsError, felt252> {
113113
}
114114
}
115115
}
116+
117+
#[derive(Copy, Drop, Debug, Serde, PartialEq)]
118+
pub enum GetSingleUpdateFeeError {
119+
UnsupportedToken,
120+
}
121+
122+
impl GetSingleUpdateFeeErrorIntoFelt252 of Into<GetSingleUpdateFeeError, felt252> {
123+
fn into(self: GetSingleUpdateFeeError) -> felt252 {
124+
match self {
125+
GetSingleUpdateFeeError::UnsupportedToken => 'unsupported token',
126+
}
127+
}
128+
}

target_chains/starknet/contracts/src/pyth/interface.cairo

+3-3
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ pub trait IPyth<T> {
3333
fn parse_unique_price_feed_updates(
3434
ref self: T, data: ByteArray, price_ids: Array<u256>, publish_time: u64, max_staleness: u64,
3535
) -> Array<PriceFeed>;
36-
fn get_update_fee(self: @T, data: ByteArray) -> u256;
36+
fn get_update_fee(self: @T, data: ByteArray, token: ContractAddress) -> u256;
3737
fn wormhole_address(self: @T) -> ContractAddress;
38-
fn fee_token_address(self: @T) -> ContractAddress;
39-
fn get_single_update_fee(self: @T) -> u256;
38+
fn fee_token_addresses(self: @T) -> Array<ContractAddress>;
39+
fn get_single_update_fee(self: @T, token: ContractAddress) -> u256;
4040
fn valid_data_sources(self: @T) -> Array<DataSource>;
4141
fn is_valid_data_source(self: @T, source: DataSource) -> bool;
4242
fn governance_data_source(self: @T) -> DataSource;

0 commit comments

Comments
 (0)