pyth.cairo 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. use snforge_std::{
  2. declare, ContractClassTrait, start_prank, stop_prank, CheatTarget, spy_events, SpyOn, EventSpy,
  3. EventFetcher, event_name_hash, Event
  4. };
  5. use pyth::pyth::{
  6. IPythDispatcher, IPythDispatcherTrait, DataSource, Event as PythEvent, PriceFeedUpdateEvent,
  7. WormholeAddressSet, GovernanceDataSourceSet,
  8. };
  9. use pyth::byte_array::{ByteArray, ByteArrayImpl};
  10. use pyth::util::{array_try_into, UnwrapWithFelt252};
  11. use pyth::wormhole::IWormholeDispatcherTrait;
  12. use core::starknet::ContractAddress;
  13. use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait};
  14. use super::wormhole::corrupted_vm;
  15. use super::data;
  16. #[generate_trait]
  17. impl DecodeEventHelpers of DecodeEventHelpersTrait {
  18. fn pop<T, +TryInto<felt252, T>>(ref self: Array<felt252>) -> T {
  19. self.pop_front().unwrap().try_into().unwrap()
  20. }
  21. fn pop_u256(ref self: Array<felt252>) -> u256 {
  22. u256 { low: self.pop(), high: self.pop(), }
  23. }
  24. fn pop_data_source(ref self: Array<felt252>) -> DataSource {
  25. DataSource { emitter_chain_id: self.pop(), emitter_address: self.pop_u256(), }
  26. }
  27. }
  28. fn decode_event(mut event: Event) -> PythEvent {
  29. let key0: felt252 = event.keys.pop();
  30. let output = if key0 == event_name_hash('PriceFeedUpdate') {
  31. let event = PriceFeedUpdateEvent {
  32. price_id: event.keys.pop_u256(),
  33. publish_time: event.data.pop(),
  34. price: event.data.pop(),
  35. conf: event.data.pop(),
  36. };
  37. PythEvent::PriceFeedUpdate(event)
  38. } else if key0 == event_name_hash('WormholeAddressSet') {
  39. let event = WormholeAddressSet {
  40. old_address: event.data.pop(), new_address: event.data.pop(),
  41. };
  42. PythEvent::WormholeAddressSet(event)
  43. } else if key0 == event_name_hash('GovernanceDataSourceSet') {
  44. let event = GovernanceDataSourceSet {
  45. old_data_source: event.data.pop_data_source(),
  46. new_data_source: event.data.pop_data_source(),
  47. last_executed_governance_sequence: event.data.pop(),
  48. };
  49. PythEvent::GovernanceDataSourceSet(event)
  50. } else {
  51. panic!("unrecognized event")
  52. };
  53. assert!(event.keys.len() == 0);
  54. assert!(event.data.len() == 0);
  55. output
  56. }
  57. #[test]
  58. fn update_price_feeds_works() {
  59. let owner = 'owner'.try_into().unwrap();
  60. let user = 'user'.try_into().unwrap();
  61. let wormhole = super::wormhole::deploy_with_mainnet_guardians();
  62. let fee_contract = deploy_fee_contract(user);
  63. let pyth = deploy_default(owner, wormhole.contract_address, fee_contract.contract_address);
  64. start_prank(CheatTarget::One(fee_contract.contract_address), user.try_into().unwrap());
  65. fee_contract.approve(pyth.contract_address, 10000);
  66. stop_prank(CheatTarget::One(fee_contract.contract_address));
  67. let mut spy = spy_events(SpyOn::One(pyth.contract_address));
  68. start_prank(CheatTarget::One(pyth.contract_address), user.try_into().unwrap());
  69. pyth.update_price_feeds(data::good_update1());
  70. stop_prank(CheatTarget::One(pyth.contract_address));
  71. spy.fetch_events();
  72. assert!(spy.events.len() == 1);
  73. let (from, event) = spy.events.pop_front().unwrap();
  74. assert!(from == pyth.contract_address);
  75. let event = decode_event(event);
  76. let expected = PriceFeedUpdateEvent {
  77. price_id: 0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43,
  78. publish_time: 1712589206,
  79. price: 7192002930010,
  80. conf: 3596501465,
  81. };
  82. assert!(event == PythEvent::PriceFeedUpdate(expected));
  83. let last_price = pyth
  84. .get_price_unsafe(0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43)
  85. .unwrap_with_felt252();
  86. assert!(last_price.price == 7192002930010);
  87. assert!(last_price.conf == 3596501465);
  88. assert!(last_price.expo == -8);
  89. assert!(last_price.publish_time == 1712589206);
  90. let last_ema_price = pyth
  91. .get_ema_price_unsafe(0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43)
  92. .unwrap_with_felt252();
  93. assert!(last_ema_price.price == 7181868900000);
  94. assert!(last_ema_price.conf == 4096812700);
  95. assert!(last_ema_price.expo == -8);
  96. assert!(last_ema_price.publish_time == 1712589206);
  97. }
  98. #[test]
  99. fn test_governance_set_fee_works() {
  100. let owner = 'owner'.try_into().unwrap();
  101. let user = 'user'.try_into().unwrap();
  102. let wormhole = super::wormhole::deploy_with_test_guardian();
  103. let fee_contract = deploy_fee_contract(user);
  104. let pyth = deploy_default(owner, wormhole.contract_address, fee_contract.contract_address);
  105. start_prank(CheatTarget::One(fee_contract.contract_address), user);
  106. fee_contract.approve(pyth.contract_address, 10000);
  107. stop_prank(CheatTarget::One(fee_contract.contract_address));
  108. let mut balance = fee_contract.balanceOf(user);
  109. start_prank(CheatTarget::One(pyth.contract_address), user);
  110. pyth.update_price_feeds(data::test_price_update1());
  111. stop_prank(CheatTarget::One(pyth.contract_address));
  112. let new_balance = fee_contract.balanceOf(user);
  113. assert!(balance - new_balance == 1000);
  114. balance = new_balance;
  115. let last_price = pyth
  116. .get_price_unsafe(0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43)
  117. .unwrap_with_felt252();
  118. assert!(last_price.price == 6281060000000);
  119. pyth.execute_governance_instruction(data::pyth_set_fee());
  120. start_prank(CheatTarget::One(pyth.contract_address), user);
  121. pyth.update_price_feeds(data::test_price_update2());
  122. stop_prank(CheatTarget::One(pyth.contract_address));
  123. let new_balance = fee_contract.balanceOf(user);
  124. assert!(balance - new_balance == 4200);
  125. let last_price = pyth
  126. .get_price_unsafe(0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43)
  127. .unwrap_with_felt252();
  128. assert!(last_price.price == 6281522520745);
  129. }
  130. #[test]
  131. #[fuzzer(runs: 100, seed: 0)]
  132. #[should_panic]
  133. fn test_rejects_corrupted_governance_instruction(pos: usize, random1: usize, random2: usize) {
  134. let owner = 'owner'.try_into().unwrap();
  135. let user = 'user'.try_into().unwrap();
  136. let wormhole = super::wormhole::deploy_with_test_guardian();
  137. let fee_contract = deploy_fee_contract(user);
  138. let pyth = deploy_default(owner, wormhole.contract_address, fee_contract.contract_address);
  139. let input = corrupted_vm(data::pyth_set_fee(), pos, random1, random2);
  140. pyth.execute_governance_instruction(input);
  141. }
  142. #[test]
  143. fn test_governance_set_data_sources_works() {
  144. let owner = 'owner'.try_into().unwrap();
  145. let user = 'user'.try_into().unwrap();
  146. let wormhole = super::wormhole::deploy_with_test_guardian();
  147. let fee_contract = deploy_fee_contract(user);
  148. let pyth = deploy_default(owner, wormhole.contract_address, fee_contract.contract_address);
  149. start_prank(CheatTarget::One(fee_contract.contract_address), user);
  150. fee_contract.approve(pyth.contract_address, 10000);
  151. stop_prank(CheatTarget::One(fee_contract.contract_address));
  152. start_prank(CheatTarget::One(pyth.contract_address), user);
  153. pyth.update_price_feeds(data::test_price_update1());
  154. stop_prank(CheatTarget::One(pyth.contract_address));
  155. let last_price = pyth
  156. .get_price_unsafe(0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43)
  157. .unwrap_with_felt252();
  158. assert!(last_price.price == 6281060000000);
  159. pyth.execute_governance_instruction(data::pyth_set_data_sources());
  160. start_prank(CheatTarget::One(pyth.contract_address), user);
  161. pyth.update_price_feeds(data::test_update2_alt_emitter());
  162. stop_prank(CheatTarget::One(pyth.contract_address));
  163. let last_price = pyth
  164. .get_price_unsafe(0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43)
  165. .unwrap_with_felt252();
  166. assert!(last_price.price == 6281522520745);
  167. }
  168. #[test]
  169. #[should_panic(expected: ('invalid update data source',))]
  170. fn test_rejects_update_after_data_source_changed() {
  171. let owner = 'owner'.try_into().unwrap();
  172. let user = 'user'.try_into().unwrap();
  173. let wormhole = super::wormhole::deploy_with_test_guardian();
  174. let fee_contract = deploy_fee_contract(user);
  175. let pyth = deploy_default(owner, wormhole.contract_address, fee_contract.contract_address);
  176. start_prank(CheatTarget::One(fee_contract.contract_address), user);
  177. fee_contract.approve(pyth.contract_address, 10000);
  178. stop_prank(CheatTarget::One(fee_contract.contract_address));
  179. start_prank(CheatTarget::One(pyth.contract_address), user);
  180. pyth.update_price_feeds(data::test_price_update1());
  181. stop_prank(CheatTarget::One(pyth.contract_address));
  182. let last_price = pyth
  183. .get_price_unsafe(0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43)
  184. .unwrap_with_felt252();
  185. assert!(last_price.price == 6281060000000);
  186. pyth.execute_governance_instruction(data::pyth_set_data_sources());
  187. start_prank(CheatTarget::One(pyth.contract_address), user);
  188. pyth.update_price_feeds(data::test_price_update2());
  189. stop_prank(CheatTarget::One(pyth.contract_address));
  190. let last_price = pyth
  191. .get_price_unsafe(0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43)
  192. .unwrap_with_felt252();
  193. assert!(last_price.price == 6281522520745);
  194. }
  195. #[test]
  196. fn test_governance_set_wormhole_works() {
  197. let wormhole_class = declare("wormhole");
  198. // Arbitrary
  199. let wormhole_address = 0x42.try_into().unwrap();
  200. let wormhole = super::wormhole::deploy_declared_with_test_guardian_at(
  201. @wormhole_class, wormhole_address
  202. );
  203. let owner = 'owner'.try_into().unwrap();
  204. let user = 'user'.try_into().unwrap();
  205. let fee_contract = deploy_fee_contract(user);
  206. let pyth = deploy_default(owner, wormhole.contract_address, fee_contract.contract_address);
  207. start_prank(CheatTarget::One(fee_contract.contract_address), user);
  208. fee_contract.approve(pyth.contract_address, 10000);
  209. stop_prank(CheatTarget::One(fee_contract.contract_address));
  210. start_prank(CheatTarget::One(pyth.contract_address), user);
  211. pyth.update_price_feeds(data::test_price_update1());
  212. stop_prank(CheatTarget::One(pyth.contract_address));
  213. let last_price = pyth
  214. .get_price_unsafe(0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43)
  215. .unwrap_with_felt252();
  216. assert!(last_price.price == 6281060000000);
  217. // Address used in the governance instruction
  218. let wormhole2_address = 0x05033f06d5c47bcce7960ea703b04a0bf64bf33f6f2eb5613496da747522d9c2
  219. .try_into()
  220. .unwrap();
  221. let wormhole2 = super::wormhole::deploy_declared_with_test_guardian_at(
  222. @wormhole_class, wormhole2_address
  223. );
  224. wormhole2.submit_new_guardian_set(data::upgrade_to_test2());
  225. let mut spy = spy_events(SpyOn::One(pyth.contract_address));
  226. pyth.execute_governance_instruction(data::pyth_set_wormhole());
  227. spy.fetch_events();
  228. assert!(spy.events.len() == 1);
  229. let (from, event) = spy.events.pop_front().unwrap();
  230. assert!(from == pyth.contract_address);
  231. let event = decode_event(event);
  232. let expected = WormholeAddressSet {
  233. old_address: wormhole_address, new_address: wormhole2_address,
  234. };
  235. assert!(event == PythEvent::WormholeAddressSet(expected));
  236. start_prank(CheatTarget::One(pyth.contract_address), user);
  237. pyth.update_price_feeds(data::test_update2_set2());
  238. stop_prank(CheatTarget::One(pyth.contract_address));
  239. let last_price = pyth
  240. .get_price_unsafe(0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43)
  241. .unwrap_with_felt252();
  242. assert!(last_price.price == 6281522520745);
  243. }
  244. #[test]
  245. #[should_panic(expected: ('invalid guardian set index',))]
  246. fn test_rejects_price_update_without_setting_wormhole() {
  247. let wormhole = super::wormhole::deploy_with_test_guardian();
  248. let owner = 'owner'.try_into().unwrap();
  249. let user = 'user'.try_into().unwrap();
  250. let fee_contract = deploy_fee_contract(user);
  251. let pyth = deploy_default(owner, wormhole.contract_address, fee_contract.contract_address);
  252. start_prank(CheatTarget::One(fee_contract.contract_address), user);
  253. fee_contract.approve(pyth.contract_address, 10000);
  254. stop_prank(CheatTarget::One(fee_contract.contract_address));
  255. start_prank(CheatTarget::One(pyth.contract_address), user);
  256. pyth.update_price_feeds(data::test_price_update1());
  257. stop_prank(CheatTarget::One(pyth.contract_address));
  258. let last_price = pyth
  259. .get_price_unsafe(0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43)
  260. .unwrap_with_felt252();
  261. assert!(last_price.price == 6281060000000);
  262. start_prank(CheatTarget::One(pyth.contract_address), user);
  263. pyth.update_price_feeds(data::test_update2_set2());
  264. }
  265. // This test doesn't pass because of an snforge bug.
  266. // See https://github.com/foundry-rs/starknet-foundry/issues/2096
  267. // TODO: update snforge and unignore when the next release is available
  268. #[test]
  269. #[should_panic]
  270. #[ignore]
  271. fn test_rejects_set_wormhole_without_deploying() {
  272. let wormhole_class = declare("wormhole");
  273. // Arbitrary
  274. let wormhole_address = 0x42.try_into().unwrap();
  275. let wormhole = super::wormhole::deploy_declared_with_test_guardian_at(
  276. @wormhole_class, wormhole_address
  277. );
  278. let owner = 'owner'.try_into().unwrap();
  279. let user = 'user'.try_into().unwrap();
  280. let fee_contract = deploy_fee_contract(user);
  281. let pyth = deploy_default(owner, wormhole.contract_address, fee_contract.contract_address);
  282. pyth.execute_governance_instruction(data::pyth_set_wormhole());
  283. }
  284. #[test]
  285. #[should_panic(expected: ('Invalid signature',))]
  286. fn test_rejects_set_wormhole_with_incompatible_guardians() {
  287. let wormhole_class = declare("wormhole");
  288. // Arbitrary
  289. let wormhole_address = 0x42.try_into().unwrap();
  290. let wormhole = super::wormhole::deploy_declared_with_test_guardian_at(
  291. @wormhole_class, wormhole_address
  292. );
  293. let owner = 'owner'.try_into().unwrap();
  294. let user = 'user'.try_into().unwrap();
  295. let fee_contract = deploy_fee_contract(user);
  296. let pyth = deploy_default(owner, wormhole.contract_address, fee_contract.contract_address);
  297. // Address used in the governance instruction
  298. let wormhole2_address = 0x05033f06d5c47bcce7960ea703b04a0bf64bf33f6f2eb5613496da747522d9c2
  299. .try_into()
  300. .unwrap();
  301. super::wormhole::deploy_declared_at(
  302. @wormhole_class,
  303. array_try_into(array![0x301]),
  304. super::wormhole::CHAIN_ID,
  305. super::wormhole::GOVERNANCE_CHAIN_ID,
  306. super::wormhole::GOVERNANCE_CONTRACT,
  307. Option::Some(wormhole2_address),
  308. );
  309. pyth.execute_governance_instruction(data::pyth_set_wormhole());
  310. }
  311. #[test]
  312. fn test_governance_transfer_works() {
  313. let owner = 'owner'.try_into().unwrap();
  314. let user = 'user'.try_into().unwrap();
  315. let wormhole = super::wormhole::deploy_with_test_guardian();
  316. let fee_contract = deploy_fee_contract(user);
  317. let pyth = deploy_default(owner, wormhole.contract_address, fee_contract.contract_address);
  318. let mut spy = spy_events(SpyOn::One(pyth.contract_address));
  319. pyth.execute_governance_instruction(data::pyth_auth_transfer());
  320. spy.fetch_events();
  321. assert!(spy.events.len() == 1);
  322. let (from, event) = spy.events.pop_front().unwrap();
  323. assert!(from == pyth.contract_address);
  324. let event = decode_event(event);
  325. let expected = GovernanceDataSourceSet {
  326. old_data_source: DataSource { emitter_chain_id: 1, emitter_address: 41, },
  327. new_data_source: DataSource { emitter_chain_id: 2, emitter_address: 43, },
  328. last_executed_governance_sequence: 1,
  329. };
  330. assert!(event == PythEvent::GovernanceDataSourceSet(expected));
  331. pyth.execute_governance_instruction(data::pyth_set_fee_alt_emitter());
  332. }
  333. #[test]
  334. #[should_panic(expected: ('invalid governance data source',))]
  335. fn test_set_fee_rejects_wrong_emitter() {
  336. let owner = 'owner'.try_into().unwrap();
  337. let user = 'user'.try_into().unwrap();
  338. let wormhole = super::wormhole::deploy_with_test_guardian();
  339. let fee_contract = deploy_fee_contract(user);
  340. let pyth = deploy_default(owner, wormhole.contract_address, fee_contract.contract_address);
  341. pyth.execute_governance_instruction(data::pyth_set_fee_alt_emitter());
  342. }
  343. #[test]
  344. #[should_panic(expected: ('invalid governance data source',))]
  345. fn test_rejects_old_emitter_after_transfer() {
  346. let owner = 'owner'.try_into().unwrap();
  347. let user = 'user'.try_into().unwrap();
  348. let wormhole = super::wormhole::deploy_with_test_guardian();
  349. let fee_contract = deploy_fee_contract(user);
  350. let pyth = deploy_default(owner, wormhole.contract_address, fee_contract.contract_address);
  351. pyth.execute_governance_instruction(data::pyth_auth_transfer());
  352. pyth.execute_governance_instruction(data::pyth_set_fee());
  353. }
  354. fn deploy_default(
  355. owner: ContractAddress, wormhole_address: ContractAddress, fee_contract_address: ContractAddress
  356. ) -> IPythDispatcher {
  357. deploy(
  358. owner,
  359. wormhole_address,
  360. fee_contract_address,
  361. 1000,
  362. array![
  363. DataSource {
  364. emitter_chain_id: 26,
  365. emitter_address: 0xe101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71,
  366. }
  367. ],
  368. 1,
  369. 41,
  370. 0,
  371. )
  372. }
  373. fn deploy(
  374. owner: ContractAddress,
  375. wormhole_address: ContractAddress,
  376. fee_contract_address: ContractAddress,
  377. single_update_fee: u256,
  378. data_sources: Array<DataSource>,
  379. governance_emitter_chain_id: u16,
  380. governance_emitter_address: u256,
  381. governance_initial_sequence: u64,
  382. ) -> IPythDispatcher {
  383. let mut args = array![];
  384. (owner, wormhole_address, fee_contract_address, single_update_fee).serialize(ref args);
  385. (data_sources, governance_emitter_chain_id).serialize(ref args);
  386. (governance_emitter_address, governance_initial_sequence).serialize(ref args);
  387. let contract = declare("pyth");
  388. let contract_address = match contract.deploy(@args) {
  389. Result::Ok(v) => { v },
  390. Result::Err(err) => {
  391. panic(err.panic_data);
  392. 0.try_into().unwrap()
  393. },
  394. };
  395. IPythDispatcher { contract_address }
  396. }
  397. fn deploy_fee_contract(recipient: ContractAddress) -> IERC20CamelDispatcher {
  398. let mut args = array![];
  399. let name: core::byte_array::ByteArray = "eth";
  400. let symbol: core::byte_array::ByteArray = "eth";
  401. (name, symbol, 100000_u256, recipient).serialize(ref args);
  402. let contract = declare("ERC20");
  403. let contract_address = match contract.deploy(@args) {
  404. Result::Ok(v) => { v },
  405. Result::Err(err) => {
  406. panic(err.panic_data);
  407. 0.try_into().unwrap()
  408. },
  409. };
  410. IERC20CamelDispatcher { contract_address }
  411. }