forked from MystenLabs/sui
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
change to comments add error names add comments changes f f
- Loading branch information
1 parent
8d60797
commit ff7c93f
Showing
2 changed files
with
324 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
// Copyright (c) 2022, Mysten Labs, Inc. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
/// The Safe standard is a minimalistic shared wrapper around a coin. It provides a way for users to provide third-party dApps with | ||
/// the capability to transfer coins away from their wallets, if they are provided with the correct permission. | ||
module sui::safe { | ||
use sui::object::{Self, ID, UID}; | ||
use sui::tx_context::{TxContext, sender}; | ||
use sui::transfer::Self; | ||
use sui::balance::{Self, Balance}; | ||
use sui::coin::{Self, Coin}; | ||
use sui::vec_set::{Self, VecSet}; | ||
|
||
const MAX_CAPABILITY_ISSUABLE: u64 = 1000; | ||
|
||
// Errors | ||
const INVALID_TRANSFER_CAPABILITY: u64 = 0; | ||
const INVALID_OWNER_CAPABILITY: u64 = 1; | ||
const TRANSFER_CAPABILITY_REVOKED: u64 = 2; | ||
const OVERDRAWN: u64 = 3; | ||
|
||
|
||
/// | ||
/// Allows any holder of a capability to transfer a fixed amount of assets from the safe. | ||
/// Useful in situations like an NFT marketplace where you wish to buy the NFTs at a specific price. | ||
/// | ||
/// @ownership: Shared | ||
/// | ||
struct Safe<phantom T> has key { | ||
id: UID, | ||
balance: Balance<T>, | ||
allowed_safes: VecSet<ID>, | ||
} | ||
|
||
struct OwnerCapability<phantom T> has key, store { | ||
id: UID, | ||
safe_id: ID, | ||
} | ||
|
||
/// | ||
/// Allows the owner of the capability to take `amount` of coins from the box. | ||
/// | ||
/// @ownership: Owned | ||
/// | ||
struct TransferCapability<phantom T> has store, key { | ||
id: UID, | ||
safe_id: ID, | ||
// The amount that the user is able to transfer. | ||
amount: u64, | ||
} | ||
|
||
////////////////////////////////////////////////////// | ||
/// HELPER FUNCTIONS | ||
////////////////////////////////////////////////////// | ||
|
||
/// Check that the capability has not yet been revoked by the owner. | ||
fun check_capability_validity<T>(safe: &Safe<T>, capability: &TransferCapability<T>) { | ||
// Check that the ids match | ||
assert!(object::id(safe) == capability.safe_id, INVALID_TRANSFER_CAPABILITY); | ||
// Check that it has not been cancelled | ||
assert!(vec_set::contains(&safe.allowed_safes, &object::id(capability)), TRANSFER_CAPABILITY_REVOKED); | ||
} | ||
|
||
fun check_owner_capability_validity<T>(safe: &Safe<T>, capability: &OwnerCapability<T>) { | ||
assert!(object::id(safe) == capability.safe_id, INVALID_OWNER_CAPABILITY); | ||
} | ||
|
||
/// Helper function to create a capability. | ||
fun create_capability_<T>(safe: &mut Safe<T>, withdraw_amount: u64, ctx: &mut TxContext): TransferCapability<T> { | ||
let cap_id = object::new(ctx); | ||
vec_set::insert(&mut safe.allowed_safes, object::uid_to_inner(&cap_id)); | ||
|
||
let capability = TransferCapability { | ||
id: cap_id, | ||
safe_id: object::uid_to_inner(&safe.id), | ||
amount: withdraw_amount, | ||
}; | ||
|
||
capability | ||
} | ||
|
||
////////////////////////////////////////////////////// | ||
/// PUBLIC FUNCTIONS | ||
////////////////////////////////////////////////////// | ||
|
||
public fun balance<T>(safe: &Safe<T>): &Balance<T> { | ||
&safe.balance | ||
} | ||
|
||
/// Wrap a coin around a safe. | ||
/// a trusted party (or smart contract) to transfer the object out. | ||
public fun create_<T>(balance: Balance<T>, ctx: &mut TxContext): OwnerCapability<T> { | ||
let safe = Safe { | ||
id: object::new(ctx), | ||
balance, | ||
allowed_safes: vec_set::empty(), | ||
}; | ||
let cap = OwnerCapability { | ||
id: object::new(ctx), | ||
safe_id: object::id(&safe), | ||
}; | ||
transfer::share_object(safe); | ||
cap | ||
} | ||
|
||
public entry fun create<T>(coin: Coin<T>, ctx: &mut TxContext) { | ||
let balance = coin::into_balance(coin); | ||
let cap = create_<T>(balance, ctx); | ||
transfer::transfer(cap, sender(ctx)); | ||
} | ||
|
||
public entry fun create_empty<T>(ctx: &mut TxContext) { | ||
let empty_balance = balance::zero<T>(); | ||
let cap = create_(empty_balance, ctx); | ||
transfer::transfer(cap, sender(ctx)); | ||
} | ||
|
||
/// Deposit funds to the safe | ||
public fun deposit_<T>(safe: &mut Safe<T>, balance: Balance<T>) { | ||
balance::join(&mut safe.balance, balance); | ||
} | ||
|
||
/// Deposit funds to the safe | ||
public entry fun deposit<T>(safe: &mut Safe<T>, coin: Coin<T>) { | ||
let balance = coin::into_balance(coin); | ||
deposit_<T>(safe, balance); | ||
} | ||
|
||
/// Withdraw coins from the safe as a `OwnerCapability` holder | ||
public fun withdraw_<T>(safe: &mut Safe<T>, capability: &OwnerCapability<T>, withdraw_amount: u64): Balance<T> { | ||
// Ensures that only the owner can withdraw from the safe. | ||
check_owner_capability_validity(safe, capability); | ||
balance::split(&mut safe.balance, withdraw_amount) | ||
} | ||
|
||
/// Withdraw coins from the safe as a `OwnerCapability` holder | ||
public entry fun withdraw<T>(safe: &mut Safe<T>, capability: &OwnerCapability<T>, withdraw_amount: u64, ctx: &mut TxContext) { | ||
let balance = withdraw_(safe, capability, withdraw_amount); | ||
let coin = coin::from_balance(balance, ctx); | ||
transfer::transfer(coin, sender(ctx)); | ||
} | ||
|
||
/// Withdraw coins from the safe as a `TransferCapability` holder. | ||
public fun debit<T>(safe: &mut Safe<T>, capability: &mut TransferCapability<T>, withdraw_amount: u64): Balance<T> { | ||
// Check the validity of the capability | ||
check_capability_validity(safe, capability); | ||
|
||
// Withdraw funds | ||
assert!(capability.amount >= withdraw_amount, OVERDRAWN); | ||
capability.amount = capability.amount - withdraw_amount; | ||
balance::split(&mut safe.balance, withdraw_amount) | ||
} | ||
|
||
/// Revoke a `TransferCapability` as an `OwnerCapability` holder | ||
public entry fun revoke_transfer_capability<T>(safe: &mut Safe<T>, capability: &OwnerCapability<T>, capability_id: ID) { | ||
// Ensures that only the owner can withdraw from the safe. | ||
check_owner_capability_validity(safe, capability); | ||
vec_set::remove(&mut safe.allowed_safes, &capability_id); | ||
} | ||
|
||
/// Revoke a `TransferCapability` as its owner | ||
public entry fun self_revoke_transfer_capability<T>(safe: &mut Safe<T>, capability: &TransferCapability<T>) { | ||
check_capability_validity(safe, capability); | ||
vec_set::remove(&mut safe.allowed_safes, &object::id(capability)); | ||
} | ||
|
||
/// Create `TransferCapability` as an `OwnerCapability` holder | ||
public fun create_transfer_capability<T>(safe: &mut Safe<T>, capability: &OwnerCapability<T>, withdraw_amount: u64, ctx: &mut TxContext): TransferCapability<T> { | ||
// Ensures that only the owner can withdraw from the safe. | ||
check_owner_capability_validity(safe, capability); | ||
create_capability_(safe, withdraw_amount, ctx) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
// Copyright (c) 2022, Mysten Labs, Inc. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
#[test_only] | ||
module sui::safe_tests { | ||
use sui::safe::{Self, Safe, TransferCapability, OwnerCapability}; | ||
use sui::test_scenario::{Self as ts, Scenario, ctx}; | ||
use sui::coin; | ||
use sui::object::{Self, ID}; | ||
use sui::balance; | ||
use sui::sui::SUI; | ||
use sui::transfer; | ||
|
||
fun create_safe(scenario: &mut Scenario, owner: address, stored_amount: u64) { | ||
ts::next_tx(scenario, &owner); | ||
{ | ||
let coin = coin::mint_for_testing<SUI>(stored_amount, ctx(scenario)); | ||
safe::create(coin, ctx(scenario)); | ||
}; | ||
} | ||
|
||
// Delegates the safe to delegatee and return the capability ID. | ||
fun delegate_safe(scenario: &mut Scenario, owner: address, delegate_to: address, delegate_amount: u64): ID { | ||
let id; | ||
ts::next_tx(scenario, &owner); | ||
{ | ||
let safe_wrapper = ts::take_shared<Safe<SUI>>(scenario); | ||
let safe = ts::borrow_mut(&mut safe_wrapper); | ||
let cap = ts::take_owned<OwnerCapability<SUI>>(scenario); | ||
|
||
let capability = safe::create_transfer_capability(safe, &cap, delegate_amount, ctx(scenario)); | ||
id = object::id(&capability); | ||
|
||
transfer::transfer(capability, delegate_to); | ||
|
||
ts::return_owned(scenario, cap); | ||
ts::return_shared(scenario, safe_wrapper); | ||
id | ||
}; | ||
|
||
id | ||
} | ||
|
||
fun withdraw_as_delegatee(scenario: &mut Scenario, delegatee: address, withdraw_amount: u64) { | ||
ts::next_tx(scenario, &delegatee); | ||
{ | ||
let safe_wrapper = ts::take_shared<Safe<SUI>>(scenario); | ||
let safe = ts::borrow_mut(&mut safe_wrapper); | ||
let capability = ts::take_owned<TransferCapability<SUI>>(scenario); | ||
|
||
let balance = safe::debit(safe, &mut capability, withdraw_amount); | ||
|
||
balance::destroy_for_testing(balance); | ||
|
||
ts::return_shared(scenario, safe_wrapper); | ||
ts::return_owned(scenario, capability); | ||
}; | ||
} | ||
|
||
fun revoke_capability(scenario: &mut Scenario, owner: address, capability_id: ID) { | ||
ts::next_tx(scenario, &owner); | ||
{ | ||
let safe_wrapper = ts::take_shared<Safe<SUI>>(scenario); | ||
let safe = ts::borrow_mut(&mut safe_wrapper); | ||
let cap = ts::take_owned<OwnerCapability<SUI>>(scenario); | ||
|
||
safe::revoke_transfer_capability(safe, &cap, capability_id); | ||
|
||
ts::return_owned(scenario, cap); | ||
ts::return_shared(scenario, safe_wrapper); | ||
}; | ||
} | ||
|
||
#[test] | ||
/// Ensure that all funds can be withdrawn by the owners | ||
fun test_safe_create_and_withdraw_funds_as_owner() { | ||
let owner = @1337; | ||
let scenario = &mut ts::begin(&@0x1); | ||
|
||
let initial_funds = 1000u64; | ||
create_safe(scenario, owner, initial_funds); | ||
|
||
ts::next_tx(scenario, &owner); | ||
{ | ||
let safe_wrapper = ts::take_shared<Safe<SUI>>(scenario); | ||
let safe = ts::borrow_mut(&mut safe_wrapper); | ||
let cap = ts::take_owned<OwnerCapability<SUI>>(scenario); | ||
|
||
let balance = safe::withdraw_(safe, &cap, initial_funds); | ||
balance::destroy_for_testing(balance); | ||
|
||
ts::return_owned(scenario, cap); | ||
ts::return_shared(scenario, safe_wrapper); | ||
} | ||
} | ||
|
||
#[test] | ||
/// Ensure that all funds can be withdrawn to a delegator | ||
fun test_safe_create_and_withdraw_funds_as_delegatee() { | ||
let owner = @0x1337; | ||
let delegatee = @0x1ce1ce1ce; | ||
let scenario = &mut ts::begin(&@0x1); | ||
|
||
let initial_funds = 1000u64; | ||
let delegated_funds = 1000u64; | ||
// Create Safe | ||
create_safe(scenario, owner, initial_funds); | ||
delegate_safe(scenario, owner, delegatee, delegated_funds); | ||
withdraw_as_delegatee(scenario, delegatee, delegated_funds); | ||
} | ||
|
||
#[test] | ||
#[expected_failure(abort_code = 3)] | ||
/// Ensure that funds cannot be over withdrawn | ||
fun test_safe_attempt_to_over_withdraw() { | ||
let owner = @0x1337; | ||
let delegatee = @0x1ce1ce1ce; | ||
let scenario = &mut ts::begin(&@0x1); | ||
|
||
let initial_funds = 1000u64; | ||
let delegated_funds = 1000u64; | ||
// Create Safe | ||
create_safe(scenario, owner, initial_funds); | ||
delegate_safe(scenario, owner, delegatee, delegated_funds); | ||
|
||
// Withdraw all funds | ||
withdraw_as_delegatee(scenario, delegatee, delegated_funds); | ||
// Attempt to withdraw by 1 coin. | ||
withdraw_as_delegatee(scenario, delegatee, 1); | ||
} | ||
|
||
#[test] | ||
#[expected_failure(abort_code = 2)] | ||
/// Ensure that funds cannot be over withdrawn | ||
fun test_safe_withdraw_revoked() { | ||
let owner = @0x1337; | ||
let delegatee = @0x1ce1ce1ce; | ||
let scenario = &mut ts::begin(&@0x1); | ||
|
||
let initial_funds = 1000u64; | ||
let delegated_funds = 1000u64; | ||
// Create Safe | ||
create_safe(scenario, owner, initial_funds); | ||
let capability_id = delegate_safe(scenario, owner, delegatee, delegated_funds); | ||
|
||
revoke_capability(scenario, owner, capability_id); | ||
|
||
// Withdraw funds | ||
withdraw_as_delegatee(scenario, delegatee, delegated_funds); | ||
} | ||
} |