pyth.cairo 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. mod errors;
  2. mod interface;
  3. mod price_update;
  4. mod governance;
  5. mod fake_upgrades;
  6. pub use pyth::{
  7. Event, PriceFeedUpdateEvent, WormholeAddressSet, GovernanceDataSourceSet, ContractUpgraded
  8. };
  9. pub use errors::{GetPriceUnsafeError, GovernanceActionError, UpdatePriceFeedsError};
  10. pub use interface::{IPyth, IPythDispatcher, IPythDispatcherTrait, DataSource, Price};
  11. #[starknet::contract]
  12. mod pyth {
  13. use super::price_update::{
  14. PriceInfo, PriceFeedMessage, read_and_verify_message, read_header_and_wormhole_proof,
  15. parse_wormhole_proof
  16. };
  17. use pyth::reader::{Reader, ReaderImpl};
  18. use pyth::byte_array::{ByteArray, ByteArrayImpl};
  19. use core::panic_with_felt252;
  20. use core::starknet::{
  21. ContractAddress, get_caller_address, get_execution_info, ClassHash, SyscallResultTrait,
  22. get_contract_address,
  23. };
  24. use core::starknet::syscalls::replace_class_syscall;
  25. use pyth::wormhole::{IWormholeDispatcher, IWormholeDispatcherTrait, VerifiedVM};
  26. use super::{
  27. DataSource, UpdatePriceFeedsError, GovernanceActionError, Price, GetPriceUnsafeError,
  28. IPythDispatcher, IPythDispatcherTrait,
  29. };
  30. use super::governance;
  31. use super::governance::GovernancePayload;
  32. use openzeppelin::token::erc20::interface::{IERC20CamelDispatcherTrait, IERC20CamelDispatcher};
  33. #[event]
  34. #[derive(Drop, PartialEq, starknet::Event)]
  35. pub enum Event {
  36. PriceFeedUpdate: PriceFeedUpdateEvent,
  37. WormholeAddressSet: WormholeAddressSet,
  38. GovernanceDataSourceSet: GovernanceDataSourceSet,
  39. ContractUpgraded: ContractUpgraded,
  40. }
  41. #[derive(Drop, PartialEq, starknet::Event)]
  42. pub struct PriceFeedUpdateEvent {
  43. #[key]
  44. pub price_id: u256,
  45. pub publish_time: u64,
  46. pub price: i64,
  47. pub conf: u64,
  48. }
  49. #[derive(Drop, PartialEq, starknet::Event)]
  50. pub struct WormholeAddressSet {
  51. pub old_address: ContractAddress,
  52. pub new_address: ContractAddress,
  53. }
  54. #[derive(Drop, PartialEq, starknet::Event)]
  55. pub struct GovernanceDataSourceSet {
  56. pub old_data_source: DataSource,
  57. pub new_data_source: DataSource,
  58. pub last_executed_governance_sequence: u64,
  59. }
  60. #[derive(Drop, PartialEq, starknet::Event)]
  61. pub struct ContractUpgraded {
  62. pub new_class_hash: ClassHash,
  63. }
  64. #[storage]
  65. struct Storage {
  66. wormhole_address: ContractAddress,
  67. fee_contract_address: ContractAddress,
  68. single_update_fee: u256,
  69. owner: ContractAddress,
  70. data_sources: LegacyMap<usize, DataSource>,
  71. num_data_sources: usize,
  72. // For fast validation.
  73. is_valid_data_source: LegacyMap<DataSource, bool>,
  74. latest_price_info: LegacyMap<u256, PriceInfo>,
  75. governance_data_source: DataSource,
  76. last_executed_governance_sequence: u64,
  77. // Governance data source index is used to prevent replay attacks,
  78. // so a claimVaa cannot be used twice.
  79. governance_data_source_index: u32,
  80. }
  81. /// Initializes the Pyth contract.
  82. ///
  83. /// `owner` is the address that will be allowed to call governance methods (it's a placeholder
  84. /// until we implement governance properly).
  85. ///
  86. /// `wormhole_address` is the address of the deployed Wormhole contract implemented in the `wormhole` module.
  87. ///
  88. /// `fee_contract_address` is the address of the ERC20 token used to pay fees to Pyth
  89. /// for price updates. There is no native token on Starknet so an ERC20 contract has to be used.
  90. /// On Katana, an ETH fee contract is pre-deployed. On Starknet testnet, ETH and STRK fee tokens are
  91. /// available. Any other ERC20-compatible token can also be used.
  92. /// In a Starknet Forge testing environment, a fee contract must be deployed manually.
  93. ///
  94. /// `single_update_fee` is the number of tokens of `fee_contract_address` charged for a single price update.
  95. ///
  96. /// `data_sources` is the list of Wormhole data sources accepted by this contract.
  97. #[constructor]
  98. fn constructor(
  99. ref self: ContractState,
  100. owner: ContractAddress,
  101. wormhole_address: ContractAddress,
  102. fee_contract_address: ContractAddress,
  103. single_update_fee: u256,
  104. data_sources: Array<DataSource>,
  105. governance_emitter_chain_id: u16,
  106. governance_emitter_address: u256,
  107. governance_initial_sequence: u64,
  108. ) {
  109. self.owner.write(wormhole_address);
  110. self.wormhole_address.write(wormhole_address);
  111. self.fee_contract_address.write(fee_contract_address);
  112. self.single_update_fee.write(single_update_fee);
  113. self.write_data_sources(data_sources);
  114. self
  115. .governance_data_source
  116. .write(
  117. DataSource {
  118. emitter_chain_id: governance_emitter_chain_id,
  119. emitter_address: governance_emitter_address,
  120. }
  121. );
  122. self.last_executed_governance_sequence.write(governance_initial_sequence);
  123. }
  124. #[abi(embed_v0)]
  125. impl PythImpl of super::IPyth<ContractState> {
  126. fn get_price_unsafe(
  127. self: @ContractState, price_id: u256
  128. ) -> Result<Price, GetPriceUnsafeError> {
  129. let info = self.latest_price_info.read(price_id);
  130. if info.publish_time == 0 {
  131. return Result::Err(GetPriceUnsafeError::PriceFeedNotFound);
  132. }
  133. let price = Price {
  134. price: info.price,
  135. conf: info.conf,
  136. expo: info.expo,
  137. publish_time: info.publish_time,
  138. };
  139. Result::Ok(price)
  140. }
  141. fn get_ema_price_unsafe(
  142. self: @ContractState, price_id: u256
  143. ) -> Result<Price, GetPriceUnsafeError> {
  144. let info = self.latest_price_info.read(price_id);
  145. if info.publish_time == 0 {
  146. return Result::Err(GetPriceUnsafeError::PriceFeedNotFound);
  147. }
  148. let price = Price {
  149. price: info.ema_price,
  150. conf: info.ema_conf,
  151. expo: info.expo,
  152. publish_time: info.publish_time,
  153. };
  154. Result::Ok(price)
  155. }
  156. fn set_data_sources(ref self: ContractState, sources: Array<DataSource>) {
  157. if self.owner.read() != get_caller_address() {
  158. panic_with_felt252(GovernanceActionError::AccessDenied.into());
  159. }
  160. self.write_data_sources(sources);
  161. }
  162. fn set_fee(ref self: ContractState, single_update_fee: u256) {
  163. if self.owner.read() != get_caller_address() {
  164. panic_with_felt252(GovernanceActionError::AccessDenied.into());
  165. }
  166. self.single_update_fee.write(single_update_fee);
  167. }
  168. fn update_price_feeds(ref self: ContractState, data: ByteArray) {
  169. let mut reader = ReaderImpl::new(data);
  170. let wormhole_proof = read_header_and_wormhole_proof(ref reader);
  171. let wormhole = IWormholeDispatcher { contract_address: self.wormhole_address.read() };
  172. let vm = wormhole.parse_and_verify_vm(wormhole_proof);
  173. let source = DataSource {
  174. emitter_chain_id: vm.emitter_chain_id, emitter_address: vm.emitter_address
  175. };
  176. if !self.is_valid_data_source.read(source) {
  177. panic_with_felt252(UpdatePriceFeedsError::InvalidUpdateDataSource.into());
  178. }
  179. let root_digest = parse_wormhole_proof(vm.payload);
  180. let num_updates = reader.read_u8();
  181. let total_fee = self.get_total_fee(num_updates);
  182. let fee_contract = IERC20CamelDispatcher {
  183. contract_address: self.fee_contract_address.read()
  184. };
  185. let execution_info = get_execution_info().unbox();
  186. let caller = execution_info.caller_address;
  187. let contract = execution_info.contract_address;
  188. if fee_contract.allowance(caller, contract) < total_fee {
  189. panic_with_felt252(UpdatePriceFeedsError::InsufficientFeeAllowance.into());
  190. }
  191. if !fee_contract.transferFrom(caller, contract, total_fee) {
  192. panic_with_felt252(UpdatePriceFeedsError::InsufficientFeeAllowance.into());
  193. }
  194. let mut i = 0;
  195. while i < num_updates {
  196. let message = read_and_verify_message(ref reader, root_digest);
  197. self.update_latest_price_if_necessary(message);
  198. i += 1;
  199. };
  200. if reader.len() != 0 {
  201. panic_with_felt252(UpdatePriceFeedsError::InvalidUpdateData.into());
  202. }
  203. }
  204. fn execute_governance_instruction(ref self: ContractState, data: ByteArray) {
  205. let wormhole = IWormholeDispatcher { contract_address: self.wormhole_address.read() };
  206. let vm = wormhole.parse_and_verify_vm(data.clone());
  207. self.verify_governance_vm(@vm);
  208. let instruction = governance::parse_instruction(vm.payload);
  209. if instruction.target_chain_id != 0
  210. && instruction.target_chain_id != wormhole.chain_id() {
  211. panic_with_felt252(GovernanceActionError::InvalidGovernanceTarget.into());
  212. }
  213. match instruction.payload {
  214. GovernancePayload::SetFee(payload) => {
  215. let value = apply_decimal_expo(payload.value, payload.expo);
  216. self.single_update_fee.write(value);
  217. },
  218. GovernancePayload::SetDataSources(payload) => {
  219. self.write_data_sources(payload.sources);
  220. },
  221. GovernancePayload::SetWormholeAddress(payload) => {
  222. if instruction.target_chain_id == 0 {
  223. panic_with_felt252(GovernanceActionError::InvalidGovernanceTarget.into());
  224. }
  225. self.check_new_wormhole(payload.address, data);
  226. self.wormhole_address.write(payload.address);
  227. let event = WormholeAddressSet {
  228. old_address: wormhole.contract_address, new_address: payload.address,
  229. };
  230. self.emit(event);
  231. },
  232. GovernancePayload::RequestGovernanceDataSourceTransfer(_) => {
  233. // RequestGovernanceDataSourceTransfer can be only part of
  234. // AuthorizeGovernanceDataSourceTransfer message
  235. panic_with_felt252(GovernanceActionError::InvalidGovernanceMessage.into());
  236. },
  237. GovernancePayload::AuthorizeGovernanceDataSourceTransfer(payload) => {
  238. self.authorize_governance_transfer(payload.claim_vaa);
  239. },
  240. GovernancePayload::UpgradeContract(payload) => {
  241. if instruction.target_chain_id == 0 {
  242. panic_with_felt252(GovernanceActionError::InvalidGovernanceTarget.into());
  243. }
  244. self.upgrade_contract(payload.new_implementation);
  245. }
  246. }
  247. }
  248. fn pyth_upgradable_magic(self: @ContractState) -> u32 {
  249. 0x97a6f304
  250. }
  251. }
  252. #[generate_trait]
  253. impl PrivateImpl of PrivateTrait {
  254. fn write_data_sources(ref self: ContractState, data_sources: Array<DataSource>) {
  255. let num_old = self.num_data_sources.read();
  256. let mut i = 0;
  257. while i < num_old {
  258. let old_source = self.data_sources.read(i);
  259. self.is_valid_data_source.write(old_source, false);
  260. self.data_sources.write(i, Default::default());
  261. i += 1;
  262. };
  263. self.num_data_sources.write(data_sources.len());
  264. i = 0;
  265. while i < data_sources.len() {
  266. let source = data_sources.at(i);
  267. self.is_valid_data_source.write(*source, true);
  268. self.data_sources.write(i, *source);
  269. i += 1;
  270. };
  271. }
  272. fn update_latest_price_if_necessary(ref self: ContractState, message: PriceFeedMessage) {
  273. let latest_publish_time = self.latest_price_info.read(message.price_id).publish_time;
  274. if message.publish_time > latest_publish_time {
  275. let info = PriceInfo {
  276. price: message.price,
  277. conf: message.conf,
  278. expo: message.expo,
  279. publish_time: message.publish_time,
  280. ema_price: message.ema_price,
  281. ema_conf: message.ema_conf,
  282. };
  283. self.latest_price_info.write(message.price_id, info);
  284. let event = PriceFeedUpdateEvent {
  285. price_id: message.price_id,
  286. publish_time: message.publish_time,
  287. price: message.price,
  288. conf: message.conf,
  289. };
  290. self.emit(event);
  291. }
  292. }
  293. fn get_total_fee(ref self: ContractState, num_updates: u8) -> u256 {
  294. self.single_update_fee.read() * num_updates.into()
  295. }
  296. fn verify_governance_vm(ref self: ContractState, vm: @VerifiedVM) {
  297. let governance_data_source = self.governance_data_source.read();
  298. if governance_data_source.emitter_chain_id != *vm.emitter_chain_id {
  299. panic_with_felt252(GovernanceActionError::InvalidGovernanceDataSource.into());
  300. }
  301. if governance_data_source.emitter_address != *vm.emitter_address {
  302. panic_with_felt252(GovernanceActionError::InvalidGovernanceDataSource.into());
  303. }
  304. if *vm.sequence <= self.last_executed_governance_sequence.read() {
  305. panic_with_felt252(GovernanceActionError::OldGovernanceMessage.into());
  306. }
  307. // Note: in case of AuthorizeGovernanceDataSourceTransfer,
  308. // last_executed_governance_sequence is later overwritten with the value from claim_vaa
  309. self.last_executed_governance_sequence.write(*vm.sequence);
  310. }
  311. fn check_new_wormhole(
  312. ref self: ContractState, wormhole_address: ContractAddress, vm: ByteArray
  313. ) {
  314. let wormhole = IWormholeDispatcher { contract_address: wormhole_address };
  315. let vm = wormhole.parse_and_verify_vm(vm);
  316. let governance_data_source = self.governance_data_source.read();
  317. if governance_data_source.emitter_chain_id != vm.emitter_chain_id {
  318. panic_with_felt252(GovernanceActionError::InvalidGovernanceDataSource.into());
  319. }
  320. if governance_data_source.emitter_address != vm.emitter_address {
  321. panic_with_felt252(GovernanceActionError::InvalidGovernanceDataSource.into());
  322. }
  323. if vm.sequence != self.last_executed_governance_sequence.read() {
  324. panic_with_felt252(GovernanceActionError::InvalidWormholeAddressToSet.into());
  325. }
  326. // Purposefully, we don't check whether the chainId is the same as the current chainId because
  327. // we might want to change the chain id of the wormhole contract.
  328. let data = governance::parse_instruction(vm.payload);
  329. match data.payload {
  330. GovernancePayload::SetWormholeAddress(payload) => {
  331. // The following check is not necessary for security, but is a sanity check that the new wormhole
  332. // contract parses the payload correctly.
  333. if payload.address != wormhole_address {
  334. panic_with_felt252(
  335. GovernanceActionError::InvalidWormholeAddressToSet.into()
  336. );
  337. }
  338. },
  339. _ => {
  340. panic_with_felt252(GovernanceActionError::InvalidWormholeAddressToSet.into());
  341. }
  342. }
  343. }
  344. fn authorize_governance_transfer(ref self: ContractState, claim_vaa: ByteArray) {
  345. let wormhole = IWormholeDispatcher { contract_address: self.wormhole_address.read() };
  346. let claim_vm = wormhole.parse_and_verify_vm(claim_vaa.clone());
  347. // Note: no verify_governance_vm() because claim_vaa is signed by the new data source
  348. let instruction = governance::parse_instruction(claim_vm.payload);
  349. if instruction.target_chain_id != 0
  350. && instruction.target_chain_id != wormhole.chain_id() {
  351. panic_with_felt252(GovernanceActionError::InvalidGovernanceTarget.into());
  352. }
  353. let request_payload = match instruction.payload {
  354. GovernancePayload::RequestGovernanceDataSourceTransfer(payload) => payload,
  355. _ => { panic_with_felt252(GovernanceActionError::InvalidGovernanceMessage.into()) }
  356. };
  357. // Governance data source index is used to prevent replay attacks,
  358. // so a claimVaa cannot be used twice.
  359. let current_index = self.governance_data_source_index.read();
  360. let new_index = request_payload.governance_data_source_index;
  361. if current_index >= new_index {
  362. panic_with_felt252(GovernanceActionError::OldGovernanceMessage.into());
  363. }
  364. self.governance_data_source_index.write(request_payload.governance_data_source_index);
  365. let old_data_source = self.governance_data_source.read();
  366. let new_data_source = DataSource {
  367. emitter_chain_id: claim_vm.emitter_chain_id,
  368. emitter_address: claim_vm.emitter_address,
  369. };
  370. self.governance_data_source.write(new_data_source);
  371. // Setting the last executed governance to the claimVaa sequence to avoid
  372. // using older sequences.
  373. let last_executed_governance_sequence = claim_vm.sequence;
  374. self.last_executed_governance_sequence.write(last_executed_governance_sequence);
  375. let event = GovernanceDataSourceSet {
  376. old_data_source, new_data_source, last_executed_governance_sequence,
  377. };
  378. self.emit(event);
  379. }
  380. fn upgrade_contract(ref self: ContractState, new_implementation: ClassHash) {
  381. let contract_address = get_contract_address();
  382. replace_class_syscall(new_implementation).unwrap_syscall();
  383. // Dispatcher uses `call_contract_syscall` so it will call the new implementation.
  384. let magic = IPythDispatcher { contract_address }.pyth_upgradable_magic();
  385. if magic != 0x97a6f304 {
  386. panic_with_felt252(GovernanceActionError::InvalidGovernanceMessage.into());
  387. }
  388. let event = ContractUpgraded { new_class_hash: new_implementation };
  389. self.emit(event);
  390. }
  391. }
  392. fn apply_decimal_expo(value: u64, expo: u64) -> u256 {
  393. let mut output: u256 = value.into();
  394. let mut i = 0;
  395. while i < expo {
  396. output *= 10;
  397. i += 1;
  398. };
  399. output
  400. }
  401. }