Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Examples] Adds closed loop module #14434

Merged
merged 9 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
new field in the ActionRequest - allows burning balances
  • Loading branch information
damirka committed Oct 26, 2023
commit 0042490a756f30c6cbf5b19eda5b49c8b3e8b9e3
6 changes: 5 additions & 1 deletion examples/move/closed_loop/examples/loyalty.move
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ module 0x0::loyalty {
/// Handy function to reward users. Can be called by the application admin
/// to reward users for their loyalty :)
///
/// Uses two actions: `from_coin` to mint the
/// `Mint` is available to the holder of the `TreasuryCap` by default and
/// hence does not need to be confirmed; however, for `transfer` operation
/// we require a confirmation.
public fun reward_user(
cap: &mut TreasuryCap<LOYALTY>,
amount: u64,
Expand All @@ -86,6 +88,8 @@ module 0x0::loyalty {
}

/// Buy a gift for 10 tokens.
///
/// We require a `TokenPolicy` since
damirka marked this conversation as resolved.
Show resolved Hide resolved
public fun buy_a_gift(
token: Token<LOYALTY>,
ctx: &mut TxContext
Expand Down
8 changes: 8 additions & 0 deletions examples/move/closed_loop/notes.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
One of the challenges in the Closed Loop request system is that most of the Coin-like systems don't allow us to store any state (unlike Kiosk, where Kiosk is the state and a container for arbitrary data - eg Extensions).

- we want to have a state for applications such as allowlist / denylist
- to keep the implementation as generic as possible we need to eliminate the need for discovery
- every user action already requires `TokenPolicy` - we can utilize it for storage
- we don't want to have too many dynamic fields (while better collection types are still on the way, a dummy implementation is okay for the time being, however long term we'd want to replace them)


62 changes: 47 additions & 15 deletions examples/move/closed_loop/sources/closed_loop.move
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ module closed_loop::closed_loop {
const EBalanceTooLow: u64 = 4;
/// The balance is not zero.
const ENotZero: u64 = 5;
/// The balance is not zero when trying to confirm with `TransferPolicyCap`.
const ECantConsumeBalance: u64 = 6;

/// A Tag for the `burn` action. Unlike other tags, it's not a part of the
/// default flow, only recommended for the issuer to use.
Expand Down Expand Up @@ -91,6 +93,8 @@ module closed_loop::closed_loop {
sender: address,
/// Recipient is only available in transfer operation
recipient: Option<address>,
/// The balance to be "burned" in the `TokenPolicy`.
damirka marked this conversation as resolved.
Show resolved Hide resolved
burned_balance: Option<Balance<T>>,
/// Collected approvals from Rules.
approvals: VecSet<TypeName>,
}
Expand Down Expand Up @@ -123,21 +127,20 @@ module closed_loop::closed_loop {
let amount = balance::value(&t.balance);
transfer::transfer(t, recipient);

new_request(string::utf8(TRANSFER), amount, option::some(recipient), ctx)
new_request(string::utf8(TRANSFER), amount, option::some(recipient), option::none(), ctx)
}

/// Spend a `Token` by converting it into a `SpentToken`.
public fun spend<T>(t: Token<T>, ctx: &mut TxContext): ActionRequest<T> {
let Token { id, balance } = t;
let amount = balance::value(&balance);

object::delete(id);
transfer::transfer(
SpentToken { id: object::new(ctx), balance },
tx_context::sender(ctx)
);

new_request(string::utf8(SPEND), amount, option::none(), ctx)
new_request(
string::utf8(SPEND),
balance::value(&balance),
option::none(),
option::some(balance),
ctx
)
}

/// Convert a `Token` into an open `Coin`.
Expand All @@ -150,7 +153,7 @@ module closed_loop::closed_loop {

(
coin::from_balance(balance, ctx),
new_request(string::utf8(TO_COIN), amount, option::none(), ctx)
new_request(string::utf8(TO_COIN), amount, option::none(), option::none(), ctx)
)
}

Expand All @@ -168,6 +171,7 @@ module closed_loop::closed_loop {
string::utf8(FROM_COIN),
amount,
option::none(),
option::none(),
ctx
)
)
Expand Down Expand Up @@ -214,12 +218,14 @@ module closed_loop::closed_loop {
name: String,
amount: u64,
recipient: Option<address>,
burned_balance: Option<Balance<T>>,
ctx: &TxContext
): ActionRequest<T> {
ActionRequest {
name,
amount,
recipient,
burned_balance,
sender: tx_context::sender(ctx),
approvals: vec_set::empty(),
}
Expand All @@ -228,13 +234,14 @@ module closed_loop::closed_loop {
/// Confirm the request against the `TokenPolicy` and return the parameters
/// of the request: (Name, Amount, Sender, Recipient).
public fun confirm_request<T>(
policy: &TokenPolicy<T>, request: ActionRequest<T>, _ctx: &mut TxContext
policy: &mut TokenPolicy<T>, request: ActionRequest<T>, _ctx: &mut TxContext
): (String, u64, address, Option<address>) {
assert!(vec_map::contains(&policy.rules, &request.name), EUnknownAction);

let ActionRequest {
name, approvals,
amount, sender, recipient
burned_balance,
amount, sender, recipient,
} = request;

let rules = vec_map::get(&policy.rules, &name);
Expand All @@ -249,6 +256,15 @@ module closed_loop::closed_loop {
i = i + 1;
};

if (option::is_some(&burned_balance)) {
balance::join(
&mut policy.burned_balance,
option::destroy_some(burned_balance)
);
} else {
option::destroy_none(burned_balance);
};

(name, amount, sender, recipient)
}

Expand All @@ -258,13 +274,24 @@ module closed_loop::closed_loop {
/// TODO: consider `&mut on TreasuryCap` as a preemptive measure and/or as
/// a way to guarantee that `TreasuryCap` is not frozen.
public fun confirm_with_treasury_cap<T>(
_treasury_cap: &TreasuryCap<T>,
treasury_cap: &mut TreasuryCap<T>,
request: ActionRequest<T>,
_ctx: &mut TxContext
): (String, u64, address, Option<address>) {
let ActionRequest {
name, amount, sender, recipient, approvals: _
name, amount, sender, recipient, approvals: _,
burned_balance
} = request;

if (option::is_some(&burned_balance)) {
balance::decrease_supply(
coin::supply_mut(treasury_cap),
option::destroy_some(burned_balance)
);
} else {
option::destroy_none(burned_balance);
};

(name, amount, sender, recipient)
}

Expand All @@ -278,9 +305,14 @@ module closed_loop::closed_loop {
request: ActionRequest<T>,
_ctx: &mut TxContext
): (String, u64, address, Option<address>) {
assert!(option::is_none(&request.burned_balance), ECantConsumeBalance);

let ActionRequest {
name, amount, sender, recipient, approvals: _
name, amount, sender, recipient, approvals: _, burned_balance
} = request;

option::destroy_none(burned_balance);

(name, amount, sender, recipient)
}

Expand Down
18 changes: 12 additions & 6 deletions examples/move/closed_loop/tests/closed_loop_tests.move
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,23 @@ module closed_loop::closed_loop_tests {

closed_loop::add_rules_for_testing(&mut policy, string::utf8(b"test"), vector[]);

let req = closed_loop::new_request(string::utf8(b"test"), 100, option::none(), ctx);
let req = closed_loop::new_request(
string::utf8(b"test"), 100, option::none(), option::none(), ctx
);

closed_loop::confirm_request(&policy, req, ctx);
closed_loop::confirm_request(&mut policy, req, ctx);
return_policy(policy)
}

#[test, expected_failure(abort_code = 0x0::closed_loop::EUnknownAction)]
fun test_confirm_request_unknown_action_fail() {
let ctx = &mut tx_context::dummy();
let policy = get_policy(ctx);
let req = closed_loop::new_request(string::utf8(b"test"), 100, option::none(), ctx);
let req = closed_loop::new_request(
string::utf8(b"test"), 100, option::none(), option::none(), ctx
);

closed_loop::confirm_request(&policy, req, ctx);
closed_loop::confirm_request(&mut policy, req, ctx);
return_policy(policy)
}

Expand All @@ -48,12 +52,14 @@ module closed_loop::closed_loop_tests {
vector[type_name::get<Rule1>()]
);

let req = closed_loop::new_request(string::utf8(b"test"), 100, option::none(), ctx);
let req = closed_loop::new_request(
string::utf8(b"test"), 100, option::none(), option::none(), ctx
);

closed_loop::add_approval(Rule1 {}, &mut req, ctx);
closed_loop::add_approval(Rule2 {}, &mut req, ctx);

closed_loop::confirm_request(&policy, req, ctx);
closed_loop::confirm_request(&mut policy, req, ctx);
return_policy(policy)
}

Expand Down