required_version.move 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. // SPDX-License-Identifier: Apache 2
  2. /// Note: This module is based on the required_version module
  3. /// from the Sui Wormhole package:
  4. /// https://github.com/wormhole-foundation/wormhole/blob/sui/integration_v2/sui/wormhole/sources/resources/required_version.move
  5. /// This module implements a mechanism for version control. While keeping track
  6. /// of the latest version of a package build, `RequiredVersion` manages the
  7. /// minimum required version number for any method in that package. For any
  8. /// upgrade where a particular method can have backward compatibility, the
  9. /// minimum version would not have to change (because the method should work the
  10. /// same way with the previous version or current version).
  11. ///
  12. /// If there happens to be a breaking change for a particular method, this
  13. /// module can force that the method's minimum requirement be the latest build.
  14. /// If a previous build were used, the method would abort if a check is in place
  15. /// with `RequiredVersion`.
  16. ///
  17. /// There is no magic behind the way ths module works. `RequiredVersion` is
  18. /// intended to live in a package's shared object that gets passed into its
  19. /// methods (e.g. pyth's `State` object).
  20. module pyth::required_version {
  21. use sui::dynamic_field::{Self as field};
  22. use sui::object::{Self, UID};
  23. use sui::package::{Self, UpgradeCap};
  24. use sui::tx_context::{TxContext};
  25. /// Build version passed does not meet method's minimum required version.
  26. const E_OUTDATED_VERSION: u64 = 0;
  27. /// Container to keep track of latest build version. Dynamic fields are
  28. /// associated with its `id`.
  29. struct RequiredVersion has store {
  30. id: UID,
  31. latest_version: u64
  32. }
  33. struct Key<phantom MethodType> has store, drop, copy {}
  34. /// Create new `RequiredVersion` with a configured starting version.
  35. public fun new(version: u64, ctx: &mut TxContext): RequiredVersion {
  36. RequiredVersion {
  37. id: object::new(ctx),
  38. latest_version: version
  39. }
  40. }
  41. /// Retrieve latest build version.
  42. public fun current(self: &RequiredVersion): u64 {
  43. self.latest_version
  44. }
  45. /// Add specific method handling via custom `MethodType`. At the time a
  46. /// method is added, the minimum build version associated with this method
  47. /// by default is the latest version.
  48. public fun add<MethodType>(self: &mut RequiredVersion) {
  49. field::add(&mut self.id, Key<MethodType> {}, self.latest_version)
  50. }
  51. /// This method will abort if the version for a particular `MethodType` is
  52. /// not up-to-date with the version of the current build.
  53. ///
  54. /// For example, if the minimum requirement for `foobar` module (with an
  55. /// appropriately named `MethodType` like `FooBar`) is `1` and the current
  56. /// implementation version is `2`, this method will succeed because the
  57. /// build meets the minimum required version of `1` in order for `foobar` to
  58. /// work. So if someone were to use an older build like version `1`, this
  59. /// method will succeed.
  60. ///
  61. /// But if `check_minimum_requirement` were invoked for `foobar` when the
  62. /// minimum requirement is `2` and the current build is only version `1`,
  63. /// then this method will abort because the build does not meet the minimum
  64. /// version requirement for `foobar`.
  65. ///
  66. /// This method also assumes that the `MethodType` being checked for is
  67. /// already a dynamic field (using `add`) during initialization.
  68. public fun check_minimum_requirement<MethodType>(
  69. self: &RequiredVersion,
  70. build_version: u64
  71. ) {
  72. assert!(
  73. build_version >= minimum_for<MethodType>(self),
  74. E_OUTDATED_VERSION
  75. );
  76. }
  77. /// At `commit_upgrade`, use this method to update the tracker's knowledge
  78. /// of the latest upgrade (build) version, which is obtained from the
  79. /// `UpgradeCap` in `sui::package`.
  80. public fun update_latest(
  81. self: &mut RequiredVersion,
  82. upgrade_cap: &UpgradeCap
  83. ) {
  84. self.latest_version = package::version(upgrade_cap);
  85. }
  86. /// Once the global version is updated via `commit_upgrade` and there is a
  87. /// particular method that has a breaking change, use this method to uptick
  88. /// that method's minimum required version to the latest.
  89. public fun require_current_version<MethodType>(self: &mut RequiredVersion) {
  90. let min_version = field::borrow_mut(&mut self.id, Key<MethodType> {});
  91. *min_version = self.latest_version;
  92. }
  93. /// Retrieve the minimum required version for a particular method (via
  94. /// `MethodType`).
  95. public fun minimum_for<MethodType>(self: &RequiredVersion): u64 {
  96. *field::borrow(&self.id, Key<MethodType> {})
  97. }
  98. #[test_only]
  99. public fun set_required_version<MethodType>(
  100. self: &mut RequiredVersion,
  101. version: u64
  102. ) {
  103. *field::borrow_mut(&mut self.id, Key<MethodType> {}) = version;
  104. }
  105. #[test_only]
  106. public fun destroy(req: RequiredVersion) {
  107. let RequiredVersion { id, latest_version: _} = req;
  108. object::delete(id);
  109. }
  110. }
  111. #[test_only]
  112. module pyth::required_version_test {
  113. use sui::hash::{keccak256};
  114. use sui::object::{Self};
  115. use sui::package::{Self};
  116. use sui::tx_context::{Self};
  117. use pyth::required_version::{Self};
  118. struct SomeMethod {}
  119. struct AnotherMethod {}
  120. #[test]
  121. public fun test_check_minimum_requirement() {
  122. let ctx = &mut tx_context::dummy();
  123. let version = 1;
  124. let req = required_version::new(version, ctx);
  125. assert!(required_version::current(&req) == version, 0);
  126. required_version::add<SomeMethod>(&mut req);
  127. assert!(required_version::minimum_for<SomeMethod>(&req) == version, 0);
  128. // Should not abort here.
  129. required_version::check_minimum_requirement<SomeMethod>(&req, version);
  130. // And should not abort if the version is anything greater than the
  131. // current.
  132. let new_version = version + 1;
  133. required_version::check_minimum_requirement<SomeMethod>(
  134. &req,
  135. new_version
  136. );
  137. // Uptick based on new upgrade.
  138. let upgrade_cap = package::test_publish(
  139. object::id_from_address(@pyth),
  140. ctx
  141. );
  142. let digest = keccak256(&x"DEADBEEF");
  143. let policy = package::upgrade_policy(&upgrade_cap);
  144. let upgrade_ticket =
  145. package::authorize_upgrade(&mut upgrade_cap, policy, digest);
  146. let upgrade_receipt = package::test_upgrade(upgrade_ticket);
  147. package::commit_upgrade(&mut upgrade_cap, upgrade_receipt);
  148. assert!(package::version(&upgrade_cap) == new_version, 0);
  149. // Update to the latest version.
  150. required_version::update_latest(&mut req, &upgrade_cap);
  151. assert!(required_version::current(&req) == new_version, 0);
  152. // Should still not abort here.
  153. required_version::check_minimum_requirement<SomeMethod>(
  154. &req,
  155. new_version
  156. );
  157. // Require new version for `SomeMethod` and show that
  158. // `check_minimum_requirement` still succeeds.
  159. required_version::require_current_version<SomeMethod>(&mut req);
  160. assert!(
  161. required_version::minimum_for<SomeMethod>(&req) == new_version,
  162. 0
  163. );
  164. required_version::check_minimum_requirement<SomeMethod>(
  165. &req,
  166. new_version
  167. );
  168. // If another method gets added to the mix, it should automatically meet
  169. // the minimum requirement because its version will be the latest.
  170. required_version::add<AnotherMethod>(&mut req);
  171. assert!(
  172. required_version::minimum_for<AnotherMethod>(&req) == new_version,
  173. 0
  174. );
  175. required_version::check_minimum_requirement<SomeMethod>(
  176. &req,
  177. new_version
  178. );
  179. // Clean up.
  180. package::make_immutable(upgrade_cap);
  181. required_version::destroy(req);
  182. }
  183. #[test]
  184. #[expected_failure(abort_code = required_version::E_OUTDATED_VERSION)]
  185. public fun test_cannot_check_minimum_requirement_with_outdated_version() {
  186. let ctx = &mut tx_context::dummy();
  187. let version = 1;
  188. let req = required_version::new(version, ctx);
  189. assert!(required_version::current(&req) == version, 0);
  190. required_version::add<SomeMethod>(&mut req);
  191. // Should not abort here.
  192. required_version::check_minimum_requirement<SomeMethod>(&req, version);
  193. // Uptick minimum requirement and fail at `check_minimum_requirement`.
  194. let new_version = 10;
  195. required_version::set_required_version<SomeMethod>(
  196. &mut req,
  197. new_version
  198. );
  199. let old_version = new_version - 1;
  200. required_version::check_minimum_requirement<SomeMethod>(
  201. &req,
  202. old_version
  203. );
  204. // Clean up.
  205. required_version::destroy(req);
  206. }
  207. }