Skip to content

Commit 4e630ed

Browse files
authored
feat(target_chains/starknet): fee collection (#1527)
* feat(target_chains/starknet): fee collection * refactor(target_chains/starknet): renames and comments
1 parent 20d99bc commit 4e630ed

File tree

5 files changed

+78
-4
lines changed

5 files changed

+78
-4
lines changed

target_chains/starknet/contracts/Scarb.lock

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
# Code generated by scarb DO NOT EDIT.
22
version = 1
33

4+
[[package]]
5+
name = "openzeppelin"
6+
version = "0.10.0"
7+
source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.10.0#d77082732daab2690ba50742ea41080eb23299d3"
8+
49
[[package]]
510
name = "pyth"
611
version = "0.1.0"
712
dependencies = [
13+
"openzeppelin",
814
"snforge_std",
915
]
1016

target_chains/starknet/contracts/Scarb.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ edition = "2023_11"
55

66
[dependencies]
77
starknet = ">=2.5.4"
8+
openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.10.0" }
89
snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.21.0" }
910

1011
[[target.starknet-contract]]
12+
build-external-contracts = ["openzeppelin::presets::erc20::ERC20"]

target_chains/starknet/contracts/deploy/local_deploy

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ wormhole_hash=$(starkli declare target/dev/pyth_wormhole.contract_class.json)
2222
# prefunded katana account
2323
owner=0x6162896d1d7ab204c7ccac6dd5f8e9e7c25ecd5ae4fcb4ad32e57786bb46e03
2424

25-
fee_token_address=0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7
25+
# predeployed fee token contract in katana
26+
fee_contract_address=0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7
2627

2728
# deploying wormhole with mainnet guardians
2829

@@ -119,15 +120,18 @@ ${sleep}
119120
pyth_address=$(starkli deploy "${pyth_hash}" \
120121
"${owner}" \
121122
"${wormhole_address}" \
122-
"${fee_token_address}" \
123+
"${fee_contract_address}" \
123124
1000 0 `# fee amount` \
124125
1 `# num_data_sources` \
125126
26 `# emitter_chain_id` \
126127
58051393581875729396504816158954007153 299086580781278228892874716333010393740 `# emitter_address` \
127128
)
128129

129130
${sleep}
130-
starkli invoke "${pyth_address}" update_price_feeds \
131+
starkli invoke "${fee_contract_address}" approve "${pyth_address}" 1000 0
132+
133+
${sleep}
134+
starkli invoke --watch --log-traffic "${pyth_address}" update_price_feeds \
131135
11 41 141887862745809943100717722154781668316147089807066324001213790862261653767 451230040559159019530944948086670994623010697390864133264612902902585665886 355897384610106978643111834734000274494997301794613218547634257521495150151 140511063638834349363702006999356227863549404051701803148734324248522745879 435849190784772134907557391544163070978531038970298390345939133663347953446 416390591179833928094641114955594939466104495718036761707729297119441316151 360454929416220920336539568461651500076647166763464050800345920693176904002 316054999864337699543932294956493808847640383114707243342262764542081441331 325277902980160684959962429721294603784343718796390808940252812862355246813 43683235854839458868457367619068018785880460427473556950900276498953667 448289429405712011882317781416869052550573589492688760675666957663813001522 118081463902430977133121147164253483958565039026724621562859841189218059803 194064310618695309465615383754562031677972810736048112738513050109934134235 133901765334590923121691219814784557892214901646312752962904032795881821509 404227501001709279944936006741063968912686453006275462577777397594240621266 81649001731335394114026683805238949464016657447685509824621946636993704965 32402065226491532148674904435794801976788068837745943243341272676331333141 431262841416902409381606630149292665102873776020834630861578112749151562174 6164523115980545628843981978797257048781800754033825701059814297149591186 408761574582108996678203805090470134287794603493622537384530614829262728153 185368533577943244707350150853170361880334596276529206938783888784867529821 173578821500714074579643724957224629379984215847383417303110192934676518530 90209855380378362490166376523380463998928070428866100240907090599465187835 97758466908511588082569287391708453107999243934457382895073183209581711489 132725011490528489913736834798247512772139171145730373610858422315799224432 117123868005849140967825260063167768530251411611975150066586827543934313288 408149062252618928234854115279677715692278734600386004492580987016428761675 164529520317122600276020522906605877985809506451193373524142111430138855019 444793051809958482843529748761971363435331354795896511243191618771787268378 247660009137502548346315865368477795392972486141407800140910365553760622080 3281582060272565111592312037403686940429019548922889497694300188 93649805131515836129946966966350066506512123780266587069413066350925286142 394112423559676785086098106350541172262729583743734966358666094809121292390 35403101004688876764673991514113473446030702766599795822870037077688984558 99366103604611980443183454746643823071419076016677225828619807954313149423 10381657217606191031071521950784155484751645280452344547752823767622424055 391045354044274401116419632681482293741435113770205621235865697077178955228 311250087759201408758984550959714865999349469611700431708031036894849650573 59953730895385399344628932835545900304309851622811198425230584225200786697 226866843267230707879834616967256711063296411939069440476882347301771901839 95752383404870925303422787
132136

133137
echo Pyth contract has been successfully deployed at "${pyth_address}"

target_chains/starknet/contracts/src/pyth.cairo

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ pub enum UpdatePriceFeedsError {
6666
Wormhole: super::wormhole::ParseAndVerifyVmError,
6767
InvalidUpdateData,
6868
InvalidUpdateDataSource,
69+
InsufficientFeeAllowance,
6970
}
7071

7172
pub impl UpdatePriceFeedsErrorUnwrapWithFelt252<T> of UnwrapWithFelt252<T, UpdatePriceFeedsError> {
@@ -84,6 +85,7 @@ impl UpdatePriceFeedsErrorIntoFelt252 of Into<UpdatePriceFeedsError, felt252> {
8485
UpdatePriceFeedsError::Wormhole(err) => err.into(),
8586
UpdatePriceFeedsError::InvalidUpdateData => 'invalid update data',
8687
UpdatePriceFeedsError::InvalidUpdateDataSource => 'invalid update data source',
88+
UpdatePriceFeedsError::InsufficientFeeAllowance => 'insufficient fee allowance',
8789
}
8890
}
8991
}
@@ -128,6 +130,7 @@ mod pyth {
128130
use pyth::hash::{Hasher, HasherImpl};
129131
use core::fmt::{Debug, Formatter};
130132
use pyth::util::{u64_as_i64, u32_as_i32};
133+
use openzeppelin::token::erc20::interface::{IERC20CamelDispatcherTrait, IERC20CamelDispatcher};
131134

132135
// Stands for PNAU (Pyth Network Accumulator Update)
133136
const ACCUMULATOR_MAGIC: u32 = 0x504e4155;
@@ -232,6 +235,22 @@ mod pyth {
232235
latest_price_info: LegacyMap<u256, PriceInfo>,
233236
}
234237

238+
/// Initializes the Pyth contract.
239+
///
240+
/// `owner` is the address that will be allowed to call governance methods (it's a placeholder
241+
/// until we implement governance properly).
242+
///
243+
/// `wormhole_address` is the address of the deployed Wormhole contract implemented in the `wormhole` module.
244+
///
245+
/// `fee_contract_address` is the address of the ERC20 token used to pay fees to Pyth
246+
/// for price updates. There is no native token on Starknet so an ERC20 contract has to be used.
247+
/// On Katana, an ETH fee contract is pre-deployed. On Starknet testnet, ETH and STRK fee tokens are
248+
/// available. Any other ERC20-compatible token can also be used.
249+
/// In a Starknet Forge testing environment, a fee contract must be deployed manually.
250+
///
251+
/// `single_update_fee` is the number of tokens of `fee_contract_address` charged for a single price update.
252+
///
253+
/// `data_sources` is the list of Wormhole data sources accepted by this contract.
235254
#[constructor]
236255
fn constructor(
237256
ref self: ContractState,
@@ -392,6 +411,20 @@ mod pyth {
392411

393412
let num_updates = reader.read_u8().map_err()?;
394413

414+
let total_fee = get_total_fee(ref self, num_updates);
415+
let fee_contract = IERC20CamelDispatcher {
416+
contract_address: self.fee_contract_address.read()
417+
};
418+
let execution_info = get_execution_info().unbox();
419+
let caller = execution_info.caller_address;
420+
let contract = execution_info.contract_address;
421+
if fee_contract.allowance(caller, contract) < total_fee {
422+
return Result::Err(UpdatePriceFeedsError::InsufficientFeeAllowance);
423+
}
424+
if !fee_contract.transferFrom(caller, contract, total_fee) {
425+
return Result::Err(UpdatePriceFeedsError::InsufficientFeeAllowance);
426+
}
427+
395428
let mut i = 0;
396429
let mut result = Result::Ok(());
397430
while i < num_updates {
@@ -468,4 +501,8 @@ mod pyth {
468501
self.emit(event);
469502
}
470503
}
504+
505+
fn get_total_fee(ref self: ContractState, num_updates: u8) -> u256 {
506+
self.single_update_fee.read() * num_updates.into()
507+
}
471508
}

target_chains/starknet/contracts/tests/pyth.cairo

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use pyth::pyth::{
88
use pyth::byte_array::{ByteArray, ByteArrayImpl};
99
use pyth::util::{array_felt252_to_bytes31, UnwrapWithFelt252};
1010
use core::starknet::ContractAddress;
11+
use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait};
1112

1213
fn decode_event(event: @Event) -> PythEvent {
1314
if *event.keys.at(0) == event_name_hash('PriceFeedUpdate') {
@@ -31,11 +32,13 @@ fn decode_event(event: @Event) -> PythEvent {
3132
#[test]
3233
fn update_price_feeds_works() {
3334
let owner = 'owner'.try_into().unwrap();
35+
let user = 'user'.try_into().unwrap();
3436
let wormhole = super::wormhole::deploy_and_init(owner);
37+
let fee_contract = deploy_fee_contract(user);
3538
let pyth = deploy(
3639
owner,
3740
wormhole.contract_address,
38-
0x42.try_into().unwrap(),
41+
fee_contract.contract_address,
3942
1000,
4043
array![
4144
DataSource {
@@ -45,9 +48,15 @@ fn update_price_feeds_works() {
4548
]
4649
);
4750

51+
start_prank(CheatTarget::One(fee_contract.contract_address), user.try_into().unwrap());
52+
fee_contract.approve(pyth.contract_address, 10000);
53+
stop_prank(CheatTarget::One(fee_contract.contract_address));
54+
4855
let mut spy = spy_events(SpyOn::One(pyth.contract_address));
4956

57+
start_prank(CheatTarget::One(pyth.contract_address), user.try_into().unwrap());
5058
pyth.update_price_feeds(good_update1()).unwrap_with_felt252();
59+
stop_prank(CheatTarget::One(pyth.contract_address));
5160

5261
spy.fetch_events();
5362
assert!(spy.events.len() == 1);
@@ -100,6 +109,22 @@ fn deploy(
100109
IPythDispatcher { contract_address }
101110
}
102111

112+
fn deploy_fee_contract(recipient: ContractAddress) -> IERC20CamelDispatcher {
113+
let mut args = array![];
114+
let name: core::byte_array::ByteArray = "eth";
115+
let symbol: core::byte_array::ByteArray = "eth";
116+
(name, symbol, 100000_u256, recipient).serialize(ref args);
117+
let contract = declare("ERC20");
118+
let contract_address = match contract.deploy(@args) {
119+
Result::Ok(v) => { v },
120+
Result::Err(err) => {
121+
panic(err.panic_data);
122+
0.try_into().unwrap()
123+
},
124+
};
125+
IERC20CamelDispatcher { contract_address }
126+
}
127+
103128
// A random update pulled from Hermes.
104129
fn good_update1() -> ByteArray {
105130
let bytes = array![

0 commit comments

Comments
 (0)