From d14c9acc91a7dc37a231df9894b24c9954fc4359 Mon Sep 17 00:00:00 2001 From: maxmysten Date: Mon, 16 Sep 2024 23:57:35 -0700 Subject: [PATCH] Adding Move On Sui Semgrep grammer (#508) * First instance of adding semgrep grammars for Move-on-Sui language. Currently everything in the tree-sitter grammer is fully supported and all tests pass. However, several things are missing from the tree-sitter grammar today: - Proper support for annotations - Support for scripts - The most recent changes to the Move language * Moving Move on Sui tree-sitter to my branch for accelerated developement * Added a package to stats, this has a lot of tests that are updated with move as this is a vscode highigher * Fixing Macro parsing bug due to meta variables eating the $ and 1 only upcase chars * updating tree-sitter-move-on-sui repo * Fixing Macros so now macro variables that start with $ parse. * Simplifying _identifier_or_metavariable * Adding ellipsis to module_access --- .gitmodules | 2 +- lang/Makefile | 3 +- lang/move-on-sui/projects.txt | 1 + lang/move-on-sui/test/ok/address.move | 79 ++ lang/move-on-sui/test/ok/annotations.move | 9 + lang/move-on-sui/test/ok/as_expressions.move | 71 ++ lang/move-on-sui/test/ok/ascii.move | 151 ++++ .../test/ok/authenticator_state.move | 406 +++++++++ lang/move-on-sui/test/ok/bag.move | 112 +++ lang/move-on-sui/test/ok/balance.move | 194 +++++ lang/move-on-sui/test/ok/bcs.move | 453 ++++++++++ lang/move-on-sui/test/ok/bit_vector.move | 164 ++++ lang/move-on-sui/test/ok/borrow.move | 121 +++ lang/move-on-sui/test/ok/clock.move | 96 +++ lang/move-on-sui/test/ok/coin.move | 426 ++++++++++ lang/move-on-sui/test/ok/damir1.move | 5 + lang/move-on-sui/test/ok/damir2.move | 5 + lang/move-on-sui/test/ok/debug.move | 9 + lang/move-on-sui/test/ok/display.move | 233 ++++++ lang/move-on-sui/test/ok/dot_call.move | 37 + .../test/ok/dotted_vector_literal_access.move | 5 + lang/move-on-sui/test/ok/dynamic_field.move | 269 ++++++ .../test/ok/dynamic_object_field.move | 115 +++ lang/move-on-sui/test/ok/event.move | 38 + lang/move-on-sui/test/ok/fixed_point32.move | 167 ++++ lang/move-on-sui/test/ok/hash.move | 11 + lang/move-on-sui/test/ok/hello-world.move | 35 + lang/move-on-sui/test/ok/hex.move | 111 +++ lang/move-on-sui/test/ok/implicit_uses.move | 10 + lang/move-on-sui/test/ok/let_mut.move | 9 + lang/move-on-sui/test/ok/linked_table.move | 200 +++++ ...iteral_address_module_and_pos_structs.move | 4 + lang/move-on-sui/test/ok/macro_calls.move | 75 ++ lang/move-on-sui/test/ok/macro_match.move | 32 + lang/move-on-sui/test/ok/math.move | 144 ++++ lang/move-on-sui/test/ok/module_labels.move | 5 + lang/move-on-sui/test/ok/module_uses.move | 22 + .../move-on-sui/test/ok/multi_index_test.move | 7 + .../test/ok/named_address_test.move | 6 + lang/move-on-sui/test/ok/native_structs.move | 5 + lang/move-on-sui/test/ok/new_syntax_file.move | 200 +++++ lang/move-on-sui/test/ok/object.move | 235 ++++++ lang/move-on-sui/test/ok/object_bag.move | 103 +++ lang/move-on-sui/test/ok/object_table.move | 98 +++ lang/move-on-sui/test/ok/option.move | 265 ++++++ lang/move-on-sui/test/ok/package.move | 318 +++++++ lang/move-on-sui/test/ok/pay.move | 89 ++ lang/move-on-sui/test/ok/priority_queue.move | 188 +++++ lang/move-on-sui/test/ok/prover.move | 77 ++ lang/move-on-sui/test/ok/random.move | 125 +++ lang/move-on-sui/test/ok/simple_module.move | 14 + lang/move-on-sui/test/ok/structs.move | 11 + lang/move-on-sui/test/ok/sui.move | 59 ++ lang/move-on-sui/test/ok/table.move | 102 +++ lang/move-on-sui/test/ok/table_vec.move | 105 +++ lang/move-on-sui/test/ok/token.move | 733 ++++++++++++++++ lang/move-on-sui/test/ok/transfer.move | 180 ++++ lang/move-on-sui/test/ok/tx_context.move | 152 ++++ lang/move-on-sui/test/ok/type_name.move | 92 ++ lang/move-on-sui/test/ok/types.move | 18 + lang/move-on-sui/test/ok/unit_test.move | 15 + lang/move-on-sui/test/ok/url.move | 35 + lang/move-on-sui/test/ok/vec_map.move | 195 +++++ lang/move-on-sui/test/ok/vec_set.move | 98 +++ lang/move-on-sui/test/ok/vector.move | 220 +++++ lang/move-on-sui/test/ok/versioned.move | 86 ++ .../src/semgrep-move-on-sui/grammar.js | 275 +++++- .../test/corpus/macros.txt | 265 ++++++ .../test/corpus/partials_and_decls.txt | 113 +++ .../test/corpus/semgrep.txt | 791 ++++++++++++++++++ .../src/tree-sitter-move-on-sui | 2 +- 71 files changed, 9097 insertions(+), 9 deletions(-) create mode 100644 lang/move-on-sui/test/ok/address.move create mode 100644 lang/move-on-sui/test/ok/annotations.move create mode 100644 lang/move-on-sui/test/ok/as_expressions.move create mode 100644 lang/move-on-sui/test/ok/ascii.move create mode 100644 lang/move-on-sui/test/ok/authenticator_state.move create mode 100644 lang/move-on-sui/test/ok/bag.move create mode 100644 lang/move-on-sui/test/ok/balance.move create mode 100644 lang/move-on-sui/test/ok/bcs.move create mode 100644 lang/move-on-sui/test/ok/bit_vector.move create mode 100644 lang/move-on-sui/test/ok/borrow.move create mode 100644 lang/move-on-sui/test/ok/clock.move create mode 100644 lang/move-on-sui/test/ok/coin.move create mode 100644 lang/move-on-sui/test/ok/damir1.move create mode 100644 lang/move-on-sui/test/ok/damir2.move create mode 100644 lang/move-on-sui/test/ok/debug.move create mode 100644 lang/move-on-sui/test/ok/display.move create mode 100644 lang/move-on-sui/test/ok/dot_call.move create mode 100644 lang/move-on-sui/test/ok/dotted_vector_literal_access.move create mode 100644 lang/move-on-sui/test/ok/dynamic_field.move create mode 100644 lang/move-on-sui/test/ok/dynamic_object_field.move create mode 100644 lang/move-on-sui/test/ok/event.move create mode 100644 lang/move-on-sui/test/ok/fixed_point32.move create mode 100644 lang/move-on-sui/test/ok/hash.move create mode 100644 lang/move-on-sui/test/ok/hello-world.move create mode 100644 lang/move-on-sui/test/ok/hex.move create mode 100644 lang/move-on-sui/test/ok/implicit_uses.move create mode 100644 lang/move-on-sui/test/ok/let_mut.move create mode 100644 lang/move-on-sui/test/ok/linked_table.move create mode 100644 lang/move-on-sui/test/ok/literal_address_module_and_pos_structs.move create mode 100644 lang/move-on-sui/test/ok/macro_calls.move create mode 100644 lang/move-on-sui/test/ok/macro_match.move create mode 100644 lang/move-on-sui/test/ok/math.move create mode 100644 lang/move-on-sui/test/ok/module_labels.move create mode 100644 lang/move-on-sui/test/ok/module_uses.move create mode 100644 lang/move-on-sui/test/ok/multi_index_test.move create mode 100644 lang/move-on-sui/test/ok/named_address_test.move create mode 100644 lang/move-on-sui/test/ok/native_structs.move create mode 100644 lang/move-on-sui/test/ok/new_syntax_file.move create mode 100644 lang/move-on-sui/test/ok/object.move create mode 100644 lang/move-on-sui/test/ok/object_bag.move create mode 100644 lang/move-on-sui/test/ok/object_table.move create mode 100644 lang/move-on-sui/test/ok/option.move create mode 100644 lang/move-on-sui/test/ok/package.move create mode 100644 lang/move-on-sui/test/ok/pay.move create mode 100644 lang/move-on-sui/test/ok/priority_queue.move create mode 100644 lang/move-on-sui/test/ok/prover.move create mode 100644 lang/move-on-sui/test/ok/random.move create mode 100644 lang/move-on-sui/test/ok/simple_module.move create mode 100644 lang/move-on-sui/test/ok/structs.move create mode 100644 lang/move-on-sui/test/ok/sui.move create mode 100644 lang/move-on-sui/test/ok/table.move create mode 100644 lang/move-on-sui/test/ok/table_vec.move create mode 100644 lang/move-on-sui/test/ok/token.move create mode 100644 lang/move-on-sui/test/ok/transfer.move create mode 100644 lang/move-on-sui/test/ok/tx_context.move create mode 100644 lang/move-on-sui/test/ok/type_name.move create mode 100644 lang/move-on-sui/test/ok/types.move create mode 100644 lang/move-on-sui/test/ok/unit_test.move create mode 100644 lang/move-on-sui/test/ok/url.move create mode 100644 lang/move-on-sui/test/ok/vec_map.move create mode 100644 lang/move-on-sui/test/ok/vec_set.move create mode 100644 lang/move-on-sui/test/ok/vector.move create mode 100644 lang/move-on-sui/test/ok/versioned.move create mode 100644 lang/semgrep-grammars/src/semgrep-move-on-sui/test/corpus/macros.txt create mode 100644 lang/semgrep-grammars/src/semgrep-move-on-sui/test/corpus/partials_and_decls.txt diff --git a/.gitmodules b/.gitmodules index 5eb2eec1..72b8fa83 100644 --- a/.gitmodules +++ b/.gitmodules @@ -123,4 +123,4 @@ url = https://github.com/Decurity/tree-sitter-circom.git [submodule "lang/semgrep-grammars/src/tree-sitter-move-on-sui"] path = lang/semgrep-grammars/src/tree-sitter-move-on-sui - url = https://github.com/tzakian/tree-sitter-move.git \ No newline at end of file + url = https://github.com/maxmysten/tree-sitter-move.git \ No newline at end of file diff --git a/lang/Makefile b/lang/Makefile index d0f2f30c..1478afe3 100644 --- a/lang/Makefile +++ b/lang/Makefile @@ -138,7 +138,8 @@ STAT_LANGUAGES2 = \ rust \ solidity \ tsx \ - typescript + typescript \ + move-on-sui STAT_LANGUAGES = $(STAT_LANGUAGES1) $(STAT_LANGUAGES2) diff --git a/lang/move-on-sui/projects.txt b/lang/move-on-sui/projects.txt index 17fa4a24..8eb64e7c 100644 --- a/lang/move-on-sui/projects.txt +++ b/lang/move-on-sui/projects.txt @@ -4,3 +4,4 @@ https://github.com/AftermathFinance/move-interfaces https://github.com/Bucket-Protocol/bucket-interface https://github.com/MystenLabs/sui-axelar +https://github.com/MystenLabs/prettier-move/ \ No newline at end of file diff --git a/lang/move-on-sui/test/ok/address.move b/lang/move-on-sui/test/ok/address.move new file mode 100644 index 00000000..43b6e691 --- /dev/null +++ b/lang/move-on-sui/test/ok/address.move @@ -0,0 +1,79 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module sui::address { + use sui::hex; + use std::ascii; + use std::bcs; + use std::string; + + /// The length of an address, in bytes + const LENGTH: u64 = 32; + + // The largest integer that can be represented with 32 bytes: 2^(8*32) - 1 + const MAX: u256 = 115792089237316195423570985008687907853269984665640564039457584007913129639935; + + #[allow(unused_const)] + /// Error from `from_bytes` when it is supplied too many or too few bytes. + const EAddressParseError: u64 = 0; + + /// Convert `a` into a u256 by interpreting `a` as the bytes of a big-endian integer + /// (e.g., `to_u256(0x1) == 1`) + public native fun to_u256(a: address): u256; + + spec to_u256 { + pragma opaque; + // TODO: stub to be replaced by actual abort conditions if any + aborts_if [abstract] true; + // TODO: specify actual function behavior + } + + /// Convert `n` into an address by encoding it as a big-endian integer (e.g., `from_u256(1) = @0x1`) + /// Aborts if `n` > `MAX_ADDRESS` + public native fun from_u256(n: u256): address; + + spec from_u256 { + pragma opaque; + // TODO: stub to be replaced by actual abort conditions if any + aborts_if [abstract] true; + // TODO: specify actual function behavior + } + + /// Convert `bytes` into an address. + /// Aborts with `EAddressParseError` if the length of `bytes` is not 32 + public native fun from_bytes(bytes: vector): address; + + spec from_bytes { + pragma opaque; + // TODO: stub to be replaced by actual abort conditions if any + aborts_if [abstract] true; + // TODO: specify actual function behavior + } + + /// Convert `a` into BCS-encoded bytes. + public fun to_bytes(a: address): vector { + bcs::to_bytes(&a) + } + + /// Convert `a` to a hex-encoded ASCII string + public fun to_ascii_string(a: address): ascii::String { + ascii::string(hex::encode(to_bytes(a))) + } + + /// Convert `a` to a hex-encoded ASCII string + public fun to_string(a: address): string::String { + string::from_ascii(to_ascii_string(a)) + } + + /// Length of a Sui address in bytes + public fun length(): u64 { + LENGTH + } + + /// Largest possible address + public fun max(): u256 { + MAX + } + + spec module { pragma verify = false; } +} diff --git a/lang/move-on-sui/test/ok/annotations.move b/lang/move-on-sui/test/ok/annotations.move new file mode 100644 index 00000000..0a897789 --- /dev/null +++ b/lang/move-on-sui/test/ok/annotations.move @@ -0,0 +1,9 @@ +#[allow(unused_const, unused_variable)] + +#[allow(unused_const, unused_variable)] +#[test, expected_failure(abort_code = ::staking::staking_tests::ENotImplemented)] +#[test, expected_failure(abort_code = other_module::ENotFound)] +#[expected_failure(arithmetic_error, location = pkg_addr::other_module)] + +// #[allow(unused_const, unused_variable)] +module c::k {} diff --git a/lang/move-on-sui/test/ok/as_expressions.move b/lang/move-on-sui/test/ok/as_expressions.move new file mode 100644 index 00000000..19dbc279 --- /dev/null +++ b/lang/move-on-sui/test/ok/as_expressions.move @@ -0,0 +1,71 @@ +module foo::bar { + fun f(): u64 { + 1 as u64; + 1 + 2 as u64; + (1 + 2) as u64; + 1 + (2 as u64); + v[1 as u64]; + 1 as u64 + 2; + (1 as u64) + 2; + } + + fun simple(cond: bool, x: u32) { + (if (cond) x else { x }) as u32; + (if (cond) x else { x } as u32); + (loop { break 0 }) as u32; + (loop { break 0 } as u32); + ('l: { 0 }) as u32; + ('l: { 0 } as u32); + (return) as u32; + (return as u32); + loop { + (break) as u32; + (break as u32); + (continue) as u32; + (continue as u32); + }; + (0 as u32) as u32; + (0 as u32 as u32); + } + + public fun f_imm(s: &S): &u64 { &s.f } + + fun ops(x: u8, y: u8) { + (x + y as u32); + (x - y as u32); + (x * y as u32); + (x / y as u32); + (x % y as u32); + (x & y as u32); + (x | y as u32); + (x ^ y as u32); + (x << y as u32); + (x >> y as u32); + + (x + y) as u32; + (x - y) as u32; + (x * y) as u32; + (x / y) as u32; + (x % y) as u32; + (x & y) as u32; + (x | y) as u32; + (x ^ y) as u32; + (x << y) as u32; + (x >> y) as u32; + } + + + public struct S has copy, drop { f: u64 } + + fun dotted(cond: bool, mut s: S) { + (1 + s.f) as u32; + (1 + s.f as u32); + (1 + S { f: 0 }.f) as u32; + (1 + S { f: 0 }.f as u32); + (*if (cond) { &0 } else { &mut 0 }) as u32; + // this case still does not work + // (*if (cond) { &0 } else { &mut 0 } as u32); + (*if (cond) { &s } else {&mut s}.f_imm()) as u32; + (*if (cond) { &s } else {&mut s}.f_imm() as u32); + } +} diff --git a/lang/move-on-sui/test/ok/ascii.move b/lang/move-on-sui/test/ok/ascii.move new file mode 100644 index 00000000..29566285 --- /dev/null +++ b/lang/move-on-sui/test/ok/ascii.move @@ -0,0 +1,151 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// The `ASCII` module defines basic string and char newtypes in Move that verify +/// that characters are valid ASCII, and that strings consist of only valid ASCII characters. +module std::ascii { + use std::vector; + use std::option::{Self, Option}; + + /// An invalid ASCII character was encountered when creating an ASCII string. + const EINVALID_ASCII_CHARACTER: u64 = 0x10000; + + /// The `String` struct holds a vector of bytes that all represent + /// valid ASCII characters. Note that these ASCII characters may not all + /// be printable. To determine if a `String` contains only "printable" + /// characters you should use the `all_characters_printable` predicate + /// defined in this module. + struct String has copy, drop, store { + bytes: vector, + } + spec String { + invariant forall i in 0..len(bytes): is_valid_char(bytes[i]); + } + + /// An ASCII character. + struct Char has copy, drop, store { + byte: u8, + } + spec Char { + invariant is_valid_char(byte); + } + + /// Convert a `byte` into a `Char` that is checked to make sure it is valid ASCII. + public fun char(byte: u8): Char { + assert!(is_valid_char(byte), EINVALID_ASCII_CHARACTER); + Char { byte } + } + spec char { + aborts_if !is_valid_char(byte) with EINVALID_ASCII_CHARACTER; + } + + /// Convert a vector of bytes `bytes` into an `String`. Aborts if + /// `bytes` contains non-ASCII characters. + public fun string(bytes: vector): String { + let x = try_string(bytes); + assert!( + option::is_some(&x), + EINVALID_ASCII_CHARACTER + ); + option::destroy_some(x) + } + spec string { + aborts_if exists i in 0..len(bytes): !is_valid_char(bytes[i]) with EINVALID_ASCII_CHARACTER; + } + + /// Convert a vector of bytes `bytes` into an `String`. Returns + /// `Some()` if the `bytes` contains all valid ASCII + /// characters. Otherwise returns `None`. + public fun try_string(bytes: vector): Option { + let len = vector::length(&bytes); + let i = 0; + while ({ + spec { + invariant i <= len; + invariant forall j in 0..i: is_valid_char(bytes[j]); + }; + i < len + }) { + let possible_byte = *vector::borrow(&bytes, i); + if (!is_valid_char(possible_byte)) return option::none(); + i = i + 1; + }; + spec { + assert i == len; + assert forall j in 0..len: is_valid_char(bytes[j]); + }; + option::some(String { bytes }) + } + + /// Returns `true` if all characters in `string` are printable characters + /// Returns `false` otherwise. Not all `String`s are printable strings. + public fun all_characters_printable(string: &String): bool { + let len = vector::length(&string.bytes); + let i = 0; + while ({ + spec { + invariant i <= len; + invariant forall j in 0..i: is_printable_char(string.bytes[j]); + }; + i < len + }) { + let byte = *vector::borrow(&string.bytes, i); + if (!is_printable_char(byte)) return false; + i = i + 1; + }; + spec { + assert i == len; + assert forall j in 0..len: is_printable_char(string.bytes[j]); + }; + true + } + spec all_characters_printable { + ensures result ==> (forall j in 0..len(string.bytes): is_printable_char(string.bytes[j])); + } + + public fun push_char(string: &mut String, char: Char) { + vector::push_back(&mut string.bytes, char.byte); + } + spec push_char { + ensures len(string.bytes) == len(old(string.bytes)) + 1; + } + + public fun pop_char(string: &mut String): Char { + Char { byte: vector::pop_back(&mut string.bytes) } + } + spec pop_char { + ensures len(string.bytes) == len(old(string.bytes)) - 1; + } + + public fun length(string: &String): u64 { + vector::length(as_bytes(string)) + } + + /// Get the inner bytes of the `string` as a reference + public fun as_bytes(string: &String): &vector { + &string.bytes + } + + /// Unpack the `string` to get its backing bytes + public fun into_bytes(string: String): vector { + let String { bytes } = string; + bytes + } + + /// Unpack the `char` into its underlying byte. + public fun byte(char: Char): u8 { + let Char { byte } = char; + byte + } + + /// Returns `true` if `b` is a valid ASCII character. Returns `false` otherwise. + public fun is_valid_char(b: u8): bool { + b <= 0x7F + } + + /// Returns `true` if `byte` is an printable ASCII character. Returns `false` otherwise. + public fun is_printable_char(byte: u8): bool { + byte >= 0x20 && // Disallow metacharacters + byte <= 0x7E // Don't allow DEL metacharacter + } +} diff --git a/lang/move-on-sui/test/ok/authenticator_state.move b/lang/move-on-sui/test/ok/authenticator_state.move new file mode 100644 index 00000000..8f19b0cc --- /dev/null +++ b/lang/move-on-sui/test/ok/authenticator_state.move @@ -0,0 +1,406 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[allow(unused_use)] +// Module for storing authenticator state, which is currently just the set of valid JWKs used by +// zklogin. +// +// This module is not currently accessible from user contracts, and is used only to record the JWK +// state to the chain for auditability + restore from snapshot purposes. +module sui::authenticator_state { + // friend bar; + // friend sui::coin; + + use std::string; + use std::option::{Self, Option}; + use std::vector; + use sui::dynamic_field; + use std::string::{String, utf8}; + use sui::object::{Self, UID}; + use sui::transfer; + use sui::tx_context::{Self, TxContext}; + use sui::math; + + /// Sender is not @0x0 the system address. + const ENotSystemAddress: u64 = 0; + const EWrongInnerVersion: u64 = 1; + const EJwksNotSorted: u64 = 2; + + const CurrentVersion: u64 = 1; + + /// Singleton shared object which stores the global authenticator state. + /// The actual state is stored in a dynamic field of type AuthenticatorStateInner to support + /// future versions of the authenticator state. + struct AuthenticatorState has key { + id: UID, + version: u64, + } + + struct AuthenticatorStateInner has store { + version: u64, + + /// List of currently active JWKs. + active_jwks: vector, + } + + #[allow(unused_field)] + /// Must match the JWK struct in fastcrypto-zkp + struct JWK has store, drop, copy { + kty: String, + e: String, + n: String, + alg: String, + } + + #[allow(unused_field)] + /// Must match the JwkId struct in fastcrypto-zkp + struct JwkId has store, drop, copy { + iss: String, + kid: String, + } + + #[allow(unused_field)] + struct ActiveJwk has store, drop, copy { + jwk_id: JwkId, + jwk: JWK, + epoch: u64, + } + + #[test_only] + public fun create_active_jwk(iss: String, kid: String, kty: String, epoch: u64): ActiveJwk { + ActiveJwk { + jwk_id: JwkId { + iss: iss, + kid: kid, + }, + jwk: JWK { + kty: kty, + e: utf8(b"AQAB"), + n: utf8(b"test"), + alg: utf8(b"RS256"), + }, + epoch, + } + } + + fun active_jwk_equal(a: &ActiveJwk, b: &ActiveJwk): bool { + // note: epoch is ignored + jwk_equal(&a.jwk, &b.jwk) && jwk_id_equal(&a.jwk_id, &b.jwk_id) + } + + fun jwk_equal(a: &JWK, b: &JWK): bool { + (&a.kty == &b.kty) && + (&a.e == &b.e) && + (&a.n == &b.n) && + (&a.alg == &b.alg) + } + + fun jwk_id_equal(a: &JwkId, b: &JwkId): bool { + (&a.iss == &b.iss) && (&a.kid == &b.kid) + } + + // Compare the underlying byte arrays lexicographically. Since the strings may be utf8 this + // ordering is not necessarily the same as the string ordering, but we just need some + // canonical that is cheap to compute. + fun string_bytes_lt(a: &String, b: &String): bool { + let a_bytes = string::bytes(a); + let b_bytes = string::bytes(b); + + let X { l, t } = b; + + if (vector::length(a_bytes) < vector::length(b_bytes)) { + true + } else if (vector::length(a_bytes) > vector::length(b_bytes)) { + false + } else { + let i = 0; + while (i < vector::length(a_bytes)) { + let a_byte = *vector::borrow(a_bytes, i); + let b_byte = *vector::borrow(b_bytes, i); + if (a_byte < b_byte) { + return true + } else if (a_byte > b_byte) { + return false + }; + i = i + 1; + }; + // all bytes are equal + false + } + } + + fun jwk_lt(a: &ActiveJwk, b: &ActiveJwk): bool { + // note: epoch is ignored + if (&a.jwk_id.iss != &b.jwk_id.iss) { + return string_bytes_lt(&a.jwk_id.iss, &b.jwk_id.iss) + }; + if (&a.jwk_id.kid != &b.jwk_id.kid) { + return string_bytes_lt(&a.jwk_id.kid, &b.jwk_id.kid) + }; + if (&a.jwk.kty != &b.jwk.kty) { + return string_bytes_lt(&a.jwk.kty, &b.jwk.kty) + }; + if (&a.jwk.e != &b.jwk.e) { + return string_bytes_lt(&a.jwk.e, &b.jwk.e) + }; + if (&a.jwk.n != &b.jwk.n) { + return string_bytes_lt(&a.jwk.n, &b.jwk.n) + }; + string_bytes_lt(&a.jwk.alg, &b.jwk.alg) + } + + #[allow(unused_function)] + /// Create and share the AuthenticatorState object. This function is call exactly once, when + /// the authenticator state object is first created. + /// Can only be called by genesis or change_epoch transactions. + fun create(ctx: &TxContext) { + assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress); + + let version = CurrentVersion; + + let inner = AuthenticatorStateInner { + version, + active_jwks: vector[], + }; + + let self = AuthenticatorState { + id: object::authenticator_state(), + version, + }; + + dynamic_field::add(&mut self.id, version, inner); + transfer::share_object(self); + } + + fun load_inner_mut( + self: &mut AuthenticatorState, + ): &mut AuthenticatorStateInner { + let version = self.version; + + // replace this with a lazy update function when we add a new version of the inner object. + assert!(version == CurrentVersion, EWrongInnerVersion); + + let inner: &mut AuthenticatorStateInner = dynamic_field::borrow_mut(&mut self.id, self.version); + + assert!(inner.version == version, EWrongInnerVersion); + inner + } + + fun load_inner( + self: &AuthenticatorState, + ): &AuthenticatorStateInner { + let version = self.version; + + // replace this with a lazy update function when we add a new version of the inner object. + assert!(version == CurrentVersion, EWrongInnerVersion); + + let inner: &AuthenticatorStateInner = dynamic_field::borrow(&self.id, self.version); + + assert!(inner.version == version, EWrongInnerVersion); + inner + } + + fun check_sorted(new_active_jwks: &vector) { + let i = 0; + while (i < vector::length(new_active_jwks) - 1) { + let a = vector::borrow(new_active_jwks, i); + let b = vector::borrow(new_active_jwks, i + 1); + assert!(jwk_lt(a, b), EJwksNotSorted); + i = i + 1; + }; + } + + #[allow(unused_function)] + /// Record a new set of active_jwks. Called when executing the AuthenticatorStateUpdate system + /// transaction. The new input vector must be sorted and must not contain duplicates. + /// If a new JWK is already present, but with a previous epoch, then the epoch is updated to + /// indicate that the JWK has been validated in the current epoch and should not be expired. + fun update_authenticator_state( + self: &mut AuthenticatorState, + new_active_jwks: vector, + ctx: &TxContext, + ) { + // Validator will make a special system call with sender set as 0x0. + assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress); + + check_sorted(&new_active_jwks); + let new_active_jwks = deduplicate(new_active_jwks); + + let inner = load_inner_mut(self); + + let res = vector[]; + let i = 0; + let j = 0; + let active_jwks_len = vector::length(&inner.active_jwks); + let new_active_jwks_len = vector::length(&new_active_jwks); + + while (i < active_jwks_len && j < new_active_jwks_len) { + let old_jwk = vector::borrow(&inner.active_jwks, i); + let new_jwk = vector::borrow(&new_active_jwks, j); + + // when they are equal, push only one, but use the max epoch of the two + if (active_jwk_equal(old_jwk, new_jwk)) { + let jwk = *old_jwk; + jwk.epoch = math::max(old_jwk.epoch, new_jwk.epoch); + vector::push_back(&mut res, jwk); + i = i + 1; + j = j + 1; + } else if (jwk_id_equal(&old_jwk.jwk_id, &new_jwk.jwk_id)) { + // if only jwk_id is equal, then the key has changed. Providers should not send + // JWKs like this, but if they do, we must ignore the new JWK to avoid having a + // liveness / forking issues + vector::push_back(&mut res, *old_jwk); + i = i + 1; + j = j + 1; + } else if (jwk_lt(old_jwk, new_jwk)) { + vector::push_back(&mut res, *old_jwk); + i = i + 1; + } else { + vector::push_back(&mut res, *new_jwk); + j = j + 1; + } + }; + + while (i < active_jwks_len) { + vector::push_back(&mut res, *vector::borrow(&inner.active_jwks, i)); + i = i + 1; + }; + while (j < new_active_jwks_len) { + vector::push_back(&mut res, *vector::borrow(&new_active_jwks, j)); + j = j + 1; + }; + + inner.active_jwks = res; + } + + fun deduplicate(jwks: vector): vector { + let res = vector[]; + let i = 0; + let prev: Option = option::none(); + while (i < vector::length(&jwks)) { + let jwk = vector::borrow(&jwks, i); + if (option::is_none(&prev)) { + option::fill(&mut prev, jwk.jwk_id); + } else if (jwk_id_equal(option::borrow(&prev), &jwk.jwk_id)) { + // skip duplicate jwks in input + i = i + 1; + continue + } else { + *option::borrow_mut(&mut prev) = jwk.jwk_id; + }; + vector::push_back(&mut res, *jwk); + i = i + 1; + }; + res + } + + #[allow(unused_function)] + // Called directly by rust when constructing the ChangeEpoch transaction. + fun expire_jwks( + self: &mut AuthenticatorState, + // any jwk below this epoch is not retained + min_epoch: u64, + ctx: &TxContext) { + // This will only be called by sui_system::advance_epoch + assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress); + + let inner = load_inner_mut(self); + + let len = vector::length(&inner.active_jwks); + + // first we count how many jwks from each issuer are above the min_epoch + // and store the counts in a vector that parallels the (sorted) active_jwks vector + let issuer_max_epochs = vector[]; + let i = 0; + let prev_issuer: Option = option::none(); + + while (i < len) { + let cur = vector::borrow(&inner.active_jwks, i); + let cur_iss = &cur.jwk_id.iss; + if (option::is_none(&prev_issuer)) { + option::fill(&mut prev_issuer, *cur_iss); + vector::push_back(&mut issuer_max_epochs, cur.epoch); + } else { + if (cur_iss == option::borrow(&prev_issuer)) { + let back = vector::length(&issuer_max_epochs) - 1; + let prev_max_epoch = vector::borrow_mut(&mut issuer_max_epochs, back); + *prev_max_epoch = math::max(*prev_max_epoch, cur.epoch); + } else { + *option::borrow_mut(&mut prev_issuer) = *cur_iss; + vector::push_back(&mut issuer_max_epochs, cur.epoch); + } + }; + i = i + 1; + }; + + // Now, filter out any JWKs that are below the min_epoch, unless that issuer has no + // JWKs >= the min_epoch, in which case we keep all of them. + let new_active_jwks: vector = vector[]; + let prev_issuer: Option = option::none(); + let i = 0; + let j = 0; + while (i < len) { + let jwk = vector::borrow(&inner.active_jwks, i); + let cur_iss = &jwk.jwk_id.iss; + + if (option::is_none(&prev_issuer)) { + option::fill(&mut prev_issuer, *cur_iss); + } else if (cur_iss != option::borrow(&prev_issuer)) { + *option::borrow_mut(&mut prev_issuer) = *cur_iss; + j = j + 1; + }; + + let mut max_epoch_for_iss = vector::borrow(&issuer_max_epochs, j); + + // TODO: if the iss for this jwk has *no* jwks that meet the minimum epoch, + // then expire nothing. + if (*max_epoch_for_iss < min_epoch || jwk.epoch >= min_epoch) { + vector::push_back(&mut new_active_jwks, *jwk); + }; + i = i + 1; + }; + inner.active_jwks = new_active_jwks; + } + + #[allow(unused_function)] + /// Get the current active_jwks. Called when the node starts up in order to load the current + /// JWK state from the chain. + fun get_active_jwks( + self: &AuthenticatorState, + ctx: &TxContext, + ): vector { + assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress); + load_inner(self).active_jwks + } + + #[test_only] + public fun create_for_testing(ctx: &TxContext) { + create(ctx); + } + + #[test_only] + public fun update_authenticator_state_for_testing( + self: &mut AuthenticatorState, + new_active_jwks: vector, + ctx: &TxContext, + ) { + update_authenticator_state(self, new_active_jwks, ctx); + } + + #[test_only] + public fun expire_jwks_for_testing( + self: &mut AuthenticatorState, + min_epoch: u64, + ctx: &TxContext, + ) { + expire_jwks(self, min_epoch, ctx); + } + + #[test_only] + public fun get_active_jwks_for_testing( + self: &AuthenticatorState, + ctx: &TxContext, + ): vector { + get_active_jwks(self, ctx) + } +} diff --git a/lang/move-on-sui/test/ok/bag.move b/lang/move-on-sui/test/ok/bag.move new file mode 100644 index 00000000..57db2636 --- /dev/null +++ b/lang/move-on-sui/test/ok/bag.move @@ -0,0 +1,112 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// A bag is a heterogeneous map-like collection. The collection is similar to `sui::table` in that +/// its keys and values are not stored within the `Bag` value, but instead are stored using Sui's +/// object system. The `Bag` struct acts only as a handle into the object system to retrieve those +/// keys and values. +/// Note that this means that `Bag` values with exactly the same key-value mapping will not be +/// equal, with `==`, at runtime. For example +/// ``` +/// let bag1 = bag::new(); +/// let bag2 = bag::new(); +/// bag::add(&mut bag1, 0, false); +/// bag::add(&mut bag1, 1, true); +/// bag::add(&mut bag2, 0, false); +/// bag::add(&mut bag2, 1, true); +/// // bag1 does not equal bag2, despite having the same entries +/// assert!(&bag1 != &bag2, 0); +/// ``` +/// At it's core, `sui::bag` is a wrapper around `UID` that allows for access to +/// `sui::dynamic_field` while preventing accidentally stranding field values. A `UID` can be +/// deleted, even if it has dynamic fields associated with it, but a bag, on the other hand, must be +/// empty to be destroyed. +module sui::bag { + use sui::object::{Self, UID}; + use sui::dynamic_field as field; + use sui::tx_context::TxContext; + + // Attempted to destroy a non-empty bag + const EBagNotEmpty: u64 = 0; + + struct Bag has key, store { + /// the ID of this bag + id: UID, + /// the number of key-value pairs in the bag + size: u64, + } + + /// Creates a new, empty bag + public fun new(ctx: &mut TxContext): Bag { + Bag { + id: object::new(ctx), + size: 0, + } + } + + /// Adds a key-value pair to the bag `bag: &mut Bag` + /// Aborts with `sui::dynamic_field::EFieldAlreadyExists` if the bag already has an entry with + /// that key `k: K`. + public fun add(bag: &mut Bag, k: K, v: V) { + field::add(&mut bag.id, k, v); + bag.size = bag.size + 1; + } + + /// Immutable borrows the value associated with the key in the bag `bag: &Bag`. + /// Aborts with `sui::dynamic_field::EFieldDoesNotExist` if the bag does not have an entry with + /// that key `k: K`. + /// Aborts with `sui::dynamic_field::EFieldTypeMismatch` if the bag has an entry for the key, but + /// the value does not have the specified type. + public fun borrow(bag: &Bag, k: K): &V { + field::borrow(&bag.id, k) + } + + /// Mutably borrows the value associated with the key in the bag `bag: &mut Bag`. + /// Aborts with `sui::dynamic_field::EFieldDoesNotExist` if the bag does not have an entry with + /// that key `k: K`. + /// Aborts with `sui::dynamic_field::EFieldTypeMismatch` if the bag has an entry for the key, but + /// the value does not have the specified type. + public fun borrow_mut(bag: &mut Bag, k: K): &mut V { + field::borrow_mut(&mut bag.id, k) + } + + /// Mutably borrows the key-value pair in the bag `bag: &mut Bag` and returns the value. + /// Aborts with `sui::dynamic_field::EFieldDoesNotExist` if the bag does not have an entry with + /// that key `k: K`. + /// Aborts with `sui::dynamic_field::EFieldTypeMismatch` if the bag has an entry for the key, but + /// the value does not have the specified type. + public fun remove(bag: &mut Bag, k: K): V { + let v = field::remove(&mut bag.id, k); + bag.size = bag.size - 1; + v + } + + /// Returns true iff there is an value associated with the key `k: K` in the bag `bag: &Bag` + public fun contains(bag: &Bag, k: K): bool { + field::exists_(&bag.id, k) + } + + /// Returns true iff there is an value associated with the key `k: K` in the bag `bag: &Bag` + /// with an assigned value of type `V` + public fun contains_with_type(bag: &Bag, k: K): bool { + field::exists_with_type(&bag.id, k) + } + + /// Returns the size of the bag, the number of key-value pairs + public fun length(bag: &Bag): u64 { + bag.size + } + + /// Returns true iff the bag is empty (if `length` returns `0`) + public fun is_empty(bag: &Bag): bool { + bag.size == 0 + } + + /// Destroys an empty bag + /// Aborts with `EBagNotEmpty` if the bag still contains values + public fun destroy_empty(bag: Bag) { + let Bag { id, size } = bag; + assert!(size == 0, EBagNotEmpty); + object::delete(id) + } +} diff --git a/lang/move-on-sui/test/ok/balance.move b/lang/move-on-sui/test/ok/balance.move new file mode 100644 index 00000000..163e89de --- /dev/null +++ b/lang/move-on-sui/test/ok/balance.move @@ -0,0 +1,194 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// A storable handler for Balances in general. Is used in the `Coin` +/// module to allow balance operations and can be used to implement +/// custom coins with `Supply` and `Balance`s. +module sui::balance { + use sui::tx_context::{Self, TxContext}; + + friend sui::sui; + + /// For when trying to destroy a non-zero balance. + const ENonZero: u64 = 0; + + /// For when an overflow is happening on Supply operations. + const EOverflow: u64 = 1; + + /// For when trying to withdraw more than there is. + const ENotEnough: u64 = 2; + + /// Sender is not @0x0 the system address. + const ENotSystemAddress: u64 = 3; + + /// A Supply of T. Used for minting and burning. + /// Wrapped into a `TreasuryCap` in the `Coin` module. + struct Supply has store { + value: u64 + } + + /// Storable balance - an inner struct of a Coin type. + /// Can be used to store coins which don't need the key ability. + struct Balance has store { + value: u64 + } + + /// Get the amount stored in a `Balance`. + public fun value(self: &Balance): u64 { + self.value + } + + /// Get the `Supply` value. + public fun supply_value(supply: &Supply): u64 { + supply.value + } + + /// Create a new supply for type T. + public fun create_supply(_: T): Supply { + Supply { value: 0 } + } + + /// Increase supply by `value` and create a new `Balance` with this value. + public fun increase_supply(self: &mut Supply, value: u64): Balance { + assert!(value < (18446744073709551615u64 - self.value), EOverflow); + self.value = self.value + value; + Balance { value } + } + + /// Burn a Balance and decrease Supply. + public fun decrease_supply(self: &mut Supply, balance: Balance): u64 { + let Balance { value } = balance; + assert!(self.value >= value, EOverflow); + self.value = self.value - value; + value + } + + /// Create a zero `Balance` for type `T`. + public fun zero(): Balance { + Balance { value: 0 } + } + + spec zero { + aborts_if false; + ensures result.value == 0; + } + + /// Join two balances together. + public fun join(self: &mut Balance, balance: Balance): u64 { + let Balance { value } = balance; + self.value = self.value + value; + self.value + } + + spec join { + ensures self.value == old(self.value) + balance.value; + ensures result == self.value; + } + + /// Split a `Balance` and take a sub balance from it. + public fun split(self: &mut Balance, value: u64): Balance { + assert!(self.value >= value, ENotEnough); + self.value = self.value - value; + Balance { value } + } + + spec split { + aborts_if self.value < value with ENotEnough; + ensures self.value == old(self.value) - value; + ensures result.value == value; + } + + /// Withdraw all balance. After this the remaining balance must be 0. + public fun withdraw_all(self: &mut Balance): Balance { + let value = self.value; + split(self, value) + } + + spec withdraw_all { + ensures self.value == 0; + } + + /// Destroy a zero `Balance`. + public fun destroy_zero(balance: Balance) { + assert!(balance.value == 0, ENonZero); + let Balance { value: _ } = balance; + } + + spec destroy_zero { + aborts_if balance.value != 0 with ENonZero; + } + + #[allow(unused_function)] + /// CAUTION: this function creates a `Balance` without increasing the supply. + /// It should only be called by the epoch change system txn to create staking rewards, + /// and nowhere else. + fun create_staking_rewards(value: u64, ctx: &TxContext): Balance { + assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress); + Balance { value } + } + + #[allow(unused_function)] + /// CAUTION: this function destroys a `Balance` without decreasing the supply. + /// It should only be called by the epoch change system txn to destroy storage rebates, + /// and nowhere else. + fun destroy_storage_rebates(self: Balance, ctx: &TxContext) { + assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress); + let Balance { value: _ } = self; + } + + /// Destroy a `Supply` preventing any further minting and burning. + public(friend) fun destroy_supply(self: Supply): u64 { + let Supply { value } = self; + value + } + + #[test_only] + /// Create a `Balance` of any coin for testing purposes. + public fun create_for_testing(value: u64): Balance { + Balance { value } + } + + #[test_only] + /// Destroy a `Balance` of any coin for testing purposes. + public fun destroy_for_testing(self: Balance): u64 { + let Balance { value } = self; + value + } + + #[test_only] + /// Create a `Supply` of any coin for testing purposes. + public fun create_supply_for_testing(): Supply { + Supply { value: 0 } + } +} + +#[test_only] +module sui::balance_tests { + use sui::balance; + use sui::sui::SUI; + use sui::test_utils; + + #[test] + fun test_balance() { + let balance = balance::zero(); + let another = balance::create_for_testing(1000); + + balance::join(&mut balance, another); + + assert!(balance::value(&balance) == 1000, 0); + + let balance1 = balance::split(&mut balance, 333); + let balance2 = balance::split(&mut balance, 333); + let balance3 = balance::split(&mut balance, 334); + + balance::destroy_zero(balance); + + assert!(balance::value(&balance1) == 333, 1); + assert!(balance::value(&balance2) == 333, 2); + assert!(balance::value(&balance3) == 334, 3); + + test_utils::destroy(balance1); + test_utils::destroy(balance2); + test_utils::destroy(balance3); + } +} diff --git a/lang/move-on-sui/test/ok/bcs.move b/lang/move-on-sui/test/ok/bcs.move new file mode 100644 index 00000000..695e7012 --- /dev/null +++ b/lang/move-on-sui/test/ok/bcs.move @@ -0,0 +1,453 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This module implements BCS (de)serialization in Move. +/// Full specification can be found here: https://github.com/diem/bcs +/// +/// Short summary (for Move-supported types): +/// +/// - address - sequence of X bytes +/// - bool - byte with 0 or 1 +/// - u8 - a single u8 byte +/// - u64 / u128 - LE bytes +/// - vector - ULEB128 length + LEN elements +/// - option - first byte bool: None (0) or Some (1), then value +/// +/// Usage example: +/// ``` +/// /// This function reads u8 and u64 value from the input +/// /// and returns the rest of the bytes. +/// fun deserialize(bytes: vector): (u8, u64, vector) { +/// use sui::bcs::{Self, BCS}; +/// +/// let prepared: BCS = bcs::new(bytes); +/// let (u8_value, u64_value) = ( +/// bcs::peel_u8(&mut prepared), +/// bcs::peel_u64(&mut prepared) +/// ); +/// +/// // unpack bcs struct +/// let leftovers = bcs::into_remainder_bytes(prepared); +/// +/// (u8_value, u64_value, leftovers) +/// } +/// ``` +module sui::bcs { + use std::option::{Self, Option}; + use std::vector as v; + use sui::address; + use std::bcs; + + /// For when bytes length is less than required for deserialization. + const EOutOfRange: u64 = 0; + + /// For when the boolean value different than `0` or `1`. + const ENotBool: u64 = 1; + + /// For when ULEB byte is out of range (or not found). + const ELenOutOfRange: u64 = 2; + + /// A helper struct that saves resources on operations. For better + /// vector performance, it stores reversed bytes of the BCS and + /// enables use of `vector::pop_back`. + struct BCS has store, copy, drop { + bytes: vector + } + + /// Get BCS serialized bytes for any value. + /// Re-exports stdlib `bcs::to_bytes`. + public fun to_bytes(value: &T): vector { + bcs::to_bytes(value) + } + + /// Creates a new instance of BCS wrapper that holds inversed + /// bytes for better performance. + public fun new(bytes: vector): BCS { + v::reverse(&mut bytes); + BCS { bytes } + } + + /// Unpack the `BCS` struct returning the leftover bytes. + /// Useful for passing the data further after partial deserialization. + public fun into_remainder_bytes(bcs: BCS): vector { + let BCS { bytes } = bcs; + v::reverse(&mut bytes); + bytes + } + + /// Read address from the bcs-serialized bytes. + public fun peel_address(bcs: &mut BCS): address { + assert!(v::length(&bcs.bytes) >= address::length(), EOutOfRange); + let (addr_bytes, i) = (v::empty(), 0); + while (i < address::length()) { + v::push_back(&mut addr_bytes, v::pop_back(&mut bcs.bytes)); + i = i + 1; + }; + address::from_bytes(addr_bytes) + } + + /// Read a `bool` value from bcs-serialized bytes. + public fun peel_bool(bcs: &mut BCS): bool { + let value = peel_u8(bcs); + if (value == 0) { + false + } else if (value == 1) { + true + } else { + abort ENotBool + } + } + + /// Read `u8` value from bcs-serialized bytes. + public fun peel_u8(bcs: &mut BCS): u8 { + assert!(v::length(&bcs.bytes) >= 1, EOutOfRange); + v::pop_back(&mut bcs.bytes) + } + + /// Read `u64` value from bcs-serialized bytes. + public fun peel_u64(bcs: &mut BCS): u64 { + assert!(v::length(&bcs.bytes) >= 8, EOutOfRange); + + let (value, i) = (0u64, 0u8); + while (i < 64) { + let byte = (v::pop_back(&mut bcs.bytes) as u64); + value = value + (byte << i); + i = i + 8; + }; + + value + } + + /// Read `u128` value from bcs-serialized bytes. + public fun peel_u128(bcs: &mut BCS): u128 { + assert!(v::length(&bcs.bytes) >= 16, EOutOfRange); + + let (value, i) = (0u128, 0u8); + while (i < 128) { + let byte = (v::pop_back(&mut bcs.bytes) as u128); + value = value + (byte << i); + i = i + 8; + }; + + value + } + + // === Vector === + + /// Read ULEB bytes expecting a vector length. Result should + /// then be used to perform `peel_*` operation LEN times. + /// + /// In BCS `vector` length is implemented with ULEB128; + /// See more here: https://en.wikipedia.org/wiki/LEB128 + public fun peel_vec_length(bcs: &mut BCS): u64 { + let (total, shift, len) = (0u64, 0, 0); + while (true) { + assert!(len <= 4, ELenOutOfRange); + let byte = (v::pop_back(&mut bcs.bytes) as u64); + len = len + 1; + total = total | ((byte & 0x7f) << shift); + if ((byte & 0x80) == 0) { + break + }; + shift = shift + 7; + }; + total + } + + /// Peel a vector of `address` from serialized bytes. + public fun peel_vec_address(bcs: &mut BCS): vector
{ + let (len, i, res) = (peel_vec_length(bcs), 0, vector[]); + while (i < len) { + v::push_back(&mut res, peel_address(bcs)); + i = i + 1; + }; + res + } + + /// Peel a vector of `address` from serialized bytes. + public fun peel_vec_bool(bcs: &mut BCS): vector { + let (len, i, res) = (peel_vec_length(bcs), 0, vector[]); + while (i < len) { + v::push_back(&mut res, peel_bool(bcs)); + i = i + 1; + }; + res + } + + /// Peel a vector of `u8` (eg string) from serialized bytes. + public fun peel_vec_u8(bcs: &mut BCS): vector { + let (len, i, res) = (peel_vec_length(bcs), 0, vector[]); + while (i < len) { + v::push_back(&mut res, peel_u8(bcs)); + i = i + 1; + }; + res + } + + /// Peel a `vector>` (eg vec of string) from serialized bytes. + public fun peel_vec_vec_u8(bcs: &mut BCS): vector> { + let (len, i, res) = (peel_vec_length(bcs), 0, vector[]); + while (i < len) { + v::push_back(&mut res, peel_vec_u8(bcs)); + i = i + 1; + }; + res + } + + /// Peel a vector of `u64` from serialized bytes. + public fun peel_vec_u64(bcs: &mut BCS): vector { + let (len, i, res) = (peel_vec_length(bcs), 0, vector[]); + while (i < len) { + v::push_back(&mut res, peel_u64(bcs)); + i = i + 1; + }; + res + } + + /// Peel a vector of `u128` from serialized bytes. + public fun peel_vec_u128(bcs: &mut BCS): vector { + let (len, i, res) = (peel_vec_length(bcs), 0, vector[]); + while (i < len) { + v::push_back(&mut res, peel_u128(bcs)); + i = i + 1; + }; + res + } + + // === Option === + + /// Peel `Option
` from serialized bytes. + public fun peel_option_address(bcs: &mut BCS): Option
{ + if (peel_bool(bcs)) { + option::some(peel_address(bcs)) + } else { + option::none() + } + } + + /// Peel `Option` from serialized bytes. + public fun peel_option_bool(bcs: &mut BCS): Option { + if (peel_bool(bcs)) { + option::some(peel_bool(bcs)) + } else { + option::none() + } + } + + /// Peel `Option` from serialized bytes. + public fun peel_option_u8(bcs: &mut BCS): Option { + if (peel_bool(bcs)) { + option::some(peel_u8(bcs)) + } else { + option::none() + } + } + + /// Peel `Option` from serialized bytes. + public fun peel_option_u64(bcs: &mut BCS): Option { + if (peel_bool(bcs)) { + option::some(peel_u64(bcs)) + } else { + option::none() + } + } + + /// Peel `Option` from serialized bytes. + public fun peel_option_u128(bcs: &mut BCS): Option { + if (peel_bool(bcs)) { + option::some(peel_u128(bcs)) + } else { + option::none() + } + } + + // TODO: re-enable once bit-wise operators in peel_vec_length are supported in the prover + spec module { pragma verify = false; } + + // === Tests === + + #[test_only] + struct Info has drop { a: bool, b: u8, c: u64, d: u128, k: vector, s: address } + + #[test] + #[expected_failure(abort_code = ELenOutOfRange)] + fun test_uleb_len_fail() { + let value = vector[0xff, 0xff, 0xff, 0xff, 0xff]; + let bytes = new(to_bytes(&value)); + let _fail = peel_vec_length(&mut bytes); + abort 2 // TODO: make this test fail + } + + #[test] + #[expected_failure(abort_code = ENotBool)] + fun test_bool_fail() { + let bytes = new(to_bytes(&10u8)); + let _fail = peel_bool(&mut bytes); + } + + #[test] + fun test_option() { + { + let value = option::some(true); + let bytes = new(to_bytes(&value)); + assert!(value == peel_option_bool(&mut bytes), 0); + }; + + { + let value = option::some(10u8); + let bytes = new(to_bytes(&value)); + assert!(value == peel_option_u8(&mut bytes), 0); + }; + + { + let value = option::some(10000u64); + let bytes = new(to_bytes(&value)); + assert!(value == peel_option_u64(&mut bytes), 0); + }; + + { + let value = option::some(10000999999u128); + let bytes = new(to_bytes(&value)); + assert!(value == peel_option_u128(&mut bytes), 0); + }; + + { + let value = option::some(@0xC0FFEE); + let bytes = new(to_bytes(&value)); + assert!(value == peel_option_address(&mut bytes), 0); + }; + + { + let value: Option = option::none(); + let bytes = new(to_bytes(&value)); + assert!(value == peel_option_bool(&mut bytes), 0); + }; + } + + #[test] + fun test_bcs() { + { + let value = @0xC0FFEE; + let bytes = new(to_bytes(&value)); + assert!(value == peel_address(&mut bytes), 0); + }; + + { // boolean: true + let value = true; + let bytes = new(to_bytes(&value)); + assert!(value == peel_bool(&mut bytes), 0); + }; + + { // boolean: false + let value = false; + let bytes = new(to_bytes(&value)); + assert!(value == peel_bool(&mut bytes), 0); + }; + + { // u8 + let value = 100u8; + let bytes = new(to_bytes(&value)); + assert!(value == peel_u8(&mut bytes), 0); + }; + + { // u64 (4 bytes) + let value = 1000100u64; + let bytes = new(to_bytes(&value)); + assert!(value == peel_u64(&mut bytes), 0); + }; + + { // u64 (8 bytes) + let value = 100000000000000u64; + let bytes = new(to_bytes(&value)); + assert!(value == peel_u64(&mut bytes), 0); + }; + + { // u128 (16 bytes) + let value = 100000000000000000000000000u128; + let bytes = new(to_bytes(&value)); + assert!(value == peel_u128(&mut bytes), 0); + }; + + { // vector length + let value = vector[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + let bytes = new(to_bytes(&value)); + assert!(v::length(&value) == peel_vec_length(&mut bytes), 0); + }; + + { // vector length (more data) + let value = vector[ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ]; + + let bytes = new(to_bytes(&value)); + assert!(v::length(&value) == peel_vec_length(&mut bytes), 0); + }; + + { // full deserialization test (ordering) + let info = Info { a: true, b: 100, c: 9999, d: 112333, k: vector[true, false, true, false], s: @0xAAAAAAAAAAA }; + let bytes = new(to_bytes(&info)); + + assert!(info.a == peel_bool(&mut bytes), 0); + assert!(info.b == peel_u8(&mut bytes), 0); + assert!(info.c == peel_u64(&mut bytes), 0); + assert!(info.d == peel_u128(&mut bytes), 0); + + let len = peel_vec_length(&mut bytes); + + assert!(v::length(&info.k) == len, 0); + + let i = 0; + while (i < v::length(&info.k)) { + assert!(*v::borrow(&info.k, i) == peel_bool(&mut bytes), 0); + i = i + 1; + }; + + assert!(info.s == peel_address(&mut bytes), 0); + }; + + { // read vector of bytes directly + let value = vector[ + vector[1,2,3,4,5], + vector[1,2,3,4,5], + vector[1,2,3,4,5] + ]; + let bytes = new(to_bytes(&value)); + assert!(value == peel_vec_vec_u8(&mut bytes), 0); + }; + + { // read vector of bytes directly + let value = vector[1,2,3,4,5]; + let bytes = new(to_bytes(&value)); + assert!(value == peel_vec_u8(&mut bytes), 0); + }; + + { // read vector of bytes directly + let value = vector[1,2,3,4,5]; + let bytes = new(to_bytes(&value)); + assert!(value == peel_vec_u64(&mut bytes), 0); + }; + + { // read vector of bytes directly + let value = vector[1,2,3,4,5]; + let bytes = new(to_bytes(&value)); + assert!(value == peel_vec_u128(&mut bytes), 0); + }; + + { // read vector of bytes directly + let value = vector[true, false, true, false]; + let bytes = new(to_bytes(&value)); + assert!(value == peel_vec_bool(&mut bytes), 0); + }; + + { // read vector of address directly + let value = vector[@0x0, @0x1, @0x2, @0x3]; + let bytes = new(to_bytes(&value)); + assert!(value == peel_vec_address(&mut bytes), 0); + }; + } +} diff --git a/lang/move-on-sui/test/ok/bit_vector.move b/lang/move-on-sui/test/ok/bit_vector.move new file mode 100644 index 00000000..a3ee1c44 --- /dev/null +++ b/lang/move-on-sui/test/ok/bit_vector.move @@ -0,0 +1,164 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module std::bit_vector { + use std::vector; + + /// The provided index is out of bounds + const EINDEX: u64 = 0x20000; + /// An invalid length of bitvector was given + const ELENGTH: u64 = 0x20001; + + const WORD_SIZE: u64 = 1; + /// The maximum allowed bitvector size + const MAX_SIZE: u64 = 1024; + + struct BitVector has copy, drop, store { + length: u64, + bit_field: vector, + } + + public fun new(length: u64): BitVector { + assert!(length > 0, ELENGTH); + assert!(length < MAX_SIZE, ELENGTH); + let counter = 0; + let bit_field = vector::empty(); + while ({spec { + invariant counter <= length; + invariant len(bit_field) == counter; + }; + (counter < length)}) { + vector::push_back(&mut bit_field, false); + counter = counter + 1; + }; + spec { + assert counter == length; + assert len(bit_field) == length; + }; + + BitVector { + length, + bit_field, + } + } + spec new { + include NewAbortsIf; + ensures result.length == length; + ensures len(result.bit_field) == length; + } + spec schema NewAbortsIf { + length: u64; + aborts_if length <= 0 with ELENGTH; + aborts_if length >= MAX_SIZE with ELENGTH; + } + + /// Set the bit at `bit_index` in the `bitvector` regardless of its previous state. + public fun set(bitvector: &mut BitVector, bit_index: u64) { + assert!(bit_index < vector::length(&bitvector.bit_field), EINDEX); + let x = vector::borrow_mut(&mut bitvector.bit_field, bit_index); + *x = true; + } + spec set { + include SetAbortsIf; + ensures bitvector.bit_field[bit_index]; + } + spec schema SetAbortsIf { + bitvector: BitVector; + bit_index: u64; + aborts_if bit_index >= length(bitvector) with EINDEX; + } + + /// Unset the bit at `bit_index` in the `bitvector` regardless of its previous state. + public fun unset(bitvector: &mut BitVector, bit_index: u64) { + assert!(bit_index < vector::length(&bitvector.bit_field), EINDEX); + let x = vector::borrow_mut(&mut bitvector.bit_field, bit_index); + *x = false; + } + spec set { + include UnsetAbortsIf; + ensures bitvector.bit_field[bit_index]; + } + spec schema UnsetAbortsIf { + bitvector: BitVector; + bit_index: u64; + aborts_if bit_index >= length(bitvector) with EINDEX; + } + + /// Shift the `bitvector` left by `amount`. If `amount` is greater than the + /// bitvector's length the bitvector will be zeroed out. + public fun shift_left(bitvector: &mut BitVector, amount: u64) { + if (amount >= bitvector.length) { + let len = vector::length(&bitvector.bit_field); + let i = 0; + while (i < len) { + let elem = vector::borrow_mut(&mut bitvector.bit_field, i); + *elem = false; + i = i + 1; + }; + } else { + let i = amount; + + while (i < bitvector.length) { + if (is_index_set(bitvector, i)) set(bitvector, i - amount) + else unset(bitvector, i - amount); + i = i + 1; + }; + + i = bitvector.length - amount; + + while (i < bitvector.length) { + unset(bitvector, i); + i = i + 1; + }; + } + } + + /// Return the value of the bit at `bit_index` in the `bitvector`. `true` + /// represents "1" and `false` represents a 0 + public fun is_index_set(bitvector: &BitVector, bit_index: u64): bool { + assert!(bit_index < vector::length(&bitvector.bit_field), EINDEX); + *vector::borrow(&bitvector.bit_field, bit_index) + } + spec is_index_set { + include IsIndexSetAbortsIf; + ensures result == bitvector.bit_field[bit_index]; + } + spec schema IsIndexSetAbortsIf { + bitvector: BitVector; + bit_index: u64; + aborts_if bit_index >= length(bitvector) with EINDEX; + } + spec fun spec_is_index_set(bitvector: BitVector, bit_index: u64): bool { + if (bit_index >= length(bitvector)) { + false + } else { + bitvector.bit_field[bit_index] + } + } + + /// Return the length (number of usable bits) of this bitvector + public fun length(bitvector: &BitVector): u64 { + vector::length(&bitvector.bit_field) + } + + /// Returns the length of the longest sequence of set bits starting at (and + /// including) `start_index` in the `bitvector`. If there is no such + /// sequence, then `0` is returned. + public fun longest_set_sequence_starting_at(bitvector: &BitVector, start_index: u64): u64 { + assert!(start_index < bitvector.length, EINDEX); + let index = start_index; + + // Find the greatest index in the vector such that all indices less than it are set. + while (index < bitvector.length) { + if (!is_index_set(bitvector, index)) break; + index = index + 1; + }; + + index - start_index + } + + #[test_only] + public fun word_size(): u64 { + WORD_SIZE + } +} diff --git a/lang/move-on-sui/test/ok/borrow.move b/lang/move-on-sui/test/ok/borrow.move new file mode 100644 index 00000000..b074e4ba --- /dev/null +++ b/lang/move-on-sui/test/ok/borrow.move @@ -0,0 +1,121 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// A simple library that enables hot-potato-locked borrow mechanics. +/// +/// With Programmable transactions, it is possible to borrow a value within +/// a transaction, use it and put back in the end. Hot-potato `Borrow` makes +/// sure the object is returned and was not swapped for another one. +module sui::borrow { + use sui::object::{Self, ID}; + use std::option::{Self, Option}; + use sui::tx_context::{Self, TxContext}; + + /// The `Borrow` does not match the `Referent`. + const EWrongBorrow: u64 = 0; + /// An attempt to swap the `Referent.value` with another object of the same type. + const EWrongValue: u64 = 1; + + /// An object wrapping a `T` and providing the borrow API. + struct Referent has store { + id: address, + value: Option + } + + /// A hot potato making sure the object is put back once borrowed. + struct Borrow { ref: address, obj: ID } + + /// Create a new `Referent` struct + public fun new(value: T, ctx: &mut TxContext): Referent { + Referent { + id: tx_context::fresh_object_address(ctx), + value: option::some(value) + } + } + + /// Borrow the `T` from the `Referent` receiving the `T` and a `Borrow` + /// hot potato. + public fun borrow(self: &mut Referent): (T, Borrow) { + let value = option::extract(&mut self.value); + let id = object::id(&value); + + (value, Borrow { + ref: self.id, + obj: id + }) + } + + /// Put an object and the `Borrow` hot potato back. + public fun put_back(self: &mut Referent, value: T, borrow: Borrow) { + let Borrow { ref, obj } = borrow; + + assert!(object::id(&value) == obj, EWrongValue); + assert!(self.id == ref, EWrongBorrow); + option::fill(&mut self.value, value); + } + + /// Unpack the `Referent` struct and return the value. + public fun destroy(self: Referent): T { + let Referent { id: _, value } = self; + option::destroy_some(value) + } + + #[test_only] + struct Test has key, store { + id: object::UID + } + + #[test] + fun test_borrow() { + let ctx = &mut sui::tx_context::dummy(); + let ref = new(Test { id: object::new(ctx) }, ctx); + + let (value, borrow) = borrow(&mut ref); + put_back(&mut ref, value, borrow); + + let Test { id } = destroy(ref); + object::delete(id); + } + + #[test] + #[expected_failure(abort_code = EWrongValue)] + /// The `value` is swapped with another instance of the type `T`. + fun test_object_swap() { + let ctx = &mut sui::tx_context::dummy(); + let ref_1 = new(Test { id: object::new(ctx) }, ctx); + let ref_2 = new(Test { id: object::new(ctx) }, ctx); + + let (v_1, b_1) = borrow(&mut ref_1); + let (v_2, b_2) = borrow(&mut ref_2); + + put_back(&mut ref_1, v_2, b_1); + put_back(&mut ref_2, v_1, b_2); + + let Test { id } = destroy(ref_1); + object::delete(id); + + let Test { id } = destroy(ref_2); + object::delete(id); + } + + #[test] + #[expected_failure(abort_code = EWrongBorrow)] + /// The both `borrow` and `value` are swapped with another `Referent`. + fun test_borrow_fail() { + let ctx = &mut sui::tx_context::dummy(); + let ref_1 = new(Test { id: object::new(ctx) }, ctx); + let ref_2 = new(Test { id: object::new(ctx) }, ctx); + + let (v_1, b_1) = borrow(&mut ref_1); + let (v_2, b_2) = borrow(&mut ref_2); + + put_back(&mut ref_1, v_2, b_2); + put_back(&mut ref_2, v_1, b_1); + + let Test { id } = destroy(ref_1); + object::delete(id); + + let Test { id } = destroy(ref_2); + object::delete(id); + } +} diff --git a/lang/move-on-sui/test/ok/clock.move b/lang/move-on-sui/test/ok/clock.move new file mode 100644 index 00000000..dd033370 --- /dev/null +++ b/lang/move-on-sui/test/ok/clock.move @@ -0,0 +1,96 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// APIs for accessing time from move calls, via the `Clock`: a unique +/// shared object that is created at 0x6 during genesis. +module sui::clock { + use sui::object::{Self, UID}; + use sui::transfer; + use sui::tx_context::{Self, TxContext}; + + /// Sender is not @0x0 the system address. + const ENotSystemAddress: u64 = 0; + + /// Singleton shared object that exposes time to Move calls. This + /// object is found at address 0x6, and can only be read (accessed + /// via an immutable reference) by entry functions. + /// + /// Entry Functions that attempt to accept `Clock` by mutable + /// reference or value will fail to verify, and honest validators + /// will not sign or execute transactions that use `Clock` as an + /// input parameter, unless it is passed by immutable reference. + struct Clock has key { + id: UID, + /// The clock's timestamp, which is set automatically by a + /// system transaction every time consensus commits a + /// schedule, or by `sui::clock::increment_for_testing` during + /// testing. + timestamp_ms: u64, + } + + /// The `clock`'s current timestamp as a running total of + /// milliseconds since an arbitrary point in the past. + public fun timestamp_ms(clock: &Clock): u64 { + clock.timestamp_ms + } + + #[allow(unused_function)] + /// Create and share the singleton Clock -- this function is + /// called exactly once, during genesis. + fun create(ctx: &TxContext) { + assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress); + + transfer::share_object(Clock { + id: object::clock(), + // Initialised to zero, but set to a real timestamp by a + // system transaction before it can be witnessed by a move + // call. + timestamp_ms: 0, + }) + } + + #[allow(unused_function)] + fun consensus_commit_prologue( + clock: &mut Clock, + timestamp_ms: u64, + ctx: &TxContext, + ) { + // Validator will make a special system call with sender set as 0x0. + assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress); + + clock.timestamp_ms = timestamp_ms + } + + #[test_only] + /// Expose the functionality of `create()` (usually only done during + /// genesis) for tests that want to create a Clock. + public fun create_for_testing(ctx: &mut sui::tx_context::TxContext): Clock { + Clock { + id: object::new(ctx), + timestamp_ms: 0, + } + } + + #[test_only] + /// For transactional tests (if a Clock is used as a shared object). + public fun share_for_testing(clock: Clock) { + transfer::share_object(clock) + } + + #[test_only] + public fun increment_for_testing(clock: &mut Clock, tick: u64) { + clock.timestamp_ms = clock.timestamp_ms + tick; + } + + #[test_only] + public fun set_for_testing(clock: &mut Clock, timestamp_ms: u64) { + assert!(timestamp_ms >= clock.timestamp_ms, 0); + clock.timestamp_ms = timestamp_ms; + } + + #[test_only] + public fun destroy_for_testing(clock: Clock) { + let Clock { id, timestamp_ms: _ } = clock; + object::delete(id); + } +} diff --git a/lang/move-on-sui/test/ok/coin.move b/lang/move-on-sui/test/ok/coin.move new file mode 100644 index 00000000..ac1eb593 --- /dev/null +++ b/lang/move-on-sui/test/ok/coin.move @@ -0,0 +1,426 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Defines the `Coin` type - platform wide representation of fungible +/// tokens and coins. `Coin` can be described as a secure wrapper around +/// `Balance` type. +module sui::coin { + use std::string; + use std::ascii; + use std::option::{Self, Option}; + use sui::balance::{Self, Balance, Supply}; + use sui::tx_context::TxContext; + use sui::object::{Self, UID}; + use sui::transfer; + use sui::url::{Self, Url}; + use std::vector; + + /// A type passed to create_supply is not a one-time witness. + const EBadWitness: u64 = 0; + /// Invalid arguments are passed to a function. + const EInvalidArg: u64 = 1; + /// Trying to split a coin more times than its balance allows. + const ENotEnough: u64 = 2; + + /// A coin of type `T` worth `value`. Transferable and storable + struct Coin has key, store { + id: UID, + balance: Balance + } + + /// Each Coin type T created through `create_currency` function will have a + /// unique instance of CoinMetadata that stores the metadata for this coin type. + struct CoinMetadata has key, store { + id: UID, + /// Number of decimal places the coin uses. + /// A coin with `value ` N and `decimals` D should be shown as N / 10^D + /// E.g., a coin with `value` 7002 and decimals 3 should be displayed as 7.002 + /// This is metadata for display usage only. + decimals: u8, + /// Name for the token + name: string::String, + /// Symbol for the token + symbol: ascii::String, + /// Description of the token + description: string::String, + /// URL for the token logo + icon_url: Option + } + + /// Capability allowing the bearer to mint and burn + /// coins of type `T`. Transferable + struct TreasuryCap has key, store { + id: UID, + total_supply: Supply + } + + // === Supply <-> TreasuryCap morphing and accessors === + + /// Return the total number of `T`'s in circulation. + public fun total_supply(cap: &TreasuryCap): u64 { + balance::supply_value(&cap.total_supply) + } + + /// Unwrap `TreasuryCap` getting the `Supply`. + /// + /// Operation is irreversible. Supply cannot be converted into a `TreasuryCap` due + /// to different security guarantees (TreasuryCap can be created only once for a type) + public fun treasury_into_supply(treasury: TreasuryCap): Supply { + let TreasuryCap { id, total_supply } = treasury; + object::delete(id); + total_supply + } + + /// Get immutable reference to the treasury's `Supply`. + public fun supply_immut(treasury: &TreasuryCap): &Supply { + &treasury.total_supply + } + + /// Get mutable reference to the treasury's `Supply`. + public fun supply_mut(treasury: &mut TreasuryCap): &mut Supply { + &mut treasury.total_supply + } + + // === Balance <-> Coin accessors and type morphing === + + /// Public getter for the coin's value + public fun value(self: &Coin): u64 { + balance::value(&self.balance) + } + + /// Get immutable reference to the balance of a coin. + public fun balance(coin: &Coin): &Balance { + &coin.balance + } + + /// Get a mutable reference to the balance of a coin. + public fun balance_mut(coin: &mut Coin): &mut Balance { + &mut coin.balance + } + + /// Wrap a balance into a Coin to make it transferable. + public fun from_balance(balance: Balance, ctx: &mut TxContext): Coin { + Coin { id: object::new(ctx), balance } + } + + /// Destruct a Coin wrapper and keep the balance. + public fun into_balance(coin: Coin): Balance { + let Coin { id, balance } = coin; + object::delete(id); + balance + } + + /// Take a `Coin` worth of `value` from `Balance`. + /// Aborts if `value > balance.value` + public fun take( + balance: &mut Balance, value: u64, ctx: &mut TxContext, + ): Coin { + Coin { + id: object::new(ctx), + balance: balance::split(balance, value) + } + } + + spec take { + let before_val = balance.value; + let post after_val = balance.value; + ensures after_val == before_val - value; + + aborts_if value > before_val; + aborts_if ctx.ids_created + 1 > MAX_U64; + } + + /// Put a `Coin` to the `Balance`. + public fun put(balance: &mut Balance, coin: Coin) { + balance::join(balance, into_balance(coin)); + } + + spec put { + let before_val = balance.value; + let post after_val = balance.value; + ensures after_val == before_val + coin.balance.value; + + aborts_if before_val + coin.balance.value > MAX_U64; + } + + // === Base Coin functionality === + + /// Consume the coin `c` and add its value to `self`. + /// Aborts if `c.value + self.value > U64_MAX` + public entry fun join(self: &mut Coin, c: Coin) { + let Coin { id, balance } = c; + object::delete(id); + balance::join(&mut self.balance, balance); + } + + spec join { + let before_val = self.balance.value; + let post after_val = self.balance.value; + ensures after_val == before_val + c.balance.value; + + aborts_if before_val + c.balance.value > MAX_U64; + } + + /// Split coin `self` to two coins, one with balance `split_amount`, + /// and the remaining balance is left is `self`. + public fun split( + self: &mut Coin, split_amount: u64, ctx: &mut TxContext + ): Coin { + take(&mut self.balance, split_amount, ctx) + } + + spec split { + let before_val = self.balance.value; + let post after_val = self.balance.value; + ensures after_val == before_val - split_amount; + + aborts_if split_amount > before_val; + aborts_if ctx.ids_created + 1 > MAX_U64; + } + + /// Split coin `self` into `n - 1` coins with equal balances. The remainder is left in + /// `self`. Return newly created coins. + public fun divide_into_n( + self: &mut Coin, n: u64, ctx: &mut TxContext + ): vector> { + assert!(n > 0, EInvalidArg); + assert!(n <= value(self), ENotEnough); + + let vec = vector::empty>(); + let i = 0; + let split_amount = value(self) / n; + while ({ + spec { + invariant i <= n-1; + invariant self.balance.value == old(self).balance.value - (i * split_amount); + invariant ctx.ids_created == old(ctx).ids_created + i; + }; + i < n - 1 + }) { + vector::push_back(&mut vec, split(self, split_amount, ctx)); + i = i + 1; + }; + vec + } + + spec divide_into_n { + let before_val = self.balance.value; + let post after_val = self.balance.value; + let split_amount = before_val / n; + ensures after_val == before_val - ((n - 1) * split_amount); + + aborts_if n == 0; + aborts_if self.balance.value < n; + aborts_if ctx.ids_created + n - 1 > MAX_U64; + } + + /// Make any Coin with a zero value. Useful for placeholding + /// bids/payments or preemptively making empty balances. + public fun zero(ctx: &mut TxContext): Coin { + Coin { id: object::new(ctx), balance: balance::zero() } + } + + /// Destroy a coin with value zero + public fun destroy_zero(c: Coin) { + let Coin { id, balance } = c; + object::delete(id); + balance::destroy_zero(balance) + } + + // === Registering new coin types and managing the coin supply === + + /// Create a new currency type `T` as and return the `TreasuryCap` for + /// `T` to the caller. Can only be called with a `one-time-witness` + /// type, ensuring that there's only one `TreasuryCap` per `T`. + public fun create_currency( + witness: T, + decimals: u8, + symbol: vector, + name: vector, + description: vector, + icon_url: Option, + ctx: &mut TxContext + ): (TreasuryCap, CoinMetadata) { + // Make sure there's only one instance of the type T + assert!(sui::types::is_one_time_witness(&witness), EBadWitness); + + ( + TreasuryCap { + id: object::new(ctx), + total_supply: balance::create_supply(witness) + }, + CoinMetadata { + id: object::new(ctx), + decimals, + name: string::utf8(name), + symbol: ascii::string(symbol), + description: string::utf8(description), + icon_url + } + ) + } + + /// Create a coin worth `value`. and increase the total supply + /// in `cap` accordingly. + public fun mint( + cap: &mut TreasuryCap, value: u64, ctx: &mut TxContext, + ): Coin { + Coin { + id: object::new(ctx), + balance: balance::increase_supply(&mut cap.total_supply, value) + } + } + + spec schema MintBalance { + cap: TreasuryCap; + value: u64; + + let before_supply = cap.total_supply.value; + let post after_supply = cap.total_supply.value; + ensures after_supply == before_supply + value; + + aborts_if before_supply + value >= MAX_U64; + } + + spec mint { + include MintBalance; + aborts_if ctx.ids_created + 1 > MAX_U64; + } + + /// Mint some amount of T as a `Balance` and increase the total + /// supply in `cap` accordingly. + /// Aborts if `value` + `cap.total_supply` >= U64_MAX + public fun mint_balance( + cap: &mut TreasuryCap, value: u64 + ): Balance { + balance::increase_supply(&mut cap.total_supply, value) + } + + spec mint_balance { + include MintBalance; + } + + /// Destroy the coin `c` and decrease the total supply in `cap` + /// accordingly. + public entry fun burn(cap: &mut TreasuryCap, c: Coin): u64 { + let Coin { id, balance } = c; + object::delete(id); + balance::decrease_supply(&mut cap.total_supply, balance) + } + + spec schema Burn { + cap: TreasuryCap; + c: Coin; + + let before_supply = cap.total_supply.value; + let post after_supply = cap.total_supply.value; + ensures after_supply == before_supply - c.balance.value; + + aborts_if before_supply < c.balance.value; + } + + spec burn { + include Burn; + } + + // === Entrypoints === + + /// Mint `amount` of `Coin` and send it to `recipient`. Invokes `mint()`. + public entry fun mint_and_transfer( + c: &mut TreasuryCap, amount: u64, recipient: address, ctx: &mut TxContext + ) { + transfer::public_transfer(mint(c, amount, ctx), recipient) + } + + // === Update coin metadata === + + /// Update name of the coin in `CoinMetadata` + public entry fun update_name( + _treasury: &TreasuryCap, metadata: &mut CoinMetadata, name: string::String + ) { + metadata.name = name; + } + + /// Update the symbol of the coin in `CoinMetadata` + public entry fun update_symbol( + _treasury: &TreasuryCap, metadata: &mut CoinMetadata, symbol: ascii::String + ) { + metadata.symbol = symbol; + } + + /// Update the description of the coin in `CoinMetadata` + public entry fun update_description( + _treasury: &TreasuryCap, metadata: &mut CoinMetadata, description: string::String + ) { + metadata.description = description; + } + + /// Update the url of the coin in `CoinMetadata` + public entry fun update_icon_url( + _treasury: &TreasuryCap, metadata: &mut CoinMetadata, url: ascii::String + ) { + metadata.icon_url = option::some(url::new_unsafe(url)); + } + + // === Get coin metadata fields for on-chain consumption === + + public fun get_decimals(metadata: &CoinMetadata): u8 { + metadata.decimals + } + + public fun get_name(metadata: &CoinMetadata): string::String { + metadata.name + } + + public fun get_symbol(metadata: &CoinMetadata): ascii::String { + metadata.symbol + } + + public fun get_description(metadata: &CoinMetadata): string::String { + metadata.description + } + + public fun get_icon_url(metadata: &CoinMetadata): Option { + metadata.icon_url + } + + // === Test-only code === + + #[test_only] + /// Mint coins of any type for (obviously!) testing purposes only + public fun mint_for_testing(value: u64, ctx: &mut TxContext): Coin { + Coin { id: object::new(ctx), balance: balance::create_for_testing(value) } + } + + #[test_only] + /// Burn coins of any type for testing purposes only + public fun burn_for_testing(coin: Coin): u64 { + let Coin { id, balance } = coin; + object::delete(id); + balance::destroy_for_testing(balance) + } + + #[test_only] + /// Create a `TreasuryCap` for any `Coin` for testing purposes. + public fun create_treasury_cap_for_testing( + ctx: &mut TxContext + ): TreasuryCap { + TreasuryCap { + id: object::new(ctx), + total_supply: balance::create_supply_for_testing() + } + } + + // === Deprecated code === + + // oops, wanted treasury: &TreasuryCap + public fun supply(treasury: &mut TreasuryCap): &Supply { + &treasury.total_supply + } + + // deprecated as we have CoinMetadata now + #[allow(unused_field)] + struct CurrencyCreated has copy, drop { + decimals: u8 + } +} diff --git a/lang/move-on-sui/test/ok/damir1.move b/lang/move-on-sui/test/ok/damir1.move new file mode 100644 index 00000000..a0b3e4f0 --- /dev/null +++ b/lang/move-on-sui/test/ok/damir1.move @@ -0,0 +1,5 @@ +module 0x0::o { + public fun test() { + to_vec_set(vector[1,5,3,4]) + } +} diff --git a/lang/move-on-sui/test/ok/damir2.move b/lang/move-on-sui/test/ok/damir2.move new file mode 100644 index 00000000..1bfe3aad --- /dev/null +++ b/lang/move-on-sui/test/ok/damir2.move @@ -0,0 +1,5 @@ +module a::y { + fun a() { + a(a); + } +} diff --git a/lang/move-on-sui/test/ok/debug.move b/lang/move-on-sui/test/ok/debug.move new file mode 100644 index 00000000..dc9d236a --- /dev/null +++ b/lang/move-on-sui/test/ok/debug.move @@ -0,0 +1,9 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Module providing debug functionality. +module std::debug { + native public fun print(x: &T); + + native public fun print_stack_trace(); +} diff --git a/lang/move-on-sui/test/ok/display.move b/lang/move-on-sui/test/ok/display.move new file mode 100644 index 00000000..b697e4ba --- /dev/null +++ b/lang/move-on-sui/test/ok/display.move @@ -0,0 +1,233 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Defines a Display struct which defines the way an Object +/// should be displayed. The intention is to keep data as independent +/// from its display as possible, protecting the development process +/// and keeping it separate from the ecosystem agreements. +/// +/// Each of the fields of the Display object should allow for pattern +/// substitution and filling-in the pieces using the data from the object T. +/// +/// More entry functions might be added in the future depending on the use cases. +module sui::display { + use sui::package::{from_package, Publisher}; + use sui::tx_context::{sender, TxContext}; + use sui::vec_map::{Self, VecMap}; + use sui::object::{Self, ID, UID}; + use sui::transfer; + use sui::event; + use std::vector; + use std::string::String; + + /// For when T does not belong to the package `Publisher`. + const ENotOwner: u64 = 0; + + /// For when vectors passed into one of the multiple insert functions + /// don't match in their lengths. + const EVecLengthMismatch: u64 = 1; + + /// The Display object. Defines the way a T instance should be + /// displayed. Display object can only be created and modified with + /// a PublisherCap, making sure that the rules are set by the owner + /// of the type. + /// + /// Each of the display properties should support patterns outside + /// of the system, making it simpler to customize Display based + /// on the property values of an Object. + /// ``` + /// // Example of a display object + /// Display<0x...::capy::Capy> { + /// fields: + /// + /// + /// + /// + /// } + /// ``` + /// + /// Uses only String type due to external-facing nature of the object, + /// the property names have a priority over their types. + struct Display has key, store { + id: UID, + /// Contains fields for display. Currently supported + /// fields are: name, link, image and description. + fields: VecMap, + /// Version that can only be updated manually by the Publisher. + version: u16 + } + + /// Event: emitted when a new Display object has been created for type T. + /// Type signature of the event corresponds to the type while id serves for + /// the discovery. + /// + /// Since Sui RPC supports querying events by type, finding a Display for the T + /// would be as simple as looking for the first event with `Display`. + struct DisplayCreated has copy, drop { + id: ID + } + + /// Version of Display got updated - + struct VersionUpdated has copy, drop { + id: ID, + version: u16, + fields: VecMap, + } + + // === Initializer Methods === + + /// Create an empty Display object. It can either be shared empty or filled + /// with data right away via cheaper `set_owned` method. + public fun new(pub: &Publisher, ctx: &mut TxContext): Display { + assert!(is_authorized(pub), ENotOwner); + create_internal(ctx) + } + + /// Create a new Display object with a set of fields. + public fun new_with_fields( + pub: &Publisher, fields: vector, values: vector, ctx: &mut TxContext + ): Display { + let len = vector::length(&fields); + assert!(len == vector::length(&values), EVecLengthMismatch); + + let i = 0; + let display = new(pub, ctx); + while (i < len) { + add_internal(&mut display, *vector::borrow(&fields, i), *vector::borrow(&values, i)); + i = i + 1; + }; + + display + } + + // === Entry functions: Create === + + #[lint_allow(self_transfer)] + /// Create a new empty Display object and keep it. + entry public fun create_and_keep(pub: &Publisher, ctx: &mut TxContext) { + transfer::public_transfer(new(pub, ctx), sender(ctx)) + } + + /// Manually bump the version and emit an event with the updated version's contents. + entry public fun update_version( + display: &mut Display + ) { + display.version = display.version + 1; + event::emit(VersionUpdated { + version: display.version, + fields: *&display.fields, + id: object::uid_to_inner(&display.id), + }) + } + + // === Entry functions: Add/Modify fields === + + /// Sets a custom `name` field with the `value`. + entry public fun add(self: &mut Display, name: String, value: String) { + add_internal(self, name, value) + } + + /// Sets multiple `fields` with `values`. + entry public fun add_multiple( + self: &mut Display, fields: vector, values: vector + ) { + let len = vector::length(&fields); + assert!(len == vector::length(&values), EVecLengthMismatch); + + let i = 0; + while (i < len) { + add_internal(self, *vector::borrow(&fields, i), *vector::borrow(&values, i)); + i = i + 1; + }; + } + + /// Change the value of the field. + /// TODO (long run): version changes; + entry public fun edit(self: &mut Display, name: String, value: String) { + let (_, _) = vec_map::remove(&mut self.fields, &name); + add_internal(self, name, value) + } + + /// Remove the key from the Display. + entry public fun remove(self: &mut Display, name: String) { + vec_map::remove(&mut self.fields, &name); + } + + // === Access fields === + + /// Authorization check; can be performed externally to implement protection rules for Display. + public fun is_authorized(pub: &Publisher): bool { + from_package(pub) + } + + /// Read the `version` field. + public fun version(d: &Display): u16 { + d.version + } + + /// Read the `fields` field. + public fun fields(d: &Display): &VecMap { + &d.fields + } + + // === Private functions === + + /// Internal function to create a new `Display`. + fun create_internal(ctx: &mut TxContext): Display { + let uid = object::new(ctx); + + event::emit(DisplayCreated { + id: object::uid_to_inner(&uid) + }); + + Display { + id: uid, + fields: vec_map::empty(), + version: 0, + } + } + + /// Private method for inserting fields without security checks. + fun add_internal(display: &mut Display, name: String, value: String) { + vec_map::insert(&mut display.fields, name, value) + } +} + +#[test_only] +module sui::display_tests { + use sui::object::UID; + use sui::test_scenario as test; + use sui::transfer; + use std::string::{utf8, String}; + use sui::package; + use sui::display; + + #[allow(unused_field)] + /// An example object. + /// Purely for visibility. + struct Capy has key { + id: UID, + name: String + } + + /// Test witness type to create a Publisher object. + struct CAPY has drop {} + + #[test] + fun capy_init() { + let test = test::begin(@0x2); + let pub = package::test_claim(CAPY {}, test::ctx(&mut test)); + + // create a new display object + let display = display::new(&pub, test::ctx(&mut test)); + + display::add(&mut display, utf8(b"name"), utf8(b"Capy {name}")); + display::add(&mut display, utf8(b"link"), utf8(b"https://capy.art/capy/{id}")); + display::add(&mut display, utf8(b"image"), utf8(b"https://api.capy.art/capy/{id}/svg")); + display::add(&mut display, utf8(b"description"), utf8(b"A Lovely Capy")); + + package::burn_publisher(pub); + transfer::public_transfer(display, @0x2); + test::end(test); + } +} diff --git a/lang/move-on-sui/test/ok/dot_call.move b/lang/move-on-sui/test/ok/dot_call.move new file mode 100644 index 00000000..9786819a --- /dev/null +++ b/lang/move-on-sui/test/ok/dot_call.move @@ -0,0 +1,37 @@ +module Move2024::M1 { + + public use fun Self::foo as SomeStruct.f1; + public use fun Move2024::M1::foo as SomeStruct.f2; + + public struct SomeStruct has drop { + some_field: u64 + } + + public fun snew(): SomeStruct { + SomeStruct { some_field: 42 } + } + + public fun foo(s: &SomeStruct): u64 { + s.some_field + } + + public fun bar(s: &SomeStruct, v: u64): u64 { + s.some_field + v + } +} + + +module Move2024::M2 { + use Move2024::M1::{Self, SomeStruct as SomeStructAlias}; + use Move2024::M1 as M1_ALIAS; + use fun M1_ALIAS::bar as Move2024::M1::SomeStruct.f3; + + public fun baz() { + use fun M1::bar as SomeStructAlias.f4; + + let some_struct: SomeStructAlias = M1::snew(); + let val = 42; + assert!(some_struct.f1() == some_struct.f2(), 0); + assert!(some_struct.f3(val) == some_struct.f4(val), 0); + } +} diff --git a/lang/move-on-sui/test/ok/dotted_vector_literal_access.move b/lang/move-on-sui/test/ok/dotted_vector_literal_access.move new file mode 100644 index 00000000..f9645f56 --- /dev/null +++ b/lang/move-on-sui/test/ok/dotted_vector_literal_access.move @@ -0,0 +1,5 @@ +module 0x1::m { + fun f() { + let i = vector[i].to_string(); + } +} diff --git a/lang/move-on-sui/test/ok/dynamic_field.move b/lang/move-on-sui/test/ok/dynamic_field.move new file mode 100644 index 00000000..b8c5366b --- /dev/null +++ b/lang/move-on-sui/test/ok/dynamic_field.move @@ -0,0 +1,269 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[allow(unused_const)] +/// In addition to the fields declared in its type definition, a Sui object can have dynamic fields +/// that can be added after the object has been constructed. Unlike ordinary field names +/// (which are always statically declared identifiers) a dynamic field name can be any value with +/// the `copy`, `drop`, and `store` abilities, e.g. an integer, a boolean, or a string. +/// This gives Sui programmers the flexibility to extend objects on-the-fly, and it also serves as a +/// building block for core collection types +module sui::dynamic_field { + use std::option::{Self, Option}; + use sui::object::{Self, ID, UID}; + use sui::prover; + + friend sui::dynamic_object_field; + + /// The object already has a dynamic field with this name (with the value and type specified) + const EFieldAlreadyExists: u64 = 0; + /// Cannot load dynamic field. + /// The object does not have a dynamic field with this name (with the value and type specified) + const EFieldDoesNotExist: u64 = 1; + /// The object has a field with that name, but the value type does not match + const EFieldTypeMismatch: u64 = 2; + /// Failed to serialize the field's name + const EBCSSerializationFailure: u64 = 3; + /// The object added as a dynamic field was previously a shared object + const ESharedObjectOperationNotSupported: u64 = 4; + + /// Internal object used for storing the field and value + struct Field has key { + /// Determined by the hash of the object ID, the field name value and it's type, + /// i.e. hash(parent.id || name || Name) + id: UID, + /// The value for the name of this field + name: Name, + /// The value bound to this field + value: Value, + } + + /// Adds a dynamic field to the object `object: &mut UID` at field specified by `name: Name`. + /// Aborts with `EFieldAlreadyExists` if the object already has that field with that name. + public fun add( + // we use &mut UID in several spots for access control + object: &mut UID, + name: Name, + value: Value, + ) { + let object_addr = object::uid_to_address(object); + let hash = hash_type_and_key(object_addr, name); + assert!(!has_child_object(object_addr, hash), EFieldAlreadyExists); + let field = Field { + id: object::new_uid_from_hash(hash), + name, + value, + }; + add_child_object(object_addr, field) + } + + spec add { + pragma opaque; + aborts_if [abstract] prover::uid_has_field(object, name); + modifies [abstract] global>(object::uid_to_address(object)); + ensures [abstract] object == old(object); + ensures [abstract] exists>(object::uid_to_address(object)); + ensures [abstract] (!old(exists>(object::uid_to_address(object)))) + ==> global>(object::uid_to_address(object)).names == vec(name); + ensures [abstract] old(exists>(object::uid_to_address(object))) + ==> global>(object::uid_to_address(object)).names == old(concat( + global>(object::uid_to_address(object)).names, + vec(name) + )); + } + + /// Immutably borrows the `object`s dynamic field with the name specified by `name: Name`. + /// Aborts with `EFieldDoesNotExist` if the object does not have a field with that name. + /// Aborts with `EFieldTypeMismatch` if the field exists, but the value does not have the specified + /// type. + public fun borrow( + object: &UID, + name: Name, + ): &Value { + let object_addr = object::uid_to_address(object); + let hash = hash_type_and_key(object_addr, name); + let field = borrow_child_object>(object, hash); + &field.value + } + + spec borrow { + pragma opaque; + aborts_if [abstract] !prover::uid_has_field(object, name); + } + + /// Mutably borrows the `object`s dynamic field with the name specified by `name: Name`. + /// Aborts with `EFieldDoesNotExist` if the object does not have a field with that name. + /// Aborts with `EFieldTypeMismatch` if the field exists, but the value does not have the specified + /// type. + public fun borrow_mut( + object: &mut UID, + name: Name, + ): &mut Value { + let object_addr = object::uid_to_address(object); + let hash = hash_type_and_key(object_addr, name); + let field = borrow_child_object_mut>(object, hash); + &mut field.value + } + + spec borrow_mut { + pragma opaque; + aborts_if [abstract] !prover::uid_has_field(object, name); + } + + /// Removes the `object`s dynamic field with the name specified by `name: Name` and returns the + /// bound value. + /// Aborts with `EFieldDoesNotExist` if the object does not have a field with that name. + /// Aborts with `EFieldTypeMismatch` if the field exists, but the value does not have the specified + /// type. + public fun remove( + object: &mut UID, + name: Name, + ): Value { + let object_addr = object::uid_to_address(object); + let hash = hash_type_and_key(object_addr, name); + let Field { id, name: _, value } = remove_child_object>(object_addr, hash); + object::delete(id); + value + } + + spec remove { + pragma opaque; + aborts_if [abstract] !prover::uid_has_field(object, name); + modifies [abstract] global>(object::uid_to_address(object)); + ensures [abstract] object.id == old(object.id); + ensures [abstract] old(prover::uid_num_fields(object)) == 1 + ==> !exists>(object::uid_to_address(object)); + ensures [abstract] old(prover::uid_num_fields(object)) > 1 + ==> global>(object::uid_to_address(object)).names == + old(prover::vec_remove(global>(object::uid_to_address(object)).names, + index_of(global>(object::uid_to_address(object)).names, name))); + // this is needed to ensure that there was only one field with a given name + ensures [abstract] !prover::uid_has_field(object, name); + } + + + /// Returns true if and only if the `object` has a dynamic field with the name specified by + /// `name: Name` but without specifying the `Value` type + public fun exists_( + object: &UID, + name: Name, + ): bool { + let object_addr = object::uid_to_address(object); + let hash = hash_type_and_key(object_addr, name); + has_child_object(object_addr, hash) + } + + /// Removes the dynamic field if it exists. Returns the `some(Value)` if it exists or none otherwise. + public fun remove_if_exists( + object: &mut UID, + name: Name + ): Option { + if (exists_(object, name)) { + option::some(remove(object, name)) + } else { + option::none() + } + } + + /// Returns true if and only if the `object` has a dynamic field with the name specified by + /// `name: Name` with an assigned value of type `Value`. + public fun exists_with_type( + object: &UID, + name: Name, + ): bool { + let object_addr = object::uid_to_address(object); + let hash = hash_type_and_key(object_addr, name); + has_child_object_with_ty>(object_addr, hash) + } + + public(friend) fun field_info( + object: &UID, + name: Name, + ): (&UID, address) { + let object_addr = object::uid_to_address(object); + let hash = hash_type_and_key(object_addr, name); + let Field { id, name: _, value } = borrow_child_object>(object, hash); + (id, object::id_to_address(value)) + } + + public(friend) fun field_info_mut( + object: &mut UID, + name: Name, + ): (&mut UID, address) { + let object_addr = object::uid_to_address(object); + let hash = hash_type_and_key(object_addr, name); + let Field { id, name: _, value } = borrow_child_object_mut>(object, hash); + (id, object::id_to_address(value)) + } + + /// May abort with `EBCSSerializationFailure`. + public(friend) native fun hash_type_and_key(parent: address, k: K): address; + + spec hash_type_and_key { + pragma opaque; + // TODO: stub to be replaced by actual abort conditions if any + aborts_if [abstract] true; + // TODO: specify actual function behavior + } + + public(friend) native fun add_child_object(parent: address, child: Child); + + spec add_child_object { + pragma opaque; + // TODO: stub to be replaced by actual abort conditions if any + aborts_if [abstract] true; + // TODO: specify actual function behavior + } + + /// throws `EFieldDoesNotExist` if a child does not exist with that ID + /// or throws `EFieldTypeMismatch` if the type does not match, + /// and may also abort with `EBCSSerializationFailure` + /// we need two versions to return a reference or a mutable reference + public(friend) native fun borrow_child_object(object: &UID, id: address): &Child; + + spec borrow_child_object { + pragma opaque; + // TODO: stub to be replaced by actual abort conditions if any + aborts_if [abstract] true; + // TODO: specify actual function behavior + } + + public(friend) native fun borrow_child_object_mut(object: &mut UID, id: address): &mut Child; + + spec borrow_child_object_mut { + pragma opaque; + // TODO: stub to be replaced by actual abort conditions if any + aborts_if [abstract] true; + // TODO: specify actual function behavior + } + + /// throws `EFieldDoesNotExist` if a child does not exist with that ID + /// or throws `EFieldTypeMismatch` if the type does not match, + /// and may also abort with `EBCSSerializationFailure`. + public(friend) native fun remove_child_object(parent: address, id: address): Child; + + spec remove_child_object { + pragma opaque; + // TODO: stub to be replaced by actual abort conditions if any + aborts_if [abstract] true; + // TODO: specify actual function behavior + } + + public(friend) native fun has_child_object(parent: address, id: address): bool; + + spec has_child_object { + pragma opaque; + // TODO: stub to be replaced by actual abort conditions if any + aborts_if [abstract] true; + // TODO: specify actual function behavior + } + + public(friend) native fun has_child_object_with_ty(parent: address, id: address): bool; + + spec has_child_object_with_ty { + pragma opaque; + // TODO: stub to be replaced by actual abort conditions if any + aborts_if [abstract] true; + // TODO: specify actual function behavior + } +} diff --git a/lang/move-on-sui/test/ok/dynamic_object_field.move b/lang/move-on-sui/test/ok/dynamic_object_field.move new file mode 100644 index 00000000..38a0a5f7 --- /dev/null +++ b/lang/move-on-sui/test/ok/dynamic_object_field.move @@ -0,0 +1,115 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Similar to `sui::dynamic_field`, this module allows for the access of dynamic fields. But +/// unlike, `sui::dynamic_field` the values bound to these dynamic fields _must_ be objects +/// themselves. This allows for the objects to still exist within in storage, which may be important +/// for external tools. The difference is otherwise not observable from within Move. +module sui::dynamic_object_field { + use std::option::{Self, Option}; + use sui::object::{Self, UID, ID}; + use sui::dynamic_field::{ + Self as field, + add_child_object, + borrow_child_object, + borrow_child_object_mut, + remove_child_object, + }; + + // Internal object used for storing the field and the name associated with the value + // The separate type is necessary to prevent key collision with direct usage of dynamic_field + struct Wrapper has copy, drop, store { + name: Name, + } + + /// Adds a dynamic object field to the object `object: &mut UID` at field specified by `name: Name`. + /// Aborts with `EFieldAlreadyExists` if the object already has that field with that name. + public fun add( + // we use &mut UID in several spots for access control + object: &mut UID, + name: Name, + value: Value, + ) { + let key = Wrapper { name }; + let id = object::id(&value); + field::add(object, key, id); + let (field, _) = field::field_info>(object, key); + add_child_object(object::uid_to_address(field), value); + } + + /// Immutably borrows the `object`s dynamic object field with the name specified by `name: Name`. + /// Aborts with `EFieldDoesNotExist` if the object does not have a field with that name. + /// Aborts with `EFieldTypeMismatch` if the field exists, but the value object does not have the + /// specified type. + public fun borrow( + object: &UID, + name: Name, + ): &Value { + let key = Wrapper { name }; + let (field, value_id) = field::field_info>(object, key); + borrow_child_object(field, value_id) + } + + /// Mutably borrows the `object`s dynamic object field with the name specified by `name: Name`. + /// Aborts with `EFieldDoesNotExist` if the object does not have a field with that name. + /// Aborts with `EFieldTypeMismatch` if the field exists, but the value object does not have the + /// specified type. + public fun borrow_mut( + object: &mut UID, + name: Name, + ): &mut Value { + let key = Wrapper { name }; + let (field, value_id) = field::field_info_mut>(object, key); + borrow_child_object_mut(field, value_id) + } + + /// Removes the `object`s dynamic object field with the name specified by `name: Name` and returns + /// the bound object. + /// Aborts with `EFieldDoesNotExist` if the object does not have a field with that name. + /// Aborts with `EFieldTypeMismatch` if the field exists, but the value object does not have the + /// specified type. + public fun remove( + object: &mut UID, + name: Name, + ): Value { + let key = Wrapper { name }; + let (field, value_id) = field::field_info>(object, key); + let value = remove_child_object(object::uid_to_address(field), value_id); + field::remove, ID>(object, key); + value + } + + /// Returns true if and only if the `object` has a dynamic object field with the name specified by + /// `name: Name`. + public fun exists_( + object: &UID, + name: Name, + ): bool { + let key = Wrapper { name }; + field::exists_with_type, ID>(object, key) + } + + /// Returns true if and only if the `object` has a dynamic field with the name specified by + /// `name: Name` with an assigned value of type `Value`. + public fun exists_with_type( + object: &UID, + name: Name, + ): bool { + let key = Wrapper { name }; + if (!field::exists_with_type, ID>(object, key)) return false; + let (field, value_id) = field::field_info>(object, key); + field::has_child_object_with_ty(object::uid_to_address(field), value_id) + } + + /// Returns the ID of the object associated with the dynamic object field + /// Returns none otherwise + public fun id( + object: &UID, + name: Name, + ): Option { + let key = Wrapper { name }; + if (!field::exists_with_type, ID>(object, key)) return option::none(); + let (_field, value_id) = field::field_info>(object, key); + option::some(object::id_from_address(value_id)) + } +} diff --git a/lang/move-on-sui/test/ok/event.move b/lang/move-on-sui/test/ok/event.move new file mode 100644 index 00000000..f6478eee --- /dev/null +++ b/lang/move-on-sui/test/ok/event.move @@ -0,0 +1,38 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Events module. Defines the `sui::event::emit` function which +/// creates and sends a custom MoveEvent as a part of the effects +/// certificate of the transaction. +/// +/// Every MoveEvent has the following properties: +/// - sender +/// - type signature (`T`) +/// - event data (the value of `T`) +/// - timestamp (local to a node) +/// - transaction digest +/// +/// Example: +/// ``` +/// module my::marketplace { +/// use sui::event; +/// /* ... */ +/// struct ItemPurchased has copy, drop { +/// item_id: ID, buyer: address +/// } +/// entry fun buy(/* .... */) { +/// /* ... */ +/// event::emit(ItemPurchased { item_id: ..., buyer: .... }) +/// } +/// } +/// ``` +module sui::event { + /// Emit a custom Move event, sending the data offchain. + /// + /// Used for creating custom indexes and tracking onchain + /// activity in a way that suits a specific application the most. + /// + /// The type `T` is the main way to index the event, and can contain + /// phantom parameters, eg `emit(MyEvent)`. + public native fun emit(event: T); +} diff --git a/lang/move-on-sui/test/ok/fixed_point32.move b/lang/move-on-sui/test/ok/fixed_point32.move new file mode 100644 index 00000000..c64d8180 --- /dev/null +++ b/lang/move-on-sui/test/ok/fixed_point32.move @@ -0,0 +1,167 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Defines a fixed-point numeric type with a 32-bit integer part and +/// a 32-bit fractional part. + +module std::fixed_point32 { + + /// Define a fixed-point numeric type with 32 fractional bits. + /// This is just a u64 integer but it is wrapped in a struct to + /// make a unique type. This is a binary representation, so decimal + /// values may not be exactly representable, but it provides more + /// than 9 decimal digits of precision both before and after the + /// decimal point (18 digits total). For comparison, double precision + /// floating-point has less than 16 decimal digits of precision, so + /// be careful about using floating-point to convert these values to + /// decimal. + struct FixedPoint32 has copy, drop, store { value: u64 } + + ///> TODO: This is a basic constant and should be provided somewhere centrally in the framework. + const MAX_U64: u128 = 18446744073709551615; + + /// The denominator provided was zero + const EDENOMINATOR: u64 = 0x10001; + /// The quotient value would be too large to be held in a `u64` + const EDIVISION: u64 = 0x20002; + /// The multiplied value would be too large to be held in a `u64` + const EMULTIPLICATION: u64 = 0x20003; + /// A division by zero was encountered + const EDIVISION_BY_ZERO: u64 = 0x10004; + /// The computed ratio when converting to a `FixedPoint32` would be unrepresentable + const ERATIO_OUT_OF_RANGE: u64 = 0x20005; + + /// Multiply a u64 integer by a fixed-point number, truncating any + /// fractional part of the product. This will abort if the product + /// overflows. + public fun multiply_u64(val: u64, multiplier: FixedPoint32): u64 { + // The product of two 64 bit values has 128 bits, so perform the + // multiplication with u128 types and keep the full 128 bit product + // to avoid losing accuracy. + let unscaled_product = (val as u128) * (multiplier.value as u128); + // The unscaled product has 32 fractional bits (from the multiplier) + // so rescale it by shifting away the low bits. + let product = unscaled_product >> 32; + // Check whether the value is too large. + assert!(product <= MAX_U64, EMULTIPLICATION); + (product as u64) + } + spec multiply_u64 { + pragma opaque; + include MultiplyAbortsIf; + ensures result == spec_multiply_u64(val, multiplier); + } + spec schema MultiplyAbortsIf { + val: num; + multiplier: FixedPoint32; + aborts_if spec_multiply_u64(val, multiplier) > MAX_U64 with EMULTIPLICATION; + } + spec fun spec_multiply_u64(val: num, multiplier: FixedPoint32): num { + (val * multiplier.value) >> 32 + } + + /// Divide a u64 integer by a fixed-point number, truncating any + /// fractional part of the quotient. This will abort if the divisor + /// is zero or if the quotient overflows. + public fun divide_u64(val: u64, divisor: FixedPoint32): u64 { + // Check for division by zero. + assert!(divisor.value != 0, EDIVISION_BY_ZERO); + // First convert to 128 bits and then shift left to + // add 32 fractional zero bits to the dividend. + let scaled_value = (val as u128) << 32; + let quotient = scaled_value / (divisor.value as u128); + // Check whether the value is too large. + assert!(quotient <= MAX_U64, EDIVISION); + // the value may be too large, which will cause the cast to fail + // with an arithmetic error. + (quotient as u64) + } + spec divide_u64 { + pragma opaque; + include DivideAbortsIf; + ensures result == spec_divide_u64(val, divisor); + } + spec schema DivideAbortsIf { + val: num; + divisor: FixedPoint32; + aborts_if divisor.value == 0 with EDIVISION_BY_ZERO; + aborts_if spec_divide_u64(val, divisor) > MAX_U64 with EDIVISION; + } + spec fun spec_divide_u64(val: num, divisor: FixedPoint32): num { + (val << 32) / divisor.value + } + + /// Create a fixed-point value from a rational number specified by its + /// numerator and denominator. Calling this function should be preferred + /// for using `Self::create_from_raw_value` which is also available. + /// This will abort if the denominator is zero. It will also + /// abort if the numerator is nonzero and the ratio is not in the range + /// 2^-32 .. 2^32-1. When specifying decimal fractions, be careful about + /// rounding errors: if you round to display N digits after the decimal + /// point, you can use a denominator of 10^N to avoid numbers where the + /// very small imprecision in the binary representation could change the + /// rounding, e.g., 0.0125 will round down to 0.012 instead of up to 0.013. + public fun create_from_rational(numerator: u64, denominator: u64): FixedPoint32 { + // If the denominator is zero, this will abort. + // Scale the numerator to have 64 fractional bits and the denominator + // to have 32 fractional bits, so that the quotient will have 32 + // fractional bits. + let scaled_numerator = (numerator as u128) << 64; + let scaled_denominator = (denominator as u128) << 32; + assert!(scaled_denominator != 0, EDENOMINATOR); + let quotient = scaled_numerator / scaled_denominator; + assert!(quotient != 0 || numerator == 0, ERATIO_OUT_OF_RANGE); + // Return the quotient as a fixed-point number. We first need to check whether the cast + // can succeed. + assert!(quotient <= MAX_U64, ERATIO_OUT_OF_RANGE); + FixedPoint32 { value: (quotient as u64) } + } + spec create_from_rational { + pragma opaque; + include CreateFromRationalAbortsIf; + ensures result == spec_create_from_rational(numerator, denominator); + } + spec schema CreateFromRationalAbortsIf { + numerator: u64; + denominator: u64; + let scaled_numerator = numerator << 64; + let scaled_denominator = denominator << 32; + let quotient = scaled_numerator / scaled_denominator; + aborts_if scaled_denominator == 0 with EDENOMINATOR; + aborts_if quotient == 0 && scaled_numerator != 0 with ERATIO_OUT_OF_RANGE; + aborts_if quotient > MAX_U64 with ERATIO_OUT_OF_RANGE; + } + spec fun spec_create_from_rational(numerator: num, denominator: num): FixedPoint32 { + FixedPoint32{value: (numerator << 64) / (denominator << 32)} + } + + /// Create a fixedpoint value from a raw value. + public fun create_from_raw_value(value: u64): FixedPoint32 { + FixedPoint32 { value } + } + spec create_from_raw_value { + pragma opaque; + aborts_if false; + ensures result.value == value; + } + + /// Accessor for the raw u64 value. Other less common operations, such as + /// adding or subtracting FixedPoint32 values, can be done using the raw + /// values directly. + public fun get_raw_value(num: FixedPoint32): u64 { + num.value + } + + /// Returns true if the ratio is zero. + public fun is_zero(num: FixedPoint32): bool { + num.value == 0 + } + + // **************** SPECIFICATIONS **************** + + spec module {} // switch documentation context to module level + + spec module { + pragma aborts_if_is_strict; + } +} diff --git a/lang/move-on-sui/test/ok/hash.move b/lang/move-on-sui/test/ok/hash.move new file mode 100644 index 00000000..ed84f18a --- /dev/null +++ b/lang/move-on-sui/test/ok/hash.move @@ -0,0 +1,11 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Module which defines SHA hashes for byte vectors. +/// +/// The functions in this module are natively declared both in the Move runtime +/// as in the Move prover's prelude. +module std::hash { + native public fun sha2_256(data: vector): vector; + native public fun sha3_256(data: vector): vector; +} diff --git a/lang/move-on-sui/test/ok/hello-world.move b/lang/move-on-sui/test/ok/hello-world.move new file mode 100644 index 00000000..e4bfb272 --- /dev/null +++ b/lang/move-on-sui/test/ok/hello-world.move @@ -0,0 +1,35 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// URL: standard Uniform Resource Locator string +module sui::url { + use std::ascii::String; + + /// Standard Uniform Resource Locator (URL) string. + public struct Url has store, copy, drop { + // TODO: validate URL format + url: String, + } + + /// Create a `Url`, with no validation + public fun new_unsafe(url: String): Url { + Url { url } + } + + /// Create a `Url` with no validation from bytes + /// Note: this will abort if `bytes` is not valid ASCII + public fun new_unsafe_from_bytes(bytes: vector): Url { + let url = bytes.to_ascii_string(); + Url { url } + } + + /// Get inner URL + public fun inner_url(self: &Url): String{ + self.url + } + + /// Update the inner URL + public fun update(self: &mut Url, url: String) { + self.url = url; + } +} diff --git a/lang/move-on-sui/test/ok/hex.move b/lang/move-on-sui/test/ok/hex.move new file mode 100644 index 00000000..64c116a0 --- /dev/null +++ b/lang/move-on-sui/test/ok/hex.move @@ -0,0 +1,111 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// HEX (Base16) encoding utility. +module sui::hex { + use std::vector; + + const EInvalidHexLength: u64 = 0; + const ENotValidHexCharacter: u64 = 1; + + /// Vector of Base16 values from `00` to `FF` + const HEX: vector> = vector[ + b"00",b"01",b"02",b"03",b"04",b"05",b"06",b"07",b"08",b"09",b"0a",b"0b",b"0c",b"0d",b"0e",b"0f",b"10",b"11",b"12",b"13",b"14",b"15",b"16",b"17",b"18",b"19",b"1a",b"1b",b"1c",b"1d",b"1e",b"1f",b"20",b"21",b"22",b"23",b"24",b"25",b"26",b"27",b"28",b"29",b"2a",b"2b",b"2c",b"2d",b"2e",b"2f",b"30",b"31",b"32",b"33",b"34",b"35",b"36",b"37",b"38",b"39",b"3a",b"3b",b"3c",b"3d",b"3e",b"3f",b"40",b"41",b"42",b"43",b"44",b"45",b"46",b"47",b"48",b"49",b"4a",b"4b",b"4c",b"4d",b"4e",b"4f",b"50",b"51",b"52",b"53",b"54",b"55",b"56",b"57",b"58",b"59",b"5a",b"5b",b"5c",b"5d",b"5e",b"5f",b"60",b"61",b"62",b"63",b"64",b"65",b"66",b"67",b"68",b"69",b"6a",b"6b",b"6c",b"6d",b"6e",b"6f",b"70",b"71",b"72",b"73",b"74",b"75",b"76",b"77",b"78",b"79",b"7a",b"7b",b"7c",b"7d",b"7e",b"7f",b"80",b"81",b"82",b"83",b"84",b"85",b"86",b"87",b"88",b"89",b"8a",b"8b",b"8c",b"8d",b"8e",b"8f",b"90",b"91",b"92",b"93",b"94",b"95",b"96",b"97",b"98",b"99",b"9a",b"9b",b"9c",b"9d",b"9e",b"9f",b"a0",b"a1",b"a2",b"a3",b"a4",b"a5",b"a6",b"a7",b"a8",b"a9",b"aa",b"ab",b"ac",b"ad",b"ae",b"af",b"b0",b"b1",b"b2",b"b3",b"b4",b"b5",b"b6",b"b7",b"b8",b"b9",b"ba",b"bb",b"bc",b"bd",b"be",b"bf",b"c0",b"c1",b"c2",b"c3",b"c4",b"c5",b"c6",b"c7",b"c8",b"c9",b"ca",b"cb",b"cc",b"cd",b"ce",b"cf",b"d0",b"d1",b"d2",b"d3",b"d4",b"d5",b"d6",b"d7",b"d8",b"d9",b"da",b"db",b"dc",b"dd",b"de",b"df",b"e0",b"e1",b"e2",b"e3",b"e4",b"e5",b"e6",b"e7",b"e8",b"e9",b"ea",b"eb",b"ec",b"ed",b"ee",b"ef",b"f0",b"f1",b"f2",b"f3",b"f4",b"f5",b"f6",b"f7",b"f8",b"f9",b"fa",b"fb",b"fc",b"fd",b"fe",b"ff" + ]; + + /// Encode `bytes` in lowercase hex + public fun encode(bytes: vector): vector { + let (i, r, l) = (0, vector[], vector::length(&bytes)); + while (i < l) { + vector::append( + &mut r, + *vector::borrow(&HEX, (*vector::borrow(&bytes, i) as u64)) + ); + i = i + 1; + }; + r + } + + /// Decode hex into `bytes` + /// Takes a hex string (no 0x prefix) (e.g. b"0f3a") + /// Returns vector of `bytes` that represents the hex string (e.g. x"0f3a") + /// Hex string can be case insensitive (e.g. b"0F3A" and b"0f3a" both return x"0f3a") + /// Aborts if the hex string does not have an even number of characters (as each hex character is 2 characters long) + /// Aborts if the hex string contains non-valid hex characters (valid characters are 0 - 9, a - f, A - F) + public fun decode(hex: vector): vector { + let (i, r, l) = (0, vector[], vector::length(&hex)); + assert!(l % 2 == 0, EInvalidHexLength); + while (i < l) { + let decimal = (decode_byte(*vector::borrow(&hex, i)) * 16) + + decode_byte(*vector::borrow(&hex, i + 1)); + vector::push_back(&mut r, decimal); + i = i + 2; + }; + r + } + + fun decode_byte(hex: u8): u8 { + if (/* 0 .. 9 */ 48 <= hex && hex < 58) { + hex - 48 + } else if (/* A .. F */ 65 <= hex && hex < 71) { + 10 + hex - 65 + } else if (/* a .. f */ 97 <= hex && hex < 103) { + 10 + hex - 97 + } else { + abort ENotValidHexCharacter + } + } + + spec module { pragma verify = false; } + + #[test] + fun test_hex_encode_string_literal() { + assert!(b"30" == encode(b"0"), 0); + assert!(b"61" == encode(b"a"), 0); + assert!(b"666666" == encode(b"fff"), 0); + } + + #[test] + fun test_hex_encode_hex_literal() { + assert!(b"ff" == encode(x"ff"), 0); + assert!(b"fe" == encode(x"fe"), 0); + assert!(b"00" == encode(x"00"), 0); + } + + #[test] + fun test_hex_decode_string_literal() { + assert!(x"ff" == decode(b"ff"), 0); + assert!(x"fe" == decode(b"fe"), 0); + assert!(x"00" == decode(b"00"), 0); + } + + #[test] + fun test_hex_decode_string_literal__lowercase_and_uppercase() { + assert!(x"ff" == decode(b"Ff"), 0); + assert!(x"ff" == decode(b"fF"), 0); + assert!(x"ff" == decode(b"FF"), 0); + } + + #[test] + fun test_hex_decode_string_literal__long_hex() { + assert!(x"036d2416252ae1db8aedad59e14b007bee6ab94a3e77a3549a81137871604456f3" == decode(b"036d2416252ae1Db8aedAd59e14b007bee6aB94a3e77a3549a81137871604456f3"), 0); + } + + #[test] + #[expected_failure(abort_code = EInvalidHexLength)] + fun test_hex_decode__invalid_length() { + decode(b"0"); + } + + #[test] + #[expected_failure(abort_code = ENotValidHexCharacter)] + fun test_hex_decode__hex_literal() { + decode(x"ffff"); + } + + #[test] + #[expected_failure(abort_code = ENotValidHexCharacter)] + fun test_hex_decode__invalid_string_literal() { + decode(b"0g"); + } +} \ No newline at end of file diff --git a/lang/move-on-sui/test/ok/implicit_uses.move b/lang/move-on-sui/test/ok/implicit_uses.move new file mode 100644 index 00000000..3cffb512 --- /dev/null +++ b/lang/move-on-sui/test/ok/implicit_uses.move @@ -0,0 +1,10 @@ +module Move2024::implicit_uses { + + public struct SomeStruct { + opt: Option, + } + + public fun foo(): SomeStruct { + SomeStruct { opt: option::some(42) } + } +} diff --git a/lang/move-on-sui/test/ok/let_mut.move b/lang/move-on-sui/test/ok/let_mut.move new file mode 100644 index 00000000..fbfa9ec2 --- /dev/null +++ b/lang/move-on-sui/test/ok/let_mut.move @@ -0,0 +1,9 @@ +module Move2024::let_mut { + + public fun foo(mut p: u64): u64 { + p = 42; + let mut v = 7; + v = v + p; + v + } +} diff --git a/lang/move-on-sui/test/ok/linked_table.move b/lang/move-on-sui/test/ok/linked_table.move new file mode 100644 index 00000000..9880f774 --- /dev/null +++ b/lang/move-on-sui/test/ok/linked_table.move @@ -0,0 +1,200 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Similar to `sui::table` but the values are linked together, allowing for ordered insertion and +/// removal +module sui::linked_table { + use std::option::{Self, Option}; + use sui::object::{Self, UID}; + use sui::dynamic_field as field; + use sui::tx_context::TxContext; + + // Attempted to destroy a non-empty table + const ETableNotEmpty: u64 = 0; + // Attempted to remove the front or back of an empty table + const ETableIsEmpty: u64 = 1; + + struct LinkedTable has key, store { + /// the ID of this table + id: UID, + /// the number of key-value pairs in the table + size: u64, + /// the front of the table, i.e. the key of the first entry + head: Option, + /// the back of the table, i.e. the key of the last entry + tail: Option, + } + + struct Node has store { + /// the previous key + prev: Option, + /// the next key + next: Option, + /// the value being stored + value: V + } + + /// Creates a new, empty table + public fun new(ctx: &mut TxContext): LinkedTable { + LinkedTable { + id: object::new(ctx), + size: 0, + head: option::none(), + tail: option::none(), + } + } + + /// Returns the key for the first element in the table, or None if the table is empty + public fun front(table: &LinkedTable): &Option { + &table.head + } + + /// Returns the key for the last element in the table, or None if the table is empty + public fun back(table: &LinkedTable): &Option { + &table.tail + } + + /// Inserts a key-value pair at the front of the table, i.e. the newly inserted pair will be + /// the first element in the table + /// Aborts with `sui::dynamic_field::EFieldAlreadyExists` if the table already has an entry with + /// that key `k: K`. + public fun push_front( + table: &mut LinkedTable, + k: K, + value: V, + ) { + let old_head = option::swap_or_fill(&mut table.head, k); + if (option::is_none(&table.tail)) option::fill(&mut table.tail, k); + let prev = option::none(); + let next = if (option::is_some(&old_head)) { + let old_head_k = option::destroy_some(old_head); + field::borrow_mut>(&mut table.id, old_head_k).prev = option::some(k); + option::some(old_head_k) + } else { + option::none() + }; + field::add(&mut table.id, k, Node { prev, next, value }); + table.size = table.size + 1; + } + + /// Inserts a key-value pair at the back of the table, i.e. the newly inserted pair will be + /// the last element in the table + /// Aborts with `sui::dynamic_field::EFieldAlreadyExists` if the table already has an entry with + /// that key `k: K`. + public fun push_back( + table: &mut LinkedTable, + k: K, + value: V, + ) { + if (option::is_none(&table.head)) option::fill(&mut table.head, k); + let old_tail = option::swap_or_fill(&mut table.tail, k); + let prev = if (option::is_some(&old_tail)) { + let old_tail_k = option::destroy_some(old_tail); + field::borrow_mut>(&mut table.id, old_tail_k).next = option::some(k); + option::some(old_tail_k) + } else { + option::none() + }; + let next = option::none(); + field::add(&mut table.id, k, Node { prev, next, value }); + table.size = table.size + 1; + } + + /// Immutable borrows the value associated with the key in the table `table: &LinkedTable`. + /// Aborts with `sui::dynamic_field::EFieldDoesNotExist` if the table does not have an entry with + /// that key `k: K`. + public fun borrow(table: &LinkedTable, k: K): &V { + &field::borrow>(&table.id, k).value + } + + /// Mutably borrows the value associated with the key in the table `table: &mut LinkedTable`. + /// Aborts with `sui::dynamic_field::EFieldDoesNotExist` if the table does not have an entry with + /// that key `k: K`. + public fun borrow_mut( + table: &mut LinkedTable, + k: K, + ): &mut V { + &mut field::borrow_mut>(&mut table.id, k).value + } + + /// Borrows the key for the previous entry of the specified key `k: K` in the table + /// `table: &LinkedTable`. Returns None if the entry does not have a predecessor. + /// Aborts with `sui::dynamic_field::EFieldDoesNotExist` if the table does not have an entry with + /// that key `k: K` + public fun prev(table: &LinkedTable, k: K): &Option { + &field::borrow>(&table.id, k).prev + } + + /// Borrows the key for the next entry of the specified key `k: K` in the table + /// `table: &LinkedTable`. Returns None if the entry does not have a predecessor. + /// Aborts with `sui::dynamic_field::EFieldDoesNotExist` if the table does not have an entry with + /// that key `k: K` + public fun next(table: &LinkedTable, k: K): &Option { + &field::borrow>(&table.id, k).next + } + + /// Removes the key-value pair in the table `table: &mut LinkedTable` and returns the value. + /// This splices the element out of the ordering. + /// Aborts with `sui::dynamic_field::EFieldDoesNotExist` if the table does not have an entry with + /// that key `k: K`. Note: this is also what happens when the table is empty. + public fun remove(table: &mut LinkedTable, k: K): V { + let Node { prev, next, value } = field::remove(&mut table.id, k); + table.size = table.size - 1; + if (option::is_some(&prev)) { + field::borrow_mut>(&mut table.id, *option::borrow(&prev)).next = next + }; + if (option::is_some(&next)) { + field::borrow_mut>(&mut table.id, *option::borrow(&next)).prev = prev + }; + if (option::borrow(&table.head) == &k) table.head = next; + if (option::borrow(&table.tail) == &k) table.tail = prev; + value + } + + /// Removes the front of the table `table: &mut LinkedTable` and returns the value. + /// Aborts with `ETableIsEmpty` if the table is empty + public fun pop_front(table: &mut LinkedTable): (K, V) { + assert!(option::is_some(&table.head), ETableIsEmpty); + let head = *option::borrow(&table.head); + (head, remove(table, head)) + } + + /// Removes the back of the table `table: &mut LinkedTable` and returns the value. + /// Aborts with `ETableIsEmpty` if the table is empty + public fun pop_back(table: &mut LinkedTable): (K, V) { + assert!(option::is_some(&table.tail), ETableIsEmpty); + let tail = *option::borrow(&table.tail); + (tail, remove(table, tail)) + } + + /// Returns true iff there is a value associated with the key `k: K` in table + /// `table: &LinkedTable` + public fun contains(table: &LinkedTable, k: K): bool { + field::exists_with_type>(&table.id, k) + } + + /// Returns the size of the table, the number of key-value pairs + public fun length(table: &LinkedTable): u64 { + table.size + } + + /// Returns true iff the table is empty (if `length` returns `0`) + public fun is_empty(table: &LinkedTable): bool { + table.size == 0 + } + + /// Destroys an empty table + /// Aborts with `ETableNotEmpty` if the table still contains values + public fun destroy_empty(table: LinkedTable) { + let LinkedTable { id, size, head: _, tail: _ } = table; + assert!(size == 0, ETableNotEmpty); + object::delete(id) + } + + /// Drop a possibly non-empty table. + /// Usable only if the value type `V` has the `drop` ability + public fun drop(table: LinkedTable) { + let LinkedTable { id, size: _, head: _, tail: _ } = table; + object::delete(id) + } +} diff --git a/lang/move-on-sui/test/ok/literal_address_module_and_pos_structs.move b/lang/move-on-sui/test/ok/literal_address_module_and_pos_structs.move new file mode 100644 index 00000000..d5e76ee5 --- /dev/null +++ b/lang/move-on-sui/test/ok/literal_address_module_and_pos_structs.move @@ -0,0 +1,4 @@ +module 0x42::test { + struct Positional() + struct Positional2() has copy; +} diff --git a/lang/move-on-sui/test/ok/macro_calls.move b/lang/move-on-sui/test/ok/macro_calls.move new file mode 100644 index 00000000..c502215d --- /dev/null +++ b/lang/move-on-sui/test/ok/macro_calls.move @@ -0,0 +1,75 @@ +//# init --edition 2024.alpha + +//# publish +module 0x42::m { + + public struct S has drop { + x: u64, + y: u64, + z: u64, + } + + fun inc_x(self: &mut S, by: u64) { + self.x = self.x + by; + } + + macro fun inc_xx($self: &mut S, $by: u64) { + let self = $self; + self.x = self.x + $by; + } + + public fun test0(): u64 { + let mut s = S { x: 1, y: 2, z: 3 }; + {inc_x(&mut s, 6); s.x} + {inc_x(&mut s, 47); s.x} + {inc_x(&mut s, 117); s.x} + } + + public fun test1(): u64 { + let mut s = S { x: 1, y: 2, z: 3 }; + {inc_xx!(&mut s, 6); s.x} + {inc_xx!(&mut s, 47); s.x} + {inc_xx!(&mut s, 117); s.x} + } + + macro fun inc($x: &mut u64): u64 { + let x = $x; + *x = *x + 1; + *x + } + + public fun test2(): u64 { + let mut x = 1; + x + inc!(&mut x) + inc!(&mut x) + } + + macro fun inc_scope($x: u64): u64 { + let mut x = $x; + x = x + 1; + x + } + + public fun test3(): u64 { + let x = 1; + x + inc_scope!(x) + inc_scope!(x) + } + + macro fun call($f: || -> u64): u64 { + $f() + } + + public fun test4(): u64 { + let mut x = 1; + x + call!(|| {x = x + 1; x}) + call!(|| {x = x + 7; x}) + } +} + +//# run +module 0x42::main { + use 0x42::m; + + public fun main() { + assert!(m::test0() == 232, 0); + assert!(m::test1() == 232, 1); + assert!(m::test2() == 6, 2); + assert!(m::test3() == 5, 3); + assert!(m::test4() == 12, 4); + } + +} diff --git a/lang/move-on-sui/test/ok/macro_match.move b/lang/move-on-sui/test/ok/macro_match.move new file mode 100644 index 00000000..85cb1c9d --- /dev/null +++ b/lang/move-on-sui/test/ok/macro_match.move @@ -0,0 +1,32 @@ +//# init --edition 2024.beta + +//# publish +module 0x42::m { + + public enum Maybe { + Just(T), + Nothing + } + + macro fun maybe<$A,$B: drop>($b: $B, $f: |$A| -> $B, $ma: Maybe<$A>): $B { + match ($ma) { + Maybe::Just(a) => $f(a), + Maybe::Nothing => $b + } + } + + public fun maybe_macro_call_2() { + let m = maybe!(10, |x| { x }, Maybe::Just(5)); + assert!(m == 5, 1); + let n = maybe!(10, |x| { x }, Maybe::Nothing); + assert!(n == 10, 2); + } +} + +//# run +module 0x42::main { + use 0x42::m; + fun main() { + m::maybe_macro_call_2(); + } +} diff --git a/lang/move-on-sui/test/ok/math.move b/lang/move-on-sui/test/ok/math.move new file mode 100644 index 00000000..e88c1a4a --- /dev/null +++ b/lang/move-on-sui/test/ok/math.move @@ -0,0 +1,144 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Basic math for nicer programmability +module sui::math { + + /// Return the larger of `x` and `y` + public fun max(x: u64, y: u64): u64 { + if (x > y) { + x + } else { + y + } + } + + /// Return the smaller of `x` and `y` + public fun min(x: u64, y: u64): u64 { + if (x < y) { + x + } else { + y + } + } + + /// Return the absolute value of x - y + public fun diff(x: u64, y: u64): u64 { + if (x > y) { + x - y + } else { + y - x + } + } + + /// Return the value of a base raised to a power + public fun pow(base: u64, exponent: u8): u64 { + let res = 1; + while (exponent >= 1) { + if (exponent % 2 == 0) { + base = base * base; + exponent = exponent / 2; + } else { + res = res * base; + exponent = exponent - 1; + } + }; + + res + } + + /// Get a nearest lower integer Square Root for `x`. Given that this + /// function can only operate with integers, it is impossible + /// to get perfect (or precise) integer square root for some numbers. + /// + /// Example: + /// ``` + /// math::sqrt(9) => 3 + /// math::sqrt(8) => 2 // the nearest lower square root is 4; + /// ``` + /// + /// In integer math, one of the possible ways to get results with more + /// precision is to use higher values or temporarily multiply the + /// value by some bigger number. Ideally if this is a square of 10 or 100. + /// + /// Example: + /// ``` + /// math::sqrt(8) => 2; + /// math::sqrt(8 * 10000) => 282; + /// // now we can use this value as if it was 2.82; + /// // but to get the actual result, this value needs + /// // to be divided by 100 (because sqrt(10000)). + /// + /// + /// math::sqrt(8 * 1000000) => 2828; // same as above, 2828 / 1000 (2.828) + /// ``` + public fun sqrt(x: u64): u64 { + let bit = 1u128 << 64; + let res = 0u128; + let x = (x as u128); + + while (bit != 0) { + if (x >= res + bit) { + x = x - (res + bit); + res = (res >> 1) + bit; + } else { + res = res >> 1; + }; + bit = bit >> 2; + }; + + (res as u64) + } + + /// Similar to math::sqrt, but for u128 numbers. Get a nearest lower integer Square Root for `x`. Given that this + /// function can only operate with integers, it is impossible + /// to get perfect (or precise) integer square root for some numbers. + /// + /// Example: + /// ``` + /// math::sqrt_u128(9) => 3 + /// math::sqrt_u128(8) => 2 // the nearest lower square root is 4; + /// ``` + /// + /// In integer math, one of the possible ways to get results with more + /// precision is to use higher values or temporarily multiply the + /// value by some bigger number. Ideally if this is a square of 10 or 100. + /// + /// Example: + /// ``` + /// math::sqrt_u128(8) => 2; + /// math::sqrt_u128(8 * 10000) => 282; + /// // now we can use this value as if it was 2.82; + /// // but to get the actual result, this value needs + /// // to be divided by 100 (because sqrt_u128(10000)). + /// + /// + /// math::sqrt_u128(8 * 1000000) => 2828; // same as above, 2828 / 1000 (2.828) + /// ``` + public fun sqrt_u128(x: u128): u128 { + let bit = 1u256 << 128; + let res = 0u256; + let x = (x as u256); + + while (bit != 0) { + if (x >= res + bit) { + x = x - (res + bit); + res = (res >> 1) + bit; + } else { + res = res >> 1; + }; + bit = bit >> 2; + }; + + (res as u128) + } + + /// Calculate x / y, but round up the result. + public fun divide_and_round_up(x: u64, y: u64): u64 { + if (x % y == 0) { + x / y + } else { + x / y + 1 + } + } +} diff --git a/lang/move-on-sui/test/ok/module_labels.move b/lang/move-on-sui/test/ok/module_labels.move new file mode 100644 index 00000000..423bde58 --- /dev/null +++ b/lang/move-on-sui/test/ok/module_labels.move @@ -0,0 +1,5 @@ +module 0x1::m; +use a::b; +use a::c; + +fun x() { } diff --git a/lang/move-on-sui/test/ok/module_uses.move b/lang/move-on-sui/test/ok/module_uses.move new file mode 100644 index 00000000..819d914b --- /dev/null +++ b/lang/move-on-sui/test/ok/module_uses.move @@ -0,0 +1,22 @@ +module foo::bar { + use std::{ + ascii::{Self, String}, + vector::push_back, + + vector::pop_back as p, + + coin, + object as O, + }; + use std::vector::foo; + use fun Self::foo as X.f1; + use fun a::m::foo as X.f2; + use fun foo as Self::X.f3; + use fun foo as a::m::X.f4; + use fun foo as X.f5; + public use fun Self::foo as X.g1; + public use fun a::m::foo as X.g2; + public use fun foo as Self::X.g3; + public use fun foo as a::m::X.g4; + public use fun foo as X.g5; +} diff --git a/lang/move-on-sui/test/ok/multi_index_test.move b/lang/move-on-sui/test/ok/multi_index_test.move new file mode 100644 index 00000000..d8ec2dd2 --- /dev/null +++ b/lang/move-on-sui/test/ok/multi_index_test.move @@ -0,0 +1,7 @@ +module 0x1::m { + fun f() { + x[]; + x[1, f()]; + x[1, 2, 3]; + } +} diff --git a/lang/move-on-sui/test/ok/named_address_test.move b/lang/move-on-sui/test/ok/named_address_test.move new file mode 100644 index 00000000..b1531c56 --- /dev/null +++ b/lang/move-on-sui/test/ok/named_address_test.move @@ -0,0 +1,6 @@ +module 0x1::t { + +fun f() { + transfer::public_transfer(old_phone, @examples); +} +} diff --git a/lang/move-on-sui/test/ok/native_structs.move b/lang/move-on-sui/test/ok/native_structs.move new file mode 100644 index 00000000..45c097d4 --- /dev/null +++ b/lang/move-on-sui/test/ok/native_structs.move @@ -0,0 +1,5 @@ +module s::foo { + public native struct Booper has drop, copy; + + public native struct Booper3; +} diff --git a/lang/move-on-sui/test/ok/new_syntax_file.move b/lang/move-on-sui/test/ok/new_syntax_file.move new file mode 100644 index 00000000..0e7d6773 --- /dev/null +++ b/lang/move-on-sui/test/ok/new_syntax_file.move @@ -0,0 +1,200 @@ +module foo::bar { + // Imports + use std::vector::foo; + use fun Self::foo as X.f1; + use fun a::m::foo as X.f2; + use fun foo as Self::X.f3; + use fun foo as a::m::X.f4; + use fun foo as X.f5; + public use fun Self::foo as X.g1; + public use fun a::m::foo as X.g2; + public use fun foo as Self::X.g3; + public use fun foo as a::m::X.g4; + public use fun foo as X.g5; + + use std::{ + ascii::{Self, String}, + vector::push_back, + vector::pop_back as p, + coin, + object as O, + }; + + // Types + public struct X {} + public struct Old has key { + x: u64 + } + public struct NewPost has key (u64) + public struct NewPoster(u64) has key, store; + public struct None() + public enum NewEnum { + V(), + V1(u64, bool), + V2 { x: u64, y: bool }, + V3 + } + public enum NewEnum has key { + NewVariant(u64), + VariantNoParams, + VariantEmptyParams(), + VariantNamedParams { x: u64 }, + } + + public struct X { + `for`: u64, + `let`: u64, + `struct`: u64, + } + + #[syntax(index)] + public fun new(mut x: T): T { } + + public fun `let`(mut `mut`: u64) { + let `let` = `mut`; + let `for` = `mut` + `let` + `let` + `mut`; + let `struct` = `let` + `for` + `let` + `mut`; + let `enum` = `struct` + `let` + `for` + `let` + `mut`; + let `let` = `let` + `for` + `let` + `mut`; + let `let` = `let` * `mut` + `let` / `for` - `struct`; + } + + public fun foo() { + let x = (x: C).y; + if (cond) { &0 } else { &mut 0 }; + () + } + + fun f(): u64 { + 1 as u64; + 1 + 2 as u64; + (1 + 2) as u64; + 1 + (2 as u64); + v[1 as u64]; + 1 as u64 + 2; + } + + public fun new_let_mut(): u64 { + let mut x = 0; + x = 1; + let t = x.new(); + let t = x.new(a, b, t.y()); + let Old { mut y } = x.new(); + let NewPost(y) = x.new(); + let NewPoster(mut y) = x.new(); + let NewPoster(mut y, z, i) = x.new(); + let NewPoster(mut y, z, i) = x.new(); + let NewPoster::Variant(mut y, z, i) = x.new(); + match (s) { + NewPoster::Variant(x) => something, + NewPoster::Variant{ x } if true => something, + y @ bar => @0x1, + }; + x.foreach!(|y| { x = y; }); + assert!(x == 1, 6); + x + } + + // blocks + public fun block() { + 'a: { + return 'a x; + break 'a x; + return'a x.foo!(); + break'a { x = x + 1; x }; + continue 'a; + }; + 'a: loop { }; + 'a: while (true) { }; + while (true) 'a: { }; + // TODO: fix precedence of this + if (true) 1 else 'a: { 2 } + 1; + } + + // Macros + macro fun ignore( + _: None, + ) {} + + macro fun for($start: u64, $stop: u64, $body: |u64|) { + let mut i = $start; + let stop = $stop; + while (i < stop) { + $body(i); + i = i + 1 + } + } + + macro fun for_each<$T>($v: &vector<$T>, $body: |&$T|) { + let v = $v; + let mut i = 0; + let n = v.length(); + while (i < n) { + $body(v.borrow(i)); + i = i + 1 + } + } + + macro fun new<$T>($len: u64, $f: |u64| -> $T): vector<$T> { + let len = $len; + let mut v = vector[]; + for!(0, len, |i| v.push_back($f(i))); + v + } + + macro fun sum($v: &vector): u64 { + let mut s = 0; + for_each!($v, |i| s = s + *i); + s + } + + fun t() { + None().ignore!() + } + + public entry fun main() { + let v = new!(10, |i| i); + assert!(sum!(&v) == 45, 0); + } + + public struct Cup has drop {} + public macro fun foo( + _: ||, + _: || -> (), + _: || -> u64, + _: || -> (u64), + _: || -> (u64, bool), + _: |&u64|, + _: |&u64| -> (), + _: |&u64| -> u64, + _: |&u64| -> (u64), + _: |&u64| -> (u64, bool), + _: |bool, address|, + _: |bool, address| -> (), + _: |bool, address| -> u64, + _: |bool, address| -> (u64), + _: |bool, address| -> (u64, bool), + _: |bool, address| -> (u64, bool, &u64), + _: || -> || -> ||, + _: || -> || -> || -> || -> (), + _: || -> | | -> || -> | | -> u64, + _: | | -> || -> | | -> || -> (u64), + _: Cup<||>, + _: Cup<|| -> u64>, + ) {} + + macro fun call<$T>($f: || -> $T): $T { + $f() + } + + fun t() { + call!(|| -> u64 'a: { 0 }); + } + + fun t() { + call!(|| -> () { }); + call!(|| -> () { () }); + call!(|| -> u64 { 0 }); + call!(|| -> (u64, u8) { (0, 0) }); + } +} diff --git a/lang/move-on-sui/test/ok/object.move b/lang/move-on-sui/test/ok/object.move new file mode 100644 index 00000000..34c76414 --- /dev/null +++ b/lang/move-on-sui/test/ok/object.move @@ -0,0 +1,235 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Sui object identifiers +module sui::object { + use std::bcs; + use sui::address; + use sui::tx_context::{Self, TxContext}; + + friend sui::clock; + friend sui::dynamic_field; + friend sui::dynamic_object_field; + friend sui::transfer; + friend sui::authenticator_state; + friend sui::random; + + #[test_only] + friend sui::test_scenario; + + /// The hardcoded ID for the singleton Sui System State Object. + const SUI_SYSTEM_STATE_OBJECT_ID: address = @0x5; + + /// The hardcoded ID for the singleton Clock Object. + const SUI_CLOCK_OBJECT_ID: address = @0x6; + + /// The hardcoded ID for the singleton AuthenticatorState Object. + const SUI_AUTHENTICATOR_STATE_ID: address = @0x7; + + /// The hardcoded ID for the singleton Random Object. + const SUI_RANDOM_ID: address = @0x8; + + /// Sender is not @0x0 the system address. + const ENotSystemAddress: u64 = 0; + + /// An object ID. This is used to reference Sui Objects. + /// This is *not* guaranteed to be globally unique--anyone can create an `ID` from a `UID` or + /// from an object, and ID's can be freely copied and dropped. + /// Here, the values are not globally unique because there can be multiple values of type `ID` + /// with the same underlying bytes. For example, `object::id(&obj)` can be called as many times + /// as you want for a given `obj`, and each `ID` value will be identical. + struct ID has copy, drop, store { + // We use `address` instead of `vector` here because `address` has a more + // compact serialization. `address` is serialized as a BCS fixed-length sequence, + // which saves us the length prefix we would pay for if this were `vector`. + // See https://github.com/diem/bcs#fixed-and-variable-length-sequences. + bytes: address + } + + /// Globally unique IDs that define an object's ID in storage. Any Sui Object, that is a struct + /// with the `key` ability, must have `id: UID` as its first field. + /// These are globally unique in the sense that no two values of type `UID` are ever equal, in + /// other words for any two values `id1: UID` and `id2: UID`, `id1` != `id2`. + /// This is a privileged type that can only be derived from a `TxContext`. + /// `UID` doesn't have the `drop` ability, so deleting a `UID` requires a call to `delete`. + struct UID has store { + id: ID, + } + + // === id === + + /// Get the raw bytes of a `ID` + public fun id_to_bytes(id: &ID): vector { + bcs::to_bytes(&id.bytes) + } + + /// Get the inner bytes of `id` as an address. + public fun id_to_address(id: &ID): address { + id.bytes + } + + /// Make an `ID` from raw bytes. + public fun id_from_bytes(bytes: vector): ID { + id_from_address(address::from_bytes(bytes)) + } + + /// Make an `ID` from an address. + public fun id_from_address(bytes: address): ID { + ID { bytes } + } + + // === uid === + + #[allow(unused_function)] + /// Create the `UID` for the singleton `SuiSystemState` object. + /// This should only be called once from `sui_system`. + fun sui_system_state(ctx: &TxContext): UID { + assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress); + UID { + id: ID { bytes: SUI_SYSTEM_STATE_OBJECT_ID }, + } + } + + /// Create the `UID` for the singleton `Clock` object. + /// This should only be called once from `clock`. + public(friend) fun clock(): UID { + UID { + id: ID { bytes: SUI_CLOCK_OBJECT_ID } + } + } + + /// Create the `UID` for the singleton `AuthenticatorState` object. + /// This should only be called once from `authenticator_state`. + public(friend) fun authenticator_state(): UID { + UID { + id: ID { bytes: SUI_AUTHENTICATOR_STATE_ID } + } + } + + /// Create the `UID` for the singleton `Random` object. + /// This should only be called once from `random`. + public(friend) fun randomness_state(): UID { + UID { + id: ID { bytes: SUI_RANDOM_ID } + } + } + + /// Get the inner `ID` of `uid` + public fun uid_as_inner(uid: &UID): &ID { + &uid.id + } + + /// Get the raw bytes of a `uid`'s inner `ID` + public fun uid_to_inner(uid: &UID): ID { + uid.id + } + + /// Get the raw bytes of a `UID` + public fun uid_to_bytes(uid: &UID): vector { + bcs::to_bytes(&uid.id.bytes) + } + + /// Get the inner bytes of `id` as an address. + public fun uid_to_address(uid: &UID): address { + uid.id.bytes + } + + // === any object === + + /// Create a new object. Returns the `UID` that must be stored in a Sui object. + /// This is the only way to create `UID`s. + public fun new(ctx: &mut TxContext): UID { + UID { + id: ID { bytes: tx_context::fresh_object_address(ctx) }, + } + } + + /// Delete the object and it's `UID`. This is the only way to eliminate a `UID`. + // This exists to inform Sui of object deletions. When an object + // gets unpacked, the programmer will have to do something with its + // `UID`. The implementation of this function emits a deleted + // system event so Sui knows to process the object deletion + public fun delete(id: UID) { + let UID { id: ID { bytes } } = id; + delete_impl(bytes) + } + + /// Get the underlying `ID` of `obj` + public fun id(obj: &T): ID { + borrow_uid(obj).id + } + + /// Borrow the underlying `ID` of `obj` + public fun borrow_id(obj: &T): &ID { + &borrow_uid(obj).id + } + + /// Get the raw bytes for the underlying `ID` of `obj` + public fun id_bytes(obj: &T): vector { + bcs::to_bytes(&borrow_uid(obj).id) + } + + /// Get the inner bytes for the underlying `ID` of `obj` + public fun id_address(obj: &T): address { + borrow_uid(obj).id.bytes + } + + /// Get the `UID` for `obj`. + /// Safe because Sui has an extra bytecode verifier pass that forces every struct with + /// the `key` ability to have a distinguished `UID` field. + /// Cannot be made public as the access to `UID` for a given object must be privileged, and + /// restrictable in the object's module. + native fun borrow_uid(obj: &T): &UID; + + /// Generate a new UID specifically used for creating a UID from a hash + public(friend) fun new_uid_from_hash(bytes: address): UID { + record_new_uid(bytes); + UID { id: ID { bytes } } + } + + // === internal functions === + + // helper for delete + native fun delete_impl(id: address); + + spec delete_impl { + pragma opaque; + aborts_if [abstract] false; + ensures [abstract] !exists(id); + } + + // marks newly created UIDs from hash + native fun record_new_uid(id: address); + + spec record_new_uid { + pragma opaque; + // TODO: stub to be replaced by actual abort conditions if any + aborts_if [abstract] true; + // TODO: specify actual function behavior + } + + #[test_only] + /// Return the most recent created object ID. + public fun last_created(ctx: &TxContext): ID { + ID { bytes: tx_context::last_created_object_id(ctx) } + } + + + // === Prover support (to avoid circular dependency === + + #[verify_only] + /// Ownership information for a given object (stored at the object's address) + struct Ownership has key { + owner: address, // only matters if status == OWNED + status: u64, + } + + #[verify_only] + /// List of fields with a given name type of an object containing fields (stored at the + /// containing object's address) + struct DynamicFields has key { + names: vector, + } + + +} diff --git a/lang/move-on-sui/test/ok/object_bag.move b/lang/move-on-sui/test/ok/object_bag.move new file mode 100644 index 00000000..bfe1b929 --- /dev/null +++ b/lang/move-on-sui/test/ok/object_bag.move @@ -0,0 +1,103 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Similar to `sui::bag`, an `ObjectBag` is a heterogeneous map-like collection. But unlike +/// `sui::bag`, the values bound to these dynamic fields _must_ be objects themselves. This allows +/// for the objects to still exist in storage, which may be important for external tools. +/// The difference is otherwise not observable from within Move. +module sui::object_bag { + use std::option::Option; + use sui::object::{Self, ID, UID}; + use sui::dynamic_object_field as ofield; + use sui::tx_context::TxContext; + + // Attempted to destroy a non-empty bag + const EBagNotEmpty: u64 = 0; + + struct ObjectBag has key, store { + /// the ID of this bag + id: UID, + /// the number of key-value pairs in the bag + size: u64, + } + + /// Creates a new, empty bag + public fun new(ctx: &mut TxContext): ObjectBag { + ObjectBag { + id: object::new(ctx), + size: 0, + } + } + + /// Adds a key-value pair to the bag `bag: &mut ObjectBag` + /// Aborts with `sui::dynamic_field::EFieldAlreadyExists` if the bag already has an entry with + /// that key `k: K`. + public fun add(bag: &mut ObjectBag, k: K, v: V) { + ofield::add(&mut bag.id, k, v); + bag.size = bag.size + 1; + } + + /// Immutably borrows the value associated with the key in the bag `bag: &ObjectBag`. + /// Aborts with `sui::dynamic_field::EFieldDoesNotExist` if the bag does not have an entry with + /// that key `k: K`. + /// Aborts with `sui::dynamic_field::EFieldTypeMismatch` if the bag has an entry for the key, but + /// the value does not have the specified type. + public fun borrow(bag: &ObjectBag, k: K): &V { + ofield::borrow(&bag.id, k) + } + + /// Mutably borrows the value associated with the key in the bag `bag: &mut ObjectBag`. + /// Aborts with `sui::dynamic_field::EFieldDoesNotExist` if the bag does not have an entry with + /// that key `k: K`. + /// Aborts with `sui::dynamic_field::EFieldTypeMismatch` if the bag has an entry for the key, but + /// the value does not have the specified type. + public fun borrow_mut(bag: &mut ObjectBag, k: K): &mut V { + ofield::borrow_mut(&mut bag.id, k) + } + + /// Mutably borrows the key-value pair in the bag `bag: &mut ObjectBag` and returns the value. + /// Aborts with `sui::dynamic_field::EFieldDoesNotExist` if the bag does not have an entry with + /// that key `k: K`. + /// Aborts with `sui::dynamic_field::EFieldTypeMismatch` if the bag has an entry for the key, but + /// the value does not have the specified type. + public fun remove(bag: &mut ObjectBag, k: K): V { + let v = ofield::remove(&mut bag.id, k); + bag.size = bag.size - 1; + v + } + + /// Returns true iff there is an value associated with the key `k: K` in the bag `bag: &ObjectBag` + public fun contains(bag: &ObjectBag, k: K): bool { + ofield::exists_(&bag.id, k) + } + + /// Returns true iff there is an value associated with the key `k: K` in the bag `bag: &ObjectBag` + /// with an assigned value of type `V` + public fun contains_with_type(bag: &ObjectBag, k: K): bool { + ofield::exists_with_type(&bag.id, k) + } + + /// Returns the size of the bag, the number of key-value pairs + public fun length(bag: &ObjectBag): u64 { + bag.size + } + + /// Returns true iff the bag is empty (if `length` returns `0`) + public fun is_empty(bag: &ObjectBag): bool { + bag.size == 0 + } + + /// Destroys an empty bag + /// Aborts with `EBagNotEmpty` if the bag still contains values + public fun destroy_empty(bag: ObjectBag) { + let ObjectBag { id, size } = bag; + assert!(size == 0, EBagNotEmpty); + object::delete(id) + } + + /// Returns the ID of the object associated with the key if the bag has an entry with key `k: K` + /// Returns none otherwise + public fun value_id(bag: &ObjectBag, k: K): Option { + ofield::id(&bag.id, k) + } +} diff --git a/lang/move-on-sui/test/ok/object_table.move b/lang/move-on-sui/test/ok/object_table.move new file mode 100644 index 00000000..7cc883b3 --- /dev/null +++ b/lang/move-on-sui/test/ok/object_table.move @@ -0,0 +1,98 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Similar to `sui::table`, an `ObjectTable` is a map-like collection. But unlike +/// `sui::table`, the values bound to these dynamic fields _must_ be objects themselves. This allows +/// for the objects to still exist within in storage, which may be important for external tools. +/// The difference is otherwise not observable from within Move. +module sui::object_table { + use std::option::Option; + use sui::object::{Self, ID, UID}; + use sui::dynamic_object_field as ofield; + use sui::tx_context::TxContext; + + // Attempted to destroy a non-empty table + const ETableNotEmpty: u64 = 0; + + struct ObjectTable has key, store { + /// the ID of this table + id: UID, + /// the number of key-value pairs in the table + size: u64, + } + + /// Creates a new, empty table + public fun new(ctx: &mut TxContext): ObjectTable { + ObjectTable { + id: object::new(ctx), + size: 0, + } + } + + /// Adds a key-value pair to the table `table: &mut ObjectTable` + /// Aborts with `sui::dynamic_field::EFieldAlreadyExists` if the table already has an entry with + /// that key `k: K`. + public fun add(table: &mut ObjectTable, k: K, v: V) { + ofield::add(&mut table.id, k, v); + table.size = table.size + 1; + } + + /// Immutable borrows the value associated with the key in the table `table: &ObjectTable`. + /// Aborts with `sui::dynamic_field::EFieldDoesNotExist` if the table does not have an entry with + /// that key `k: K`. + public fun borrow(table: &ObjectTable, k: K): &V { + ofield::borrow(&table.id, k) + } + + /// Mutably borrows the value associated with the key in the table `table: &mut ObjectTable`. + /// Aborts with `sui::dynamic_field::EFieldDoesNotExist` if the table does not have an entry with + /// that key `k: K`. + public fun borrow_mut( + table: &mut ObjectTable, + k: K, + ): &mut V { + ofield::borrow_mut(&mut table.id, k) + } + + /// Removes the key-value pair in the table `table: &mut ObjectTable` and returns the value. + /// Aborts with `sui::dynamic_field::EFieldDoesNotExist` if the table does not have an entry with + /// that key `k: K`. + public fun remove(table: &mut ObjectTable, k: K): V { + let v = ofield::remove(&mut table.id, k); + table.size = table.size - 1; + v + } + + /// Returns true iff there is a value associated with the key `k: K` in table + /// `table: &ObjectTable` + public fun contains(table: &ObjectTable, k: K): bool { + ofield::exists_(&table.id, k) + } + + /// Returns the size of the table, the number of key-value pairs + public fun length(table: &ObjectTable): u64 { + table.size + } + + /// Returns true iff the table is empty (if `length` returns `0`) + public fun is_empty(table: &ObjectTable): bool { + table.size == 0 + } + + /// Destroys an empty table + /// Aborts with `ETableNotEmpty` if the table still contains values + public fun destroy_empty(table: ObjectTable) { + let ObjectTable { id, size } = table; + assert!(size == 0, ETableNotEmpty); + object::delete(id) + } + + /// Returns the ID of the object associated with the key if the table has an entry with key `k: K` + /// Returns none otherwise + public fun value_id( + table: &ObjectTable, + k: K, + ): Option { + ofield::id(&table.id, k) + } +} diff --git a/lang/move-on-sui/test/ok/option.move b/lang/move-on-sui/test/ok/option.move new file mode 100644 index 00000000..95c32942 --- /dev/null +++ b/lang/move-on-sui/test/ok/option.move @@ -0,0 +1,265 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This module defines the Option type and its methods to represent and handle an optional value. +module std::option { + use std::vector; + + /// Abstraction of a value that may or may not be present. Implemented with a vector of size + /// zero or one because Move bytecode does not have ADTs. + struct Option has copy, drop, store { + vec: vector + } + spec Option { + // The size of vector is always less than equal to 1 + // because it's 0 for "none" or 1 for "some". + // TODO: disable temporarily to avoid an error triggered after + // https://github.com/move-language/move/pull/820 was implemented + // (change the comment above to doc comment once this id fixed) + // invariant len(vec) <= 1; + } + + /// The `Option` is in an invalid state for the operation attempted. + /// The `Option` is `Some` while it should be `None`. + const EOPTION_IS_SET: u64 = 0x40000; + /// The `Option` is in an invalid state for the operation attempted. + /// The `Option` is `None` while it should be `Some`. + const EOPTION_NOT_SET: u64 = 0x40001; + + /// Return an empty `Option` + public fun none(): Option { + Option { vec: vector::empty() } + } + spec none { + pragma opaque; + aborts_if false; + ensures result == spec_none(); + } + spec fun spec_none(): Option { + Option{ vec: vec() } + } + + /// Return an `Option` containing `e` + public fun some(e: Element): Option { + Option { vec: vector::singleton(e) } + } + spec some { + pragma opaque; + aborts_if false; + ensures result == spec_some(e); + } + spec fun spec_some(e: Element): Option { + Option{ vec: vec(e) } + } + + /// Return true if `t` does not hold a value + public fun is_none(t: &Option): bool { + vector::is_empty(&t.vec) + } + spec is_none { + pragma opaque; + aborts_if false; + ensures result == is_none(t); + } + + /// Return true if `t` holds a value + public fun is_some(t: &Option): bool { + !vector::is_empty(&t.vec) + } + spec is_some { + pragma opaque; + aborts_if false; + ensures result == is_some(t); + } + + /// Return true if the value in `t` is equal to `e_ref` + /// Always returns `false` if `t` does not hold a value + public fun contains(t: &Option, e_ref: &Element): bool { + vector::contains(&t.vec, e_ref) + } + spec contains { + pragma opaque; + aborts_if false; + ensures result == spec_contains(t, e_ref); + } + spec fun spec_contains(t: Option, e: Element): bool { + is_some(t) && borrow(t) == e + } + + /// Return an immutable reference to the value inside `t` + /// Aborts if `t` does not hold a value + public fun borrow(t: &Option): &Element { + assert!(is_some(t), EOPTION_NOT_SET); + vector::borrow(&t.vec, 0) + } + spec borrow { + pragma opaque; + include AbortsIfNone; + ensures result == borrow(t); + } + + /// Return a reference to the value inside `t` if it holds one + /// Return `default_ref` if `t` does not hold a value + public fun borrow_with_default(t: &Option, default_ref: &Element): &Element { + let vec_ref = &t.vec; + if (vector::is_empty(vec_ref)) default_ref + else vector::borrow(vec_ref, 0) + } + spec borrow_with_default { + pragma opaque; + aborts_if false; + ensures result == (if (is_some(t)) borrow(t) else default_ref); + } + + /// Return the value inside `t` if it holds one + /// Return `default` if `t` does not hold a value + public fun get_with_default( + t: &Option, + default: Element, + ): Element { + let vec_ref = &t.vec; + if (vector::is_empty(vec_ref)) default + else *vector::borrow(vec_ref, 0) + } + spec get_with_default { + pragma opaque; + aborts_if false; + ensures result == (if (is_some(t)) borrow(t) else default); + } + + /// Convert the none option `t` to a some option by adding `e`. + /// Aborts if `t` already holds a value + public fun fill(t: &mut Option, e: Element) { + let vec_ref = &mut t.vec; + if (vector::is_empty(vec_ref)) vector::push_back(vec_ref, e) + else abort EOPTION_IS_SET + } + spec fill { + pragma opaque; + aborts_if is_some(t) with EOPTION_IS_SET; + ensures is_some(t); + ensures borrow(t) == e; + } + + /// Convert a `some` option to a `none` by removing and returning the value stored inside `t` + /// Aborts if `t` does not hold a value + public fun extract(t: &mut Option): Element { + assert!(is_some(t), EOPTION_NOT_SET); + vector::pop_back(&mut t.vec) + } + spec extract { + pragma opaque; + include AbortsIfNone; + ensures result == borrow(old(t)); + ensures is_none(t); + } + + /// Return a mutable reference to the value inside `t` + /// Aborts if `t` does not hold a value + public fun borrow_mut(t: &mut Option): &mut Element { + assert!(is_some(t), EOPTION_NOT_SET); + vector::borrow_mut(&mut t.vec, 0) + } + spec borrow_mut { + pragma opaque; + include AbortsIfNone; + ensures result == borrow(t); + } + + /// Swap the old value inside `t` with `e` and return the old value + /// Aborts if `t` does not hold a value + public fun swap(t: &mut Option, e: Element): Element { + assert!(is_some(t), EOPTION_NOT_SET); + let vec_ref = &mut t.vec; + let old_value = vector::pop_back(vec_ref); + vector::push_back(vec_ref, e); + old_value + } + spec swap { + pragma opaque; + include AbortsIfNone; + ensures result == borrow(old(t)); + ensures is_some(t); + ensures borrow(t) == e; + } + + /// Swap the old value inside `t` with `e` and return the old value; + /// or if there is no old value, fill it with `e`. + /// Different from swap(), swap_or_fill() allows for `t` not holding a value. + public fun swap_or_fill(t: &mut Option, e: Element): Option { + let vec_ref = &mut t.vec; + let old_value = if (vector::is_empty(vec_ref)) none() + else some(vector::pop_back(vec_ref)); + vector::push_back(vec_ref, e); + old_value + } + spec swap_or_fill { + pragma opaque; + ensures result == old(t); + ensures borrow(t) == e; + } + + /// Destroys `t.` If `t` holds a value, return it. Returns `default` otherwise + public fun destroy_with_default(t: Option, default: Element): Element { + let Option { vec } = t; + if (vector::is_empty(&mut vec)) default + else vector::pop_back(&mut vec) + } + spec destroy_with_default { + pragma opaque; + aborts_if false; + ensures result == (if (is_some(t)) borrow(t) else default); + } + + /// Unpack `t` and return its contents + /// Aborts if `t` does not hold a value + public fun destroy_some(t: Option): Element { + assert!(is_some(&t), EOPTION_NOT_SET); + let Option { vec } = t; + let elem = vector::pop_back(&mut vec); + vector::destroy_empty(vec); + elem + } + spec destroy_some { + pragma opaque; + include AbortsIfNone; + ensures result == borrow(t); + } + + /// Unpack `t` + /// Aborts if `t` holds a value + public fun destroy_none(t: Option) { + assert!(is_none(&t), EOPTION_IS_SET); + let Option { vec } = t; + vector::destroy_empty(vec) + } + spec destroy_none { + pragma opaque; + aborts_if is_some(t) with EOPTION_IS_SET; + } + + /// Convert `t` into a vector of length 1 if it is `Some`, + /// and an empty vector otherwise + public fun to_vec(t: Option): vector { + let Option { vec } = t; + vec + } + spec to_vec { + pragma opaque; + aborts_if false; + ensures result == t.vec; + } + + spec module {} // switch documentation context back to module level + + spec module { + pragma aborts_if_is_strict; + } + + /// # Helper Schema + + spec schema AbortsIfNone { + t: Option; + aborts_if is_none(t) with EOPTION_NOT_SET; + } +} diff --git a/lang/move-on-sui/test/ok/package.move b/lang/move-on-sui/test/ok/package.move new file mode 100644 index 00000000..33b25c01 --- /dev/null +++ b/lang/move-on-sui/test/ok/package.move @@ -0,0 +1,318 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Functions for operating on Move packages from within Move: +/// - Creating proof-of-publish objects from one-time witnesses +/// - Administering package upgrades through upgrade policies. +module sui::package { + use sui::object::{Self, ID, UID}; + use sui::tx_context::{TxContext, sender}; + use std::ascii::String; + use std::type_name; + use sui::types; + + /// Tried to create a `Publisher` using a type that isn't a + /// one-time witness. + const ENotOneTimeWitness: u64 = 0; + /// Tried to set a less restrictive policy than currently in place. + const ETooPermissive: u64 = 1; + /// This `UpgradeCap` has already authorized a pending upgrade. + const EAlreadyAuthorized: u64 = 2; + /// This `UpgradeCap` has not authorized an upgrade. + const ENotAuthorized: u64 = 3; + /// Trying to commit an upgrade to the wrong `UpgradeCap`. + const EWrongUpgradeCap: u64 = 4; + + /// Update any part of the package (function implementations, add new + /// functions or types, change dependencies) + const COMPATIBLE: u8 = 0; + /// Add new functions or types, or change dependencies, existing + /// functions can't change. + const ADDITIVE: u8 = 128; + /// Only be able to change dependencies. + const DEP_ONLY: u8 = 192; + + /// This type can only be created in the transaction that + /// generates a module, by consuming its one-time witness, so it + /// can be used to identify the address that published the package + /// a type originated from. + struct Publisher has key, store { + id: UID, + package: String, + module_name: String, + } + + /// Capability controlling the ability to upgrade a package. + struct UpgradeCap has key, store { + id: UID, + /// (Mutable) ID of the package that can be upgraded. + package: ID, + /// (Mutable) The number of upgrades that have been applied + /// successively to the original package. Initially 0. + version: u64, + /// What kind of upgrades are allowed. + policy: u8, + } + + /// Permission to perform a particular upgrade (for a fixed version of + /// the package, bytecode to upgrade with and transitive dependencies to + /// depend against). + /// + /// An `UpgradeCap` can only issue one ticket at a time, to prevent races + /// between concurrent updates or a change in its upgrade policy after + /// issuing a ticket, so the ticket is a "Hot Potato" to preserve forward + /// progress. + struct UpgradeTicket { + /// (Immutable) ID of the `UpgradeCap` this originated from. + cap: ID, + /// (Immutable) ID of the package that can be upgraded. + package: ID, + /// (Immutable) The policy regarding what kind of upgrade this ticket + /// permits. + policy: u8, + /// (Immutable) SHA256 digest of the bytecode and transitive + /// dependencies that will be used in the upgrade. + digest: vector, + } + + /// Issued as a result of a successful upgrade, containing the + /// information to be used to update the `UpgradeCap`. This is a "Hot + /// Potato" to ensure that it is used to update its `UpgradeCap` before + /// the end of the transaction that performed the upgrade. + struct UpgradeReceipt { + /// (Immutable) ID of the `UpgradeCap` this originated from. + cap: ID, + /// (Immutable) ID of the package after it was upgraded. + package: ID, + } + + /// Claim a Publisher object. + /// Requires a One-Time-Witness to prove ownership. Due to this + /// constraint there can be only one Publisher object per module + /// but multiple per package (!). + public fun claim(otw: OTW, ctx: &mut TxContext): Publisher { + assert!(types::is_one_time_witness(&otw), ENotOneTimeWitness); + + let type = type_name::get_with_original_ids(); + + Publisher { + id: object::new(ctx), + package: type_name::get_address(&type), + module_name: type_name::get_module(&type), + } + } + + #[lint_allow(self_transfer)] + /// Claim a Publisher object and send it to transaction sender. + /// Since this function can only be called in the module initializer, + /// the sender is the publisher. + public fun claim_and_keep(otw: OTW, ctx: &mut TxContext) { + sui::transfer::public_transfer(claim(otw, ctx), sender(ctx)) + } + + /// Destroy a Publisher object effectively removing all privileges + /// associated with it. + public fun burn_publisher(self: Publisher) { + let Publisher { id, package: _, module_name: _ } = self; + object::delete(id); + } + + /// Check whether type belongs to the same package as the publisher object. + public fun from_package(self: &Publisher): bool { + let type = type_name::get_with_original_ids(); + + (type_name::get_address(&type) == self.package) + } + + /// Check whether a type belongs to the same module as the publisher object. + public fun from_module(self: &Publisher): bool { + let type = type_name::get_with_original_ids(); + + (type_name::get_address(&type) == self.package) + && (type_name::get_module(&type) == self.module_name) + } + + /// Read the name of the module. + public fun published_module(self: &Publisher): &String { + &self.module_name + } + + /// Read the package address string. + public fun published_package(self: &Publisher): &String { + &self.package + } + + /// The ID of the package that this cap authorizes upgrades for. + /// Can be `0x0` if the cap cannot currently authorize an upgrade + /// because there is already a pending upgrade in the transaction. + /// Otherwise guaranteed to be the latest version of any given + /// package. + public fun upgrade_package(cap: &UpgradeCap): ID { + cap.package + } + + /// The most recent version of the package, increments by one for each + /// successfully applied upgrade. + public fun version(cap: &UpgradeCap): u64 { + cap.version + } + + /// The most permissive kind of upgrade currently supported by this + /// `cap`. + public fun upgrade_policy(cap: &UpgradeCap): u8 { + cap.policy + } + + /// The package that this ticket is authorized to upgrade + public fun ticket_package(ticket: &UpgradeTicket): ID { + ticket.package + } + + /// The kind of upgrade that this ticket authorizes. + public fun ticket_policy(ticket: &UpgradeTicket): u8 { + ticket.policy + } + + /// ID of the `UpgradeCap` that this `receipt` should be used to + /// update. + public fun receipt_cap(receipt: &UpgradeReceipt): ID { + receipt.cap + } + + /// ID of the package that was upgraded to: the latest version of + /// the package, as of the upgrade represented by this `receipt`. + public fun receipt_package(receipt: &UpgradeReceipt): ID { + receipt.package + } + + /// A hash of the package contents for the new version of the + /// package. This ticket only authorizes an upgrade to a package + /// that matches this digest. A package's contents are identified + /// by two things: + /// + /// - modules: [[u8]] a list of the package's module contents + /// - deps: [[u8; 32]] a list of 32 byte ObjectIDs of the + /// package's transitive dependencies + /// + /// A package's digest is calculated as: + /// + /// sha3_256(sort(modules ++ deps)) + public fun ticket_digest(ticket: &UpgradeTicket): &vector { + &ticket.digest + } + + /// Expose the constants representing various upgrade policies + public fun compatible_policy(): u8 { COMPATIBLE } + public fun additive_policy(): u8 { ADDITIVE } + public fun dep_only_policy(): u8 { DEP_ONLY } + + /// Restrict upgrades through this upgrade `cap` to just add code, or + /// change dependencies. + public entry fun only_additive_upgrades(cap: &mut UpgradeCap) { + restrict(cap, ADDITIVE) + } + + /// Restrict upgrades through this upgrade `cap` to just change + /// dependencies. + public entry fun only_dep_upgrades(cap: &mut UpgradeCap) { + restrict(cap, DEP_ONLY) + } + + /// Discard the `UpgradeCap` to make a package immutable. + public entry fun make_immutable(cap: UpgradeCap) { + let UpgradeCap { id, package: _, version: _, policy: _ } = cap; + object::delete(id); + } + + /// Issue a ticket authorizing an upgrade to a particular new bytecode + /// (identified by its digest). A ticket will only be issued if one has + /// not already been issued, and if the `policy` requested is at least as + /// restrictive as the policy set out by the `cap`. + /// + /// The `digest` supplied and the `policy` will both be checked by + /// validators when running the upgrade. I.e. the bytecode supplied in + /// the upgrade must have a matching digest, and the changes relative to + /// the parent package must be compatible with the policy in the ticket + /// for the upgrade to succeed. + public fun authorize_upgrade( + cap: &mut UpgradeCap, + policy: u8, + digest: vector + ): UpgradeTicket { + let id_zero = object::id_from_address(@0x0); + assert!(cap.package != id_zero, EAlreadyAuthorized); + assert!(policy >= cap.policy, ETooPermissive); + + let package = cap.package; + cap.package = id_zero; + + UpgradeTicket { + cap: object::id(cap), + package, + policy, + digest, + } + } + + /// Consume an `UpgradeReceipt` to update its `UpgradeCap`, finalizing + /// the upgrade. + public fun commit_upgrade( + cap: &mut UpgradeCap, + receipt: UpgradeReceipt, + ) { + let UpgradeReceipt { cap: cap_id, package } = receipt; + + assert!(object::id(cap) == cap_id, EWrongUpgradeCap); + assert!(object::id_to_address(&cap.package) == @0x0, ENotAuthorized); + + cap.package = package; + cap.version = cap.version + 1; + } + + #[test_only] + /// Test-only function to claim a Publisher object bypassing OTW check. + public fun test_claim(_: OTW, ctx: &mut TxContext): Publisher { + let type = type_name::get_with_original_ids(); + + Publisher { + id: object::new(ctx), + package: type_name::get_address(&type), + module_name: type_name::get_module(&type), + } + } + + #[test_only] + /// Test-only function to simulate publishing a package at address + /// `ID`, to create an `UpgradeCap`. + public fun test_publish(package: ID, ctx: &mut TxContext): UpgradeCap { + UpgradeCap { + id: object::new(ctx), + package, + version: 1, + policy: COMPATIBLE, + } + } + + #[test_only] + /// Test-only function that takes the role of the actual `Upgrade` + /// command, converting the ticket for the pending upgrade to a + /// receipt for a completed upgrade. + public fun test_upgrade(ticket: UpgradeTicket): UpgradeReceipt { + let UpgradeTicket { cap, package, policy: _, digest: _ } = ticket; + + // Generate a fake package ID for the upgraded package by + // hashing the existing package and cap ID. + let data = object::id_to_bytes(&cap); + std::vector::append(&mut data, object::id_to_bytes(&package)); + let package = object::id_from_bytes(sui::hash::blake2b256(&data)); + + UpgradeReceipt { + cap, package + } + } + + fun restrict(cap: &mut UpgradeCap, policy: u8) { + assert!(cap.policy <= policy, ETooPermissive); + cap.policy = policy; + } +} diff --git a/lang/move-on-sui/test/ok/pay.move b/lang/move-on-sui/test/ok/pay.move new file mode 100644 index 00000000..47e84baa --- /dev/null +++ b/lang/move-on-sui/test/ok/pay.move @@ -0,0 +1,89 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This module provides handy functionality for wallets and `sui::Coin` management. +module sui::pay { + use sui::tx_context::{Self, TxContext}; + use sui::coin::{Self, Coin}; + use sui::transfer; + use std::vector; + + /// For when empty vector is supplied into join function. + const ENoCoins: u64 = 0; + + #[lint_allow(self_transfer)] + /// Transfer `c` to the sender of the current transaction + public fun keep(c: Coin, ctx: &TxContext) { + transfer::public_transfer(c, tx_context::sender(ctx)) + } + + /// Split coin `self` to two coins, one with balance `split_amount`, + /// and the remaining balance is left is `self`. + public entry fun split( + self: &mut Coin, split_amount: u64, ctx: &mut TxContext + ) { + keep(coin::split(self, split_amount, ctx), ctx) + } + + /// Split coin `self` into multiple coins, each with balance specified + /// in `split_amounts`. Remaining balance is left in `self`. + public entry fun split_vec( + self: &mut Coin, split_amounts: vector, ctx: &mut TxContext + ) { + let (i, len) = (0, vector::length(&split_amounts)); + while (i < len) { + split(self, *vector::borrow(&split_amounts, i), ctx); + i = i + 1; + }; + } + + /// Send `amount` units of `c` to `recipient` + /// Aborts with `EVALUE` if `amount` is greater than or equal to `amount` + public entry fun split_and_transfer( + c: &mut Coin, amount: u64, recipient: address, ctx: &mut TxContext + ) { + transfer::public_transfer(coin::split(c, amount, ctx), recipient) + } + + + #[lint_allow(self_transfer)] + /// Divide coin `self` into `n - 1` coins with equal balances. If the balance is + /// not evenly divisible by `n`, the remainder is left in `self`. + public entry fun divide_and_keep( + self: &mut Coin, n: u64, ctx: &mut TxContext + ) { + let vec: vector> = coin::divide_into_n(self, n, ctx); + let (i, len) = (0, vector::length(&vec)); + while (i < len) { + transfer::public_transfer(vector::pop_back(&mut vec), tx_context::sender(ctx)); + i = i + 1; + }; + vector::destroy_empty(vec); + } + + /// Join `coin` into `self`. Re-exports `coin::join` function. + public entry fun join(self: &mut Coin, coin: Coin) { + coin::join(self, coin) + } + + /// Join everything in `coins` with `self` + public entry fun join_vec(self: &mut Coin, coins: vector>) { + let (i, len) = (0, vector::length(&coins)); + while (i < len) { + let coin = vector::pop_back(&mut coins); + coin::join(self, coin); + i = i + 1 + }; + // safe because we've drained the vector + vector::destroy_empty(coins) + } + + /// Join a vector of `Coin` into a single object and transfer it to `receiver`. + public entry fun join_vec_and_transfer(coins: vector>, receiver: address) { + assert!(vector::length(&coins) > 0, ENoCoins); + + let self = vector::pop_back(&mut coins); + join_vec(&mut self, coins); + transfer::public_transfer(self, receiver) + } +} diff --git a/lang/move-on-sui/test/ok/priority_queue.move b/lang/move-on-sui/test/ok/priority_queue.move new file mode 100644 index 00000000..1ae7f635 --- /dev/null +++ b/lang/move-on-sui/test/ok/priority_queue.move @@ -0,0 +1,188 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Priority queue implemented using a max heap. +module sui::priority_queue { + use std::vector; + + /// For when heap is empty and there's no data to pop. + const EPopFromEmptyHeap: u64 = 0; + + /// Struct representing a priority queue. The `entries` vector represents a max + /// heap structure, where entries[0] is the root, entries[1] and entries[2] are the + /// left child and right child of the root, etc. More generally, the children of + /// entries[i] are at at i * 2 + 1 and i * 2 + 2. The max heap should have the invariant + /// that the parent node's priority is always higher than its child nodes' priorities. + struct PriorityQueue has store, drop { + entries: vector>, + } + + struct Entry has store, drop { + priority: u64, // higher value means higher priority and will be popped first + value: T, + } + + /// Create a new priority queue from the input entry vectors. + public fun new(entries: vector>) : PriorityQueue { + let len = vector::length(&entries); + let i = len / 2; + // Max heapify from the first node that is a parent (node at len / 2). + while (i > 0) { + i = i - 1; + max_heapify_recursive(&mut entries, len, i); + }; + PriorityQueue { entries } + } + + /// Pop the entry with the highest priority value. + public fun pop_max(pq: &mut PriorityQueue) : (u64, T) { + let len = vector::length(&pq.entries); + assert!(len > 0, EPopFromEmptyHeap); + // Swap the max element with the last element in the entries and remove the max element. + let Entry { priority, value } = vector::swap_remove(&mut pq.entries, 0); + // Now the max heap property has been violated at the root node, but nowhere else + // so we call max heapify on the root node. + max_heapify_recursive(&mut pq.entries, len - 1, 0); + (priority, value) + } + + /// Insert a new entry into the queue. + public fun insert(pq: &mut PriorityQueue, priority: u64, value: T) { + vector::push_back(&mut pq.entries, Entry { priority, value}); + let index = vector::length(&pq.entries) - 1; + restore_heap_recursive(&mut pq.entries, index); + } + + public fun new_entry(priority: u64, value: T): Entry { + Entry { priority, value } + } + + public fun create_entries(p: vector, v: vector): vector> { + let len = vector::length(&p); + assert!(vector::length(&v) == len, 0); + let res = vector::empty(); + let i = 0; + while (i < len) { + let priority = vector::remove(&mut p, 0); + let value = vector::remove(&mut v, 0); + vector::push_back(&mut res, Entry { priority, value }); + i = i + 1; + }; + res + } + + // TODO: implement iterative version too and see performance difference. + fun restore_heap_recursive(v: &mut vector>, i: u64) { + if (i == 0) { + return + }; + let parent = (i - 1) / 2; + + // If new elem is greater than its parent, swap them and recursively + // do the restoration upwards. + if (vector::borrow(v, i).priority > vector::borrow(v, parent).priority) { + vector::swap(v, i, parent); + restore_heap_recursive(v, parent); + } + } + + spec restore_heap_recursive { + pragma opaque; + } + + /// Max heapify the subtree whose root is at index `i`. That means after this function + /// finishes, the subtree should have the property that the parent node has higher priority + /// than both child nodes. + /// This function assumes that all the other nodes in the subtree (nodes other than the root) + /// do satisfy the max heap property. + fun max_heapify_recursive(v: &mut vector>, len: u64, i: u64) { + if (len == 0) { + return + }; + assert!(i < len, 1); + let left = i * 2 + 1; + let right = left + 1; + let max = i; + // Find the node with highest priority among node `i` and its two children. + if (left < len && vector::borrow(v, left).priority> vector::borrow(v, max).priority) { + max = left; + }; + if (right < len && vector::borrow(v, right).priority > vector::borrow(v, max).priority) { + max = right; + }; + // If the parent node (node `i`) doesn't have the highest priority, we swap the parent with the + // max priority node. + if (max != i) { + vector::swap(v, max, i); + // After the swap, we have restored the property at node `i` but now the max heap property + // may be violated at node `max` since this node now has a new value. So we need to now + // max heapify the subtree rooted at node `max`. + max_heapify_recursive(v, len, max); + } + } + + spec max_heapify_recursive { + pragma opaque; + } + + public fun priorities(pq: &PriorityQueue): vector { + let res = vector[]; + let i = 0; + while (i < vector::length(&pq.entries)) { + vector::push_back(&mut res, vector::borrow(&pq.entries, i).priority); + i = i +1; + }; + res + } + + #[test] + fun test_pq() { + let h = new(create_entries(vector[3,1,4,2,5,2], vector[10, 20, 30, 40, 50, 60])); + check_pop_max(&mut h, 5, 50); + check_pop_max(&mut h, 4, 30); + check_pop_max(&mut h, 3, 10); + insert(&mut h, 7, 70); + check_pop_max(&mut h, 7, 70); + check_pop_max(&mut h, 2, 40); + insert(&mut h, 0, 80); + check_pop_max(&mut h, 2, 60); + check_pop_max(&mut h, 1, 20); + check_pop_max(&mut h, 0, 80); + + + let h = new(create_entries(vector[5,3,1,2,4], vector[10, 20, 30, 40, 50])); + check_pop_max(&mut h, 5, 10); + check_pop_max(&mut h, 4, 50); + check_pop_max(&mut h, 3, 20); + check_pop_max(&mut h, 2, 40); + check_pop_max(&mut h, 1, 30); + } + + #[test] + fun test_swap_remove_edge_case() { + // This test would fail if `remove` is used incorrectly instead of `swap_remove` in `pop_max`. + // It's hard to characterize exactly under what condition this bug is triggered but roughly + // it happens when the entire tree vector is shifted left by one because of the incorrect usage + // of `remove`, and the resulting new root and its two children appear to satisfy the heap invariant + // so we stop max-heapifying there, while the rest of the tree is all messed up because of the shift. + let priorities = vector[8, 7, 3, 6, 2, 1, 0, 5, 4]; + let values = vector[0, 0, 0, 0, 0, 0, 0, 0, 0]; + let h = new(create_entries(priorities, values)); + check_pop_max(&mut h, 8, 0); + check_pop_max(&mut h, 7, 0); + check_pop_max(&mut h, 6, 0); + check_pop_max(&mut h, 5, 0); + check_pop_max(&mut h, 4, 0); + check_pop_max(&mut h, 3, 0); + check_pop_max(&mut h, 2, 0); + check_pop_max(&mut h, 1, 0); + check_pop_max(&mut h, 0, 0); + } + + #[test_only] + fun check_pop_max(h: &mut PriorityQueue, expected_priority: u64, expected_value: u64) { + let (priority, value) = pop_max(h); + assert!(priority == expected_priority, 0); + assert!(value == expected_value, 0); + } +} diff --git a/lang/move-on-sui/test/ok/prover.move b/lang/move-on-sui/test/ok/prover.move new file mode 100644 index 00000000..319fd880 --- /dev/null +++ b/lang/move-on-sui/test/ok/prover.move @@ -0,0 +1,77 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module sui::prover { + use sui::object; + + #[allow(unused_const)] + const OWNED: u64 = 1; + #[allow(unused_const)] + const SHARED: u64 = 2; + #[allow(unused_const)] + const IMMUTABLE: u64 = 3; + + // "public" functions to be used in specs as an equivalent of core Prover's builtins + + /// Verifies if a given object it owned. + spec fun owned(obj: T): bool { + let addr = object::id(obj).bytes; + exists(addr) && + global(addr).status == OWNED + } + + /// Verifies if a given object is owned. + spec fun owned_by(obj: T, owner: address): bool { + let addr = object::id(obj).bytes; + exists(addr) && + global(addr).status == OWNED && + global(addr).owner == owner + } + + /// Verifies if a given object is shared. + spec fun shared(obj: T): bool { + let addr = object::id(obj).bytes; + exists(addr) && + global(addr).status == SHARED + } + + /// Verifies if a given object is immutable. + spec fun immutable(obj: T): bool { + let addr = object::id(obj).bytes; + exists(addr) && + global(addr).status == IMMUTABLE + } + + /// Verifies if a given object has field with a given name. + spec fun has_field(obj: T, name: K): bool { + let uid = object::borrow_uid(obj); + uid_has_field(uid, name) + } + + /// Returns number of K-type fields of a given object. + spec fun num_fields(obj: T): u64 { + let uid = object::borrow_uid(obj); + uid_num_fields(uid) + } + + // "helper" function - may also be used in specs but mostly opaque ones defining behavior of key + // framework functions + + spec fun uid_has_field(uid: sui::object::UID, name: K): bool { + let addr = object::uid_to_address(uid); + exists>(addr) && contains(global>(addr).names, name) + } + + spec fun uid_num_fields(uid: sui::object::UID): u64 { + let addr = object::uid_to_address(uid); + if (!exists>(addr)) { + 0 + } else { + len(global>(addr).names) + } + } + + // remove an element at index from a vector and return the resulting vector (redirects to a + // function in vector theory) + spec native fun vec_remove(v: vector, elem_idx: u64): vector; +} diff --git a/lang/move-on-sui/test/ok/random.move b/lang/move-on-sui/test/ok/random.move new file mode 100644 index 00000000..739f660c --- /dev/null +++ b/lang/move-on-sui/test/ok/random.move @@ -0,0 +1,125 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[allow(unused_use)] +// This module provides functionality for generating and using secure randomness. +// +// Randomness is currently write-only, until user-facing API is implemented. +module sui::random { + use std::vector; + use sui::object::{Self, UID}; + use sui::transfer; + use sui::tx_context::{Self, TxContext}; + use sui::versioned::{Self, Versioned}; + + // Sender is not @0x0 the system address. + const ENotSystemAddress: u64 = 0; + const EWrongInnerVersion: u64 = 1; + const EInvalidRandomnessUpdate: u64 = 2; + + const CURRENT_VERSION: u64 = 1; + + /// Singleton shared object which stores the global randomness state. + /// The actual state is stored in a versioned inner field. + struct Random has key { + id: UID, + inner: Versioned, + } + + struct RandomInner has store { + version: u64, + + epoch: u64, + randomness_round: u64, + random_bytes: vector, + } + + #[allow(unused_function)] + /// Create and share the Random object. This function is called exactly once, when + /// the Random object is first created. + /// Can only be called by genesis or change_epoch transactions. + fun create(ctx: &mut TxContext) { + assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress); + + let version = CURRENT_VERSION; + + let inner = RandomInner { + version, + epoch: tx_context::epoch(ctx), + randomness_round: 0, + random_bytes: vector[], + }; + + let self = Random { + id: object::randomness_state(), + inner: versioned::create(version, inner, ctx), + }; + transfer::share_object(self); + } + + #[test_only] + public fun create_for_testing(ctx: &mut TxContext) { + create(ctx); + } + + fun load_inner_mut( + self: &mut Random, + ): &mut RandomInner { + let version = versioned::version(&self.inner); + + // Replace this with a lazy update function when we add a new version of the inner object. + assert!(version == CURRENT_VERSION, EWrongInnerVersion); + let inner: &mut RandomInner = versioned::load_value_mut(&mut self.inner); + assert!(inner.version == version, EWrongInnerVersion); + inner + } + + #[allow(unused_function)] // TODO: remove annotation after implementing user-facing API + fun load_inner( + self: &Random, + ): &RandomInner { + let version = versioned::version(&self.inner); + + // Replace this with a lazy update function when we add a new version of the inner object. + assert!(version == CURRENT_VERSION, EWrongInnerVersion); + let inner: &RandomInner = versioned::load_value(&self.inner); + assert!(inner.version == version, EWrongInnerVersion); + inner + } + + #[allow(unused_function)] + /// Record new randomness. Called when executing the RandomnessStateUpdate system + /// transaction. + fun update_randomness_state( + self: &mut Random, + new_round: u64, + new_bytes: vector, + ctx: &TxContext, + ) { + // Validator will make a special system call with sender set as 0x0. + assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress); + + // Randomness should only be incremented. + let epoch = tx_context::epoch(ctx); + let inner = load_inner_mut(self); + assert!( + (epoch == inner.epoch + 1 && inner.randomness_round == 0) || + (new_round == inner.randomness_round + 1), + EInvalidRandomnessUpdate + ); + + inner.epoch = tx_context::epoch(ctx); + inner.randomness_round = new_round; + inner.random_bytes = new_bytes; + } + + #[test_only] + public fun update_randomness_state_for_testing( + self: &mut Random, + new_round: u64, + new_bytes: vector, + ctx: &TxContext, + ) { + update_randomness_state(self, new_round, new_bytes, ctx); + } +} diff --git a/lang/move-on-sui/test/ok/simple_module.move b/lang/move-on-sui/test/ok/simple_module.move new file mode 100644 index 00000000..c7389aa0 --- /dev/null +++ b/lang/move-on-sui/test/ok/simple_module.move @@ -0,0 +1,14 @@ +module sui::foo { + public struct Bar { x: u64 } + + fun f() { } + + fun g(x: u64): u64 { x } + + fun h(x: Bar): u64 { x.x } + + fun j(x: Bar): u64 { + let mut x = x.x(); + x.foo!() + } +} diff --git a/lang/move-on-sui/test/ok/structs.move b/lang/move-on-sui/test/ok/structs.move new file mode 100644 index 00000000..8247c40a --- /dev/null +++ b/lang/move-on-sui/test/ok/structs.move @@ -0,0 +1,11 @@ +module Move2024::structs { + + public struct SomeStruct {} has drop, copy; + + public struct Positional(u64, SomeStruct) has drop, copy; + + public fun foo(positional: Positional): (u64, SomeStruct) { + (positional.0, positional.1) + } + +} diff --git a/lang/move-on-sui/test/ok/sui.move b/lang/move-on-sui/test/ok/sui.move new file mode 100644 index 00000000..51a1ef56 --- /dev/null +++ b/lang/move-on-sui/test/ok/sui.move @@ -0,0 +1,59 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Coin is the token used to pay for gas in Sui. +/// It has 9 decimals, and the smallest unit (10^-9) is called "mist". +module sui::sui { + use std::option; + use sui::tx_context::{Self, TxContext}; + use sui::balance::{Self, Balance}; + use sui::transfer; + use sui::coin; + + const EAlreadyMinted: u64 = 0; + /// Sender is not @0x0 the system address. + const ENotSystemAddress: u64 = 1; + + #[allow(unused_const)] + /// The amount of Mist per Sui token based on the the fact that mist is + /// 10^-9 of a Sui token + const MIST_PER_SUI: u64 = 1_000_000_000; + + #[allow(unused_const)] + /// The total supply of Sui denominated in whole Sui tokens (10 Billion) + const TOTAL_SUPPLY_SUI: u64 = 10_000_000_000; + + /// The total supply of Sui denominated in Mist (10 Billion * 10^9) + const TOTAL_SUPPLY_MIST: u64 = 10_000_000_000_000_000_000; + + /// Name of the coin + struct SUI has drop {} + + #[allow(unused_function)] + /// Register the `SUI` Coin to acquire its `Supply`. + /// This should be called only once during genesis creation. + fun new(ctx: &mut TxContext): Balance { + assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress); + assert!(tx_context::epoch(ctx) == 0, EAlreadyMinted); + + let (treasury, metadata) = coin::create_currency( + SUI {}, + 9, + b"SUI", + b"Sui", + // TODO: add appropriate description and logo url + b"", + option::none(), + ctx + ); + transfer::public_freeze_object(metadata); + let supply = coin::treasury_into_supply(treasury); + let total_sui = balance::increase_supply(&mut supply, TOTAL_SUPPLY_MIST); + balance::destroy_supply(supply); + total_sui + } + + public entry fun transfer(c: coin::Coin, recipient: address) { + transfer::public_transfer(c, recipient) + } +} diff --git a/lang/move-on-sui/test/ok/table.move b/lang/move-on-sui/test/ok/table.move new file mode 100644 index 00000000..097cc9a9 --- /dev/null +++ b/lang/move-on-sui/test/ok/table.move @@ -0,0 +1,102 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// A table is a map-like collection. But unlike a traditional collection, it's keys and values are +/// not stored within the `Table` value, but instead are stored using Sui's object system. The +/// `Table` struct acts only as a handle into the object system to retrieve those keys and values. +/// Note that this means that `Table` values with exactly the same key-value mapping will not be +/// equal, with `==`, at runtime. For example +/// ``` +/// let table1 = table::new(); +/// let table2 = table::new(); +/// table::add(&mut table1, 0, false); +/// table::add(&mut table1, 1, true); +/// table::add(&mut table2, 0, false); +/// table::add(&mut table2, 1, true); +/// // table1 does not equal table2, despite having the same entries +/// assert!(&table1 != &table2, 0); +/// ``` +module sui::table { + use sui::object::{Self, UID}; + use sui::dynamic_field as field; + use sui::tx_context::TxContext; + + // Attempted to destroy a non-empty table + const ETableNotEmpty: u64 = 0; + + struct Table has key, store { + /// the ID of this table + id: UID, + /// the number of key-value pairs in the table + size: u64, + } + + /// Creates a new, empty table + public fun new(ctx: &mut TxContext): Table { + Table { + id: object::new(ctx), + size: 0, + } + } + + /// Adds a key-value pair to the table `table: &mut Table` + /// Aborts with `sui::dynamic_field::EFieldAlreadyExists` if the table already has an entry with + /// that key `k: K`. + public fun add(table: &mut Table, k: K, v: V) { + field::add(&mut table.id, k, v); + table.size = table.size + 1; + } + + /// Immutable borrows the value associated with the key in the table `table: &Table`. + /// Aborts with `sui::dynamic_field::EFieldDoesNotExist` if the table does not have an entry with + /// that key `k: K`. + public fun borrow(table: &Table, k: K): &V { + field::borrow(&table.id, k) + } + + /// Mutably borrows the value associated with the key in the table `table: &mut Table`. + /// Aborts with `sui::dynamic_field::EFieldDoesNotExist` if the table does not have an entry with + /// that key `k: K`. + public fun borrow_mut(table: &mut Table, k: K): &mut V { + field::borrow_mut(&mut table.id, k) + } + + /// Removes the key-value pair in the table `table: &mut Table` and returns the value. + /// Aborts with `sui::dynamic_field::EFieldDoesNotExist` if the table does not have an entry with + /// that key `k: K`. + public fun remove(table: &mut Table, k: K): V { + let v = field::remove(&mut table.id, k); + table.size = table.size - 1; + v + } + + /// Returns true iff there is a value associated with the key `k: K` in table `table: &Table` + public fun contains(table: &Table, k: K): bool { + field::exists_with_type(&table.id, k) + } + + /// Returns the size of the table, the number of key-value pairs + public fun length(table: &Table): u64 { + table.size + } + + /// Returns true iff the table is empty (if `length` returns `0`) + public fun is_empty(table: &Table): bool { + table.size == 0 + } + + /// Destroys an empty table + /// Aborts with `ETableNotEmpty` if the table still contains values + public fun destroy_empty(table: Table) { + let Table { id, size } = table; + assert!(size == 0, ETableNotEmpty); + object::delete(id) + } + + /// Drop a possibly non-empty table. + /// Usable only if the value type `V` has the `drop` ability + public fun drop(table: Table) { + let Table { id, size: _ } = table; + object::delete(id) + } +} diff --git a/lang/move-on-sui/test/ok/table_vec.move b/lang/move-on-sui/test/ok/table_vec.move new file mode 100644 index 00000000..a3ab65c9 --- /dev/null +++ b/lang/move-on-sui/test/ok/table_vec.move @@ -0,0 +1,105 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// A basic scalable vector library implemented using `Table`. +module sui::table_vec { + use sui::table::{Self, Table}; + use sui::tx_context::TxContext; + + struct TableVec has store { + /// The contents of the table vector. + contents: Table, + } + + const EIndexOutOfBound: u64 = 0; + const ETableNonEmpty: u64 = 1; + + /// Create an empty TableVec. + public fun empty(ctx: &mut TxContext): TableVec { + TableVec { + contents: table::new(ctx) + } + } + + /// Return a TableVec of size one containing element `e`. + public fun singleton(e: Element, ctx: &mut TxContext): TableVec { + let mut t = empty(ctx); + push_back(&mut t, e); + t + } + + /// Return the length of the TableVec. + public fun length(t: &TableVec): u64 { + table::length(&t.contents) + } + + /// Return if the TableVec is empty or not. + public fun is_empty(t: &TableVec): bool { + length(t) == 0 + } + + /// Acquire an immutable reference to the `i`th element of the TableVec `t`. + /// Aborts if `i` is out of bounds. + public fun borrow(t: &TableVec, i: u64): &Element { + assert!(length(t) > i, EIndexOutOfBound); + table::borrow(&t.contents, i) + } + + /// Add element `e` to the end of the TableVec `t`. + public fun push_back(t: &mut TableVec, e: Element) { + let key = length(t); + table::add(&mut t.contents, key, e); + } + + /// Return a mutable reference to the `i`th element in the TableVec `t`. + /// Aborts if `i` is out of bounds. + public fun borrow_mut(t: &mut TableVec, i: u64): &mut Element { + assert!(length(t) > i, EIndexOutOfBound); + table::borrow_mut(&mut t.contents, i) + } + + /// Pop an element from the end of TableVec `t`. + /// Aborts if `t` is empty. + public fun pop_back(t: &mut TableVec): Element { + let length = length(t); + assert!(length > 0, EIndexOutOfBound); + table::remove(&mut t.contents, length - 1) + } + + /// Destroy the TableVec `t`. + /// Aborts if `t` is not empty. + public fun destroy_empty(t: TableVec) { + assert!(length(&t) == 0, ETableNonEmpty); + let TableVec { contents } = t; + table::destroy_empty(contents); + } + + /// Drop a possibly non-empty TableVec `t`. + /// Usable only if the value type `Element` has the `drop` ability + public fun drop(t: TableVec) { + let TableVec { contents } = t; + table::drop(contents) + } + + /// Swaps the elements at the `i`th and `j`th indices in the TableVec `t`. + /// Aborts if `i` or `j` is out of bounds. + public fun swap(t: &mut TableVec, i: u64, j: u64) { + assert!(length(t) > i, EIndexOutOfBound); + assert!(length(t) > j, EIndexOutOfBound); + if (i == j) { return }; + let element_i = table::remove(&mut t.contents, i); + let element_j = table::remove(&mut t.contents, j); + table::add(&mut t.contents, j, element_i); + table::add(&mut t.contents, i, element_j); + } + + /// Swap the `i`th element of the TableVec `t` with the last element and then pop the TableVec. + /// This is O(1), but does not preserve ordering of elements in the TableVec. + /// Aborts if `i` is out of bounds. + public fun swap_remove(t: &mut TableVec, i: u64): Element { + assert!(length(t) > i, EIndexOutOfBound); + let last_idx = length(t) - 1; + swap(t, i, last_idx); + pop_back(t) + } +} diff --git a/lang/move-on-sui/test/ok/token.move b/lang/move-on-sui/test/ok/token.move new file mode 100644 index 00000000..0c5b3bda --- /dev/null +++ b/lang/move-on-sui/test/ok/token.move @@ -0,0 +1,733 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// The Token module which implements a Closed Loop Token with a configurable +/// policy. The policy is defined by a set of rules that must be satisfied for +/// an action to be performed on the token. +/// +/// The module is designed to be used with a `TreasuryCap` to allow for minting +/// and burning of the `Token`s. And can act as a replacement / extension or a +/// companion to existing open-loop (`Coin`) systems. +/// +/// ``` +/// Module: sui::balance sui::coin sui::token +/// Main type: Balance Coin Token +/// Capability: Supply <----> TreasuryCap <----> TreasuryCap +/// Abilities: store key + store key +/// ``` +/// +/// The Token system allows for fine-grained control over the actions performed +/// on the token. And hence it is highly suitable for applications that require +/// control over the currency which a simple open-loop system can't provide. +module sui::token { + use std::vector; + use std::string::{Self, String}; + use std::option::{Self, Option}; + use std::type_name::{Self, TypeName}; + use sui::tx_context::{Self, TxContext}; + use sui::coin::{Self, Coin, TreasuryCap}; + use sui::balance::{Self, Balance}; + use sui::object::{Self, ID, UID}; + use sui::vec_map::{Self, VecMap}; + use sui::vec_set::{Self, VecSet}; + use sui::dynamic_field as df; + use sui::transfer; + + /// The action is not allowed (defined) in the policy. + const EUnknownAction: u64 = 0; + /// The rule was not approved. + const ENotApproved: u64 = 1; + /// Trying to perform an admin action with a wrong cap. + const ENotAuthorized: u64 = 2; + /// The balance is too low to perform the action. + const EBalanceTooLow: u64 = 3; + /// The balance is not zero. + const ENotZero: u64 = 4; + /// The balance is not zero when trying to confirm with `TransferPolicyCap`. + const ECantConsumeBalance: u64 = 5; + /// Rule is trying to access a missing config (with type). + const ENoConfig: u64 = 6; + /// Using `confirm_request_mut` without `spent_balance`. Immutable version + /// of the function must be used instead. + const EUseImmutableConfirm: u64 = 7; + + // === Protected Actions === + + /// A Tag for the `spend` action. + const SPEND: vector = b"spend"; + /// A Tag for the `transfer` action. + const TRANSFER: vector = b"transfer"; + /// A Tag for the `to_coin` action. + const TO_COIN: vector = b"to_coin"; + /// A Tag for the `from_coin` action. + const FROM_COIN: vector = b"from_coin"; + + /// A single `Token` with `Balance` inside. Can only be owned by an address, + /// and actions performed on it must be confirmed in a matching `TokenPolicy`. + struct Token has key { + id: UID, + /// The Balance of the `Token`. + balance: Balance, + } + + /// A Capability that manages a single `TokenPolicy` specified in the `for` + /// field. Created together with `TokenPolicy` in the `new` function. + struct TokenPolicyCap has key, store { id: UID, for: ID } + + /// `TokenPolicy` represents a set of rules that define what actions can be + /// performed on a `Token` and which `Rules` must be satisfied for the + /// action to succeed. + /// + /// - For the sake of availability, `TokenPolicy` is a `key`-only object. + /// - Each `TokenPolicy` is managed by a matching `TokenPolicyCap`. + /// - For an action to become available, there needs to be a record in the + /// `rules` VecMap. To allow an action to be performed freely, there's an + /// `allow` function that can be called by the `TokenPolicyCap` owner. + struct TokenPolicy has key { + id: UID, + /// The balance that is effectively spent by the user on the "spend" + /// action. However, actual decrease of the supply can only be done by + /// the `TreasuryCap` owner when `flush` is called. + /// + /// This balance is effectively spent and cannot be accessed by anyone + /// but the `TreasuryCap` owner. + spent_balance: Balance, + /// The set of rules that define what actions can be performed on the + /// token. For each "action" there's a set of Rules that must be + /// satisfied for the `ActionRequest` to be confirmed. + rules: VecMap> + } + + /// A request to perform an "Action" on a token. Stores the information + /// about the action to be performed and must be consumed by the `confirm_request` + /// or `confirm_request_mut` functions when the Rules are satisfied. + struct ActionRequest { + /// Name of the Action to look up in the Policy. Name can be one of the + /// default actions: `transfer`, `spend`, `to_coin`, `from_coin` or a + /// custom action. + name: String, + /// Amount is present in all of the txs + amount: u64, + /// Sender is a permanent field always + sender: address, + /// Recipient is only available in `transfer` action. + recipient: Option
, + /// The balance to be "spent" in the `TokenPolicy`, only available + /// in the `spend` action. + spent_balance: Option>, + /// Collected approvals (stamps) from completed `Rules`. They're matched + /// against `TokenPolicy.rules` to determine if the request can be + /// confirmed. + approvals: VecSet, + } + + /// Dynamic field key for the `TokenPolicy` to store the `Config` for a + /// specific action `Rule`. There can be only one configuration per + /// `Rule` per `TokenPolicy`. + struct RuleKey has store, copy, drop { is_protected: bool } + + /// Create a new `TokenPolicy` and a matching `TokenPolicyCap`. + /// The `TokenPolicy` must then be shared using the `share_policy` method. + /// + /// `TreasuryCap` guarantees full ownership over the currency, and is unique, + /// hence it is safe to use it for authorization. + public fun new( + _treasury_cap: &TreasuryCap, ctx: &mut TxContext + ): (TokenPolicy, TokenPolicyCap) { + let policy = TokenPolicy { + id: object::new(ctx), + spent_balance: balance::zero(), + rules: vec_map::empty() + }; + + let cap = TokenPolicyCap { + id: object::new(ctx), + for: object::id(&policy) + }; + + (policy, cap) + } + + #[lint_allow(share_owned)] + /// Share the `TokenPolicy`. Due to `key`-only restriction, it must be + /// shared after initialization. + public fun share_policy(policy: TokenPolicy) { + transfer::share_object(policy) + } + + // === Protected Actions === + + /// Transfer a `Token` to a `recipient`. Creates an `ActionRequest` for the + /// "transfer" action. The `ActionRequest` contains the `recipient` field + /// to be used in verification. + public fun transfer( + t: Token, recipient: address, ctx: &mut TxContext + ): ActionRequest { + let amount = balance::value(&t.balance); + transfer::transfer(t, recipient); + + new_request( + transfer_action(), + amount, + option::some(recipient), + option::none(), + ctx + ) + } + + /// Spend a `Token` by unwrapping it and storing the `Balance` in the + /// `ActionRequest` for the "spend" action. The `ActionRequest` contains + /// the `spent_balance` field to be used in verification. + /// + /// Spend action requires `confirm_request_mut` to be called to confirm the + /// request and join the spent balance with the `TokenPolicy.spent_balance`. + public fun spend(t: Token, ctx: &mut TxContext): ActionRequest { + let Token { id, balance } = t; + object::delete(id); + + new_request( + spend_action(), + balance::value(&balance), + option::none(), + option::some(balance), + ctx + ) + } + + /// Convert `Token` into an open `Coin`. Creates an `ActionRequest` for the + /// "to_coin" action. + public fun to_coin( + t: Token, ctx: &mut TxContext + ): (Coin, ActionRequest) { + let Token { id, balance } = t; + let amount = balance::value(&balance); + object::delete(id); + + ( + coin::from_balance(balance, ctx), + new_request( + to_coin_action(), + amount, + option::none(), + option::none(), + ctx + ) + ) + } + + /// Convert an open `Coin` into a `Token`. Creates an `ActionRequest` for + /// the "from_coin" action. + public fun from_coin( + coin: Coin, ctx: &mut TxContext + ): (Token, ActionRequest) { + let amount = coin::value(&coin); + let token = Token { + id: object::new(ctx), + balance: coin::into_balance(coin) + }; + + ( + token, + new_request( + from_coin_action(), + amount, + option::none(), + option::none(), + ctx + ) + ) + } + + // === Public Actions === + + /// Join two `Token`s into one, always available. + public fun join(token: &mut Token, another: Token) { + let Token { id, balance } = another; + balance::join(&mut token.balance, balance); + object::delete(id); + } + + /// Split a `Token` with `amount`. + /// Aborts if the `Token.balance` is lower than `amount`. + public fun split( + token: &mut Token, amount: u64, ctx: &mut TxContext + ): Token { + assert!(balance::value(&token.balance) >= amount, EBalanceTooLow); + Token { + id: object::new(ctx), + balance: balance::split(&mut token.balance, amount), + } + } + + /// Create a zero `Token`. + public fun zero(ctx: &mut TxContext): Token { + Token { + id: object::new(ctx), + balance: balance::zero(), + } + } + + /// Destroy an empty `Token`, fails if the balance is non-zero. + /// Aborts if the `Token.balance` is not zero. + public fun destroy_zero(token: Token) { + let Token { id, balance } = token; + assert!(balance::value(&balance) == 0, ENotZero); + balance::destroy_zero(balance); + object::delete(id); + } + + #[lint_allow(self_transfer)] + /// Transfer the `Token` to the transaction sender. + public fun keep(token: Token, ctx: &mut TxContext) { + transfer::transfer(token, tx_context::sender(ctx)) + } + + // === Request Handling === + + /// Create a new `ActionRequest`. + /// Publicly available method to allow for custom actions. + public fun new_request( + name: String, + amount: u64, + recipient: Option
, + spent_balance: Option>, + ctx: &TxContext + ): ActionRequest { + ActionRequest { + name, + amount, + recipient, + spent_balance, + sender: tx_context::sender(ctx), + approvals: vec_set::empty(), + } + } + + /// Confirm the request against the `TokenPolicy` and return the parameters + /// of the request: (Name, Amount, Sender, Recipient). + /// + /// Cannot be used for `spend` and similar actions that deliver `spent_balance` + /// to the `TokenPolicy`. For those actions use `confirm_request_mut`. + /// + /// Aborts if: + /// - the action is not allowed (missing record in `rules`) + /// - action contains `spent_balance` (use `confirm_request_mut`) + /// - the `ActionRequest` does not meet the `TokenPolicy` rules for the action + public fun confirm_request( + policy: &TokenPolicy, + request: ActionRequest, + _ctx: &mut TxContext + ): (String, u64, address, Option
) { + assert!(option::is_none(&request.spent_balance), ECantConsumeBalance); + assert!(vec_map::contains(&policy.rules, &request.name), EUnknownAction); + + let ActionRequest { + name, approvals, + spent_balance, + amount, sender, recipient, + } = request; + + option::destroy_none(spent_balance); + + let rules = &vec_set::into_keys(*vec_map::get(&policy.rules, &name)); + let rules_len = vector::length(rules); + let i = 0; + + while (i < rules_len) { + let rule = vector::borrow(rules, i); + assert!(vec_set::contains(&approvals, rule), ENotApproved); + i = i + 1; + }; + + (name, amount, sender, recipient) + } + + /// Confirm the request against the `TokenPolicy` and return the parameters + /// of the request: (Name, Amount, Sender, Recipient). + /// + /// Unlike `confirm_request` this function requires mutable access to the + /// `TokenPolicy` and must be used on `spend` action. After dealing with the + /// spent balance it calls `confirm_request` internally. + /// + /// See `confirm_request` for the list of abort conditions. + public fun confirm_request_mut( + policy: &mut TokenPolicy, + request: ActionRequest, + ctx: &mut TxContext + ): (String, u64, address, Option
) { + assert!(vec_map::contains(&policy.rules, &request.name), EUnknownAction); + assert!(option::is_some(&request.spent_balance), EUseImmutableConfirm); + + balance::join( + &mut policy.spent_balance, + option::extract(&mut request.spent_balance) + ); + + confirm_request(policy, request, ctx) + } + + /// Confirm an `ActionRequest` as the `TokenPolicyCap` owner. This function + /// allows `TokenPolicy` owner to perform Capability-gated actions ignoring + /// the ruleset specified in the `TokenPolicy`. + /// + /// Aborts if request contains `spent_balance` due to inability of the + /// `TokenPolicyCap` to decrease supply. For scenarios like this a + /// `TreasuryCap` is required (see `confirm_with_treasury_cap`). + public fun confirm_with_policy_cap( + _policy_cap: &TokenPolicyCap, + request: ActionRequest, + _ctx: &mut TxContext + ): (String, u64, address, Option
) { + assert!(option::is_none(&request.spent_balance), ECantConsumeBalance); + + let ActionRequest { + name, amount, sender, recipient, approvals: _, spent_balance + } = request; + + option::destroy_none(spent_balance); + + (name, amount, sender, recipient) + } + + /// Confirm an `ActionRequest` as the `TreasuryCap` owner. This function + /// allows `TreasuryCap` owner to perform Capability-gated actions ignoring + /// the ruleset specified in the `TokenPolicy`. + /// + /// Unlike `confirm_with_policy_cap` this function allows `spent_balance` + /// to be consumed, decreasing the `total_supply` of the `Token`. + public fun confirm_with_treasury_cap( + treasury_cap: &mut TreasuryCap, + request: ActionRequest, + _ctx: &mut TxContext + ): (String, u64, address, Option
) { + let ActionRequest { + name, amount, sender, recipient, approvals: _, + spent_balance + } = request; + + if (option::is_some(&spent_balance)) { + balance::decrease_supply( + coin::supply_mut(treasury_cap), + option::destroy_some(spent_balance) + ); + } else { + option::destroy_none(spent_balance); + }; + + (name, amount, sender, recipient) + } + + // === Rules API === + + /// Add an "approval" to the `ActionRequest` by providing a Witness. + /// Intended to be used by Rules to add their own approvals, however, can + /// be used to add arbitrary approvals to the request (not only the ones + /// required by the `TokenPolicy`). + public fun add_approval( + _t: W, request: &mut ActionRequest, _ctx: &mut TxContext + ) { + vec_set::insert(&mut request.approvals, type_name::get()) + } + + /// Add a `Config` for a `Rule` in the `TokenPolicy`. Rule configuration is + /// independent from the `TokenPolicy.rules` and needs to be managed by the + /// Rule itself. Configuration is stored per `Rule` and not per `Rule` per + /// `Action` to allow reuse in different actions. + /// + /// - Rule witness guarantees that the `Config` is approved by the Rule. + /// - `TokenPolicyCap` guarantees that the `Config` setup is initiated by + /// the `TokenPolicy` owner. + public fun add_rule_config( + _rule: Rule, + self: &mut TokenPolicy, + cap: &TokenPolicyCap, + config: Config, + _ctx: &mut TxContext + ) { + assert!(object::id(self) == cap.for, ENotAuthorized); + df::add(&mut self.id, key(), config) + } + + /// Get a `Config` for a `Rule` in the `TokenPolicy`. Requires `Rule` + /// witness, hence can only be read by the `Rule` itself. This requirement + /// guarantees safety of the stored `Config` and allows for simpler dynamic + /// field management inside the Rule Config (custom type keys are not needed + /// for access gating). + /// + /// Aborts if the Config is not present. + public fun rule_config( + _rule: Rule, self: &TokenPolicy + ): &Config { + assert!(has_rule_config_with_type(self), ENoConfig); + df::borrow(&self.id, key()) + } + + /// Get mutable access to the `Config` for a `Rule` in the `TokenPolicy`. + /// Requires `Rule` witness, hence can only be read by the `Rule` itself, + /// as well as `TokenPolicyCap` to guarantee that the `TokenPolicy` owner + /// is the one who initiated the `Config` modification. + /// + /// Aborts if: + /// - the Config is not present + /// - `TokenPolicyCap` is not matching the `TokenPolicy` + public fun rule_config_mut( + _rule: Rule, self: &mut TokenPolicy, cap: &TokenPolicyCap + ): &mut Config { + assert!(has_rule_config_with_type(self), ENoConfig); + assert!(object::id(self) == cap.for, ENotAuthorized); + df::borrow_mut(&mut self.id, key()) + } + + /// Remove a `Config` for a `Rule` in the `TokenPolicy`. + /// Unlike the `add_rule_config`, this function does not require a `Rule` + /// witness, hence can be performed by the `TokenPolicy` owner on their own. + /// + /// Rules need to make sure that the `Config` is present when performing + /// verification of the `ActionRequest`. + /// + /// Aborts if: + /// - the Config is not present + /// - `TokenPolicyCap` is not matching the `TokenPolicy` + public fun remove_rule_config( + self: &mut TokenPolicy, + cap: &TokenPolicyCap, + _ctx: &mut TxContext + ): Config { + assert!(has_rule_config_with_type(self), ENoConfig); + assert!(object::id(self) == cap.for, ENotAuthorized); + df::remove(&mut self.id, key()) + } + + /// Check if a config for a `Rule` is set in the `TokenPolicy` without + /// checking the type of the `Config`. + public fun has_rule_config(self: &TokenPolicy): bool { + df::exists_>(&self.id, key()) + } + + /// Check if a `Config` for a `Rule` is set in the `TokenPolicy` and that + /// it matches the type provided. + public fun has_rule_config_with_type( + self: &TokenPolicy + ): bool { + df::exists_with_type, Config>(&self.id, key()) + } + + // === Protected: Setting Rules === + + /// Allows an `action` to be performed on the `Token` freely by adding an + /// empty set of `Rules` for the `action`. + /// + /// Aborts if the `TokenPolicyCap` is not matching the `TokenPolicy`. + public fun allow( + self: &mut TokenPolicy, + cap: &TokenPolicyCap, + action: String, + _ctx: &mut TxContext + ) { + assert!(object::id(self) == cap.for, ENotAuthorized); + vec_map::insert(&mut self.rules, action, vec_set::empty()); + } + + /// Completely disallows an `action` on the `Token` by removing the record + /// from the `TokenPolicy.rules`. + /// + /// Aborts if the `TokenPolicyCap` is not matching the `TokenPolicy`. + public fun disallow( + self: &mut TokenPolicy, + cap: &TokenPolicyCap, + action: String, + _ctx: &mut TxContext + ) { + assert!(object::id(self) == cap.for, ENotAuthorized); + vec_map::remove(&mut self.rules, &action); + } + + /// Adds a Rule for an action with `name` in the `TokenPolicy`. + /// + /// Aborts if the `TokenPolicyCap` is not matching the `TokenPolicy`. + public fun add_rule_for_action( + self: &mut TokenPolicy, + cap: &TokenPolicyCap, + action: String, + ctx: &mut TxContext + ) { + assert!(object::id(self) == cap.for, ENotAuthorized); + if (!vec_map::contains(&self.rules, &action)) { + allow(self, cap, action, ctx); + }; + + vec_set::insert( + vec_map::get_mut(&mut self.rules, &action), + type_name::get() + ) + } + + /// Removes a rule for an action with `name` in the `TokenPolicy`. Returns + /// the config object to be handled by the sender (or a Rule itself). + /// + /// Aborts if the `TokenPolicyCap` is not matching the `TokenPolicy`. + public fun remove_rule_for_action( + self: &mut TokenPolicy, + cap: &TokenPolicyCap, + action: String, + _ctx: &mut TxContext + ) { + assert!(object::id(self) == cap.for, ENotAuthorized); + + vec_set::remove( + vec_map::get_mut(&mut self.rules, &action), + &type_name::get() + ) + } + + // === Protected: Treasury Management === + + /// Mint a `Token` with a given `amount` using the `TreasuryCap`. + public fun mint( + cap: &mut TreasuryCap, amount: u64, ctx: &mut TxContext + ): Token { + let balance = balance::increase_supply(coin::supply_mut(cap), amount); + Token { id: object::new(ctx), balance } + } + + /// Burn a `Token` using the `TreasuryCap`. + public fun burn(cap: &mut TreasuryCap, token: Token) { + let Token { id, balance } = token; + balance::decrease_supply(coin::supply_mut(cap), balance); + object::delete(id); + } + + /// Flush the `TokenPolicy.spent_balance` into the `TreasuryCap`. This + /// action is only available to the `TreasuryCap` owner. + public fun flush( + self: &mut TokenPolicy, + cap: &mut TreasuryCap, + _ctx: &mut TxContext + ): u64 { + let amount = balance::value(&self.spent_balance); + let balance = balance::split(&mut self.spent_balance, amount); + balance::decrease_supply(coin::supply_mut(cap), balance) + } + + // === Getters: `TokenPolicy` and `Token` === + + /// Check whether an action is present in the rules VecMap. + public fun is_allowed(self: &TokenPolicy, action: &String): bool { + vec_map::contains(&self.rules, action) + } + + /// Returns the rules required for a specific action. + public fun rules( + self: &TokenPolicy, action: &String + ): VecSet { + *vec_map::get(&self.rules, action) + } + + /// Returns the `spent_balance` of the `TokenPolicy`. + public fun spent_balance(self: &TokenPolicy): u64 { + balance::value(&self.spent_balance) + } + + /// Returns the `balance` of the `Token`. + public fun value(t: &Token): u64 { + balance::value(&t.balance) + } + + // === Action Names === + + /// Name of the Transfer action. + public fun transfer_action(): String { string::utf8(TRANSFER) } + + /// Name of the `Spend` action. + public fun spend_action(): String { string::utf8(SPEND) } + + /// Name of the `ToCoin` action. + public fun to_coin_action(): String { string::utf8(TO_COIN) } + + /// Name of the `FromCoin` action. + public fun from_coin_action(): String { string::utf8(FROM_COIN) } + + // === Action Request Fields == + + /// The Action in the `ActionRequest`. + public fun action(self: &ActionRequest): String { self.name } + + /// Amount of the `ActionRequest`. + public fun amount(self: &ActionRequest): u64 { self.amount } + + /// Sender of the `ActionRequest`. + public fun sender(self: &ActionRequest): address { self.sender } + + /// Recipient of the `ActionRequest`. + public fun recipient(self: &ActionRequest): Option
{ + self.recipient + } + + /// Approvals of the `ActionRequest`. + public fun approvals(self: &ActionRequest): VecSet { + self.approvals + } + + /// Burned balance of the `ActionRequest`. + public fun spent(self: &ActionRequest): Option { + if (option::is_some(&self.spent_balance)) { + option::some(balance::value(option::borrow(&self.spent_balance))) + } else { + option::none() + } + } + + // === Internal === + + /// Create a new `RuleKey` for a `Rule`. The `is_protected` field is kept + /// for potential future use, if Rules were to have a freely modifiable + /// storage as addition / replacement for the `Config` system. + /// + /// The goal of `is_protected` is to potentially allow Rules store a mutable + /// version of their configuration and mutate state on user action. + fun key(): RuleKey { RuleKey { is_protected: true } } + + // === Testing === + + #[test_only] + public fun new_policy_for_testing( + ctx: &mut TxContext + ): (TokenPolicy, TokenPolicyCap) { + let policy = TokenPolicy { + id: object::new(ctx), + rules: vec_map::empty(), + spent_balance: balance::zero(), + }; + let cap = TokenPolicyCap { + id: object::new(ctx), + for: object::id(&policy) + }; + + (policy, cap) + } + + #[test_only] + public fun burn_policy_for_testing( + policy: TokenPolicy, + cap: TokenPolicyCap + ) { + let TokenPolicyCap { id: cap_id, for: _ } = cap; + let TokenPolicy { id, rules: _, spent_balance } = policy; + balance::destroy_for_testing(spent_balance); + object::delete(cap_id); + object::delete(id); + } + + #[test_only] + public fun mint_for_testing(amount: u64, ctx: &mut TxContext): Token { + let balance = balance::create_for_testing(amount); + Token { id: object::new(ctx), balance } + } + + #[test_only] + public fun burn_for_testing(token: Token) { + let Token { id, balance } = token; + balance::destroy_for_testing(balance); + object::delete(id); + } +} diff --git a/lang/move-on-sui/test/ok/transfer.move b/lang/move-on-sui/test/ok/transfer.move new file mode 100644 index 00000000..7de3e8ec --- /dev/null +++ b/lang/move-on-sui/test/ok/transfer.move @@ -0,0 +1,180 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[allow(unused_const)] +module sui::transfer { + + use sui::object::{Self, ID, UID}; + use sui::prover; + + #[test_only] + friend sui::test_scenario; + + /// This represents the ability to `receive` an object of type `T`. + /// This type is ephemeral per-transaction and cannot be stored on-chain. + /// This does not represent the obligation to receive the object that it + /// references, but simply the ability to receive the object with object ID + /// `id` at version `version` if you can prove mutable access to the parent + /// object during the transaction. + /// Internals of this struct are opaque outside this module. + struct Receiving has drop { + id: ID, + version: u64, + } + + /// Shared an object that was previously created. Shared objects must currently + /// be constructed in the transaction they are created. + const ESharedNonNewObject: u64 = 0; + + #[allow(unused_const)] + /// Serialization of the object failed. + const EBCSSerializationFailure: u64 = 1; + + #[allow(unused_const)] + /// The object being received is not of the expected type. + const EReceivingObjectTypeMismatch: u64 = 2; + + #[allow(unused_const)] + /// Represents both the case where the object does not exist and the case where the object is not + /// able to be accessed through the parent that is passed-in. + const EUnableToReceiveObject: u64 = 3; + + #[allow(unused_const)] + /// Shared object operations such as wrapping, freezing, and converting to owned are not allowed. + const ESharedObjectOperationNotSupported: u64 = 4; + + + /// Transfer ownership of `obj` to `recipient`. `obj` must have the `key` attribute, + /// which (in turn) ensures that `obj` has a globally unique ID. Note that if the recipient + /// address represents an object ID, the `obj` sent will be inaccessible after the transfer + /// (though they will be retrievable at a future date once new features are added). + /// This function has custom rules performed by the Sui Move bytecode verifier that ensures + /// that `T` is an object defined in the module where `transfer` is invoked. Use + /// `public_transfer` to transfer an object with `store` outside of its module. + public fun transfer(obj: T, recipient: address) { + transfer_impl(obj, recipient) + } + + /// Transfer ownership of `obj` to `recipient`. `obj` must have the `key` attribute, + /// which (in turn) ensures that `obj` has a globally unique ID. Note that if the recipient + /// address represents an object ID, the `obj` sent will be inaccessible after the transfer + /// (though they will be retrievable at a future date once new features are added). + /// The object must have `store` to be transferred outside of its module. + public fun public_transfer(obj: T, recipient: address) { + transfer_impl(obj, recipient) + } + + /// Freeze `obj`. After freezing `obj` becomes immutable and can no longer be transferred or + /// mutated. + /// This function has custom rules performed by the Sui Move bytecode verifier that ensures + /// that `T` is an object defined in the module where `freeze_object` is invoked. Use + /// `public_freeze_object` to freeze an object with `store` outside of its module. + public fun freeze_object(obj: T) { + freeze_object_impl(obj) + } + + /// Freeze `obj`. After freezing `obj` becomes immutable and can no longer be transferred or + /// mutated. + /// The object must have `store` to be frozen outside of its module. + public fun public_freeze_object(obj: T) { + freeze_object_impl(obj) + } + + /// Turn the given object into a mutable shared object that everyone can access and mutate. + /// This is irreversible, i.e. once an object is shared, it will stay shared forever. + /// Aborts with `ESharedNonNewObject` of the object being shared was not created in this + /// transaction. This restriction may be relaxed in the future. + /// This function has custom rules performed by the Sui Move bytecode verifier that ensures + /// that `T` is an object defined in the module where `share_object` is invoked. Use + /// `public_share_object` to share an object with `store` outside of its module. + public fun share_object(obj: T) { + share_object_impl(obj) + } + + /// Turn the given object into a mutable shared object that everyone can access and mutate. + /// This is irreversible, i.e. once an object is shared, it will stay shared forever. + /// Aborts with `ESharedNonNewObject` of the object being shared was not created in this + /// transaction. This restriction may be relaxed in the future. + /// The object must have `store` to be shared outside of its module. + public fun public_share_object(obj: T) { + share_object_impl(obj) + } + + /// Given mutable (i.e., locked) access to the `parent` and a `Receiving` argument + /// referencing an object of type `T` owned by `parent` use the `to_receive` + /// argument to receive and return the referenced owned object of type `T`. + /// This function has custom rules performed by the Sui Move bytecode verifier that ensures + /// that `T` is an object defined in the module where `receive` is invoked. Use + /// `public_receive` to receivne an object with `store` outside of its module. + public fun receive(parent: &mut UID, to_receive: Receiving): T { + let Receiving { + id, + version, + } = to_receive; + receive_impl(object::uid_to_address(parent), id, version) + } + + /// Given mutable (i.e., locked) access to the `parent` and a `Receiving` argument + /// referencing an object of type `T` owned by `parent` use the `to_receive` + /// argument to receive and return the referenced owned object of type `T`. + /// The object must have `store` to be received outside of its defining module. + public fun public_receive(parent: &mut UID, to_receive: Receiving): T { + let Receiving { + id, + version, + } = to_receive; + receive_impl(object::uid_to_address(parent), id, version) + } + + /// Return the object ID that the given `Receiving` argument references. + public fun receiving_object_id(receiving: &Receiving): ID { + receiving.id + } + + public(friend) native fun freeze_object_impl(obj: T); + + spec freeze_object_impl { + pragma opaque; + // aborts if shared object: + // - it's OK to freeze whether object is fresh or owned + // - immutable object cannot be passed by value + aborts_if [abstract] sui::prover::shared(obj); + modifies [abstract] global(object::id(obj).bytes); + ensures [abstract] exists(object::id(obj).bytes); + ensures [abstract] global(object::id(obj).bytes).status == prover::IMMUTABLE; + } + + public(friend) native fun share_object_impl(obj: T); + + spec share_object_impl { + pragma opaque; + aborts_if [abstract] sui::prover::owned(obj); + modifies [abstract] global(object::id(obj).bytes); + ensures [abstract] exists(object::id(obj).bytes); + ensures [abstract] global(object::id(obj).bytes).status == prover::SHARED; + } + + + public(friend) native fun transfer_impl(obj: T, recipient: address); + + spec transfer_impl { + pragma opaque; + // aborts if shared object: + // - it's OK to transfer whether object is fresh or already owned + // - immutable object cannot be passed by value + aborts_if [abstract] sui::prover::shared(obj); + modifies [abstract] global(object::id(obj).bytes); + ensures [abstract] exists(object::id(obj).bytes); + ensures [abstract] global(object::id(obj).bytes).owner == recipient; + ensures [abstract] global(object::id(obj).bytes).status == prover::OWNED; + } + + native fun receive_impl(parent: address, to_receive: object::ID, version: u64): T; + + spec receive_impl { + pragma opaque; + // TODO: stub to be replaced by actual abort conditions if any + aborts_if [abstract] true; + // TODO: specify actual function behavior + } +} diff --git a/lang/move-on-sui/test/ok/tx_context.move b/lang/move-on-sui/test/ok/tx_context.move new file mode 100644 index 00000000..9f67ffee --- /dev/null +++ b/lang/move-on-sui/test/ok/tx_context.move @@ -0,0 +1,152 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module sui::tx_context { + + #[test_only] + use std::vector; + + #[test_only] + /// Number of bytes in an tx hash (which will be the transaction digest) + const TX_HASH_LENGTH: u64 = 32; + + #[test_only] + /// Expected an tx hash of length 32, but found a different length + const EBadTxHashLength: u64 = 0; + + #[test_only] + /// Attempt to get the most recent created object ID when none has been created. + const ENoIDsCreated: u64 = 1; + + /// Information about the transaction currently being executed. + /// This cannot be constructed by a transaction--it is a privileged object created by + /// the VM and passed in to the entrypoint of the transaction as `&mut TxContext`. + struct TxContext has drop { + /// The address of the user that signed the current transaction + sender: address, + /// Hash of the current transaction + tx_hash: vector, + /// The current epoch number + epoch: u64, + /// Timestamp that the epoch started at + epoch_timestamp_ms: u64, + /// Counter recording the number of fresh id's created while executing + /// this transaction. Always 0 at the start of a transaction + ids_created: u64 + } + + /// Return the address of the user that signed the current + /// transaction + public fun sender(self: &TxContext): address { + self.sender + } + + /// Return the transaction digest (hash of transaction inputs). + /// Please do not use as a source of randomness. + public fun digest(self: &TxContext): &vector { + &self.tx_hash + } + + /// Return the current epoch + public fun epoch(self: &TxContext): u64 { + self.epoch + } + + /// Return the epoch start time as a unix timestamp in milliseconds. + public fun epoch_timestamp_ms(self: &TxContext): u64 { + self.epoch_timestamp_ms + } + + /// Create an `address` that has not been used. As it is an object address, it will never + /// occur as the address for a user. + /// In other words, the generated address is a globally unique object ID. + public fun fresh_object_address(ctx: &mut TxContext): address { + let ids_created = ctx.ids_created; + let id = derive_id(*&ctx.tx_hash, ids_created); + ctx.ids_created = ids_created + 1; + id + } + + #[allow(unused_function)] + /// Return the number of id's created by the current transaction. + /// Hidden for now, but may expose later + fun ids_created(self: &TxContext): u64 { + self.ids_created + } + + /// Native function for deriving an ID via hash(tx_hash || ids_created) + native fun derive_id(tx_hash: vector, ids_created: u64): address; + + spec derive_id { + pragma opaque; + // this function never aborts + aborts_if [abstract] false; + // TODO: specify actual function behavior + } + + // ==== test-only functions ==== + + #[test_only] + /// Create a `TxContext` for testing + public fun new( + sender: address, + tx_hash: vector, + epoch: u64, + epoch_timestamp_ms: u64, + ids_created: u64, + ): TxContext { + assert!(vector::length(&tx_hash) == TX_HASH_LENGTH, EBadTxHashLength); + TxContext { sender, tx_hash, epoch, epoch_timestamp_ms, ids_created } + } + + #[test_only] + /// Create a `TxContext` for testing, with a potentially non-zero epoch number. + public fun new_from_hint( + addr: address, + hint: u64, + epoch: u64, + epoch_timestamp_ms: u64, + ids_created: u64, + ): TxContext { + new(addr, dummy_tx_hash_with_hint(hint), epoch, epoch_timestamp_ms, ids_created) + } + + #[test_only] + /// Create a dummy `TxContext` for testing + public fun dummy(): TxContext { + let tx_hash = x"3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532"; + new(@0x0, tx_hash, 0, 0, 0) + } + + #[test_only] + /// Utility for creating 256 unique input hashes. + /// These hashes are guaranteed to be unique given a unique `hint: u64` + fun dummy_tx_hash_with_hint(hint: u64): vector { + let tx_hash = std::bcs::to_bytes(&hint); + while (vector::length(&tx_hash) < TX_HASH_LENGTH) vector::push_back(&mut tx_hash, 0); + tx_hash + } + + #[test_only] + public fun get_ids_created(self: &TxContext): u64 { + ids_created(self) + } + + #[test_only] + /// Return the most recent created object ID. + public fun last_created_object_id(self: &TxContext): address { + let ids_created = self.ids_created; + assert!(ids_created > 0, ENoIDsCreated); + derive_id(*&self.tx_hash, ids_created - 1) + } + + #[test_only] + public fun increment_epoch_number(self: &mut TxContext) { + self.epoch = self.epoch + 1 + } + + #[test_only] + public fun increment_epoch_timestamp(self: &mut TxContext, delta_ms: u64) { + self.epoch_timestamp_ms = self.epoch_timestamp_ms + delta_ms + } +} diff --git a/lang/move-on-sui/test/ok/type_name.move b/lang/move-on-sui/test/ok/type_name.move new file mode 100644 index 00000000..df48dfe1 --- /dev/null +++ b/lang/move-on-sui/test/ok/type_name.move @@ -0,0 +1,92 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Functionality for converting Move types into values. Use with care! +module std::type_name { + use std::ascii::{Self, String}; + use std::address; + use std::vector; + + /// ASCII Character code for the `:` (colon) symbol. + const ASCII_COLON: u8 = 58; + + struct TypeName has copy, drop, store { + /// String representation of the type. All types are represented + /// using their source syntax: + /// "u8", "u64", "u128", "bool", "address", "vector", "signer" for ground types. + /// Struct types are represented as fully qualified type names; e.g. + /// `00000000000000000000000000000001::string::String` or + /// `0000000000000000000000000000000a::module_name1::type_name1<0000000000000000000000000000000a::module_name2::type_name2>` + /// Addresses are hex-encoded lowercase values of length ADDRESS_LENGTH (16, 20, or 32 depending on the Move platform) + name: String + } + + /// Return a value representation of the type `T`. Package IDs + /// that appear in fully qualified type names in the output from + /// this function are defining IDs (the ID of the package in + /// storage that first introduced the type). + public native fun get(): TypeName; + spec get { + pragma opaque; + } + + /// Return a value representation of the type `T`. Package IDs + /// that appear in fully qualified type names in the output from + /// this function are original IDs (the ID of the first version of + /// the package, even if the type in question was introduced in a + /// later upgrade). + public native fun get_with_original_ids(): TypeName; + spec get_with_original_ids { + pragma opaque; + } + + /// Get the String representation of `self` + public fun borrow_string(self: &TypeName): &String { + &self.name + } + + /// Get Address string (Base16 encoded), first part of the TypeName. + public fun get_address(self: &TypeName): String { + // Base16 (string) representation of an address has 2 symbols per byte. + let len = address::length() * 2; + let str_bytes = ascii::as_bytes(&self.name); + let addr_bytes = vector[]; + let i = 0; + + // Read `len` bytes from the type name and push them to addr_bytes. + while (i < len) { + vector::push_back( + &mut addr_bytes, + *vector::borrow(str_bytes, i) + ); + i = i + 1; + }; + + ascii::string(addr_bytes) + } + + /// Get name of the module. + public fun get_module(self: &TypeName): String { + // Starts after address and a double colon: `::` + let i = address::length() * 2 + 2; + let str_bytes = ascii::as_bytes(&self.name); + let module_name = vector[]; + + loop { + let char = vector::borrow(str_bytes, i); + if (char != &ASCII_COLON) { + vector::push_back(&mut module_name, *char); + i = i + 1; + } else { + break + } + }; + + ascii::string(module_name) + } + + /// Convert `self` into its inner String + public fun into_string(self: TypeName): String { + self.name + } +} diff --git a/lang/move-on-sui/test/ok/types.move b/lang/move-on-sui/test/ok/types.move new file mode 100644 index 00000000..ca844125 --- /dev/null +++ b/lang/move-on-sui/test/ok/types.move @@ -0,0 +1,18 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Sui types helpers and utilities +module sui::types { + // === one-time witness === + + /// Tests if the argument type is a one-time witness, that is a type with only one instantiation + /// across the entire code base. + public native fun is_one_time_witness(_: &T): bool; + + spec is_one_time_witness { + pragma opaque; + // TODO: stub to be replaced by actual abort conditions if any + aborts_if [abstract] true; + // TODO: specify actual function behavior + } +} diff --git a/lang/move-on-sui/test/ok/unit_test.move b/lang/move-on-sui/test/ok/unit_test.move new file mode 100644 index 00000000..818f059c --- /dev/null +++ b/lang/move-on-sui/test/ok/unit_test.move @@ -0,0 +1,15 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +/// Module providing testing functionality. Only included for tests. +module std::unit_test { + /// Return a `num_signers` number of unique signer values. No ordering or + /// starting value guarantees are made, only that the order and values of + /// the signers in the returned vector is deterministic. + /// + /// This function is also used to poison modules compiled in `test` mode. + /// This will cause a linking failure if an attempt is made to publish a + /// test module in a VM that isn't in unit test mode. + native public fun create_signers_for_testing(num_signers: u64): vector; +} diff --git a/lang/move-on-sui/test/ok/url.move b/lang/move-on-sui/test/ok/url.move new file mode 100644 index 00000000..b40ce91a --- /dev/null +++ b/lang/move-on-sui/test/ok/url.move @@ -0,0 +1,35 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// URL: standard Uniform Resource Locator string +module sui::url { + use std::ascii::{Self, String}; + + /// Standard Uniform Resource Locator (URL) string. + struct Url has store, copy, drop { + // TODO: validate URL format + url: String, + } + + /// Create a `Url`, with no validation + public fun new_unsafe(url: String): Url { + Url { url } + } + + /// Create a `Url` with no validation from bytes + /// Note: this will abort if `bytes` is not valid ASCII + public fun new_unsafe_from_bytes(bytes: vector): Url { + let url = ascii::string(bytes); + Url { url } + } + + /// Get inner URL + public fun inner_url(self: &Url): String{ + self.url + } + + /// Update the inner URL + public fun update(self: &mut Url, url: String) { + self.url = url; + } +} diff --git a/lang/move-on-sui/test/ok/vec_map.move b/lang/move-on-sui/test/ok/vec_map.move new file mode 100644 index 00000000..60ad2b06 --- /dev/null +++ b/lang/move-on-sui/test/ok/vec_map.move @@ -0,0 +1,195 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module sui::vec_map { + use std::option::{Self, Option}; + use std::vector; + + /// This key already exists in the map + const EKeyAlreadyExists: u64 = 0; + + /// This key does not exist in the map + const EKeyDoesNotExist: u64 = 1; + + /// Trying to destroy a map that is not empty + const EMapNotEmpty: u64 = 2; + + /// Trying to access an element of the map at an invalid index + const EIndexOutOfBounds: u64 = 3; + + /// Trying to pop from a map that is empty + const EMapEmpty: u64 = 4; + + /// A map data structure backed by a vector. The map is guaranteed not to contain duplicate keys, but entries + /// are *not* sorted by key--entries are included in insertion order. + /// All operations are O(N) in the size of the map--the intention of this data structure is only to provide + /// the convenience of programming against a map API. + /// Large maps should use handwritten parent/child relationships instead. + /// Maps that need sorted iteration rather than insertion order iteration should also be handwritten. + struct VecMap has copy, drop, store { + contents: vector>, + } + + /// An entry in the map + struct Entry has copy, drop, store { + key: K, + value: V, + } + + /// Create an empty `VecMap` + public fun empty(): VecMap { + VecMap { contents: vector::empty() } + } + + /// Insert the entry `key` |-> `value` into `self`. + /// Aborts if `key` is already bound in `self`. + public fun insert(self: &mut VecMap, key: K, value: V) { + assert!(!contains(self, &key), EKeyAlreadyExists); + vector::push_back(&mut self.contents, Entry { key, value }) + } + + /// Remove the entry `key` |-> `value` from self. Aborts if `key` is not bound in `self`. + public fun remove(self: &mut VecMap, key: &K): (K, V) { + let idx = get_idx(self, key); + let Entry { key, value } = vector::remove(&mut self.contents, idx); + (key, value) + } + + /// Pop the most recently inserted entry from the map. Aborts if the map is empty. + public fun pop(self: &mut VecMap): (K, V) { + assert!(!vector::is_empty(&self.contents), EMapEmpty); + let Entry { key, value } = vector::pop_back(&mut self.contents); + (key, value) + } + + /// Get a mutable reference to the value bound to `key` in `self`. + /// Aborts if `key` is not bound in `self`. + public fun get_mut(self: &mut VecMap, key: &K): &mut V { + let idx = get_idx(self, key); + let entry = vector::borrow_mut(&mut self.contents, idx); + &mut entry.value + } + + /// Get a reference to the value bound to `key` in `self`. + /// Aborts if `key` is not bound in `self`. + public fun get(self: &VecMap, key: &K): &V { + let idx = get_idx(self, key); + let entry = vector::borrow(&self.contents, idx); + &entry.value + } + + /// Safely try borrow a value bound to `key` in `self`. + /// Return Some(V) if the value exists, None otherwise. + /// Only works for a "copyable" value as references cannot be stored in `vector`. + public fun try_get(self: &VecMap, key: &K): Option { + if (contains(self, key)) { + option::some(*get(self, key)) + } else { + option::none() + } + } + + /// Return true if `self` contains an entry for `key`, false otherwise + public fun contains(self: &VecMap, key: &K): bool { + option::is_some(&get_idx_opt(self, key)) + } + + /// Return the number of entries in `self` + public fun size(self: &VecMap): u64 { + vector::length(&self.contents) + } + + /// Return true if `self` has 0 elements, false otherwise + public fun is_empty(self: &VecMap): bool { + size(self) == 0 + } + + /// Destroy an empty map. Aborts if `self` is not empty + public fun destroy_empty(self: VecMap) { + let VecMap { contents } = self; + assert!(vector::is_empty(&contents), EMapNotEmpty); + vector::destroy_empty(contents) + } + + /// Unpack `self` into vectors of its keys and values. + /// The output keys and values are stored in insertion order, *not* sorted by key. + public fun into_keys_values(self: VecMap): (vector, vector) { + let VecMap { contents } = self; + // reverse the vector so the output keys and values will appear in insertion order + vector::reverse(&mut contents); + let i = 0; + let n = vector::length(&contents); + let keys = vector::empty(); + let values = vector::empty(); + while (i < n) { + let Entry { key, value } = vector::pop_back(&mut contents); + vector::push_back(&mut keys, key); + vector::push_back(&mut values, value); + i = i + 1; + }; + vector::destroy_empty(contents); + (keys, values) + } + + /// Returns a list of keys in the map. + /// Do not assume any particular ordering. + public fun keys(self: &VecMap): vector { + let i = 0; + let n = vector::length(&self.contents); + let keys = vector::empty(); + while (i < n) { + let entry = vector::borrow(&self.contents, i); + vector::push_back(&mut keys, entry.key); + i = i + 1; + }; + keys + } + + /// Find the index of `key` in `self`. Return `None` if `key` is not in `self`. + /// Note that map entries are stored in insertion order, *not* sorted by key. + public fun get_idx_opt(self: &VecMap, key: &K): Option { + let i = 0; + let n = size(self); + while (i < n) { + if (&vector::borrow(&self.contents, i).key == key) { + return option::some(i) + }; + i = i + 1; + }; + option::none() + } + + /// Find the index of `key` in `self`. Aborts if `key` is not in `self`. + /// Note that map entries are stored in insertion order, *not* sorted by key. + public fun get_idx(self: &VecMap, key: &K): u64 { + let idx_opt = get_idx_opt(self, key); + assert!(option::is_some(&idx_opt), EKeyDoesNotExist); + option::destroy_some(idx_opt) + } + + /// Return a reference to the `idx`th entry of `self`. This gives direct access into the backing array of the map--use with caution. + /// Note that map entries are stored in insertion order, *not* sorted by key. + /// Aborts if `idx` is greater than or equal to `size(self)` + public fun get_entry_by_idx(self: &VecMap, idx: u64): (&K, &V) { + assert!(idx < size(self), EIndexOutOfBounds); + let entry = vector::borrow(&self.contents, idx); + (&entry.key, &entry.value) + } + + /// Return a mutable reference to the `idx`th entry of `self`. This gives direct access into the backing array of the map--use with caution. + /// Note that map entries are stored in insertion order, *not* sorted by key. + /// Aborts if `idx` is greater than or equal to `size(self)` + public fun get_entry_by_idx_mut(self: &mut VecMap, idx: u64): (&K, &mut V) { + assert!(idx < size(self), EIndexOutOfBounds); + let entry = vector::borrow_mut(&mut self.contents, idx); + (&entry.key, &mut entry.value) + } + + /// Remove the entry at index `idx` from self. + /// Aborts if `idx` is greater than or equal to `size(self)` + public fun remove_entry_by_idx(self: &mut VecMap, idx: u64): (K, V) { + assert!(idx < size(self), EIndexOutOfBounds); + let Entry { key, value } = vector::remove(&mut self.contents, idx); + (key, value) + } +} diff --git a/lang/move-on-sui/test/ok/vec_set.move b/lang/move-on-sui/test/ok/vec_set.move new file mode 100644 index 00000000..7f491967 --- /dev/null +++ b/lang/move-on-sui/test/ok/vec_set.move @@ -0,0 +1,98 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module sui::vec_set { + use std::option::{Self, Option}; + use std::vector; + + /// This key already exists in the map + const EKeyAlreadyExists: u64 = 0; + + /// This key does not exist in the map + const EKeyDoesNotExist: u64 = 1; + + /// A set data structure backed by a vector. The set is guaranteed not to + /// contain duplicate keys. All operations are O(N) in the size of the set + /// - the intention of this data structure is only to provide the convenience + /// of programming against a set API. Sets that need sorted iteration rather + /// than insertion order iteration should be handwritten. + struct VecSet has copy, drop, store { + contents: vector, + } + + /// Create an empty `VecSet` + public fun empty(): VecSet { + VecSet { contents: vector::empty() } + } + + /// Create a singleton `VecSet` that only contains one element. + public fun singleton(key: K): VecSet { + VecSet { contents: vector::singleton(key) } + } + + /// Insert a `key` into self. + /// Aborts if `key` is already present in `self`. + public fun insert(self: &mut VecSet, key: K) { + assert!(!contains(self, &key), EKeyAlreadyExists); + vector::push_back(&mut self.contents, key) + } + + /// Remove the entry `key` from self. Aborts if `key` is not present in `self`. + public fun remove(self: &mut VecSet, key: &K) { + let idx = get_idx(self, key); + vector::remove(&mut self.contents, idx); + } + + /// Return true if `self` contains an entry for `key`, false otherwise + public fun contains(self: &VecSet, key: &K): bool { + option::is_some(&get_idx_opt(self, key)) + } + + /// Return the number of entries in `self` + public fun size(self: &VecSet): u64 { + vector::length(&self.contents) + } + + /// Return true if `self` has 0 elements, false otherwise + public fun is_empty(self: &VecSet): bool { + size(self) == 0 + } + + /// Unpack `self` into vectors of keys. + /// The output keys are stored in insertion order, *not* sorted. + public fun into_keys(self: VecSet): vector { + let VecSet { contents } = self; + contents + } + + /// Borrow the `contents` of the `VecSet` to access content by index + /// without unpacking. The contents are stored in insertion order, + /// *not* sorted. + public fun keys(self: &VecSet): &vector { + &self.contents + } + + // == Helper functions == + + /// Find the index of `key` in `self`. Return `None` if `key` is not in `self`. + /// Note that keys are stored in insertion order, *not* sorted. + fun get_idx_opt(self: &VecSet, key: &K): Option { + let i = 0; + let n = size(self); + while (i < n) { + if (vector::borrow(&self.contents, i) == key) { + return option::some(i) + }; + i = i + 1; + }; + option::none() + } + + /// Find the index of `key` in `self`. Aborts if `key` is not in `self`. + /// Note that map entries are stored in insertion order, *not* sorted. + fun get_idx(self: &VecSet, key: &K): u64 { + let idx_opt = get_idx_opt(self, key); + assert!(option::is_some(&idx_opt), EKeyDoesNotExist); + option::destroy_some(idx_opt) + } +} diff --git a/lang/move-on-sui/test/ok/vector.move b/lang/move-on-sui/test/ok/vector.move new file mode 100644 index 00000000..50641448 --- /dev/null +++ b/lang/move-on-sui/test/ok/vector.move @@ -0,0 +1,220 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// A variable-sized container that can hold any type. Indexing is 0-based, and +/// vectors are growable. This module has many native functions. +/// Verification of modules that use this one uses model functions that are implemented +/// directly in Boogie. The specification language has built-in functions operations such +/// as `singleton_vector`. There are some helper functions defined here for specifications in other +/// modules as well. +/// +/// >Note: We did not verify most of the +/// Move functions here because many have loops, requiring loop invariants to prove, and +/// the return on investment didn't seem worth it for these simple functions. +module std::vector { + + /// The index into the vector is out of bounds + const EINDEX_OUT_OF_BOUNDS: u64 = 0x20000; + + #[bytecode_instruction] + /// Create an empty vector. + native public fun empty(): vector; + + #[bytecode_instruction] + /// Return the length of the vector. + native public fun length(v: &vector): u64; + + #[bytecode_instruction] + /// Acquire an immutable reference to the `i`th element of the vector `v`. + /// Aborts if `i` is out of bounds. + native public fun borrow(v: &vector, i: u64): ∈ + + #[bytecode_instruction] + /// Add element `e` to the end of the vector `v`. + native public fun push_back(v: &mut vector, e: Element); + + #[bytecode_instruction] + /// Return a mutable reference to the `i`th element in the vector `v`. + /// Aborts if `i` is out of bounds. + native public fun borrow_mut(v: &mut vector, i: u64): &mut Element; + + #[bytecode_instruction] + /// Pop an element from the end of vector `v`. + /// Aborts if `v` is empty. + native public fun pop_back(v: &mut vector): Element; + + #[bytecode_instruction] + /// Destroy the vector `v`. + /// Aborts if `v` is not empty. + native public fun destroy_empty(v: vector); + + #[bytecode_instruction] + /// Swaps the elements at the `i`th and `j`th indices in the vector `v`. + /// Aborts if `i` or `j` is out of bounds. + native public fun swap(v: &mut vector, i: u64, j: u64); + + /// Return an vector of size one containing element `e`. + public fun singleton(e: Element): vector { + let v = empty(); + push_back(&mut v, e); + v + } + spec singleton { + // TODO: when using opaque here, we get verification errors. + // pragma opaque; + aborts_if false; + ensures result == vec(e); + } + + /// Reverses the order of the elements in the vector `v` in place. + public fun reverse(v: &mut vector) { + let len = length(v); + if (len == 0) return (); + + let front_index = 0; + let back_index = len -1; + while (front_index < back_index) { + swap(v, front_index, back_index); + front_index = front_index + 1; + back_index = back_index - 1; + } + } + spec reverse { + pragma intrinsic = true; + } + + + /// Pushes all of the elements of the `other` vector into the `lhs` vector. + public fun append(lhs: &mut vector, other: vector) { + reverse(&mut other); + while (!is_empty(&other)) push_back(lhs, pop_back(&mut other)); + destroy_empty(other); + } + spec append { + pragma intrinsic = true; + } + spec is_empty { + pragma intrinsic = true; + } + + + /// Return `true` if the vector `v` has no elements and `false` otherwise. + public fun is_empty(v: &vector): bool { + length(v) == 0 + } + + /// Return true if `e` is in the vector `v`. + /// Otherwise, returns false. + public fun contains(v: &vector, e: &Element): bool { + let i = 0; + let len = length(v); + while (i < len) { + if (borrow(v, i) == e) return true; + i = i + 1; + }; + false + } + spec contains { + pragma intrinsic = true; + } + + /// Return `(true, i)` if `e` is in the vector `v` at index `i`. + /// Otherwise, returns `(false, 0)`. + public fun index_of(v: &vector, e: &Element): (bool, u64) { + let i = 0; + let len = length(v); + while (i < len) { + if (borrow(v, i) == e) return (true, i); + i = i + 1; + }; + (false, 0) + } + spec index_of { + pragma intrinsic = true; + } + + /// Remove the `i`th element of the vector `v`, shifting all subsequent elements. + /// This is O(n) and preserves ordering of elements in the vector. + /// Aborts if `i` is out of bounds. + public fun remove(v: &mut vector, i: u64): Element { + let len = length(v); + // i out of bounds; abort + if (i >= len) abort EINDEX_OUT_OF_BOUNDS; + + len = len - 1; + while (i < len) swap(v, i, { i = i + 1; i }); + pop_back(v) + } + spec remove { + pragma intrinsic = true; + } + + /// Insert `e` at position `i` in the vector `v`. + /// If `i` is in bounds, this shifts the old `v[i]` and all subsequent elements to the right. + /// If `i == length(v)`, this adds `e` to the end of the vector. + /// This is O(n) and preserves ordering of elements in the vector. + /// Aborts if `i > length(v)` + public fun insert(v: &mut vector, e: Element, i: u64) { + let len = length(v); + // i too big abort + if (i > len) abort EINDEX_OUT_OF_BOUNDS; + + push_back(v, e); + while (i < len) { + swap(v, i, len); + i = i + 1 + } + } + spec insert { + pragma intrinsic = true; + } + + /// Swap the `i`th element of the vector `v` with the last element and then pop the vector. + /// This is O(1), but does not preserve ordering of elements in the vector. + /// Aborts if `i` is out of bounds. + public fun swap_remove(v: &mut vector, i: u64): Element { + assert!(!is_empty(v), EINDEX_OUT_OF_BOUNDS); + let last_idx = length(v) - 1; + swap(v, i, last_idx); + pop_back(v) + } + spec swap_remove { + pragma intrinsic = true; + } + + // ================================================================= + // Module Specification + + spec module {} // Switch to module documentation context + + /// # Helper Functions + + spec module { + /// Check if `v1` is equal to the result of adding `e` at the end of `v2` + fun eq_push_back(v1: vector, v2: vector, e: Element): bool { + len(v1) == len(v2) + 1 && + v1[len(v1)-1] == e && + v1[0..len(v1)-1] == v2[0..len(v2)] + } + + /// Check if `v` is equal to the result of concatenating `v1` and `v2` + fun eq_append(v: vector, v1: vector, v2: vector): bool { + len(v) == len(v1) + len(v2) && + v[0..len(v1)] == v1 && + v[len(v1)..len(v)] == v2 + } + + /// Check `v1` is equal to the result of removing the first element of `v2` + fun eq_pop_front(v1: vector, v2: vector): bool { + len(v1) + 1 == len(v2) && + v1 == v2[1..len(v2)] + } + + /// Check that `v1` is equal to the result of removing the element at index `i` from `v2`. + fun eq_remove_elem_at_index(i: u64, v1: vector, v2: vector): bool { + len(v1) + 1 == len(v2) && + v1[0..i] == v2[0..i] && + v1[i..len(v1)] == v2[i + 1..len(v2)] + } + } +} diff --git a/lang/move-on-sui/test/ok/versioned.move b/lang/move-on-sui/test/ok/versioned.move new file mode 100644 index 00000000..b906ae81 --- /dev/null +++ b/lang/move-on-sui/test/ok/versioned.move @@ -0,0 +1,86 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module sui::versioned { + use sui::object::{UID, ID}; + use sui::tx_context::TxContext; + use sui::object; + use sui::dynamic_field; + + /// Failed to upgrade the inner object due to invalid capability or new version. + const EInvalidUpgrade: u64 = 0; + + /// A wrapper type that supports versioning of the inner type. + /// The inner type is a dynamic field of the Versioned object, and is keyed using version. + /// User of this type could load the inner object using corresponding type based on the version. + /// You can also upgrade the inner object to a new type version. + /// If you want to support lazy upgrade of the inner type, one caveat is that all APIs would have + /// to use mutable reference even if it's a read-only API. + struct Versioned has key, store { + id: UID, + version: u64, + } + + /// Represents a hot potato object generated when we take out the dynamic field. + /// This is to make sure that we always put a new value back. + struct VersionChangeCap { + versioned_id: ID, + old_version: u64, + } + + /// Create a new Versioned object that contains a initial value of type `T` with an initial version. + public fun create(init_version: u64, init_value: T, ctx: &mut TxContext): Versioned { + let self = Versioned { + id: object::new(ctx), + version: init_version, + }; + dynamic_field::add(&mut self.id, init_version, init_value); + self + } + + /// Get the current version of the inner type. + public fun version(self: &Versioned): u64 { + self.version + } + + /// Load the inner value based on the current version. Caller specifies an expected type T. + /// If the type mismatch, the load will fail. + public fun load_value(self: &Versioned): &T { + dynamic_field::borrow(&self.id, self.version) + } + + /// Similar to load_value, but return a mutable reference. + public fun load_value_mut(self: &mut Versioned): &mut T { + dynamic_field::borrow_mut(&mut self.id, self.version) + } + + /// Take the inner object out for upgrade. To ensure we always upgrade properly, a capability object is returned + /// and must be used when we upgrade. + public fun remove_value_for_upgrade(self: &mut Versioned): (T, VersionChangeCap) { + ( + dynamic_field::remove(&mut self.id, self.version), + VersionChangeCap { + versioned_id: object::id(self), + old_version: self.version, + } + ) + } + + /// Upgrade the inner object with a new version and new value. Must use the capability returned + /// by calling remove_value_for_upgrade. + public fun upgrade(self: &mut Versioned, new_version: u64, new_value: T, cap: VersionChangeCap) { + let VersionChangeCap { versioned_id, old_version } = cap; + assert!(versioned_id == object::id(self), EInvalidUpgrade); + assert!(old_version < new_version, EInvalidUpgrade); + dynamic_field::add(&mut self.id, new_version, new_value); + self.version = new_version; + } + + /// Destroy this Versioned container, and return the inner object. + public fun destroy(self: Versioned): T { + let Versioned { id, version } = self; + let ret = dynamic_field::remove(&mut id, version); + object::delete(id); + ret + } +} diff --git a/lang/semgrep-grammars/src/semgrep-move-on-sui/grammar.js b/lang/semgrep-grammars/src/semgrep-move-on-sui/grammar.js index bbb6564b..709a6d53 100644 --- a/lang/semgrep-grammars/src/semgrep-move-on-sui/grammar.js +++ b/lang/semgrep-grammars/src/semgrep-move-on-sui/grammar.js @@ -5,11 +5,38 @@ */ const base_grammar = require('tree-sitter-move-on-sui/grammar'); +const _UNNARY_PREC = 10; +const UNARY_PREC = 13; +const FIELD_PREC = 14; +const CALL_PREC = 15; + module.exports = grammar(base_grammar, { name: 'move_on_sui', - + conflicts: ($, previous) => previous.concat([ + [$.typed_metavariable, $.module_access], + [$.module_body, $.block], + [$.module_body ,$._expression_term], + [$.module_body], + [$._expression_term], + [$.semgrep_statement,$._expression_term], + [$.typed_metavariable, $.annotation_expression], + ]), + + precedences: ($, previous) => previous.concat([ + [$.block_item, $.block, $.module_body, $.module_access], + [$._expression_term, $.module_access], + [$._bind, $.module_access], + [$.block_identifier ,$.break_expression], + [$.block_item, $._expression, $.module_access], + [$.annotation_item, $.module_access], + [$.module_access, $.field_access_ellipsis_expr], + [$.bind_unpack, $.name_expression], + [$.type_arguments, $._type, $.module_access], + [$.annotation_expression, $.typed_metavariable], + [$._semgrep_metavar_ellipsis, $.typed_metavariable], + ]), /* @@ -17,13 +44,249 @@ module.exports = grammar(base_grammar, { if they're not already part of the base grammar. */ rules: { - /* - semgrep_ellipsis: $ => '...', + // Semgrep components, source: semgrep-rust + ellipsis: $ => '...', + deep_ellipsis: $ => seq('<...',$._expression, '...>'), + + + // Typed metavariable (an expression, not a parameter) + // This is grammatically indistinguishable from `$.type_hint_expr: $ => seq('(', $._expr, ':', $.type, ')')`. + // This will be handled by the semgrep converter by checking the metavariable name (`$`). + + + typed_metavariable: $ => seq('(', $.identifier, ':', $._type, ')'), + + _clean_identifier: $ => /(`)?[a-zA-Z_][0-9a-zA-Z_]*(`)?/, + + _macro_identifier_dollar : $ => /\$[a-zA-Z][0-9a-zA-Z_]*/, + _semgrep_metavar_ellipsis: $ => /\$\.\.\.[A-Z_][A-Z_0-9]*/, + _semgrep_metavar_var: $ => /\$[A-Z_][A-Z_0-9]*/, + + + + // Alternate "entry point". Allows parsing a standalone expression. + semgrep_expression: $ => choice( + $._expression, + $.let_statement, + ), + // Alternate "entry point". Allows parsing a standalone list of sequence items (statements). + semgrep_statement: $ => repeat1(choice( + $.block_item, + $.use_declaration, + $.friend_declaration, + $.constant, + $._function_item, + $._struct_item, + $._enum_item, + $.spec_block, + $.module_body + )), + + // Alternate "entry point". Allows parsing partial declarations (signatures). + semgrep_partial: $ => seq( + choice( + $._function_signature, + $._struct_signature, + $._enum_signature, + ) + ), + + // Extend the source_file rule to allow semgrep constructs + source_file: ($, previous) => choice( + previous, + $.semgrep_expression, + $.semgrep_statement, + $.semgrep_partial, + ), + + _identifier_or_metavariable: ($, previous) => choice( + choice( + $._macro_identifier_dollar, + $._semgrep_metavar_ellipsis, + $._semgrep_metavar_var, + '_', + ), + seq( + optional('phantom'), + $._clean_identifier, + ) + ), + + // Module declaration + module_body: ($, previous) => choice( + previous, + $.ellipsis, + ), + + module_access: ($, previous) => choice( + // macro variable access + field('member', alias(prec.right(choice( + $._semgrep_metavar_ellipsis, + $._macro_identifier_dollar, + prec(-1,$.identifier), + )), $.identifier) + ), + // address access + seq('@', field('member', $.identifier)), + field('member', alias($._reserved_identifier, $.identifier)), + seq( + field('module', $._module_identifier), + '::', + field('member', $.identifier) + ), + seq( + $.module_identity, + '::', + field('member', $.identifier) + ), + seq($.module_identity, '::', field('enum_name', $.identifier), '::', field('variant', $.identifier)), + $.ellipsis, + ), + + // Spec block members + _spec_block_memeber: ($, previous) => choice( + previous, + $.ellipsis, + ), + + // statement (block item) + block_item: ($, previous) => choice( + previous, + $.ellipsis, + ), + + // struct field annotations + field_annotation: ($, previous) => choice( + previous, + $.ellipsis, + ), + + // struct field bindings + // (e.g. `let T { field_1, ... } = 0;`) + bind_field: ($, previous) => choice( + previous, + $.ellipsis, + ), + + // struct binding + // (e.g. `let T { field_1, var: ..., } = obj;`) + // (e.g. `let ... = obj;`) + _bind: ($, previous) => choice( + ...previous.members, + $.ellipsis, + ), + + // (e.g. `#[..., attr(...)]`) + annotation_item: ($, previous) => choice( + previous, + $.ellipsis + ), + + // use member + // (e.g. `use module_ident::...;`, `use module_ident::{..., item_ident}`) + use_member: ($, previous) => choice( + previous, + $.ellipsis, + ), + + // expression term + _expression_term: ($, previous) => choice( + previous, + $.ellipsis, + $.deep_ellipsis, + $.typed_metavariable, + ), + + // expression _expression: ($, previous) => choice( - $.semgrep_ellipsis, - ...previous.members + previous, + $.ellipsis, + $.deep_ellipsis, ), - */ + + // unary expression + _unary_expression: ($, previous) => choice( + previous, + prec(UNARY_PREC, $.ellipsis), + prec(UNARY_PREC, $.deep_ellipsis), + prec(UNARY_PREC, $.field_access_ellipsis_expr), + ), + _dot_or_index_chain: ($, previous) => choice( + previous, + $.field_access_ellipsis_expr + ), + + // function parameter + // (e.g. `call( ..., arg, ...)`) + function_parameter: ($, previous) => choice( + seq( + optional('mut'), + field('name', alias($._identifier_or_metavariable, $.variable_identifier)), + ':', + field('type', $._type), + ), + $.ellipsis, + ), + + // type + // (e.g. `call( ..., arg, ...)`) + _type: ($, previous) => choice( + previous, + $.ellipsis, + ), + + // abilities + // (e.g. struct XXX has ..., YYY) + ability: ($, previous) => choice( + previous, + $.ellipsis, + ), + + // type parameter + // (e.g. `Type<..., T>`) + type_parameter: ($, previous) => choice( + seq( + alias($._identifier_or_metavariable, $.type_parameter_identifier), + optional(seq(':', + sepBy1('+', $.ability) + )), + ), + $.ellipsis, + ), + + + // type arguments + // (e.g. `Type<..., T>`) + type_arguments: ($, previous) => choice( + previous, + $.ellipsis, + seq('<',$.ellipsis, '>'), + ), + // type + // pack field + // (e.g. `Pack { ..., field }`) + exp_field: ($, previous) => choice( + previous, + $.ellipsis, + ), + + newline: ($, previous) => choice(), + + field_access_ellipsis_expr: $ => prec.left(FIELD_PREC, seq( + field('element', $._dot_or_index_chain), '.',$.ellipsis )), + + identifier: $ => /(`)?[a-zA-Z_][0-9a-zA-Z_]*(`)?|\$\.\.\.[A-Z_][A-Z_0-9]*]|\$[A-Z_][A-Z_0-9]*/, + } }); + + +// ( 'sep')* ? +// Note that this allows an optional trailing `sep`. +function sepBy(sep, rule) { + return seq(repeat(seq(rule, sep)), optional(rule)); +} +function sepBy1(sep, rule) { + return seq(rule, repeat(seq(sep, rule)), optional(sep)); +} diff --git a/lang/semgrep-grammars/src/semgrep-move-on-sui/test/corpus/macros.txt b/lang/semgrep-grammars/src/semgrep-move-on-sui/test/corpus/macros.txt new file mode 100644 index 00000000..282bd2a2 --- /dev/null +++ b/lang/semgrep-grammars/src/semgrep-move-on-sui/test/corpus/macros.txt @@ -0,0 +1,265 @@ +======================= +Macro Upper Lower var +======================= + +public macro fun fold<$T, $Acc>( + $v: vector<$T>, + $init: $Acc, + $f: |$Acc, $T| -> $Acc, +): $Acc { + let v = $v; + let mut acc = $init; + v.do!(|e| acc = $f(acc, e)); + acc +} + +--- + + +(source_file + (semgrep_statement + (macro_function_definition + (modifier) + (function_identifier) + (type_parameters + (type_parameter + (type_parameter_identifier)) + (type_parameter + (type_parameter_identifier))) + (function_parameters + (function_parameter + (variable_identifier) + (apply_type + (module_access + (identifier)) + (type_arguments + (apply_type + (module_access + (identifier)))))) + (function_parameter + (variable_identifier) + (apply_type + (module_access + (identifier)))) + (function_parameter + (variable_identifier) + (function_type + (function_type_parameters + (apply_type + (module_access + (identifier))) + (apply_type + (module_access + (identifier)))) + (apply_type + (module_access + (identifier)))))) + (ret_type + (apply_type + (module_access + (identifier)))) + (block + (block_item + (let_statement + (bind_list + (bind_var + (variable_identifier))) + (name_expression + (module_access + (identifier))))) + (block_item + (let_statement + (bind_list + (bind_var + (variable_identifier))) + (name_expression + (module_access + (identifier))))) + (block_item + (receiver_macro_call + (name_expression + (module_access + (identifier))) + (identifier) + (arg_list + (lambda_expression + (lambda_bindings + (bind_var + (variable_identifier))) + (assign_expression + (name_expression + (module_access + (identifier))) + (call_expression + (module_access + (identifier)) + (arg_list + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))))))))) + (name_expression + (module_access + (identifier))))))) + + +======================= +Macro Upper Lower dynamic fields +======================= + +macro fun add_impl<$Name: copy + drop + store, $Value: key>( + // we use &mut UID in several spots for access control + $object: &mut UID, + $name: $Name, + $value: $Value, +) { + let object = $object; + let name = $name; + let value = $value; + let key = Wrapper { name }; + let id = object::id(&value); + field::add(object, key, id); + let (field, _) = field::field_info>(object, key); + add_child_object(field.to_address(), value); +} +--- + + +(source_file + (semgrep_statement + (macro_function_definition + (function_identifier) + (type_parameters + (type_parameter + (type_parameter_identifier) + (ability) + (ability) + (ability)) + (type_parameter + (type_parameter_identifier) + (ability))) + (function_parameters + (line_comment) + (function_parameter + (variable_identifier) + (ref_type + (apply_type + (module_access + (identifier))))) + (function_parameter + (variable_identifier) + (apply_type + (module_access + (identifier)))) + (function_parameter + (variable_identifier) + (apply_type + (module_access + (identifier))))) + (block + (block_item + (let_statement + (bind_list + (bind_var + (variable_identifier))) + (name_expression + (module_access + (identifier))))) + (block_item + (let_statement + (bind_list + (bind_var + (variable_identifier))) + (name_expression + (module_access + (identifier))))) + (block_item + (let_statement + (bind_list + (bind_var + (variable_identifier))) + (name_expression + (module_access + (identifier))))) + (block_item + (let_statement + (bind_list + (bind_var + (variable_identifier))) + (pack_expression + (module_access + (identifier)) + (field_initialize_list + (exp_field + (field_identifier)))))) + (block_item + (let_statement + (bind_list + (bind_var + (variable_identifier))) + (call_expression + (module_access + (module_identifier) + (identifier)) + (arg_list + (borrow_expression + (name_expression + (module_access + (identifier)))))))) + (block_item + (call_expression + (module_access + (module_identifier) + (identifier)) + (arg_list + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))))) + (block_item + (let_statement + (bind_list + (bind_var + (variable_identifier)) + (bind_var + (variable_identifier))) + (call_expression + (module_access + (module_identifier) + (identifier)) + (type_arguments + (apply_type + (module_access + (identifier)) + (type_arguments + (apply_type + (module_access + (identifier)))))) + (arg_list + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier))))))) + (block_item + (call_expression + (module_access + (identifier)) + (arg_list + (receiver_call + (name_expression + (module_access + (identifier))) + (identifier) + (arg_list)) + (name_expression + (module_access + (identifier)))))))))) \ No newline at end of file diff --git a/lang/semgrep-grammars/src/semgrep-move-on-sui/test/corpus/partials_and_decls.txt b/lang/semgrep-grammars/src/semgrep-move-on-sui/test/corpus/partials_and_decls.txt new file mode 100644 index 00000000..7b0a96a0 --- /dev/null +++ b/lang/semgrep-grammars/src/semgrep-move-on-sui/test/corpus/partials_and_decls.txt @@ -0,0 +1,113 @@ +======================= +Function in Statement +======================= + +fun $TEST (...) : ... { ... } + +--- + +(source_file + (semgrep_statement + (function_definition + (function_identifier) + (function_parameters + (function_parameter + (ellipsis))) + (ret_type + (ellipsis)) + (block + (ellipsis))))) + +======================= +Declarations +======================= + +use 0xabcd::ff; +struct $STRUCT has ... { ... } +const $CONST : ... = $FOO(...); +... +fun hello (...) { + let XXX = $STRUCT { ... }; + ... +} + +--- + +(source_file + (semgrep_statement + (use_declaration + (use_module + (module_identity + (num_literal + (untyped_num_literal)) + (module_identifier)))) + (struct_definition + (struct_identifier) + (ability_decls + (ability + (ellipsis))) + (datatype_fields + (named_fields + (field_annotation + (ellipsis))))) + (constant + (constant_identifier) + (ellipsis) + (call_expression + (module_access + (identifier)) + (arg_list + (ellipsis)))) + (block_item + (ellipsis)) + (function_definition + (function_identifier) + (function_parameters + (function_parameter + (ellipsis))) + (block + (block_item + (let_statement + (bind_list + (bind_var + (variable_identifier))) + (pack_expression + (module_access + (identifier)) + (field_initialize_list + (exp_field + (ellipsis)))))) + (ellipsis))))) + + +======================= +Function Signature +======================= + +public(friend) fun $FUNC (...) + +--- + +(source_file + (semgrep_partial + (modifier) + (function_identifier) + (function_parameters + (function_parameter + (ellipsis))))) + +======================= +Struct Signature +======================= + +public native struct $STRUCT has ... + +--- +(source_file + (semgrep_statement + (native_struct_definition + (struct_identifier) + (ability_decls + (ability + (ellipsis))) + (MISSING ";")))) \ No newline at end of file diff --git a/lang/semgrep-grammars/src/semgrep-move-on-sui/test/corpus/semgrep.txt b/lang/semgrep-grammars/src/semgrep-move-on-sui/test/corpus/semgrep.txt index e69de29b..a33da9ef 100644 --- a/lang/semgrep-grammars/src/semgrep-move-on-sui/test/corpus/semgrep.txt +++ b/lang/semgrep-grammars/src/semgrep-move-on-sui/test/corpus/semgrep.txt @@ -0,0 +1,791 @@ +==================== +Semgrep Ellipsis +==================== + +... + +--- + +(source_file + (semgrep_expression + (ellipsis))) +======================== +Semgrep Metavariables +======================== + +call(..., $VAR) + +--- + +(source_file + (semgrep_expression + (call_expression + (module_access + (identifier)) + (arg_list + (ellipsis) + (name_expression + (module_access + (identifier))))))) + +==== +Semgrep Statement +==== + +let _: u64 = if (cond) { ... } else { s2 + <... $...SOME ...> }.f; + +--- + +(source_file + (semgrep_statement + (block_item + (let_statement + (bind_list + (bind_var + (variable_identifier))) + (primitive_type) + (if_expression + (name_expression + (module_access + (identifier))) + (block + (ellipsis)) + (access_field + (block + (binary_expression + (name_expression + (module_access + (identifier))) + (binary_operator) + (deep_ellipsis + (name_expression + (module_access + (identifier)))))) + (name_expression + (module_access + (identifier))))))))) + +========= +struct +========= + +module 0xdeadbeef::mod { + struct $STRUCT has key { + ... + } +} + +--- + +(source_file + (module_definition + (module_identity + (num_literal + (untyped_num_literal)) + (module_identifier)) + (module_body + (struct_definition + (struct_identifier) + (ability_decls + (ability)) + (datatype_fields + (named_fields + (field_annotation + (ellipsis)))))))) + + +======================= +Typed Metavariables +======================= + +call(arr1, arr2, ($VAR: u32), $VAR2) + +--- +(source_file + (semgrep_expression + (call_expression + (module_access + (identifier)) + (arg_list + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier))) + (typed_metavariable + (identifier) + (primitive_type)) + (name_expression + (module_access + (identifier))))))) + +======================= +Type Parameters & Arguments; Struct Bind Field & Pack Field; For Ellipsis +======================= + +module 0xdeadbeef::mod { + struct $STRUCT<$T1, ...> has key { + ... + } + + fun $FUN() { + let $STRUCT { ... } = Struct2 { + ..., + field1: $VAR, + $VAR2, + }; + + while (...) { ... } + } +} + +--- + +(source_file + (module_definition + (module_identity + (num_literal + (untyped_num_literal)) + (module_identifier)) + (module_body + (struct_definition + (struct_identifier) + (type_parameters + (type_parameter + (type_parameter_identifier)) + (type_parameter + (ellipsis))) + (ability_decls + (ability)) + (datatype_fields + (named_fields + (field_annotation + (ellipsis))))) + (function_definition + (function_identifier) + (function_parameters) + + (block + (block_item + (let_statement + (bind_list + (bind_unpack + (module_access + (identifier)) + (type_arguments + (primitive_type) + (ellipsis) + (apply_type + (module_access + (identifier)))) + (bind_fields + (bind_named_fields + (bind_field + (ellipsis)))))) + (pack_expression + (module_access + (identifier)) + (field_initialize_list + (exp_field + (ellipsis)) + (exp_field + (field_identifier) + (name_expression + (module_access + (identifier)))) + (exp_field + (field_identifier)))))) + (while_expression + (ellipsis) + (block + (ellipsis)))))))) + +======================= +Function Call with Type Arguments +======================= + +{ + borrow_global_mut>(...); + exists<...> (...); + cat<...> (...); + + test_fun<..., $T1, ...> (...); +} + +--- + +(source_file + (semgrep_expression + (block + (block_item + (call_expression + (module_access + (identifier)) + (type_arguments + (apply_type + (module_access + (identifier)) + (type_arguments + (apply_type + (module_access + (identifier)))))) + + (arg_list + (ellipsis)))) + (block_item + (call_expression + (module_access + (identifier)) + (type_arguments + (ellipsis)) + (arg_list + (ellipsis)))) + (block_item + (call_expression + (module_access + (identifier)) + (type_arguments + (ellipsis)) + (arg_list + (ellipsis)))) + (block_item + (call_expression + (module_access + (identifier)) + (type_arguments + + (ellipsis) + (apply_type + (module_access + (identifier))) + (ellipsis)) + (arg_list + (ellipsis))))))) + + +======================= +Let Expression Match +======================= + +let test::Token { ... } = $VAR + +--- + +(source_file + (semgrep_expression + (let_statement + (bind_list + (bind_unpack + (module_access + (module_identifier) + (identifier)) + (bind_fields + (bind_named_fields + (bind_field + (ellipsis)))))) + (name_expression + (module_access + + (identifier)))))) + +======================= +Let Expr 2 +======================= + +let Lock { coins, $VAR2: ..., } = ... + +--- + +(source_file + (semgrep_expression + (let_statement + (bind_list + (bind_unpack + + (module_access + (identifier)) + (bind_fields + (bind_named_fields + (bind_field + (name_expression + (module_access + (identifier)))) + (bind_field + (name_expression + (module_access + (identifier))) + (ellipsis)))))) + (ellipsis)))) + + +======================= +Multiline Statements 1 +======================= + +... +... + +--- + +(source_file + (semgrep_statement + (block_item + (ellipsis)) + (block_item + (ellipsis)))) + +======================= +Multiline Statements 2 +======================= + +{ + let t = 100; + ... + t +} + +--- + +(source_file + (semgrep_expression + (block + (block_item + (let_statement + (bind_list + (bind_var + (variable_identifier))) + (num_literal + (typed_num_literal)))) + (block_item + (ellipsis)) + (name_expression + (module_access + (identifier)))))) + + +======================= +More Ellipsis in Field Access +======================= + + +let $LEFT = <... $OBJ ...>.inner; +let $LEFT2 = ... . ... . ...; +let $LEFT3 = ... . ... .func(...); + +--- + +(source_file + (semgrep_statement + (block_item + (let_statement + (bind_list + (bind_var + (variable_identifier))) + (access_field + (deep_ellipsis + (name_expression + (module_access + (identifier)))) + (name_expression + (module_access + (identifier)))))) + + (block_item + (let_statement + (bind_list + (bind_var + (variable_identifier))) + (field_access_ellipsis_expr + (field_access_ellipsis_expr + (ellipsis) + (ellipsis)) + (ellipsis)))) + (block_item + (let_statement + (bind_list + (bind_var + (variable_identifier))) + (receiver_call + (field_access_ellipsis_expr + (ellipsis) + (ellipsis)) + (identifier) + (arg_list + (ellipsis))))))) + + + +======================= +Dot Expressions +======================= + +use 0xabcd::ff; +... +fun jwk_lt (a: &ActiveJwk, b: &ActiveJwk): bool { + if (&a.jwk_id.iss != &b.jwk_id.iss) { + return string_bytes_lt(&a.jwk_id.iss, &b.jwk_id.iss) + }; + if (&a.jwk_id.kid != &b.jwk_id.kid) { + return string_bytes_lt(&a.jwk_id.kid, &b.jwk_id.kid) + }; + if (&a.jwk.kty != &b.jwk.kty) { + return string_bytes_lt(&a.jwk.kty, &b.jwk.kty) + }; + if (&a.jwk.e != &b.jwk.e) { + return string_bytes_lt(&a.jwk.e, &b.jwk.e) + }; + if (&a.jwk.n != &b.jwk.n) { + return string_bytes_lt(&a.jwk.n, &b.jwk.n) + }; + string_bytes_lt(&a.jwk.alg, &b.jwk.alg) +} + +--- + + (source_file + (semgrep_statement + (use_declaration + (use_module + (module_identity + (num_literal + (untyped_num_literal)) + (module_identifier)))) + (block_item + (ellipsis)) + (function_definition + (function_identifier) + (function_parameters + (function_parameter + (variable_identifier) + (ref_type + (apply_type + (module_access + (identifier))))) + (function_parameter + (variable_identifier) + (ref_type + (apply_type + (module_access + (identifier)))))) + (ret_type + (primitive_type)) + (block + (block_item + (if_expression + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (binary_expression + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))) + (binary_operator) + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier))))))))) + (block + (return_expression + (call_expression + (module_access + (identifier)) + (arg_list + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))))) + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))))))))))) + (block_item + (if_expression + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (binary_expression + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))) + (binary_operator) + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier))))))))) + (block + (return_expression + (call_expression + (module_access + (identifier)) + (arg_list + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))))) + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))))))))))) + (block_item + (if_expression + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (binary_expression + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))) + (binary_operator) + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier))))))))) + (block + (return_expression + (call_expression + (module_access + (identifier)) + (arg_list + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))))) + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))))))))))) + (block_item + (if_expression + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (binary_expression + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))) + (binary_operator) + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier))))))))) + (block + (return_expression + (call_expression + (module_access + (identifier)) + (arg_list + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))))) + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))))))))))) + (block_item + (if_expression + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (binary_expression + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))) + (binary_operator) + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier))))))))) + (block + (return_expression + (call_expression + (module_access + (identifier)) + (arg_list + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))))) + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))))))))))) + (call_expression + (module_access + (identifier)) + (arg_list + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))))) + (borrow_expression + (access_field + (name_expression + (module_access + (identifier))) + (access_field + (name_expression + (module_access + (identifier))) + (name_expression + (module_access + (identifier)))))))))))) \ No newline at end of file diff --git a/lang/semgrep-grammars/src/tree-sitter-move-on-sui b/lang/semgrep-grammars/src/tree-sitter-move-on-sui index b454ad6d..260eb892 160000 --- a/lang/semgrep-grammars/src/tree-sitter-move-on-sui +++ b/lang/semgrep-grammars/src/tree-sitter-move-on-sui @@ -1 +1 @@ -Subproject commit b454ad6dd9d5decd66889b2c71448f79f3c3b39c +Subproject commit 260eb89274bb75821ca9f5cf9b5a5b8d3bdb44b4