diff --git a/move-starter-kit/.gitignore b/move-starter-kit/.gitignore new file mode 100644 index 0000000..243f545 --- /dev/null +++ b/move-starter-kit/.gitignore @@ -0,0 +1,2 @@ +.aptos/ +build/ \ No newline at end of file diff --git a/move-starter-kit/ChainlinkDataFeeds/Move.toml b/move-starter-kit/ChainlinkDataFeeds/Move.toml new file mode 100644 index 0000000..6400226 --- /dev/null +++ b/move-starter-kit/ChainlinkDataFeeds/Move.toml @@ -0,0 +1,20 @@ +[package] +name = "ChainlinkDataFeeds" +version = "1.0.0" +authors = [] + +[addresses] +data_feeds = "_" +owner = "_" +platform = "_" + +[dev-addresses] +data_feeds = "0x100" +owner = "0xcafe" +platform = "0xbaba" + +[dependencies] +AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", rev = "mainnet", subdir = "aptos-move/framework/aptos-framework" } +ChainlinkPlatform = { local = "../ChainlinkPlatform" } + +[dev-dependencies] diff --git a/move-starter-kit/ChainlinkDataFeeds/sources/registry.move b/move-starter-kit/ChainlinkDataFeeds/sources/registry.move new file mode 100644 index 0000000..654f1e5 --- /dev/null +++ b/move-starter-kit/ChainlinkDataFeeds/sources/registry.move @@ -0,0 +1,812 @@ +module data_feeds::registry { + use std::error; + use std::event; + use std::option; + use std::signer; + use std::simple_map::{Self, SimpleMap}; + use std::string::{Self, String}; + use std::vector; + + use aptos_framework::object::{Self, ExtendRef, TransferRef, Object}; + + friend data_feeds::router; + + const APP_OBJECT_SEED: vector = b"REGISTRY"; + + struct Registry has key, store, drop { + extend_ref: ExtendRef, + transfer_ref: TransferRef, + owner_address: address, + pending_owner_address: address, + feeds: SimpleMap, Feed>, + allowed_workflow_owners: vector>, + allowed_workflow_names: vector> + } + + struct Feed has key, store, drop, copy { + description: String, + config_id: vector, + benchmark: u256, + report: vector, + observation_timestamp: u256 + } + + struct Benchmark has store, drop { + benchmark: u256, + observation_timestamp: u256 + } + + struct Report has store, drop { + report: vector, + observation_timestamp: u256 + } + + struct FeedMetadata has store, drop, key { + description: String, + config_id: vector + } + + struct WorkflowConfig { + allowed_workflow_owners: vector>, + allowed_workflow_names: vector> + } + + struct FeedConfig { + feed_id: vector, + feed: Feed + } + + #[event] + struct FeedDescriptionUpdated has drop, store { + feed_id: vector, + description: String + } + + #[event] + struct FeedRemoved has drop, store { + feed_id: vector + } + + #[event] + struct FeedSet has drop, store { + feed_id: vector, + description: String, + config_id: vector + } + + #[event] + struct FeedUpdated has drop, store { + feed_id: vector, + timestamp: u256, + benchmark: u256, + report: vector + } + + #[event] + struct StaleReport has drop, store { + feed_id: vector, + latest_timestamp: u256, + report_timestamp: u256 + } + + #[event] + struct OwnershipTransferRequested has drop, store { + from: address, + to: address + } + + #[event] + struct OwnershipTransferred has drop, store { + from: address, + to: address + } + + // Errors + const ENOT_OWNER: u64 = 1; + const EDUPLICATE_ELEMENTS: u64 = 2; + const EFEED_EXISTS: u64 = 3; + const EFEED_NOT_CONFIGURED: u64 = 4; + const ECONFIG_NOT_CONFIGURED: u64 = 5; + const EUNEQUAL_ARRAY_LENGTHS: u64 = 6; + const EINVALID_REPORT: u64 = 7; + const EUNAUTHORIZED_WORKFLOW_NAME: u64 = 8; + const EUNAUTHORIZED_WORKFLOW_OWNER: u64 = 9; + const ECANNOT_TRANSFER_TO_SELF: u64 = 10; + const ENOT_PROPOSED_OWNER: u64 = 11; + const EEMPTY_WORKFLOW_OWNERS: u64 = 12; + + // Schema types + const SCHEMA_V3: u16 = 3; + const SCHEMA_V4: u16 = 4; + + inline fun assert_is_owner( + registry: &Registry, target_address: address + ) { + assert!( + registry.owner_address == target_address, + error::permission_denied(ENOT_OWNER) + ); + } + + fun assert_no_duplicates(a: &vector) { + let len = vector::length(a); + for (i in 0..len) { + for (j in (i + 1)..len) { + assert!( + vector::borrow(a, i) != vector::borrow(a, j), + error::invalid_argument(EDUPLICATE_ELEMENTS) + ); + } + } + } + + fun init_module(publisher: &signer) { + assert!(signer::address_of(publisher) == @data_feeds, 1); + + let constructor_ref = object::create_named_object(publisher, APP_OBJECT_SEED); + + let extend_ref = object::generate_extend_ref(&constructor_ref); + let transfer_ref = object::generate_transfer_ref(&constructor_ref); + let object_signer = object::generate_signer(&constructor_ref); + + // register to receive platform::forwarder reports + let cb = + aptos_framework::function_info::new_function_info( + publisher, + string::utf8(b"registry"), + string::utf8(b"on_report") + ); + platform::storage::register(publisher, cb, new_proof()); + + move_to( + &object_signer, + Registry { + owner_address: @owner, + pending_owner_address: @0x0, + extend_ref, + transfer_ref, + feeds: simple_map::new(), + allowed_workflow_names: vector[], + allowed_workflow_owners: vector[] + } + ); + } + + inline fun get_state_addr(): address { + object::create_object_address(&@data_feeds, APP_OBJECT_SEED) + } + + public entry fun set_feeds( + authority: &signer, + feed_ids: vector>, + descriptions: vector, + config_id: vector + ) acquires Registry { + let registry = borrow_global_mut(get_state_addr()); + assert_is_owner(registry, signer::address_of(authority)); + set_feeds_internal(registry, feed_ids, descriptions, config_id); + } + + public(friend) fun set_feeds_unchecked( + feed_ids: vector>, + descriptions: vector, + config_id: vector + ) acquires Registry { + let registry = borrow_global_mut(get_state_addr()); + set_feeds_internal(registry, feed_ids, descriptions, config_id); + } + + fun set_feeds_internal( + registry: &mut Registry, + feed_ids: vector>, + descriptions: vector, + config_id: vector + ) { + assert_no_duplicates(&feed_ids); + + assert!( + vector::length(&feed_ids) == vector::length(&descriptions), + error::invalid_argument(EUNEQUAL_ARRAY_LENGTHS) + ); + + vector::zip_ref( + &feed_ids, + &descriptions, + |feed_id, description| { + assert!( + !simple_map::contains_key(®istry.feeds, feed_id), + error::invalid_argument(EFEED_EXISTS) + ); + + let feed = Feed { + description: *description, + config_id, + benchmark: 0, + report: vector::empty(), + observation_timestamp: 0 + }; + simple_map::add(&mut registry.feeds, *feed_id, feed); + + event::emit( + FeedSet { feed_id: *feed_id, description: *description, config_id } + ); + } + ); + } + + public entry fun remove_feeds( + authority: &signer, feed_ids: vector> + ) acquires Registry { + let registry = borrow_global_mut(get_state_addr()); + assert_is_owner(registry, signer::address_of(authority)); + + assert_no_duplicates(&feed_ids); + + vector::for_each( + feed_ids, + |feed_id| { + assert!( + simple_map::contains_key(®istry.feeds, &feed_id), + error::invalid_argument(EFEED_NOT_CONFIGURED) + ); + simple_map::remove(&mut registry.feeds, &feed_id); + } + ); + } + + public entry fun update_descriptions( + authority: &signer, feed_ids: vector>, descriptions: vector + ) acquires Registry { + let registry = borrow_global_mut(get_state_addr()); + assert_is_owner(registry, signer::address_of(authority)); + + assert!( + vector::length(&feed_ids) == vector::length(&descriptions), + error::invalid_argument(EUNEQUAL_ARRAY_LENGTHS) + ); + + vector::zip_ref( + &feed_ids, + &descriptions, + |feed_id, description| { + assert!( + simple_map::contains_key(®istry.feeds, feed_id), + error::invalid_argument(EFEED_NOT_CONFIGURED) + ); + + let feed = simple_map::borrow_mut(&mut registry.feeds, feed_id); + feed.description = *description; + + event::emit( + FeedDescriptionUpdated { feed_id: *feed_id, description: *description } + ); + } + ); + } + + inline fun to_u16be(data: vector): u16 { + // reverse big endian to little endian + vector::reverse(&mut data); + aptos_std::from_bcs::to_u16(data) + } + + inline fun to_u32be(data: vector): u32 { + // reverse big endian to little endian + vector::reverse(&mut data); + aptos_std::from_bcs::to_u32(data) + } + + inline fun to_u256be(data: vector): u256 { + // reverse big endian to little endian + vector::reverse(&mut data); + aptos_std::from_bcs::to_u256(data) + } + + /// Serves as a proof type for the dispatch engine, used to authenticate and handle incoming message callbacks. + /// This identifier links callback registration with the `on_report` event and enables secure retrieval of callback data. + /// Only has the `drop` ability to prevent copying and persisting in global storage. + struct OnReceive has drop {} + + /// Creates a new OnReceive object. + inline fun new_proof(): OnReceive { + OnReceive {} + } + + // Platform receiver function interface + public fun on_report(_metadata: Object): option::Option acquires Registry { + let registry = borrow_global_mut(get_state_addr()); + + let (metadata, data) = platform::storage::retrieve(new_proof()); + + let parsed_metadata = platform::storage::parse_report_metadata(metadata); + + let workflow_owner = + platform::storage::get_report_metadata_workflow_owner(&parsed_metadata); + assert!( + vector::contains(®istry.allowed_workflow_owners, &workflow_owner), + EUNAUTHORIZED_WORKFLOW_OWNER + ); + + let workflow_name = + platform::storage::get_report_metadata_workflow_name(&parsed_metadata); + assert!( + vector::is_empty(®istry.allowed_workflow_names) + || vector::contains(®istry.allowed_workflow_names, &workflow_name), + EUNAUTHORIZED_WORKFLOW_NAME + ); + + let (feed_ids, reports) = parse_raw_report(data); + vector::zip_ref( + &feed_ids, + &reports, + |feed_id, report| { + perform_update(registry, *feed_id, *report); + } + ); + + option::none() + } + + public entry fun set_workflow_config( + authority: &signer, + allowed_workflow_owners: vector>, + allowed_workflow_names: vector> + ) acquires Registry { + let registry = borrow_global_mut(get_state_addr()); + assert_is_owner(registry, signer::address_of(authority)); + assert!( + !vector::is_empty(&allowed_workflow_owners), + error::invalid_argument(EEMPTY_WORKFLOW_OWNERS) + ); + + registry.allowed_workflow_owners = allowed_workflow_owners; + registry.allowed_workflow_names = allowed_workflow_names; + } + + #[view] + public fun get_workflow_config(): WorkflowConfig acquires Registry { + let registry = borrow_global(get_state_addr()); + + WorkflowConfig { + allowed_workflow_owners: registry.allowed_workflow_owners, + allowed_workflow_names: registry.allowed_workflow_names + } + } + + #[view] + public fun get_feeds(): vector acquires Registry { + let registry = borrow_global(get_state_addr()); + let feed_configs = vector[]; + let (feed_ids, feeds) = simple_map::to_vec_pair(registry.feeds); + vector::zip_ref( + &feed_ids, + &feeds, + |feed_id, feed| { + vector::push_back( + &mut feed_configs, + FeedConfig { feed_id: *feed_id, feed: *feed } + ); + } + ); + feed_configs + } + + // Parse ETH ABI encoded raw data into multiple reports + fun parse_raw_report(data: vector): (vector>, vector>) { + let offset = 0; + assert!( + to_u256be(vector::slice(&data, offset, offset + 32)) == 32, + 32 + ); + offset = offset + 32; + + let count = to_u256be(vector::slice(&data, offset, offset + 32)); + offset = offset + 32; + + for (i in 0..count) { + // skip len * offsets table + offset = offset + 32; + }; + + let feed_ids = vector[]; + let reports = vector[]; + + for (i in 0..count) { + let feed_id = vector::slice(&data, offset, offset + 32); + vector::push_back(&mut feed_ids, feed_id); + offset = offset + 32; + + assert!( + to_u256be(vector::slice(&data, offset, offset + 32)) == 64, + 64 + ); + offset = offset + 32; + + let len = (to_u256be(vector::slice(&data, offset, offset + 32)) as u64); + offset = offset + 32; + + let report = vector::slice(&data, offset, offset + len); + vector::push_back(&mut reports, report); + offset = offset + len; + }; + + (feed_ids, reports) + } + + fun perform_update( + registry: &mut Registry, feed_id: vector, report_data: vector + ) { + assert!( + simple_map::contains_key(®istry.feeds, &feed_id), + error::invalid_argument(EFEED_NOT_CONFIGURED) + ); + let feed = simple_map::borrow_mut(&mut registry.feeds, &feed_id); + + let report_feed_id = vector::slice(&report_data, 0, 32); + // schema is based on first two bytes of the feed id + let schema = to_u16be(vector::slice(&report_feed_id, 0, 2)); + + let observation_timestamp: u256; + let benchmark_price: u256; + if (schema == SCHEMA_V3 || schema == SCHEMA_V4) { + // offsets are the same for timestamp and benchmark in v3 and v4. + observation_timestamp = ( + to_u32be(vector::slice(&report_data, 3 * 32 - 4, 3 * 32)) as u256 + ); + // NOTE: aptos has no signed integer types, so can't parse as i196, this is a raw representation + benchmark_price = to_u256be(vector::slice(&report_data, 6 * 32, 7 * 32)); + } else { + abort error::invalid_argument(EINVALID_REPORT) + }; + + if (feed.observation_timestamp >= observation_timestamp) { + event::emit( + StaleReport { + feed_id, + latest_timestamp: feed.observation_timestamp, + report_timestamp: observation_timestamp + } + ); + }; + + feed.observation_timestamp = observation_timestamp; + feed.benchmark = benchmark_price; + feed.report = report_data; + + event::emit( + FeedUpdated { + feed_id, + timestamp: observation_timestamp, + benchmark: benchmark_price, + report: report_data + } + ); + } + + // Getters + + public fun get_benchmarks( + authority: &signer, feed_ids: vector> + ): vector acquires Registry { + let registry = borrow_global(get_state_addr()); + assert_is_owner(registry, signer::address_of(authority)); + get_benchmarks_internal(registry, feed_ids) + } + + public(friend) fun get_benchmarks_unchecked( + feed_ids: vector> + ): vector acquires Registry { + let registry = borrow_global(get_state_addr()); + get_benchmarks_internal(registry, feed_ids) + } + + fun get_benchmarks_internal( + registry: &Registry, feed_ids: vector> + ): vector { + vector::map( + feed_ids, + |feed_id| { + assert!( + simple_map::contains_key(®istry.feeds, &feed_id), + error::invalid_argument(EFEED_NOT_CONFIGURED) + ); + let feed = simple_map::borrow(®istry.feeds, &feed_id); + Benchmark { + benchmark: feed.benchmark, + observation_timestamp: feed.observation_timestamp + } + } + ) + } + + public fun get_reports( + authority: &signer, feed_ids: vector> + ): vector acquires Registry { + let registry = borrow_global(get_state_addr()); + assert_is_owner(registry, signer::address_of(authority)); + get_reports_internal(registry, feed_ids) + } + + public(friend) fun get_reports_unchecked( + feed_ids: vector> + ): vector acquires Registry { + let registry = borrow_global(get_state_addr()); + get_reports_internal(registry, feed_ids) + } + + fun get_reports_internal( + registry: &Registry, feed_ids: vector> + ): vector { + vector::map( + feed_ids, + |feed_id| { + assert!( + simple_map::contains_key(®istry.feeds, &feed_id), + error::invalid_argument(EFEED_NOT_CONFIGURED) + ); + + let feed = simple_map::borrow(®istry.feeds, &feed_id); + Report { + report: feed.report, + observation_timestamp: feed.observation_timestamp + } + } + ) + } + + #[view] + public fun get_feed_metadata( + feed_ids: vector> + ): vector acquires Registry { + let registry = borrow_global(get_state_addr()); + + vector::map( + feed_ids, + |feed_id| { + assert!( + simple_map::contains_key(®istry.feeds, &feed_id), + error::invalid_argument(EFEED_NOT_CONFIGURED) + ); + + let feed = simple_map::borrow(®istry.feeds, &feed_id); + + FeedMetadata { description: feed.description, config_id: feed.config_id } + } + ) + } + + // Ownership functions + + #[view] + public fun get_owner(): address acquires Registry { + let registry = borrow_global(get_state_addr()); + registry.owner_address + } + + public entry fun transfer_ownership(authority: &signer, to: address) acquires Registry { + let registry = borrow_global_mut(get_state_addr()); + assert_is_owner(registry, signer::address_of(authority)); + assert!( + registry.owner_address != to, + error::invalid_argument(ECANNOT_TRANSFER_TO_SELF) + ); + + registry.pending_owner_address = to; + + event::emit(OwnershipTransferRequested { from: registry.owner_address, to }); + } + + public entry fun accept_ownership(authority: &signer) acquires Registry { + let registry = borrow_global_mut(get_state_addr()); + assert!( + registry.pending_owner_address == signer::address_of(authority), + error::permission_denied(ENOT_PROPOSED_OWNER) + ); + + let old_owner_address = registry.owner_address; + registry.owner_address = registry.pending_owner_address; + registry.pending_owner_address = @0x0; + + event::emit( + OwnershipTransferred { from: old_owner_address, to: registry.owner_address } + ); + } + + // Struct accessors + + public fun get_benchmark_value(result: &Benchmark): u256 { + result.benchmark + } + + public fun get_benchmark_timestamp(result: &Benchmark): u256 { + result.observation_timestamp + } + + public fun get_report_value(result: &Report): vector { + result.report + } + + public fun get_report_timestamp(result: &Report): u256 { + result.observation_timestamp + } + + public fun get_feed_metadata_description(result: &FeedMetadata): String { + result.description + } + + public fun get_feed_metadata_config_id(result: &FeedMetadata): vector { + result.config_id + } + + #[test] + fun test_parse_raw_report() { + // request_context = 00018463f564e082c55b7237add2a03bd6b3c35789d38be0f6964d9aba82f1a8000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000 + // metadata = 1019256d85b84c7ba85cd9b7bb94fe15b73d7ec99e3cc0f470ee5dd2a1eaac88c000000000000000000000000bc3a8582cc08d3df797ab13a6c567eadb2517b3f0f931b7145b218016bf9dde43030303045544842544300000000000000000000000000000000000000aa00010 + // 0000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c + // raw report = + // 0000000000000000000000000000000000000000000000000000000000000020 32 + // 0000000000000000000000000000000000000000000000000000000000000002 len=2 + // 0000000000000000000000000000000000000000000000000000000000000040 offset + // 00000000000000000000000000000000000000000000000000000000000001c0 offset + // 0003111111111111111100000000000000000000000000000000000000000000 feed_id + // 0000000000000000000000000000000000000000000000000000000000000040 offset + // 0000000000000000000000000000000000000000000000000000000000000120 len=228 + // 0003111111111111111100000000000000000000000000000000000000000000 + // 0000000000000000000000000000000000000000000000000000000066b3a12c + // 0000000000000000000000000000000000000000000000000000000066b3a12c + // 00000000000000000000000000000000000000000000000000000000000494a8 + // 00000000000000000000000000000000000000000000000000000000000494a8 + // 0000000000000000000000000000000000000000000000000000000066c2e36c + // 00000000000000000000000000000000000000000000000000000000000494a8 + // 00000000000000000000000000000000000000000000000000000000000494a8 + // 00000000000000000000000000000000000000000000000000000000000494a8 + // 0003222222222222222200000000000000000000000000000000000000000000 feed_id + // 0000000000000000000000000000000000000000000000000000000000000040 offset + // 0000000000000000000000000000000000000000000000000000000000000120 len=228 + // 0003222222222222222200000000000000000000000000000000000000000000 + // 0000000000000000000000000000000000000000000000000000000066b3a12c + // 0000000000000000000000000000000000000000000000000000000066b3a12c + // 00000000000000000000000000000000000000000000000000000000000494a8 + // 00000000000000000000000000000000000000000000000000000000000494a8 + // 0000000000000000000000000000000000000000000000000000000066c2e36c + // 00000000000000000000000000000000000000000000000000000000000494a8 + // 00000000000000000000000000000000000000000000000000000000000494a8 + // 00000000000000000000000000000000000000000000000000000000000494a8 + + let data = + x"00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c000031111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000012000031111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066b3a12c0000000000000000000000000000000000000000000000000000000066b3a12c00000000000000000000000000000000000000000000000000000000000494a800000000000000000000000000000000000000000000000000000000000494a80000000000000000000000000000000000000000000000000000000066c2e36c00000000000000000000000000000000000000000000000000000000000494a800000000000000000000000000000000000000000000000000000000000494a800000000000000000000000000000000000000000000000000000000000494a800032222222222222222000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000012000032222222222222222000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066b3a12c0000000000000000000000000000000000000000000000000000000066b3a12c00000000000000000000000000000000000000000000000000000000000494a800000000000000000000000000000000000000000000000000000000000494a80000000000000000000000000000000000000000000000000000000066c2e36c00000000000000000000000000000000000000000000000000000000000494a800000000000000000000000000000000000000000000000000000000000494a800000000000000000000000000000000000000000000000000000000000494a8"; + + let (feed_ids, reports) = parse_raw_report(data); + std::debug::print(&feed_ids); + std::debug::print(&reports); + + assert!( + feed_ids + == vector[ + x"0003111111111111111100000000000000000000000000000000000000000000", + x"0003222222222222222200000000000000000000000000000000000000000000" + ], + 1 + ); + + let expected_reports = vector[ + x"00031111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066b3a12c0000000000000000000000000000000000000000000000000000000066b3a12c00000000000000000000000000000000000000000000000000000000000494a800000000000000000000000000000000000000000000000000000000000494a80000000000000000000000000000000000000000000000000000000066c2e36c00000000000000000000000000000000000000000000000000000000000494a800000000000000000000000000000000000000000000000000000000000494a800000000000000000000000000000000000000000000000000000000000494a8", + x"00032222222222222222000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066b3a12c0000000000000000000000000000000000000000000000000000000066b3a12c00000000000000000000000000000000000000000000000000000000000494a800000000000000000000000000000000000000000000000000000000000494a80000000000000000000000000000000000000000000000000000000066c2e36c00000000000000000000000000000000000000000000000000000000000494a800000000000000000000000000000000000000000000000000000000000494a800000000000000000000000000000000000000000000000000000000000494a8" + ]; + assert!(reports == expected_reports, 1); + } + + #[test_only] + fun set_up_test(publisher: &signer, platform: &signer) { + use aptos_framework::account::{Self}; + account::create_account_for_test(signer::address_of(publisher)); + + platform::forwarder::init_module_for_testing(platform); + platform::storage::init_module_for_testing(platform); + + init_module(publisher); + } + + #[test(owner = @owner, publisher = @data_feeds, platform = @platform)] + fun test_perform_update_v3( + owner: &signer, publisher: &signer, platform: &signer + ) acquires Registry { + set_up_test(publisher, platform); + + let report_data = + x"0003fbba4fce42f65d6032b18aee53efdf526cc734ad296cb57565979d883bdd0000000000000000000000000000000000000000000000000000000066ed173e0000000000000000000000000000000000000000000000000000000066ed174200000000000000007fffffffffffffffffffffffffffffffffffffffffffffff00000000000000007fffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000066ee68c2000000000000000000000000000000000000000000000d808cc35e6ed670bd00000000000000000000000000000000000000000000000d808590c35425347980000000000000000000000000000000000000000000000d8093f5f989878e7c00"; + let feed_id = vector::slice(&report_data, 0, 32); + let expected_timestamp = 0x000066ed1742; + let expected_benchmark = 0x000d808cc35e6ed670bd00; + + let config_id = vector[1]; + + set_feeds( + owner, + vector[feed_id], + vector[string::utf8(b"description")], + config_id + ); + + let registry = borrow_global_mut(get_state_addr()); + perform_update(registry, feed_id, report_data); + + let benchmarks = get_benchmarks(owner, vector[feed_id]); + assert!(vector::length(&benchmarks) == 1, 1); + + let benchmark = vector::borrow(&benchmarks, 0); + assert!(benchmark.benchmark == expected_benchmark, 1); + assert!(benchmark.observation_timestamp == expected_timestamp, 1); + } + + #[ + test( + owner = @owner, + publisher = @data_feeds, + platform = @platform, + new_owner = @0xbeef + ) + ] + fun test_transfer_ownership_success( + owner: &signer, + publisher: &signer, + platform: &signer, + new_owner: &signer + ) acquires Registry { + set_up_test(publisher, platform); + + assert!(get_owner() == @owner, 1); + + transfer_ownership(owner, signer::address_of(new_owner)); + accept_ownership(new_owner); + + assert!(get_owner() == signer::address_of(new_owner), 2); + } + + #[test(publisher = @data_feeds, platform = @platform, unknown_user = @0xbeef)] + #[expected_failure(abort_code = 327681, location = data_feeds::registry)] + fun test_transfer_ownership_failure_not_owner( + publisher: &signer, platform: &signer, unknown_user: &signer + ) acquires Registry { + set_up_test(publisher, platform); + + assert!(get_owner() == @owner, 1); + + transfer_ownership(unknown_user, signer::address_of(unknown_user)); + } + + #[test(owner = @owner, publisher = @data_feeds, platform = @platform)] + #[expected_failure(abort_code = 65546, location = data_feeds::registry)] + fun test_transfer_ownership_failure_transfer_to_self( + owner: &signer, publisher: &signer, platform: &signer + ) acquires Registry { + set_up_test(publisher, platform); + + assert!(get_owner() == @owner, 1); + + transfer_ownership(owner, signer::address_of(owner)); + } + + #[ + test( + owner = @owner, + publisher = @data_feeds, + platform = @platform, + new_owner = @0xbeef + ) + ] + #[expected_failure(abort_code = 327691, location = data_feeds::registry)] + fun test_transfer_ownership_failure_not_proposed_owner( + owner: &signer, + publisher: &signer, + platform: &signer, + new_owner: &signer + ) acquires Registry { + set_up_test(publisher, platform); + + assert!(get_owner() == @owner, 1); + + transfer_ownership(owner, @0xfeeb); + accept_ownership(new_owner); + } +} diff --git a/move-starter-kit/ChainlinkDataFeeds/sources/router.move b/move-starter-kit/ChainlinkDataFeeds/sources/router.move new file mode 100644 index 0000000..e8697d4 --- /dev/null +++ b/move-starter-kit/ChainlinkDataFeeds/sources/router.move @@ -0,0 +1,197 @@ +module data_feeds::router { + use std::error; + use std::event; + use std::signer; + use std::string::String; + use std::vector; + + use aptos_framework::object::{Self, ExtendRef, TransferRef}; + + use data_feeds::registry::{Self, Benchmark, Report}; + + const APP_OBJECT_SEED: vector = b"ROUTER"; + + struct Router has key, store, drop { + owner_address: address, + pending_owner_address: address, + extend_ref: ExtendRef, + transfer_ref: TransferRef + } + + #[event] + struct OwnershipTransferRequested has drop, store { + from: address, + to: address + } + + #[event] + struct OwnershipTransferred has drop, store { + from: address, + to: address + } + + const ENOT_OWNER: u64 = 0; + const ECANNOT_TRANSFER_TO_SELF: u64 = 1; + const ENOT_PROPOSED_OWNER: u64 = 2; + + fun assert_is_owner(router: &Router, target_address: address) { + assert!( + router.owner_address == target_address, error::invalid_argument(ENOT_OWNER) + ); + } + + fun init_module(publisher: &signer) { + assert!(signer::address_of(publisher) == @data_feeds, 1); + + let constructor_ref = object::create_named_object(publisher, APP_OBJECT_SEED); + + let extend_ref = object::generate_extend_ref(&constructor_ref); + let transfer_ref = object::generate_transfer_ref(&constructor_ref); + let object_signer = object::generate_signer(&constructor_ref); + + move_to( + &object_signer, + Router { + owner_address: @owner, + pending_owner_address: @0x0, + extend_ref, + transfer_ref + } + ); + } + + inline fun get_state_addr(): address { + object::create_object_address(&@data_feeds, APP_OBJECT_SEED) + } + + public fun get_benchmarks( + _authority: &signer, feed_ids: vector>, _billing_data: vector + ): vector acquires Router { + let _router = borrow_global(get_state_addr()); + + registry::get_benchmarks_unchecked(feed_ids) + } + + public fun get_reports( + _authority: &signer, feed_ids: vector>, _billing_data: vector + ): vector acquires Router { + let _router = borrow_global(get_state_addr()); + + registry::get_reports_unchecked(feed_ids) + } + + #[view] + public fun get_descriptions(feed_ids: vector>): vector acquires Router { + let _router = borrow_global(get_state_addr()); + + let results = registry::get_feed_metadata(feed_ids); + vector::map( + results, |metadata| registry::get_feed_metadata_description(&metadata) + ) + } + + public entry fun configure_feeds( + authority: &signer, + feed_ids: vector>, + descriptions: vector, + config_id: vector, + _fee_config_id: vector + ) acquires Router { + let router = borrow_global(get_state_addr()); + assert_is_owner(router, signer::address_of(authority)); + + registry::set_feeds_unchecked(feed_ids, descriptions, config_id); + } + + // Ownership functions + #[view] + public fun get_owner(): address acquires Router { + let router = borrow_global(get_state_addr()); + router.owner_address + } + + public entry fun transfer_ownership(authority: &signer, to: address) acquires Router { + let router = borrow_global_mut(get_state_addr()); + assert_is_owner(router, signer::address_of(authority)); + assert!( + router.owner_address != to, + error::invalid_argument(ECANNOT_TRANSFER_TO_SELF) + ); + + router.pending_owner_address = to; + + event::emit(OwnershipTransferRequested { from: router.owner_address, to }); + } + + public entry fun accept_ownership(authority: &signer) acquires Router { + let router = borrow_global_mut(get_state_addr()); + assert!( + router.pending_owner_address == signer::address_of(authority), + error::permission_denied(ENOT_PROPOSED_OWNER) + ); + + let old_owner_address = router.owner_address; + router.owner_address = router.pending_owner_address; + router.pending_owner_address = @0x0; + + event::emit( + OwnershipTransferred { from: old_owner_address, to: router.owner_address } + ); + } + + #[test_only] + fun set_up_test(publisher: &signer) { + init_module(publisher); + } + + #[test(owner = @owner, publisher = @data_feeds, new_owner = @0xbeef)] + fun test_transfer_ownership_success( + owner: &signer, publisher: &signer, new_owner: &signer + ) acquires Router { + set_up_test(publisher); + + assert!(get_owner() == @owner, 1); + + transfer_ownership(owner, signer::address_of(new_owner)); + accept_ownership(new_owner); + + assert!(get_owner() == signer::address_of(new_owner), 2); + } + + #[test(publisher = @data_feeds, unknown_user = @0xbeef)] + #[expected_failure(abort_code = 65536, location = data_feeds::router)] + fun test_transfer_ownership_failure_not_owner( + publisher: &signer, unknown_user: &signer + ) acquires Router { + set_up_test(publisher); + + assert!(get_owner() == @owner, 1); + + transfer_ownership(unknown_user, signer::address_of(unknown_user)); + } + + #[test(owner = @owner, publisher = @data_feeds)] + #[expected_failure(abort_code = 65537, location = data_feeds::router)] + fun test_transfer_ownership_failure_transfer_to_self( + owner: &signer, publisher: &signer + ) acquires Router { + set_up_test(publisher); + + assert!(get_owner() == @owner, 1); + + transfer_ownership(owner, signer::address_of(owner)); + } + + #[test(owner = @owner, publisher = @data_feeds, new_owner = @0xbeef)] + #[expected_failure(abort_code = 327682, location = data_feeds::router)] + fun test_transfer_ownership_failure_not_proposed_owner( + owner: &signer, publisher: &signer, new_owner: &signer + ) acquires Router { + set_up_test(publisher); + + assert!(get_owner() == @owner, 1); + + transfer_ownership(owner, @0xfeeb); + accept_ownership(new_owner); + } +} diff --git a/move-starter-kit/ChainlinkPlatform/Move.toml b/move-starter-kit/ChainlinkPlatform/Move.toml new file mode 100644 index 0000000..b6fa725 --- /dev/null +++ b/move-starter-kit/ChainlinkPlatform/Move.toml @@ -0,0 +1,17 @@ +[package] +name = "ChainlinkPlatform" +version = "1.0.0" +authors = [] + +[addresses] +platform = "_" +owner = "_" + +[dev-addresses] +platform = "0x100" +owner = "0xcafe" + +[dependencies] +AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", rev = "mainnet", subdir = "aptos-move/framework/aptos-framework" } + +[dev-dependencies] diff --git a/move-starter-kit/ChainlinkPlatform/sources/forwarder.move b/move-starter-kit/ChainlinkPlatform/sources/forwarder.move new file mode 100644 index 0000000..98b58ef --- /dev/null +++ b/move-starter-kit/ChainlinkPlatform/sources/forwarder.move @@ -0,0 +1,571 @@ +module platform::forwarder { + use aptos_framework::object::{Self, ExtendRef, TransferRef}; + use aptos_std::smart_table::{SmartTable, Self}; + + use std::error; + use std::event; + use std::vector; + use std::bit_vector; + use std::option::{Self, Option}; + use std::signer; + use std::bcs; + + const E_INVALID_DATA_LENGTH: u64 = 1; + const E_INVALID_SIGNER: u64 = 2; + const E_DUPLICATE_SIGNER: u64 = 3; + const E_INVALID_SIGNATURE_COUNT: u64 = 4; + const E_INVALID_SIGNATURE: u64 = 5; + const E_ALREADY_PROCESSED: u64 = 6; + const E_NOT_OWNER: u64 = 7; + const E_MALFORMED_SIGNATURE: u64 = 8; + const E_FAULT_TOLERANCE_MUST_BE_POSITIVE: u64 = 9; + const E_EXCESS_SIGNERS: u64 = 10; + const E_INSUFFICIENT_SIGNERS: u64 = 11; + const E_CALLBACK_DATA_NOT_CONSUMED: u64 = 12; + const E_CANNOT_TRANSFER_TO_SELF: u64 = 13; + const E_NOT_PROPOSED_OWNER: u64 = 14; + const E_CONFIG_ID_NOT_FOUND: u64 = 15; + const E_INVALID_REPORT_VERSION: u64 = 16; + + const MAX_ORACLES: u64 = 31; + + const APP_OBJECT_SEED: vector = b"FORWARDER"; + + struct ConfigId has key, store, drop, copy { + don_id: u32, + config_version: u32 + } + + struct State has key { + owner_address: address, + pending_owner_address: address, + extend_ref: ExtendRef, + transfer_ref: TransferRef, + + // (don_id, config_version) => config + configs: SmartTable, + reports: SmartTable, address> + } + + struct Config has key, store, drop, copy { + f: u8, + // oracles: SimpleMap, + oracles: vector + } + + #[event] + struct ConfigSet has drop, store { + don_id: u32, + config_version: u32, + f: u8, + signers: vector> + } + + #[event] + struct ReportProcessed has drop, store { + receiver: address, + workflow_execution_id: vector, + report_id: u16 + } + + #[event] + struct OwnershipTransferRequested has drop, store { + from: address, + to: address + } + + #[event] + struct OwnershipTransferred has drop, store { + from: address, + to: address + } + + inline fun assert_is_owner(state: &State, target_address: address) { + assert!( + state.owner_address == target_address, + error::permission_denied(E_NOT_OWNER) + ); + } + + fun init_module(publisher: &signer) { + assert!(signer::address_of(publisher) == @platform, 1); + + let constructor_ref = object::create_named_object(publisher, APP_OBJECT_SEED); + + let extend_ref = object::generate_extend_ref(&constructor_ref); + let transfer_ref = object::generate_transfer_ref(&constructor_ref); + let app_signer = &object::generate_signer(&constructor_ref); + + move_to( + app_signer, + State { + owner_address: @owner, + pending_owner_address: @0x0, + configs: smart_table::new(), + reports: smart_table::new(), + extend_ref, + transfer_ref + } + ); + } + + inline fun get_state_addr(): address { + object::create_object_address(&@platform, APP_OBJECT_SEED) + } + + public entry fun set_config( + authority: &signer, + don_id: u32, + config_version: u32, + f: u8, + oracles: vector> + ) acquires State { + let state = borrow_global_mut(get_state_addr()); + + assert_is_owner(state, signer::address_of(authority)); + + assert!(f != 0, error::invalid_argument(E_FAULT_TOLERANCE_MUST_BE_POSITIVE)); + assert!( + vector::length(&oracles) <= MAX_ORACLES, + error::invalid_argument(E_EXCESS_SIGNERS) + ); + assert!( + vector::length(&oracles) >= 3 * (f as u64) + 1, + error::invalid_argument(E_INSUFFICIENT_SIGNERS) + ); + + smart_table::upsert( + &mut state.configs, + ConfigId { don_id, config_version }, + Config { + f, + oracles: vector::map( + oracles, + |oracle| { ed25519::new_unvalidated_public_key_from_bytes(oracle) } + ) + } + ); + + event::emit( + ConfigSet { don_id, config_version, f, signers: oracles } + ); + } + + public entry fun clear_config( + authority: &signer, don_id: u32, config_version: u32 + ) acquires State { + let state = borrow_global_mut(get_state_addr()); + + assert_is_owner(state, signer::address_of(authority)); + + smart_table::remove(&mut state.configs, ConfigId { don_id, config_version }); + + event::emit( + ConfigSet { don_id, config_version, f: 0, signers: vector::empty() } + ); + } + + use aptos_std::aptos_hash::blake2b_256; + use aptos_std::ed25519; + + struct Signature has drop { + public_key: ed25519::UnvalidatedPublicKey, // TODO: pass signer index rather than key to save on space and gas? + sig: ed25519::Signature + } + + public fun signature_from_bytes(bytes: vector): Signature { + assert!( + vector::length(&bytes) == 96, + error::invalid_argument(E_MALFORMED_SIGNATURE) + ); + let public_key = + ed25519::new_unvalidated_public_key_from_bytes(vector::slice(&bytes, 0, 32)); + let sig = ed25519::new_signature_from_bytes(vector::slice(&bytes, 32, 96)); + Signature { sig, public_key } + } + + inline fun transmission_id( + receiver: address, workflow_execution_id: vector, report_id: u16 + ): vector { + let id = bcs::to_bytes(&receiver); + vector::append(&mut id, workflow_execution_id); + vector::append(&mut id, bcs::to_bytes(&report_id)); + id + } + + /// The dispatch call knows both storage and indirectly the callback, thus the separate module. + fun dispatch( + receiver: address, metadata: vector, data: vector + ) { + let meta = platform::storage::insert(receiver, metadata, data); + aptos_framework::dispatchable_fungible_asset::derived_supply(meta); + let obj_address = + object::object_address(&meta); + assert!( + !platform::storage::storage_exists(obj_address), + E_CALLBACK_DATA_NOT_CONSUMED + ); + } + + entry fun report( + transmitter: &signer, + receiver: address, + raw_report: vector, + signatures: vector> + ) acquires State { + let signatures = vector::map( + signatures, |signature| signature_from_bytes(signature) + ); + + let (metadata, data) = + validate_and_process_report(transmitter, receiver, raw_report, signatures); + // NOTE: unable to catch failure here + dispatch(receiver, metadata, data); + } + + inline fun to_u16be(data: vector): u16 { + // reverse big endian to little endian + vector::reverse(&mut data); + aptos_std::from_bcs::to_u16(data) + } + + inline fun to_u32be(data: vector): u32 { + // reverse big endian to little endian + vector::reverse(&mut data); + aptos_std::from_bcs::to_u32(data) + } + + fun validate_and_process_report( + transmitter: &signer, + receiver: address, + raw_report: vector, + signatures: vector + ): (vector, vector) acquires State { + let state = borrow_global_mut(get_state_addr()); + + // report_context = vector::slice(&raw_report, 0, 96); + let report = vector::slice(&raw_report, 96, vector::length(&raw_report)); + + // parse out report metadata + // version | workflow_execution_id | timestamp | don_id | config_version | ... + let report_version = *vector::borrow(&report, 0); + assert!(report_version == 1, E_INVALID_REPORT_VERSION); + + let workflow_execution_id = vector::slice(&report, 1, 33); + // _timestamp + let don_id = vector::slice(&report, 37, 41); + let don_id = to_u32be(don_id); + let config_version = vector::slice(&report, 41, 45); + let config_version = to_u32be(config_version); + let report_id = vector::slice(&report, 107, 109); + let report_id = to_u16be(report_id); + let metadata = vector::slice(&report, 45, 109); + let data = vector::slice(&report, 109, vector::length(&report)); + + let config_id = ConfigId { don_id, config_version }; + assert!(smart_table::contains(&state.configs, config_id), E_CONFIG_ID_NOT_FOUND); + let config = smart_table::borrow(&state.configs, config_id); + + // check if report was already delivered + let transmission_id = transmission_id(receiver, workflow_execution_id, report_id); + let processed = smart_table::contains(&state.reports, transmission_id); + assert!(!processed, E_ALREADY_PROCESSED); + + let required_signatures = (config.f as u64) + 1; + assert!( + vector::length(&signatures) == required_signatures, + error::invalid_argument(E_INVALID_SIGNATURE_COUNT) + ); + + // blake2b(report_context | report) + let msg = blake2b_256(raw_report); + + let signed = bit_vector::new(vector::length(&config.oracles)); + + vector::for_each_ref( + &signatures, + |signature| { + let signature: &Signature = signature; // some compiler versions can't infer the type here + + let (valid, index) = vector::index_of( + &config.oracles, &signature.public_key + ); + assert!(valid, error::invalid_argument(E_INVALID_SIGNER)); + + // check for duplicate signers + let duplicate = bit_vector::is_index_set(&signed, index); + assert!(!duplicate, error::invalid_argument(E_DUPLICATE_SIGNER)); + bit_vector::set(&mut signed, index); + + let result = + ed25519::signature_verify_strict( + &signature.sig, &signature.public_key, msg + ); + assert!(result, error::invalid_argument(E_INVALID_SIGNATURE)); + } + ); + + // mark as delivered + smart_table::add( + &mut state.reports, transmission_id, signer::address_of(transmitter) + ); + + event::emit(ReportProcessed { receiver, workflow_execution_id, report_id }); + + (metadata, data) + } + + #[view] + public fun get_transmission_state( + receiver: address, workflow_execution_id: vector, report_id: u16 + ): bool acquires State { + let state = borrow_global(get_state_addr()); + let transmission_id = transmission_id(receiver, workflow_execution_id, report_id); + + return smart_table::contains(&state.reports, transmission_id) + } + + #[view] + public fun get_transmitter( + receiver: address, workflow_execution_id: vector, report_id: u16 + ): Option
acquires State { + let state = borrow_global(get_state_addr()); + let transmission_id = transmission_id(receiver, workflow_execution_id, report_id); + + if (!smart_table::contains(&state.reports, transmission_id)) { + return option::none() + }; + option::some(*smart_table::borrow(&state.reports, transmission_id)) + } + + // Ownership functions + + #[view] + public fun get_owner(): address acquires State { + let state = borrow_global(get_state_addr()); + state.owner_address + } + + #[view] + public fun get_config(don_id: u32, config_version: u32): Config acquires State { + let state = borrow_global(get_state_addr()); + let config_id = ConfigId { don_id, config_version }; + *smart_table::borrow(&state.configs, config_id) + } + + public entry fun transfer_ownership(authority: &signer, to: address) acquires State { + let state = borrow_global_mut(get_state_addr()); + assert_is_owner(state, signer::address_of(authority)); + assert!( + state.owner_address != to, + error::invalid_argument(E_CANNOT_TRANSFER_TO_SELF) + ); + + state.pending_owner_address = to; + + event::emit(OwnershipTransferRequested { from: state.owner_address, to }); + } + + public entry fun accept_ownership(authority: &signer) acquires State { + let state = borrow_global_mut(get_state_addr()); + assert!( + state.pending_owner_address == signer::address_of(authority), + error::permission_denied(E_NOT_PROPOSED_OWNER) + ); + + let old_owner_address = state.owner_address; + state.owner_address = state.pending_owner_address; + state.pending_owner_address = @0x0; + + event::emit( + OwnershipTransferred { from: old_owner_address, to: state.owner_address } + ); + } + + #[test_only] + public fun init_module_for_testing(publisher: &signer) { + init_module(publisher); + } + + #[test_only] + public entry fun set_up_test(owner: &signer, publisher: &signer) { + use aptos_framework::account::{Self}; + account::create_account_for_test(signer::address_of(owner)); + account::create_account_for_test(signer::address_of(publisher)); + + init_module(publisher); + } + + #[test_only] + struct OracleSet has drop { + don_id: u32, + config_version: u32, + f: u8, + oracles: vector>, + signers: vector + } + + #[test_only] + fun generate_oracle_set(): OracleSet { + let don_id = 0; + let f = 1; + + let signers = vector[]; + let oracles = vector[]; + for (i in 0..31) { + let (sk, pk) = ed25519::generate_keys(); + vector::push_back(&mut signers, sk); + vector::push_back(&mut oracles, ed25519::validated_public_key_to_bytes(&pk)); + }; + OracleSet { don_id, config_version: 1, f, oracles, signers } + } + + #[test_only] + fun sign_report( + config: &OracleSet, report: vector, report_context: vector + ): vector { + // blake2b(report_context, report) + let msg = report_context; + vector::append(&mut msg, report); + let msg = blake2b_256(msg); + + let signatures = vector[]; + let required_signatures = config.f + 1; + for (i in 0..required_signatures) { + let config_signer = vector::borrow(&config.signers, (i as u64)); + let public_key = + ed25519::new_unvalidated_public_key_from_bytes( + *vector::borrow(&config.oracles, (i as u64)) + ); + let sig = ed25519::sign_arbitrary_bytes(config_signer, msg); + vector::push_back(&mut signatures, Signature { sig, public_key }); + }; + signatures + } + + #[test(owner = @owner, publisher = @platform)] + public entry fun test_happy_path(owner: &signer, publisher: &signer) acquires State { + set_up_test(owner, publisher); + + let config = generate_oracle_set(); + + // configure DON + set_config( + owner, + config.don_id, + config.config_version, + config.f, + config.oracles + ); + + // generate report + let version = 1; + let timestamp: u32 = 1; + let workflow_id = + x"6d795f6964000000000000000000000000000000000000000000000000000000"; + let workflow_name = x"000000000000DEADBEEF"; + let workflow_owner = x"0000000000000000000000000000000000000051"; + let report_id = x"0001"; + let execution_id = + x"6d795f657865637574696f6e5f69640000000000000000000000000000000000"; + let mercury_reports = vector[x"010203", x"aabbcc"]; + + let report = vector[]; + // header + vector::push_back(&mut report, version); + vector::append(&mut report, execution_id); + + let bytes = bcs::to_bytes(×tamp); + // convert little-endian to big-endian + vector::reverse(&mut bytes); + vector::append(&mut report, bytes); + + let bytes = bcs::to_bytes(&config.don_id); + // convert little-endian to big-endian + vector::reverse(&mut bytes); + vector::append(&mut report, bytes); + + let bytes = bcs::to_bytes(&config.config_version); + // convert little-endian to big-endian + vector::reverse(&mut bytes); + vector::append(&mut report, bytes); + + // metadata + vector::append(&mut report, workflow_id); + vector::append(&mut report, workflow_name); + vector::append(&mut report, workflow_owner); + vector::append(&mut report, report_id); + // report + vector::append(&mut report, bcs::to_bytes(&mercury_reports)); + + let report_context = + x"a0b000000000000000000000000000000000000000000000000000000000000a0b000000000000000000000000000000000000000000000000000000000000a0b000000000000000000000000000000000000000000000000000000000000000"; + assert!(vector::length(&report_context) == 96, 1); + + let raw_report = vector[]; + vector::append(&mut raw_report, report_context); + vector::append(&mut raw_report, report); + + // sign report + let signatures = sign_report(&config, report, report_context); + + // call entrypoint + validate_and_process_report( + owner, + signer::address_of(publisher), + raw_report, + signatures + ); + } + + #[test(owner = @owner, publisher = @platform, new_owner = @0xbeef)] + fun test_transfer_ownership_success( + owner: &signer, publisher: &signer, new_owner: &signer + ) acquires State { + set_up_test(owner, publisher); + + assert!(get_owner() == @owner, 1); + + transfer_ownership(owner, signer::address_of(new_owner)); + accept_ownership(new_owner); + + assert!(get_owner() == signer::address_of(new_owner), 2); + } + + #[test(owner = @owner, publisher = @platform, unknown_user = @0xbeef)] + #[expected_failure(abort_code = 327687, location = platform::forwarder)] + fun test_transfer_ownership_failure_not_owner( + owner: &signer, publisher: &signer, unknown_user: &signer + ) acquires State { + set_up_test(owner, publisher); + + assert!(get_owner() == @owner, 1); + + transfer_ownership(unknown_user, signer::address_of(unknown_user)); + } + + #[test(owner = @owner, publisher = @platform)] + #[expected_failure(abort_code = 65549, location = platform::forwarder)] + fun test_transfer_ownership_failure_transfer_to_self( + owner: &signer, publisher: &signer + ) acquires State { + set_up_test(owner, publisher); + + assert!(get_owner() == @owner, 1); + + transfer_ownership(owner, signer::address_of(owner)); + } + + #[test(owner = @owner, publisher = @platform, new_owner = @0xbeef)] + #[expected_failure(abort_code = 327694, location = platform::forwarder)] + fun test_transfer_ownership_failure_not_proposed_owner( + owner: &signer, publisher: &signer, new_owner: &signer + ) acquires State { + set_up_test(owner, publisher); + + assert!(get_owner() == @owner, 1); + + transfer_ownership(owner, @0xfeeb); + accept_ownership(new_owner); + } +} diff --git a/move-starter-kit/ChainlinkPlatform/sources/storage.move b/move-starter-kit/ChainlinkPlatform/sources/storage.move new file mode 100644 index 0000000..be72030 --- /dev/null +++ b/move-starter-kit/ChainlinkPlatform/sources/storage.move @@ -0,0 +1,517 @@ +/// The storage module stores all the state associated with the dispatch service. +module platform::storage { + use std::option; + use std::string; + use std::signer; + use std::vector; + + use aptos_std::table::{Self, Table}; + use aptos_std::smart_table::{SmartTable, Self}; + use aptos_std::type_info::{Self, TypeInfo}; + + use aptos_framework::dispatchable_fungible_asset; + use aptos_framework::function_info::FunctionInfo; + use aptos_framework::fungible_asset::{Self, Metadata}; + use aptos_framework::object::{Self, ExtendRef, TransferRef, Object}; + + const APP_OBJECT_SEED: vector = b"STORAGE"; + + friend platform::forwarder; + + const E_UNKNOWN_RECEIVER: u64 = 1; + const E_INVALID_METADATA_LENGTH: u64 = 2; + + struct Entry has key, store, drop { + metadata: Object, + extend_ref: ExtendRef + } + + struct Dispatcher has key { + /// Tracks the input type to the dispatch handler. + dispatcher: Table, + address_to_typeinfo: Table, + /// Used to store temporary data for dispatching. + extend_ref: ExtendRef, + transfer_ref: TransferRef + } + + struct DispatcherV2 has key { + dispatcher: SmartTable, + address_to_typeinfo: SmartTable + } + + /// Store the data to dispatch here. + struct Storage has drop, key { + metadata: vector, + data: vector + } + + struct ReportMetadata has key, store, drop { + workflow_cid: vector, + workflow_name: vector, + workflow_owner: vector, + report_id: vector + } + + /// Registers an account and callback for future dispatching, and a proof type `T` + /// for the callback function to retrieve arguments. Note that the function will + /// abort if the account has already been registered. + /// + /// The address of `account` is used to represent the callback by the dispatcher. + /// See the `dispatch` function in `forwarder.move`. + /// + /// Providing an instance of `T` guarantees that only a privileged module can call `register` for that type. + /// The type `T` should ideally only have the `drop` ability and no other abilities to prevent + /// copying and persisting in global storage. + public fun register( + account: &signer, callback: FunctionInfo, _proof: T + ) acquires Dispatcher, DispatcherV2 { + let typename = type_info::type_name(); + let constructor_ref = + object::create_named_object(&storage_signer(), *string::bytes(&typename)); + let extend_ref = object::generate_extend_ref(&constructor_ref); + let metadata = + fungible_asset::add_fungibility( + &constructor_ref, + option::none(), + // this was `typename` but it fails due to ENAME_TOO_LONG + string::utf8(b"storage"), + string::utf8(b"dis"), + 0, + string::utf8(b""), + string::utf8(b"") + ); + dispatchable_fungible_asset::register_derive_supply_dispatch_function( + &constructor_ref, option::some(callback) + ); + + let dispatcher = borrow_global_mut(storage_address()); + smart_table::add( + &mut dispatcher.dispatcher, + type_info::type_of(), + Entry { metadata, extend_ref } + ); + smart_table::add( + &mut dispatcher.address_to_typeinfo, + signer::address_of(account), + type_info::type_of() + ); + } + + public entry fun migrate_to_v2( + callback_addresses: vector
+ ) acquires Dispatcher, DispatcherV2 { + let addr = storage_address(); + + if (!exists(addr)) { + move_to( + &storage_signer(), + DispatcherV2 { + dispatcher: smart_table::new(), + address_to_typeinfo: smart_table::new() + } + ); + }; + + let dispatcher = borrow_global_mut(addr); + let dispatcher_v2 = borrow_global_mut(addr); + + vector::for_each_ref( + &callback_addresses, + |callback_address| { + // Aborts if the callback address does not exist. + let type_info = + table::remove( + &mut dispatcher.address_to_typeinfo, *callback_address + ); + let entry = table::remove(&mut dispatcher.dispatcher, type_info); + + smart_table::add( + &mut dispatcher_v2.address_to_typeinfo, + *callback_address, + type_info + ); + smart_table::add(&mut dispatcher_v2.dispatcher, type_info, entry); + } + ); + } + + /// Insert into this module as the callback needs to retrieve and avoid a cyclical dependency: + /// engine -> storage and then engine -> callback -> storage + public(friend) fun insert( + receiver: address, callback_metadata: vector, callback_data: vector + ): Object acquires Dispatcher, DispatcherV2 { + // TODO: delete this clause after migration completes + if (!exists(storage_address())) { + let dispatcher = borrow_global(storage_address()); + let typeinfo = *table::borrow(&dispatcher.address_to_typeinfo, receiver); + assert!( + table::contains(&dispatcher.dispatcher, typeinfo), + E_UNKNOWN_RECEIVER + ); + let Entry { metadata: asset_metadata, extend_ref } = + table::borrow(&dispatcher.dispatcher, typeinfo); + let obj_signer = object::generate_signer_for_extending(extend_ref); + move_to(&obj_signer, Storage { data: callback_data, metadata: callback_metadata }); + return *asset_metadata + }; + + let dispatcher = borrow_global(storage_address()); + let typeinfo = *smart_table::borrow(&dispatcher.address_to_typeinfo, receiver); + assert!( + smart_table::contains(&dispatcher.dispatcher, typeinfo), + E_UNKNOWN_RECEIVER + ); + let Entry { metadata: asset_metadata, extend_ref } = + smart_table::borrow(&dispatcher.dispatcher, typeinfo); + let obj_signer = object::generate_signer_for_extending(extend_ref); + move_to(&obj_signer, Storage { data: callback_data, metadata: callback_metadata }); + *asset_metadata + } + + public(friend) fun storage_exists(obj_address: address): bool { + object::object_exists(obj_address) + } + + /// Second half of the process for retrieving. This happens outside engine to prevent the + /// cyclical dependency. + public fun retrieve(_proof: T): (vector, vector) acquires Dispatcher, DispatcherV2, Storage { + // TODO: delete this clause after migration completes + if (!exists(storage_address())) { + let dispatcher = borrow_global(storage_address()); + let typeinfo = type_info::type_of(); + let Entry { metadata: _, extend_ref } = + table::borrow(&dispatcher.dispatcher, typeinfo); + let obj_address = object::address_from_extend_ref(extend_ref); + let data = move_from(obj_address); + return (data.metadata, data.data) + }; + let dispatcher = borrow_global(storage_address()); + let typeinfo = type_info::type_of(); + let Entry { metadata: _, extend_ref } = + smart_table::borrow(&dispatcher.dispatcher, typeinfo); + let obj_address = object::address_from_extend_ref(extend_ref); + let data = move_from(obj_address); + (data.metadata, data.data) + } + + #[view] + public fun parse_report_metadata(metadata: vector): ReportMetadata { + // workflow_cid // offset 0, size 32 + // workflow_name // offset 32, size 10 + // workflow_owner // offset 42, size 20 + // report_id // offset 62, size 2 + assert!(vector::length(&metadata) == 64, E_INVALID_METADATA_LENGTH); + + let workflow_cid = vector::slice(&metadata, 0, 32); + let workflow_name = vector::slice(&metadata, 32, 42); + let workflow_owner = vector::slice(&metadata, 42, 62); + let report_id = vector::slice(&metadata, 62, 64); + + ReportMetadata { workflow_cid, workflow_name, workflow_owner, report_id } + } + + /// Prepares the dispatch table. + fun init_module(publisher: &signer) { + assert!(signer::address_of(publisher) == @platform, 1); + + let constructor_ref = object::create_named_object(publisher, APP_OBJECT_SEED); + + let extend_ref = object::generate_extend_ref(&constructor_ref); + let transfer_ref = object::generate_transfer_ref(&constructor_ref); + let object_signer = object::generate_signer(&constructor_ref); + + move_to( + &object_signer, + Dispatcher { + dispatcher: table::new(), + address_to_typeinfo: table::new(), + extend_ref, + transfer_ref + } + ); + + move_to( + &object_signer, + DispatcherV2 { + dispatcher: smart_table::new(), + address_to_typeinfo: smart_table::new() + } + ); + } + + inline fun storage_address(): address { + object::create_object_address(&@platform, APP_OBJECT_SEED) + } + + inline fun storage_signer(): signer acquires Dispatcher { + object::generate_signer_for_extending( + &borrow_global(storage_address()).extend_ref + ) + } + + // Struct accessors + + public fun get_report_metadata_workflow_cid( + report_metadata: &ReportMetadata + ): vector { + report_metadata.workflow_cid + } + + public fun get_report_metadata_workflow_name( + report_metadata: &ReportMetadata + ): vector { + report_metadata.workflow_name + } + + public fun get_report_metadata_workflow_owner( + report_metadata: &ReportMetadata + ): vector { + report_metadata.workflow_owner + } + + public fun get_report_metadata_report_id( + report_metadata: &ReportMetadata + ): vector { + report_metadata.report_id + } + + #[test_only] + public fun init_module_for_testing(publisher: &signer) { + init_module(publisher); + } + + #[test] + fun test_parse_report_metadata() { + let metadata = + x"6d795f6964000000000000000000000000000000000000000000000000000000000000000000deadbeef00000000000000000000000000000000000000510001"; + let expected_workflow_cid = + x"6d795f6964000000000000000000000000000000000000000000000000000000"; + let expected_workflow_name = x"000000000000DEADBEEF"; + let expected_workflow_owner = x"0000000000000000000000000000000000000051"; + let expected_report_id = x"0001"; + + let parsed_metadata = parse_report_metadata(metadata); + assert!(parsed_metadata.workflow_cid == expected_workflow_cid, 1); + assert!(parsed_metadata.workflow_name == expected_workflow_name, 1); + assert!(parsed_metadata.workflow_owner == expected_workflow_owner, 1); + assert!(parsed_metadata.report_id == expected_report_id, 1); + } + + #[test_only] + fun init_module_deprecated(publisher: &signer) { + assert!(signer::address_of(publisher) == @platform, 1); + + let constructor_ref = object::create_named_object(publisher, APP_OBJECT_SEED); + + let extend_ref = object::generate_extend_ref(&constructor_ref); + let transfer_ref = object::generate_transfer_ref(&constructor_ref); + let object_signer = object::generate_signer(&constructor_ref); + + move_to( + &object_signer, + Dispatcher { + dispatcher: table::new(), + address_to_typeinfo: table::new(), + extend_ref, + transfer_ref + } + ); + } + + #[test_only] + fun register_deprecated( + account: &signer, callback: FunctionInfo, _proof: T + ) acquires Dispatcher { + let typename = type_info::type_name(); + let constructor_ref = + object::create_named_object(&storage_signer(), *string::bytes(&typename)); + let extend_ref = object::generate_extend_ref(&constructor_ref); + let metadata = + fungible_asset::add_fungibility( + &constructor_ref, + option::none(), + // this was `typename` but it fails due to ENAME_TOO_LONG + string::utf8(b"storage"), + string::utf8(b"dis"), + 0, + string::utf8(b""), + string::utf8(b"") + ); + dispatchable_fungible_asset::register_derive_supply_dispatch_function( + &constructor_ref, option::some(callback) + ); + + let dispatcher = borrow_global_mut(storage_address()); + table::add( + &mut dispatcher.dispatcher, + type_info::type_of(), + Entry { metadata, extend_ref } + ); + table::add( + &mut dispatcher.address_to_typeinfo, + signer::address_of(account), + type_info::type_of() + ); + } + + #[test_only] + struct TestProof has drop {} + + #[test_only] + struct TestProof2 has drop {} + + #[test_only] + struct TestProof3 has drop {} + + #[test_only] + struct TestProof4 has drop {} + + #[test_only] + public fun test_callback(_metadata: Object): option::Option { + option::none() + } + + #[test(publisher = @platform)] + fun test_v2_migration(publisher: &signer) acquires Dispatcher, DispatcherV2, Storage { + init_module_deprecated(publisher); + + let test_callback = + aptos_framework::function_info::new_function_info( + publisher, + string::utf8(b"storage"), + string::utf8(b"test_callback") + ); + + register_deprecated(publisher, test_callback, TestProof {}); + + let (derived_publisher, _) = + aptos_framework::account::create_resource_account( + publisher, b"TEST_V2_MIGRATION" + ); + let (derived_publisher2, _) = + aptos_framework::account::create_resource_account( + publisher, b"TEST_V2_MIGRATION_2" + ); + let (derived_publisher3, _) = + aptos_framework::account::create_resource_account( + publisher, b"TEST_V2_MIGRATION_3" + ); + + register_deprecated(&derived_publisher, test_callback, TestProof2 {}); + register_deprecated(&derived_publisher2, test_callback, TestProof3 {}); + + let callback_metadata = vector[1,2,3,4]; + let callback_data = vector[5,6,7,8,9]; + + // test initial migration + { + // test that insert and retrieve work before migration + insert(signer::address_of(publisher), callback_metadata, callback_data); + let (received_metadata, received_data) = retrieve(TestProof{}); + assert!(callback_metadata == received_metadata, 1); + assert!(callback_data == received_data, 1); + + let derived_addr = signer::address_of(&derived_publisher); + migrate_to_v2(vector[@platform, derived_addr]); + + // test that insert and retrieve still work after migration + insert(signer::address_of(publisher), callback_metadata, callback_data); + let (received_metadata, received_data) = retrieve(TestProof{}); + assert!(callback_metadata == received_metadata, 1); + assert!(callback_data == received_data, 1); + + let dispatcher = borrow_global(storage_address()); + assert!( + !table::contains( + &dispatcher.dispatcher, type_info::type_of() + ), + 1 + ); + assert!(!table::contains(&dispatcher.address_to_typeinfo, @platform), 1); + assert!( + !table::contains( + &dispatcher.dispatcher, type_info::type_of() + ), + 1 + ); + assert!(!table::contains(&dispatcher.address_to_typeinfo, derived_addr), 1); + + let dispatcher_v2 = borrow_global(storage_address()); + assert!( + smart_table::contains( + &dispatcher_v2.dispatcher, type_info::type_of() + ), + 1 + ); + assert!( + smart_table::contains(&dispatcher_v2.address_to_typeinfo, @platform), + 1 + ); + assert!( + smart_table::contains( + &dispatcher_v2.dispatcher, type_info::type_of() + ), + 1 + ); + assert!( + smart_table::contains(&dispatcher_v2.address_to_typeinfo, derived_addr), + 1 + ); + }; + + // migrate a second time, when DispatcherV2 already exists. + { + let derived_addr = signer::address_of(&derived_publisher2); + migrate_to_v2(vector[derived_addr]); + + let dispatcher = borrow_global(storage_address()); + assert!( + !table::contains( + &dispatcher.dispatcher, type_info::type_of() + ), + 1 + ); + assert!(!table::contains(&dispatcher.address_to_typeinfo, derived_addr), 1); + + let dispatcher_v2 = borrow_global(storage_address()); + assert!( + smart_table::contains( + &dispatcher_v2.dispatcher, type_info::type_of() + ), + 1 + ); + assert!( + smart_table::contains(&dispatcher_v2.address_to_typeinfo, derived_addr), + 1 + ); + }; + + // test the upgraded register function + { + let derived_addr = signer::address_of(&derived_publisher3); + register(&derived_publisher3, test_callback, TestProof4 {}); + + let dispatcher = borrow_global(storage_address()); + assert!( + !table::contains( + &dispatcher.dispatcher, type_info::type_of() + ), + 1 + ); + assert!(!table::contains(&dispatcher.address_to_typeinfo, derived_addr), 1); + + let dispatcher_v2 = borrow_global(storage_address()); + assert!( + smart_table::contains( + &dispatcher_v2.dispatcher, type_info::type_of() + ), + 1 + ); + assert!( + smart_table::contains(&dispatcher_v2.address_to_typeinfo, derived_addr), + 1 + ); + } + } +} diff --git a/move-starter-kit/Move.toml b/move-starter-kit/Move.toml new file mode 100644 index 0000000..bbed3bd --- /dev/null +++ b/move-starter-kit/Move.toml @@ -0,0 +1,19 @@ +[package] +name = "move_starter_kit" +version = "1.0.0" +authors = [] + +[addresses] +sender = "" +owner = "" +data_feeds = "0xf1099f135ddddad1c065203431be328a408b0ca452ada70374ce26bd2b32fdd3" +platform = "0x516e771e1b4a903afe74c27d057c65849ecc1383782f6642d7ff21425f4f9c99" +move_stdlib = "0x1" +aptos_std = "0x1" + +[dev-addresses] + +[dependencies] +AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework", rev = "main" } +MoveStdlib = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/move-stdlib", rev = "main" } +ChainlinkDataFeeds = { local = "./ChainlinkDataFeeds" } \ No newline at end of file diff --git a/move-starter-kit/README.md b/move-starter-kit/README.md new file mode 100644 index 0000000..fa58c49 --- /dev/null +++ b/move-starter-kit/README.md @@ -0,0 +1,53 @@ +# Move starter kit + +## prerequisites +1. Install [Aptos CLI](https://aptos.dev/en/build/cli)
+2. Config an account on Aptos testnet + +Create an aptos account on aptos testnet, and get test tokens from the [official faucet](https://aptos.dev/en/network/faucet). +``` +aptos init --network testnet --assume-yes +``` +Set the `sender` and `owner` as your generated `account` in the file `~/Move.toml`. The account address can be found in `~/.aptos/config.yaml` +```toml +[package] +name = "move_starter_kit" +version = "1.0.0" +authors = [] + +[addresses] +sender = +owner = +data_feeds = "0xf1099f135ddddad1c065203431be328a408b0ca452ada70374ce26bd2b32fdd3" +platform = "0x516e771e1b4a903afe74c27d057c65849ecc1383782f6642d7ff21425f4f9c99" +move_stdlib = "0x1" +aptos_std = "0x1" + +[dev-addresses] + +[dependencies] +AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework", rev = "main" } +MoveStdlib = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/move-stdlib", rev = "main" } +ChainlinkDataFeeds = { local = "./ChainlinkDataFeeds" } +``` + +## Data Feeds on Aptos +1. Deploy the module with command. +```shell +aptos move publish --skip-fetch-latest-git-deps +``` + +2. fetch the BTC/USD feed and save to the account's global storage. +```shell +aptos move run \ +--function-id '::price_feed_demo::fetch_price' \ +--args hex:0x01a0b4d920000332000000000000000000000000000000000000000000000000 +``` +Other data feed module addresses can be found [here](https://docs.chain.link/data-feeds/price-feeds/addresses?page=1&testnetPage=1&network=aptos). + +3. Retrieve this data using the view function. +```shell +aptos move view \ +--function-id '::price_feed_demo::get_price_data' \ +--args address: +``` \ No newline at end of file diff --git a/move-starter-kit/sources/price_feed_demo.move b/move-starter-kit/sources/price_feed_demo.move new file mode 100644 index 0000000..2d68755 --- /dev/null +++ b/move-starter-kit/sources/price_feed_demo.move @@ -0,0 +1,45 @@ +module sender::price_feed_demo { + use std::vector; + use std::signer; + use data_feeds::router::get_benchmarks; + use data_feeds::registry::{Benchmark, get_benchmark_value, get_benchmark_timestamp}; + use move_stdlib::option::{Option, some, none}; + + struct PriceData has copy, key, store { + /// The price value with 18 decimal places of precision + price: u256, + /// Unix timestamp in seconds + timestamp: u256, + } + + // Function to fetch and store the price data for a given feed ID + public entry fun fetch_price(account: &signer, feed_id: vector) acquires PriceData { + let feed_ids = vector[feed_id]; // Use the passed feed_id + let billing_data = vector[]; + let benchmarks: vector = get_benchmarks(account, feed_ids, billing_data); + let benchmark = vector::pop_back(&mut benchmarks); + let price: u256 = get_benchmark_value(&benchmark); + let timestamp: u256 = get_benchmark_timestamp(&benchmark); + + // Check if PriceData exists and update it + if (exists(signer::address_of(account))) { + let data = borrow_global_mut(signer::address_of(account)); + data.price = price; + data.timestamp = timestamp; + } else { + // If PriceData does not exist, create a new one + move_to(account, PriceData { price, timestamp }); + } + } + + // View function to get the stored price data + #[view] + public fun get_price_data(account_address: address): Option acquires PriceData { + if (exists(account_address)) { + let data = borrow_global(account_address); + some(*data) + } else { + none() + } + } +}