Shared vs Owned Objects
Owned objects:- Owned by a single address
- Fast - no consensus required
- Suitable for wallets, NFTs, personal items
- Accessible by anyone
- Require consensus
- Necessary for multi-user applications
Creating Shared Objects
Usetransfer::share_object to make an object shared:
module my_package::counter {
public struct Counter has key {
id: UID,
value: u64,
}
fun init(ctx: &mut TxContext) {
let counter = Counter {
id: object::new(ctx),
value: 0,
};
// Make it shared so anyone can increment
transfer::share_object(counter);
}
public fun increment(counter: &mut Counter) {
counter.value = counter.value + 1;
}
}
Real Example: Cash Register
Fromexamples/move/transfer-to-object/shared-no-tto:
module shared_no_tto::shared_cash_register {
use sui::coin::Coin;
use sui::sui::SUI;
use sui::dynamic_field;
use sui::vec_set::{Self, VecSet};
use std::string::String;
const EInvalidOwner: u64 = 0;
const EInvalidPaymentID: u64 = 1;
const ENotAuthorized: u64 = 2;
/// Shared cash register for a business
public struct CashRegister has key {
id: UID,
authorized_individuals: VecSet<address>,
business_name: String,
register_owner: address,
}
/// Create a shared cash register
public fun create_cash_register(
mut authorized_individuals_vec: vector<address>,
business_name: String,
ctx: &mut TxContext,
) {
let mut authorized_individuals = vec_set::empty();
while (!vector::is_empty(&authorized_individuals_vec)) {
let addr = vector::pop_back(&mut authorized_individuals_vec);
vec_set::insert(&mut authorized_individuals, addr);
};
let register = CashRegister {
id: object::new(ctx),
authorized_individuals,
business_name,
register_owner: tx_context::sender(ctx),
};
transfer::share_object(register);
}
/// Transfer ownership
public fun transfer_cash_register_ownership(
register: &mut CashRegister,
new_owner: address,
ctx: &TxContext,
) {
assert!(register.register_owner == tx_context::sender(ctx), EInvalidOwner);
register.register_owner = new_owner;
}
/// Process payment (authorized only)
public fun process_payment(
register: &mut CashRegister,
payment_id: u64,
ctx: &TxContext,
): Coin<SUI> {
let sender = tx_context::sender(ctx);
assert!(
vec_set::contains(®ister.authorized_individuals, &sender)
|| sender == register.register_owner,
ENotAuthorized,
);
assert!(dynamic_field::exists_(®ister.id, payment_id), EInvalidPaymentID);
let payment = dynamic_field::remove(&mut register.id, payment_id);
payment
}
/// Make a payment (anyone can pay)
public fun pay(
register: &mut CashRegister,
payment_id: u64,
coin: Coin<SUI>,
ctx: &mut TxContext
) {
dynamic_field::add(&mut register.id, payment_id, coin);
}
}
Access Control Patterns
Owner-only functions
public struct SharedVault has key {
id: UID,
owner: address,
balance: u64,
}
public fun withdraw(
vault: &mut SharedVault,
amount: u64,
ctx: &TxContext
) {
assert!(vault.owner == ctx.sender(), ENotOwner);
vault.balance = vault.balance - amount;
}
Role-based access
use sui::vec_set::{Self, VecSet};
public struct DAO has key {
id: UID,
admins: VecSet<address>,
members: VecSet<address>,
}
public fun admin_action(
dao: &mut DAO,
ctx: &TxContext
) {
assert!(vec_set::contains(&dao.admins, &ctx.sender()), ENotAdmin);
// Admin logic...
}
public fun member_action(
dao: &mut DAO,
ctx: &TxContext
) {
assert!(vec_set::contains(&dao.members, &ctx.sender()), ENotMember);
// Member logic...
}
Capability-based access
public struct Pool has key {
id: UID,
balance: u64,
}
public struct AdminCap has key, store {
id: UID,
pool_id: ID,
}
public fun withdraw(
pool: &mut Pool,
cap: &AdminCap,
amount: u64,
) {
assert!(object::id(pool) == cap.pool_id, EInvalidCap);
pool.balance = pool.balance - amount;
}
Flash Loan Example
Fromexamples/move/flash_lender:
module flash_lender::example {
use sui::balance::{Self, Balance};
use sui::coin::{Self, Coin};
/// Shared flash lender
public struct FlashLender<phantom T> has key {
id: UID,
to_lend: Balance<T>,
fee: u64,
}
/// Hot potato receipt - must be repaid
public struct Receipt<phantom T> {
flash_lender_id: ID,
repay_amount: u64,
}
/// Admin capability
public struct AdminCap has key, store {
id: UID,
flash_lender_id: ID,
}
const ELoanTooLarge: u64 = 0;
const EInvalidRepaymentAmount: u64 = 1;
const ERepayToWrongLender: u64 = 2;
/// Create a flash lender
public fun new<T>(
to_lend: Balance<T>,
fee: u64,
ctx: &mut TxContext
): AdminCap {
let id = object::new(ctx);
let flash_lender_id = id.uid_to_inner();
let flash_lender = FlashLender { id, to_lend, fee };
transfer::share_object(flash_lender);
AdminCap { id: object::new(ctx), flash_lender_id }
}
/// Borrow funds
public fun loan<T>(
self: &mut FlashLender<T>,
amount: u64,
ctx: &mut TxContext,
): (Coin<T>, Receipt<T>) {
assert!(self.to_lend.value() >= amount, ELoanTooLarge);
let loan = coin::take(&mut self.to_lend, amount, ctx);
let repay_amount = amount + self.fee;
let receipt = Receipt {
flash_lender_id: object::id(self),
repay_amount,
};
(loan, receipt)
}
/// Repay loan
public fun repay<T>(
self: &mut FlashLender<T>,
payment: Coin<T>,
receipt: Receipt<T>
) {
let Receipt { flash_lender_id, repay_amount } = receipt;
assert!(object::id(self) == flash_lender_id, ERepayToWrongLender);
assert!(payment.value() == repay_amount, EInvalidRepaymentAmount);
coin::put(&mut self.to_lend, payment)
}
}
Testing Shared Objects
#[test]
fun test_shared_counter() {
use sui::test_scenario as ts;
let admin = @0xAD;
let alice = @0xA;
let bob = @0xB;
let mut scenario = ts::begin(admin);
// Create shared counter
{
let counter = Counter {
id: object::new(scenario.ctx()),
value: 0,
};
transfer::share_object(counter);
};
// Alice increments
scenario.next_tx(alice);
{
let mut counter = scenario.take_shared<Counter>();
increment(&mut counter);
assert!(counter.value == 1, 0);
ts::return_shared(counter);
};
// Bob increments
scenario.next_tx(bob);
{
let mut counter = scenario.take_shared<Counter>();
increment(&mut counter);
assert!(counter.value == 2, 1);
ts::return_shared(counter);
};
scenario.end();
}
Performance Considerations
Minimize shared object usage
Use owned objects when possible:// Good: Personal wallet (owned)
public struct Wallet has key {
id: UID,
owner: address,
balance: u64,
}
// Good: Global liquidity pool (shared)
public struct Pool has key {
id: UID,
reserves: Balance<SUI>,
}
Avoid contention
Design to minimize concurrent access:// Avoid: Single global counter (high contention)
public struct GlobalCounter has key {
id: UID,
value: u64,
}
// Better: Per-user counters (low contention)
public struct UserCounter has key {
id: UID,
owner: address,
value: u64,
}
Common Patterns
Registry pattern
public struct Registry has key {
id: UID,
items: Table<address, Item>,
}
public fun register(
registry: &mut Registry,
item: Item,
ctx: &TxContext
) {
table::add(&mut registry.items, ctx.sender(), item);
}
Pool pattern
public struct LiquidityPool has key {
id: UID,
token_a: Balance<TokenA>,
token_b: Balance<TokenB>,
}
public fun swap_a_to_b(
pool: &mut LiquidityPool,
input: Coin<TokenA>,
ctx: &mut TxContext
): Coin<TokenB> {
// Swap logic...
}
Best Practices
1. Use shared objects sparingly
Only when multiple users need concurrent access.2. Implement proper access control
Always validate callers:public fun admin_function(
obj: &mut SharedObject,
ctx: &TxContext
) {
assert!(obj.admin == ctx.sender(), ENotAuthorized);
// ...
}
3. Document shared object semantics
/// Shared pool accessible by all users.
/// - Anyone can deposit
/// - Only owner can withdraw
public struct Pool has key { /* ... */ }
4. Consider object composition
Combine shared and owned objects:// Shared pool
public struct Pool has key { /* ... */ }
// Owned receipt
public struct DepositReceipt has key, store {
id: UID,
pool_id: ID,
amount: u64,
}