diff --git a/vm/move-examples/defi/Move.toml b/vm/move-examples/defi/Move.toml new file mode 100644 index 0000000000..20e65965e0 --- /dev/null +++ b/vm/move-examples/defi/Move.toml @@ -0,0 +1,9 @@ +[package] +name = "defi" +version = "0.0.1" + +[addresses] +defi = "0xed9ea1f3533c14e1b52d9ff6475776ba" + +[dependencies] +StarcoinFramework = { local = "../../framework/starcoin-framework" } diff --git a/vm/move-examples/defi/sources/locked_coins.move b/vm/move-examples/defi/sources/locked_coins.move new file mode 100644 index 0000000000..788a0821dc --- /dev/null +++ b/vm/move-examples/defi/sources/locked_coins.move @@ -0,0 +1,489 @@ +/** + * This provides an example for sending locked coins to recipients to be unlocked after a specific time. + * + * Locked coins flow: + * 1. Deploy the lockup contract. Deployer can decide if the contract is upgradable or not. + * 2. Sponsor accounts (sponsors) call initialize_sponsor with the appropriate CoinType to set up their account for + * creating locks later. + * 2. Sponsors add locked APTs for custom expiration time + amount for recipients. Each lockup is called a "lock". + * 3. Sponsors can revoke a lock or change lockup (reduce or extend) anytime. This gives flexibility in case of + * contract violation or special circumstances. If this is not desired, the deployer can remove these functionalities + * before deploying. If a lock is canceled, the locked coins will be sent back to the withdrawal address. This + * withdrawal address is set when initilizing the sponsor account and can only be changed when there are no active or + * unclaimed locks. + * 4. Once the lockup has expired, the recipient can call claim to get the unlocked tokens. + **/ +module defi::locked_coins { + use starcoin_framework::coin::{Self, Coin}; + use starcoin_framework::event; + use starcoin_framework::timestamp; + use starcoin_std::table::{Self, Table}; + use std::error; + use std::signer; + use std::vector; + + /// Represents a lock of coins until some specified unlock time. Afterward, the recipient can claim the coins. + struct Lock has store { + coins: Coin, + unlock_time_secs: u64, + } + + /// Holder for a map from recipients => locks. + /// There can be at most one lock per recipient. + struct Locks has key { + // Map from recipient address => locked coins. + locks: Table>, + // Predefined withdrawal address. This cannot be changed if there's any active lock. + withdrawal_address: address, + // Number of locks that have not yet been claimed. + total_locks: u64, + } + + #[event] + /// Event emitted when a lock is canceled. + struct CancelLockup has drop, store { + sponsor: address, + recipient: address, + amount: u64, + } + + #[event] + /// Event emitted when a recipient claims unlocked coins. + struct Claim has drop, store { + sponsor: address, + recipient: address, + amount: u64, + claimed_time_secs: u64, + } + + #[event] + /// Event emitted when lockup is updated for an existing lock. + struct UpdateLockup has drop, store { + sponsor: address, + recipient: address, + old_unlock_time_secs: u64, + new_unlock_time_secs: u64, + } + + #[event] + /// Event emitted when withdrawal address is updated. + struct UpdateWithdrawalAddress has drop, store { + sponsor: address, + old_withdrawal_address: address, + new_withdrawal_address: address, + } + + /// No locked coins found to claim. + const ELOCK_NOT_FOUND: u64 = 1; + /// Lockup has not expired yet. + const ELOCKUP_HAS_NOT_EXPIRED: u64 = 2; + /// Can only create one active lock per recipient at once. + const ELOCK_ALREADY_EXISTS: u64 = 3; + /// The length of the recipients list doesn't match the amounts. + const EINVALID_RECIPIENTS_LIST_LENGTH: u64 = 3; + /// Sponsor account has not been set up to create locks for the specified CoinType yet. + const ESPONSOR_ACCOUNT_NOT_INITIALIZED: u64 = 4; + /// Cannot update the withdrawal address because there are still active/unclaimed locks. + const EACTIVE_LOCKS_EXIST: u64 = 5; + + #[view] + /// Return the total number of locks created by the sponsor for the given CoinType. + public fun total_locks(sponsor: address): u64 acquires Locks { + assert!(exists>(sponsor), error::not_found(ESPONSOR_ACCOUNT_NOT_INITIALIZED)); + let locks = borrow_global>(sponsor); + locks.total_locks + } + + #[view] + /// Return the number of coins a sponsor has locked up for the given recipient. + /// This throws an error if there are no locked coins setup for the given recipient. + public fun locked_amount(sponsor: address, recipient: address): u64 acquires Locks { + assert!(exists>(sponsor), error::not_found(ESPONSOR_ACCOUNT_NOT_INITIALIZED)); + let locks = borrow_global>(sponsor); + assert!(table::contains(&locks.locks, recipient), error::not_found(ELOCK_NOT_FOUND)); + coin::value(&table::borrow(&locks.locks, recipient).coins) + } + + #[view] + /// Return the timestamp (in seconds) when the given recipient can claim coins locked up for them by the sponsor. + /// This throws an error if there are no locked coins setup for the given recipient. + public fun claim_time_secs(sponsor: address, recipient: address): u64 acquires Locks { + assert!(exists>(sponsor), error::not_found(ESPONSOR_ACCOUNT_NOT_INITIALIZED)); + let locks = borrow_global>(sponsor); + assert!(table::contains(&locks.locks, recipient), error::not_found(ELOCK_NOT_FOUND)); + table::borrow(&locks.locks, recipient).unlock_time_secs + } + + #[view] + /// Return the withdrawal address for a sponsor's locks (where canceled locks' funds are sent to). + public fun withdrawal_address(sponsor: address): address acquires Locks { + assert!(exists>(sponsor), error::not_found(ESPONSOR_ACCOUNT_NOT_INITIALIZED)); + let locks = borrow_global>(sponsor); + locks.withdrawal_address + } + + /// Initialize the sponsor account to allow creating locks. + public entry fun initialize_sponsor(sponsor: &signer, withdrawal_address: address) { + move_to(sponsor, Locks { + locks: table::new>(), + withdrawal_address, + total_locks: 0, + }) + } + + /// Update the withdrawal address. This is only allowed if there are currently no active locks. + public entry fun update_withdrawal_address( + sponsor: &signer, new_withdrawal_address: address) acquires Locks { + let sponsor_address = signer::address_of(sponsor); + assert!(exists>(sponsor_address), error::not_found(ESPONSOR_ACCOUNT_NOT_INITIALIZED)); + + let locks = borrow_global_mut>(sponsor_address); + assert!(locks.total_locks == 0, error::invalid_state(EACTIVE_LOCKS_EXIST)); + let old_withdrawal_address = locks.withdrawal_address; + locks.withdrawal_address = new_withdrawal_address; + + event::emit(UpdateWithdrawalAddress { + sponsor: sponsor_address, + old_withdrawal_address, + new_withdrawal_address, + }); + } + + /// Batch version of add_locked_coins to process multiple recipients and corresponding amounts. + public entry fun batch_add_locked_coins( + sponsor: &signer, recipients: vector
, amounts: vector, unlock_time_secs: u64) acquires Locks { + let len = vector::length(&recipients); + assert!(len == vector::length(&amounts), error::invalid_argument(EINVALID_RECIPIENTS_LIST_LENGTH)); + vector::enumerate_ref(&recipients, |i, recipient| { + let amount = *vector::borrow(&amounts, i); + add_locked_coins(sponsor, *recipient, amount, unlock_time_secs); + }); + } + + /// `Sponsor` can add locked coins for `recipient` with given unlock timestamp (in seconds). + /// There's no restriction on unlock timestamp so sponsors could technically add coins for an unlocked time in the + /// past, which means the coins are immediately unlocked. + public entry fun add_locked_coins( + sponsor: &signer, recipient: address, amount: u64, unlock_time_secs: u64) acquires Locks { + let sponsor_address = signer::address_of(sponsor); + assert!(exists>(sponsor_address), error::not_found(ESPONSOR_ACCOUNT_NOT_INITIALIZED)); + + let locks = borrow_global_mut>(sponsor_address); + let coins = coin::withdraw(sponsor, amount); + assert!(!table::contains(&locks.locks, recipient), error::already_exists(ELOCK_ALREADY_EXISTS)); + table::add(&mut locks.locks, recipient, Lock { coins, unlock_time_secs }); + locks.total_locks = locks.total_locks + 1; + } + + /// Recipient can claim coins that are fully unlocked (unlock time has passed). + /// To claim, `recipient` would need the sponsor's address. In the case where each sponsor always deploys this + /// module anew, it'd just be the module's hosted account address. + public entry fun claim(recipient: &signer, sponsor: address) acquires Locks { + assert!(exists>(sponsor), error::not_found(ESPONSOR_ACCOUNT_NOT_INITIALIZED)); + let locks = borrow_global_mut>(sponsor); + let recipient_address = signer::address_of(recipient); + assert!(table::contains(&locks.locks, recipient_address), error::not_found(ELOCK_NOT_FOUND)); + + // Delete the lock entry both to keep records clean and keep storage usage minimal. + // This would be reverted if validations fail later (transaction atomicity). + let Lock { coins, unlock_time_secs } = table::remove(&mut locks.locks, recipient_address); + locks.total_locks = locks.total_locks - 1; + let now_secs = timestamp::now_seconds(); + assert!(now_secs >= unlock_time_secs, error::invalid_state(ELOCKUP_HAS_NOT_EXPIRED)); + + let amount = coin::value(&coins); + // This would fail if the recipient account is not registered to receive CoinType. + coin::deposit(recipient_address, coins); + + event::emit(Claim { + sponsor, + recipient: recipient_address, + amount, + claimed_time_secs: now_secs, + }); + } + + /// Batch version of update_lockup. + public entry fun batch_update_lockup( + sponsor: &signer, recipients: vector
, new_unlock_time_secs: u64) acquires Locks { + let sponsor_address = signer::address_of(sponsor); + assert!(exists>(sponsor_address), error::not_found(ESPONSOR_ACCOUNT_NOT_INITIALIZED)); + + vector::for_each_ref(&recipients, |recipient| { + update_lockup(sponsor, *recipient, new_unlock_time_secs); + }); + } + + /// Sponsor can update the lockup of an existing lock. + public entry fun update_lockup( + sponsor: &signer, recipient: address, new_unlock_time_secs: u64) acquires Locks { + let sponsor_address = signer::address_of(sponsor); + assert!(exists>(sponsor_address), error::not_found(ESPONSOR_ACCOUNT_NOT_INITIALIZED)); + let locks = borrow_global_mut>(sponsor_address); + assert!(table::contains(&locks.locks, recipient), error::not_found(ELOCK_NOT_FOUND)); + + let lock = table::borrow_mut(&mut locks.locks, recipient); + let old_unlock_time_secs = lock.unlock_time_secs; + lock.unlock_time_secs = new_unlock_time_secs; + + event::emit(UpdateLockup { + sponsor: sponsor_address, + recipient, + old_unlock_time_secs, + new_unlock_time_secs, + }); + } + + /// Batch version of cancel_lockup to cancel the lockup for multiple recipients. + public entry fun batch_cancel_lockup(sponsor: &signer, recipients: vector
) acquires Locks { + let sponsor_address = signer::address_of(sponsor); + assert!(exists>(sponsor_address), error::not_found(ESPONSOR_ACCOUNT_NOT_INITIALIZED)); + + vector::for_each_ref(&recipients, |recipient| { + cancel_lockup(sponsor, *recipient); + }); + } + + /// Sponsor can cancel an existing lock. + public entry fun cancel_lockup(sponsor: &signer, recipient: address) acquires Locks { + let sponsor_address = signer::address_of(sponsor); + assert!(exists>(sponsor_address), error::not_found(ESPONSOR_ACCOUNT_NOT_INITIALIZED)); + let locks = borrow_global_mut>(sponsor_address); + assert!(table::contains(&locks.locks, recipient), error::not_found(ELOCK_NOT_FOUND)); + + // Remove the lock and deposit coins backed into the sponsor account. + let Lock { coins, unlock_time_secs: _ } = table::remove(&mut locks.locks, recipient); + locks.total_locks = locks.total_locks - 1; + let amount = coin::value(&coins); + coin::deposit(locks.withdrawal_address, coins); + + event::emit( + CancelLockup { + sponsor: sponsor_address, + recipient, + amount + }); + } + + #[test_only] + use starcoin_framework::account; + #[test_only] + use starcoin_framework::coin::BurnCapability; + #[test_only] + use starcoin_framework::starcoin_coin::{Self,STC}; + #[test_only] + use starcoin_framework::starcoin_account; + + #[test_only] + fun setup(starcoin_framework: &signer, sponsor: &signer): BurnCapability { + timestamp::set_time_has_started_for_testing(starcoin_framework); + + let (burn_cap, mint_cap) = starcoin_coin::initialize_for_test(starcoin_framework); + account::create_account_for_test(signer::address_of(sponsor)); + coin::register(sponsor); + let coins = coin::mint(2000, &mint_cap); + coin::deposit(signer::address_of(sponsor), coins); + coin::destroy_mint_cap(mint_cap); + + burn_cap + } + + #[test(starcoin_framework = @0x1, sponsor = @0x123, recipient = @0x234)] + public entry fun test_recipient_can_claim_coins( + starcoin_framework: &signer, sponsor: &signer, recipient: &signer) acquires Locks { + let burn_cap = setup(starcoin_framework, sponsor); + let recipient_addr = signer::address_of(recipient); + starcoin_account::create_account(recipient_addr); + let sponsor_address = signer::address_of(sponsor); + initialize_sponsor(sponsor, sponsor_address); + add_locked_coins(sponsor, recipient_addr, 1000, 1000); + assert!(total_locks(sponsor_address) == 1, 0); + timestamp::fast_forward_seconds(1000); + claim(recipient, sponsor_address); + assert!(total_locks(sponsor_address) == 0, 1); + assert!(coin::balance(recipient_addr) == 1000, 0); + coin::destroy_burn_cap(burn_cap); + } + + #[test(starcoin_framework = @0x1, sponsor = @0x123, recipient = @0x234)] + #[expected_failure(abort_code = 0x30002, location = Self)] + public entry fun test_recipient_cannot_claim_coins_if_lockup_has_not_expired( + starcoin_framework: &signer, sponsor: &signer, recipient: &signer) acquires Locks { + let burn_cap = setup(starcoin_framework, sponsor); + let recipient_addr = signer::address_of(recipient); + starcoin_account::create_account(recipient_addr); + let sponsor_address = signer::address_of(sponsor); + initialize_sponsor(sponsor, sponsor_address); + add_locked_coins(sponsor, recipient_addr, 1000, 1000); + timestamp::fast_forward_seconds(500); + claim(recipient, sponsor_address); + coin::destroy_burn_cap(burn_cap); + } + + #[test(starcoin_framework = @0x1, sponsor = @0x123, recipient = @0x234)] + #[expected_failure(abort_code = 0x60001, location = Self)] + public entry fun test_recipient_cannot_claim_twice( + starcoin_framework: &signer, sponsor: &signer, recipient: &signer) acquires Locks { + let burn_cap = setup(starcoin_framework, sponsor); + let recipient_addr = signer::address_of(recipient); + starcoin_account::create_account(recipient_addr); + let sponsor_address = signer::address_of(sponsor); + initialize_sponsor(sponsor, sponsor_address); + add_locked_coins(sponsor, recipient_addr, 1000, 1000); + timestamp::fast_forward_seconds(1000); + claim(recipient, sponsor_address); + claim(recipient, sponsor_address); + coin::destroy_burn_cap(burn_cap); + } + + #[test(starcoin_framework = @0x1, sponsor = @0x123, recipient = @0x234)] + public entry fun test_sponsor_can_update_lockup( + starcoin_framework: &signer, sponsor: &signer, recipient: &signer) acquires Locks { + let burn_cap = setup(starcoin_framework, sponsor); + let recipient_addr = signer::address_of(recipient); + starcoin_account::create_account(recipient_addr); + let sponsor_address = signer::address_of(sponsor); + initialize_sponsor(sponsor, sponsor_address); + add_locked_coins(sponsor, recipient_addr, 1000, 1000); + assert!(total_locks(sponsor_address) == 1, 0); + assert!(claim_time_secs(sponsor_address, recipient_addr) == 1000, 0); + // Extend lockup. + update_lockup(sponsor, recipient_addr, 2000); + assert!(claim_time_secs(sponsor_address, recipient_addr) == 2000, 1); + // Reduce lockup. + update_lockup(sponsor, recipient_addr, 1500); + assert!(claim_time_secs(sponsor_address, recipient_addr) == 1500, 2); + assert!(total_locks(sponsor_address) == 1, 1); + + coin::destroy_burn_cap(burn_cap); + } + + #[test(starcoin_framework = @0x1, sponsor = @0x123, recipient_1 = @0x234, recipient_2 = @0x345)] + public entry fun test_sponsor_can_batch_update_lockup( + starcoin_framework: &signer, sponsor: &signer, recipient_1: &signer, recipient_2: &signer) acquires Locks { + let burn_cap = setup(starcoin_framework, sponsor); + let sponsor_addr = signer::address_of(sponsor); + let recipient_1_addr = signer::address_of(recipient_1); + let recipient_2_addr = signer::address_of(recipient_2); + starcoin_account::create_account(recipient_1_addr); + starcoin_account::create_account(recipient_2_addr); + let sponsor_address = signer::address_of(sponsor); + initialize_sponsor(sponsor, sponsor_address); + batch_add_locked_coins( + sponsor, + vector[recipient_1_addr, recipient_2_addr], + vector[1000, 1000], + 1000 + ); + assert!(claim_time_secs(sponsor_addr, recipient_1_addr) == 1000, 0); + assert!(claim_time_secs(sponsor_addr, recipient_2_addr) == 1000, 0); + // Extend lockup. + batch_update_lockup(sponsor, vector[recipient_1_addr, recipient_2_addr], 2000); + assert!(claim_time_secs(sponsor_addr, recipient_1_addr) == 2000, 1); + assert!(claim_time_secs(sponsor_addr, recipient_2_addr) == 2000, 1); + // Reduce lockup. + batch_update_lockup(sponsor, vector[recipient_1_addr, recipient_2_addr], 1500); + assert!(claim_time_secs(sponsor_addr, recipient_1_addr) == 1500, 2); + assert!(claim_time_secs(sponsor_addr, recipient_2_addr) == 1500, 2); + + coin::destroy_burn_cap(burn_cap); + } + + #[test(starcoin_framework = @0x1, sponsor = @0x123, recipient = @0x234, withdrawal = @0x345)] + public entry fun test_sponsor_can_cancel_lockup( + starcoin_framework: &signer, sponsor: &signer, recipient: &signer, withdrawal: &signer) acquires Locks { + let burn_cap = setup(starcoin_framework, sponsor); + let recipient_addr = signer::address_of(recipient); + let withdrawal_addr = signer::address_of(withdrawal); + starcoin_account::create_account(withdrawal_addr); + starcoin_account::create_account(recipient_addr); + let sponsor_address = signer::address_of(sponsor); + initialize_sponsor(sponsor, withdrawal_addr); + add_locked_coins(sponsor, recipient_addr, 1000, 1000); + assert!(total_locks(sponsor_address) == 1, 0); + assert!(coin::balance(withdrawal_addr) == 0, 0); + cancel_lockup(sponsor, recipient_addr); + assert!(total_locks(sponsor_address) == 0, 0); + let locks = borrow_global_mut>(sponsor_address); + assert!(!table::contains(&locks.locks, recipient_addr), 0); + + // Funds from canceled locks should be sent to the withdrawal address. + assert!(coin::balance(withdrawal_addr) == 1000, 0); + + coin::destroy_burn_cap(burn_cap); + } + + #[test(starcoin_framework = @0x1, sponsor = @0x123, recipient_1 = @0x234, recipient_2 = @0x345, withdrawal = @0x456)] + public entry fun test_sponsor_can_batch_cancel_lockup( + starcoin_framework: &signer, + sponsor: &signer, + recipient_1: &signer, + recipient_2: &signer, + withdrawal: &signer, + ) acquires Locks { + let burn_cap = setup(starcoin_framework, sponsor); + let recipient_1_addr = signer::address_of(recipient_1); + let recipient_2_addr = signer::address_of(recipient_2); + let withdrawal_addr = signer::address_of(withdrawal); + starcoin_account::create_account(recipient_1_addr); + starcoin_account::create_account(recipient_2_addr); + starcoin_account::create_account(withdrawal_addr); + let sponsor_address = signer::address_of(sponsor); + initialize_sponsor(sponsor, withdrawal_addr); + batch_add_locked_coins( + sponsor, + vector[recipient_1_addr, recipient_2_addr], + vector[1000, 1000], + 1000 + ); + batch_cancel_lockup(sponsor, vector[recipient_1_addr, recipient_2_addr]); + let locks = borrow_global_mut>(sponsor_address); + assert!(!table::contains(&locks.locks, recipient_1_addr), 0); + assert!(!table::contains(&locks.locks, recipient_2_addr), 0); + // Funds from canceled locks should be sent to the withdrawal address. + assert!(coin::balance(withdrawal_addr) == 2000, 0); + coin::destroy_burn_cap(burn_cap); + } + + #[test(starcoin_framework = @0x1, sponsor = @0x123, recipient = @0x234, withdrawal = @0x456)] + #[expected_failure(abort_code = 0x30005, location = Self)] + public entry fun test_cannot_change_withdrawal_address_if_active_locks_exist( + starcoin_framework: &signer, + sponsor: &signer, + recipient: &signer, + withdrawal: &signer, + ) acquires Locks { + let burn_cap = setup(starcoin_framework, sponsor); + let recipient_addr = signer::address_of(recipient); + let withdrawal_addr = signer::address_of(withdrawal); + starcoin_account::create_account(recipient_addr); + starcoin_account::create_account(withdrawal_addr); + let sponsor_address = signer::address_of(sponsor); + initialize_sponsor(sponsor, withdrawal_addr); + add_locked_coins(sponsor, recipient_addr, 1000, 1000); + update_withdrawal_address(sponsor, sponsor_address); + coin::destroy_burn_cap(burn_cap); + } + + #[test(starcoin_framework = @0x1, sponsor = @0x123, recipient = @0x234, withdrawal = @0x456)] + public entry fun test_can_change_withdrawal_address_if_no_active_locks_exist( + starcoin_framework: &signer, + sponsor: &signer, + recipient: &signer, + withdrawal: &signer, + ) acquires Locks { + let burn_cap = setup(starcoin_framework, sponsor); + let recipient_addr = signer::address_of(recipient); + let withdrawal_addr = signer::address_of(withdrawal); + starcoin_account::create_account(recipient_addr); + starcoin_account::create_account(withdrawal_addr); + let sponsor_address = signer::address_of(sponsor); + initialize_sponsor(sponsor, withdrawal_addr); + assert!(withdrawal_address(sponsor_address) == withdrawal_addr, 0); + add_locked_coins(sponsor, recipient_addr, 1000, 1000); + cancel_lockup(sponsor, recipient_addr); + update_withdrawal_address(sponsor, sponsor_address); + assert!(withdrawal_address(sponsor_address) == sponsor_address, 0); + coin::destroy_burn_cap(burn_cap); + } +} diff --git a/vm/move-examples/hello_blockchain/Move.toml b/vm/move-examples/hello_blockchain/Move.toml new file mode 100644 index 0000000000..c1266b0123 --- /dev/null +++ b/vm/move-examples/hello_blockchain/Move.toml @@ -0,0 +1,9 @@ +[package] +name = "hello-blockchain" +version = "0.0.1" + +[addresses] +hello_blockchain = "0xed9ea1f3533c14e1b52d9ff6475776ba" + +[dependencies] +StarcoinFramework = { local = "../../framework/starcoin-framework" } diff --git a/vm/move-examples/hello_blockchain/sources/hello_blockchain.move b/vm/move-examples/hello_blockchain/sources/hello_blockchain.move new file mode 100644 index 0000000000..e819e917d1 --- /dev/null +++ b/vm/move-examples/hello_blockchain/sources/hello_blockchain.move @@ -0,0 +1,59 @@ +module hello_blockchain::message { + use std::error; + use std::signer; + use std::string; + use starcoin_framework::event; + + //:!:>resource + struct MessageHolder has key { + message: string::String, + } + //<:!:resource + + #[event] + struct MessageChange has drop, store { + account: address, + from_message: string::String, + to_message: string::String, + } + + /// There is no message present + const ENO_MESSAGE: u64 = 0; + + #[view] + public fun get_message(addr: address): string::String acquires MessageHolder { + assert!(exists(addr), error::not_found(ENO_MESSAGE)); + borrow_global(addr).message + } + + public entry fun set_message(account: signer, message: string::String) + acquires MessageHolder { + let account_addr = signer::address_of(&account); + if (!exists(account_addr)) { + move_to(&account, MessageHolder { + message, + }) + } else { + let old_message_holder = borrow_global_mut(account_addr); + let from_message = old_message_holder.message; + event::emit(MessageChange { + account: account_addr, + from_message, + to_message: copy message, + }); + old_message_holder.message = message; + } + } + + #[test(account = @0x1)] + public entry fun sender_can_set_message(account: signer) acquires MessageHolder { + let addr = signer::address_of(&account); + starcoin_framework::account::create_account_for_test(addr); + set_message(account, string::utf8(b"Hello, Blockchain")); + + assert!( + get_message(addr) == string::utf8(b"Hello, Blockchain"), + ENO_MESSAGE + ); + } +} diff --git a/vm/move-examples/hello_blockchain/sources/hello_blockchain_test.move b/vm/move-examples/hello_blockchain/sources/hello_blockchain_test.move new file mode 100644 index 0000000000..2524f20551 --- /dev/null +++ b/vm/move-examples/hello_blockchain/sources/hello_blockchain_test.move @@ -0,0 +1,26 @@ +#[test_only] +module hello_blockchain::message_tests { + use std::signer; + use std::unit_test; + use std::vector; + use std::string; + + use hello_blockchain::message; + + fun get_account(): signer { + vector::pop_back(&mut unit_test::create_signers_for_testing(1)) + } + + #[test] + public entry fun sender_can_set_message() { + let account = get_account(); + let addr = signer::address_of(&account); + starcoin_framework::account::create_account_for_test(addr); + message::set_message(account, string::utf8(b"Hello, Blockchain")); + + assert!( + message::get_message(addr) == string::utf8(b"Hello, Blockchain"), + 0 + ); + } +} diff --git a/vm/move-examples/resource_groups/primary/Move.toml b/vm/move-examples/resource_groups/primary/Move.toml new file mode 100644 index 0000000000..aad663a5dd --- /dev/null +++ b/vm/move-examples/resource_groups/primary/Move.toml @@ -0,0 +1,9 @@ +[package] +name = "ResourceGroupsPrimary" +version = "0.0.1" + +[addresses] +resource_groups_primary = "0xed9ea1f3533c14e1b52d9ff6475776ba" + +[dependencies] +StarcoinFramework = { local = "../../../framework/starcoin-framework" } diff --git a/vm/move-examples/resource_groups/primary/sources/primary.move b/vm/move-examples/resource_groups/primary/sources/primary.move new file mode 100644 index 0000000000..7fb201fc8c --- /dev/null +++ b/vm/move-examples/resource_groups/primary/sources/primary.move @@ -0,0 +1,58 @@ +/// This demonstrates how to use a resource group within a single module +/// See resource_groups_primary::secondary for cross module and multiple resources +module resource_groups_primary::primary { + use std::signer; + + #[resource_group(scope = global)] + struct ResourceGroupContainer { } + + #[resource_group_member(group = resource_groups_primary::primary::ResourceGroupContainer)] + struct Primary has drop, key { + value: u64, + } + + public entry fun init(account: &signer, value: u64) { + move_to(account, Primary { value }); + } + + public entry fun set_value(account: &signer, value: u64) acquires Primary { + let primary = borrow_global_mut(signer::address_of(account)); + primary.value = value; + } + + public fun read(account: address): u64 acquires Primary { + borrow_global(account).value + } + + public entry fun remove(account: &signer) acquires Primary { + move_from(signer::address_of(account)); + } + + public fun exists_at(account: address): bool { + exists(account) + } + + fun init_module(owner: &signer) { + move_to(owner, Primary { value: 3 }); + } + + #[test(account = @0x3)] + fun test_multiple(account: &signer) acquires Primary { + // Do it once to verify normal flow + test_primary(account); + + // Do it again to verify it can be recreated + test_primary(account); + } + + public fun test_primary(account: &signer) acquires Primary { + let addr = signer::address_of(account); + assert!(!exists_at(addr), 0); + init(account, 5); + assert!(read(addr) == 5, 1); + set_value(account, 12); + assert!(read(addr) == 12, 1); + remove(account); + assert!(!exists_at(addr), 0); + } +} diff --git a/vm/move-examples/resource_groups/secondary/Move.toml b/vm/move-examples/resource_groups/secondary/Move.toml new file mode 100644 index 0000000000..f970eac393 --- /dev/null +++ b/vm/move-examples/resource_groups/secondary/Move.toml @@ -0,0 +1,11 @@ +[package] +name = "ResourceGroupsSecondary" +version = "0.0.1" + +[addresses] +resource_groups_primary = "0xed9ea1f3533c14e1b52d9ff6475776ba" +resource_groups_secondary = "0xed9ea1f3533c14e1b52d9ff6475776ba" + +[dependencies] +StarcoinFramework = { local = "../../../framework/starcoin-framework" } +ResourceGroupsPrimary = { local = "../primary" } diff --git a/vm/move-examples/resource_groups/secondary/sources/secondary.move b/vm/move-examples/resource_groups/secondary/sources/secondary.move new file mode 100644 index 0000000000..e7dd22bbba --- /dev/null +++ b/vm/move-examples/resource_groups/secondary/sources/secondary.move @@ -0,0 +1,58 @@ +/// This demonstrates how to use a resource group accross modules +module resource_groups_secondary::secondary { + use std::signer; + use resource_groups_primary::primary; + + #[resource_group_member(group = resource_groups_primary::primary::ResourceGroupContainer)] + struct Secondary has drop, key { + value: u32, + } + + public entry fun init(account: &signer, value: u32) { + move_to(account, Secondary { value }); + } + + public entry fun set_value(account: &signer, value: u32) acquires Secondary { + let primary = borrow_global_mut(signer::address_of(account)); + primary.value = value; + } + + public fun read(account: address): u32 acquires Secondary { + borrow_global(account).value + } + + public entry fun remove(account: &signer) acquires Secondary { + move_from(signer::address_of(account)); + } + + public fun exists_at(account: address): bool { + exists(account) + } + + // This boiler plate function exists just so that primary is loaded with secondary + // We'll need to explore how to load resource_group_containers without necessarily + // having loaded their module via the traditional module graph. + public fun primary_exists(account: address): bool { + primary::exists_at(account) + } + + #[test(account = @0x3)] + fun test_multiple(account: &signer) acquires Secondary { + let addr = signer::address_of(account); + assert!(!exists_at(addr), 0); + init(account, 7); + assert!(read(addr) == 7, 1); + set_value(account, 13); + assert!(read(addr) == 13, 1); + + // Verify that primary can be added and removed without affecting secondary + primary::test_primary(account); + assert!(read(addr) == 13, 1); + + remove(account); + assert!(!exists_at(addr), 0); + + // Verify that primary can be re-added after secondary has been removed + primary::test_primary(account); + } +} diff --git a/vm/move-examples/shared_account/Move.toml b/vm/move-examples/shared_account/Move.toml new file mode 100644 index 0000000000..76c010ba2c --- /dev/null +++ b/vm/move-examples/shared_account/Move.toml @@ -0,0 +1,9 @@ +[package] +name = "shared_account" +version = "0.0.1" + +[addresses] +shared_account = "0xed9ea1f3533c14e1b52d9ff6475776ba" + +[dependencies] +StarcoinFramework = { local = "../../framework/starcoin-framework" } diff --git a/vm/move-examples/shared_account/sources/shared_account.move b/vm/move-examples/shared_account/sources/shared_account.move new file mode 100644 index 0000000000..1fc0d19b2d --- /dev/null +++ b/vm/move-examples/shared_account/sources/shared_account.move @@ -0,0 +1,136 @@ +// This module demonstrates a basic shared account that could be used for NFT royalties +// Users can (1) create a shared account (2) disperse the coins to multiple creators +module shared_account::SharedAccount { + use std::error; + use std::signer; + use std::vector; + use starcoin_framework::account; + use starcoin_framework::coin; + + // struct Share records the address of the share_holder and their corresponding number of shares + struct Share has store { + share_holder: address, + num_shares: u64, + } + + // Resource representing a shared account + struct SharedAccount has key { + share_record: vector, + total_shares: u64, + signer_capability: account::SignerCapability, + } + + struct SharedAccountEvent has key { + resource_addr: address, + } + + const EACCOUNT_NOT_FOUND: u64 = 0; + const ERESOURCE_DNE: u64 = 1; + const EINSUFFICIENT_BALANCE: u64 = 2; + + // Create and initialize a shared account + public entry fun initialize(source: &signer, seed: vector, addresses: vector
, numerators: vector) { + let total = 0; + let share_record = vector::empty(); + + vector::enumerate_ref(&addresses, |i, addr|{ + let addr = *addr; + let num_shares = *vector::borrow(&numerators, i); + + // make sure that the account exists, so when we call disperse() it wouldn't fail + // because one of the accounts does not exist + assert!(account::exists_at(addr), error::invalid_argument(EACCOUNT_NOT_FOUND)); + + vector::push_back(&mut share_record, Share { share_holder: addr, num_shares }); + total = total + num_shares; + }); + + let (resource_signer, resource_signer_cap) = account::create_resource_account(source, seed); + + move_to( + &resource_signer, + SharedAccount { + share_record, + total_shares: total, + signer_capability: resource_signer_cap, + } + ); + + move_to(source, SharedAccountEvent { + resource_addr: signer::address_of(&resource_signer) + }); + } + + // Disperse all available balance to addresses in the shared account + public entry fun disperse(resource_addr: address) acquires SharedAccount { + assert!(exists(resource_addr), error::invalid_argument(ERESOURCE_DNE)); + + let total_balance = coin::balance(resource_addr); + assert!(total_balance > 0, error::out_of_range(EINSUFFICIENT_BALANCE)); + + let shared_account = borrow_global(resource_addr); + let resource_signer = account::create_signer_with_capability(&shared_account.signer_capability); + + vector::for_each_ref(&shared_account.share_record, |shared_record|{ + let shared_record: &Share = shared_record; + let current_amount = shared_record.num_shares * total_balance / shared_account.total_shares; + coin::transfer(&resource_signer, shared_record.share_holder, current_amount); + }); + } + + #[test_only] + public fun set_up(user: signer, test_user1: signer, test_user2: signer) : address acquires SharedAccountEvent { + let addresses = vector::empty
(); + let numerators = vector::empty(); + let seed = x"01"; + let user_addr = signer::address_of(&user); + let user_addr1 = signer::address_of(&test_user1); + let user_addr2 = signer::address_of(&test_user2); + + starcoin_framework::starcoin_account::create_account(user_addr); + starcoin_framework::starcoin_account::create_account(user_addr1); + starcoin_framework::starcoin_account::create_account(user_addr2); + + vector::push_back(&mut addresses, user_addr1); + vector::push_back(&mut addresses, user_addr2); + + vector::push_back(&mut numerators, 1); + vector::push_back(&mut numerators, 4); + + initialize(&user, seed, addresses, numerators); + + assert!(exists(user_addr), error::not_found(EACCOUNT_NOT_FOUND)); + borrow_global(user_addr).resource_addr + } + + #[test(user = @0x1111, test_user1 = @0x1112, test_user2 = @0x1113, core_framework = @starcoin_framework)] + public entry fun test_disperse(user: signer, test_user1: signer, test_user2: signer, core_framework: signer) acquires SharedAccount, SharedAccountEvent { + use starcoin_framework::starcoin_coin::{Self, STC}; + let user_addr1 = signer::address_of(&test_user1); + let user_addr2 = signer::address_of(&test_user2); + let (burn_cap, mint_cap) = starcoin_coin::initialize_for_test(&core_framework); + let resource_addr = set_up(user, test_user1, test_user2); + + let shared_account = borrow_global(resource_addr); + let resource_signer = account::create_signer_with_capability(&shared_account.signer_capability); + coin::register(&resource_signer); + coin::deposit(resource_addr, coin::mint(1000, &mint_cap)); + disperse(resource_addr); + coin::destroy_mint_cap(mint_cap); + coin::destroy_burn_cap(burn_cap); + + assert!(coin::balance(user_addr1) == 200, 0); + assert!(coin::balance(user_addr2) == 800, 1); + } + + #[test(user = @0x1111, test_user1 = @0x1112, test_user2 = @0x1113)] + #[expected_failure] + public entry fun test_disperse_insufficient_balance(user: signer, test_user1: signer, test_user2: signer) acquires SharedAccount, SharedAccountEvent { + use starcoin_framework::starcoin_coin::STC; + let resource_addr = set_up(user, test_user1, test_user2); + let shared_account = borrow_global(resource_addr); + let resource_signer = account::create_signer_with_capability(&shared_account.signer_capability); + coin::register(&resource_signer); + disperse(resource_addr); + } +}