Skip to content
This repository has been archived by the owner on Sep 5, 2022. It is now read-only.

Wallet Plugins #38

Closed
oleganza opened this issue Oct 14, 2021 · 2 comments
Closed

Wallet Plugins #38

oleganza opened this issue Oct 14, 2021 · 2 comments

Comments

@oleganza
Copy link

oleganza commented Oct 14, 2021

This is a proposal to expand automation capabilities to the wallets with "plugins": contracts that are able to use the wallet's funds.

Overview

Currently wallet V3 contract supports push model, where user's agent (a wallet app) sends an external message to the wallet that instructs it to send some internal message down to another contract. The architecture of TON network permits long chains of such messages, where some contracts react to an incoming message by creating some more outgoing messages.

This proposal adds pull model to the wallets: plugins have access to the same funds as the user's agent, but can be messaged by some other user. The other way to look at it is that we allow users dynamically extending their wallets with additional methods and storage without having to hardcode those into the wallet's contract code.

Use case: subscriptions

User deploys a "plugin" contract that permits some other party to withdraw some amount of coins from the wallet at some interval. Now instead of the user, it is the service provider that periodically messages that contract in attempt to collect the payment.

Use case: order books and liquidity curves

User may set up multiple offers in various currencies in a form of a "plugin", so that any other user may lift them and collect user's funds by sending money back to them.

Use case: joint custody and timelocks

User may allow joint custody of the funds with plugins. The mechanism is essentially the same as with subscriptions: the plugin specifies the allowance amount and the time interval, and keeps track of how much was withdrawn so far.

Proposal

Roll out a new standard contract WalletV4 that support the simplest mechanism for plugins similar to ERC20 allowances, but with unlimited allowance amount.

The security model is that user agent either knows trusted contracts, or constructs and deploys them on their own. The wallet application never needs to attempt to verify some externally provided contract code to add as a plugin. For instance, if a user wants to allocate funds to a DEX with a liquidity curve, the wallet needs to be able to construct a contract that specifies the asset ratios, deploy it and add it as a plugin to its wallet. For this reason, unlike ERC20, this proposal does not keep track of allowances: those, along with other conditions, need to be checked within plugins.

addPlugin(address) adds contract at address to the list of plugins.

removePlugin(address) removes the given contract.

listPlugins() returns the list of registered contracts.

Wallet v4 draft

;; Simple wallet smart contract with plugins

(slice, int) dict_get?(cell dict, int key_len, slice index) asm(index dict key_len) "DICTGET" "NULLSWAPIFNOT";
(cell, int) dict_add_builder?(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTADDB";
(cell, int) dict_delete?(cell dict, int key_len, slice index) asm(index dict key_len) "DICTDEL";

() recv_internal(cell in_msg_cell, slice in_msg) impure {
  var cs = in_msg_cell.begin_parse();
  var flags = cs~load_uint(4);  ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
  if (flags & 1) {
    ;; ignore all bounced messages
    return ();
  }
  if((in_msg.slice_bits() < 32) || (in_msg~load_uint(32) != 0x706c7567)) { ;; "plug" prefix
    ;; ignore all messages withour `request subscription` op
    return ();
  }
  slice s_addr = cs~load_msg_addr();
  (int wc, int addr_hash) = parse_std_addr(s_addr);
  var ds = get_data().begin_parse();
  var (unused, plugins) = (ds~load_bits(32 + 32 + 256), ds~load_dict());
  var (v, success?) = plugins.dict_get?( 8 + 256, begin_cell().store_int(wc, 8).store_uint(addr_hash, 256).end_cell().begin_parse());
  throw_unless(40, success?);
  accept_message();
  (int toncoins, cell extra) = (in_msg~load_grams(), in_msg~load_dict());
  ;; TODO check that we have enough money and send notification if not
  var msg = begin_cell()
      .store_uint(0x18, 6)
      .store_slice(s_addr)
      .store_grams(toncoins)
      .store_dict(extra)
      .store_uint(0, 4 + 4 + 64 + 32 + 1 + 1)
      .store_uint(0x706c7567,32);
    send_raw_message(msg.end_cell(), 1);
}

() recv_external(slice in_msg) impure {
  var signature = in_msg~load_bits(512);
  var cs = in_msg;
  var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(32), cs~load_uint(32), cs~load_uint(32));
  throw_if(35, valid_until <= now());
  var ds = get_data().begin_parse();
  var (stored_seqno, stored_subwallet, public_key, plugins) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256), ds~load_dict());
  ds.end_parse();
  throw_unless(33, msg_seqno == stored_seqno);
  throw_unless(34, subwallet_id == stored_subwallet);
  throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key));
  accept_message();
  cs~touch();
  int op = cs~load_uint(8);
  if (op == 0) { ;; simple send
    while (cs.slice_refs()) {
      var mode = cs~load_uint(8);
      send_raw_message(cs~load_ref(), mode);
    }
  }
  if (op == 1) { ;; deploy and install plugin
    int plugin_workchain = cs~load_int(8);
    int plugin_balance = cs~load_grams();
    (cell state_init, cell body) = (cs~load_ref(), cs~load_ref());
    int plugin_address = cell_hash(state_init);
    slice wc_n_address = begin_cell().store_int(plugin_workchain,8).store_uint(plugin_address,256).end_cell().begin_parse();
    var msg = begin_cell()
      .store_uint(0x18, 6)
      .store_uint(4,3).store_slice(wc_n_address)
      .store_grams(plugin_balance)
      .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1)
      .store_ref(state_init)
      .store_ref(body);
    send_raw_message(msg.end_cell(), 1);
    (plugins, int success?) = plugins.dict_add_builder?(8 + 256, wc_n_address, begin_cell());
    throw_unless(39, success?);
  }

  if (op == 2) { ;; install plugin
    slice wc_n_address = cs~load_bits(8 + 256);
    (plugins, int success?) = plugins.dict_add_builder?(8 + 256, wc_n_address, begin_cell());
    throw_unless(39, success?);
    ;; TODO notify plugin
  }

  if (op == 2) { ;; remove plugin
    slice wc_n_address = cs~load_bits(8 + 256);
    (plugins, int success?) = plugins.dict_delete?(8 + 256, wc_n_address);
    throw_unless(39, success?);
    ;;TODO request self-destruct
  }

  set_data(begin_cell()
    .store_uint(stored_seqno + 1, 32)
    .store_uint(stored_subwallet, 32)
    .store_uint(public_key, 256)
    .store_dict(plugins)
    .end_cell());
}

;; Get methods

int seqno() method_id {
  return get_data().begin_parse().preload_uint(32);
}

int get_public_key() method_id {
  var cs = get_data().begin_parse();
  cs~load_uint(64);
  return cs.preload_uint(256);
}

int is_plugin_installed(int wc, int addr_hash) method_id {
  var ds = get_data().begin_parse();
  var (unused, plugins) = (ds~load_bits(32 + 32 + 256), ds~load_dict());
  var (v, success?) = plugins.dict_get?( 8 + 256, begin_cell().store_int(wc, 8).store_uint(addr_hash, 256).end_cell().begin_parse());
  return success?;
}

tuple get_plugin_list() method_id {
  var list = null();
  var ds = get_data().begin_parse();
  var (unused, plugins) = (ds~load_bits(32 + 32 + 256), ds~load_dict());
  do {
    var (load_dict, wc_n_address, value, f) = plugins.dict_delete_get_min( 8 + 256 );
    f~touch();
    if (f) {
      (int wc, int addr) = (wc_n_address~load_int(8), wc_n_address~load_uint(256));
      list = cons(pair(wc, addr), list);
    }
  } until (~ f);
  return list;
}

Example of subscription plugin

;; Simple subscription plugin for wallet-v4

(int) equal_slices (slice s1, slice s2) asm "SDEQ";

;; storage$_ payer_address:MsgAddressInt 
;;           payee_address:MsgAddressInt
;;           amount:uint120 
;;           subs_period:uint32 last_payment:uint32
;;           request_timeout:uint32 last_request:uint32 = Storage;

(slice, slice, int, int, int, int, int) load_storage () {
  var ds = get_data().begin_parse();
  return
      ( ds~load_msg_addr(),
        ds~load_msg_addr(),
        ds~load_uint(120),
        ds~load_uint(32),
        ds~load_uint(32),
        ds~load_uint(32),
        ds~load_uint(32)
      );
}

() save_storage (slice payer_address, 
                 slice payee_address, 
                 int amount, int period, int last_payment,
                 int timeout, int last_request) impure {
  set_data(begin_cell()
                       .store_slice(payer_address)
                       .store_slice(payee_address)
                       .store_uint(amount, 120)
                       .store_uint(period, 32)
                       .store_uint(last_payment,32)
                       .store_uint(timeout, 32)
                       .store_uint(last_request,32)
           .end_cell());
}


() forward_funds (slice destination, int self_destruct) impure {
  if (~ self_destruct) {
    raw_reserve(1000000000, 2);
  }
  var msg = begin_cell()
      .store_uint(0x18, 6)
      .store_slice(destination)
      .store_grams(0)
      .store_dict(pair_second(get_balance()))
      .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1);
  int mode = 128;
  if (self_destruct) {
    mode += 32;
  }
  send_raw_message(msg.end_cell(), mode);
}

() request_subscription_payment(slice payer_address, int requested_amount) impure {
  var msg = begin_cell()
      .store_uint(0x18, 6)
      .store_slice(payer_address)
      .store_grams(100000000)
      .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
      .store_uint(0x706c7567, 32) ;; request op
      .store_grams(requested_amount)
      .store_uint(0,1); ;; empty extra
  send_raw_message(msg.end_cell(), 1);
}

() recv_internal(int msg_value, cell in_msg_cell, slice in_msg) impure {
  var cs = in_msg_cell.begin_parse();
  var flags = cs~load_uint(4);
  slice s_addr = cs~load_msg_addr();

  (slice payer_address, slice payee_address, 
   int amount, int period, int last_payment,
   int timeout, int last_request) =
    load_storage();
  if(last_request == 0) {
    request_subscription_payment(payer_address, amount);
    save_storage(payer_address, payee_address, amount, period, last_payment, timeout, now());
  }
  
  if ( ~ equal_slices(s_addr, payer_address)) {
    ;; proxy all funds to payer_address
    ;; TODO check whether here should be payee_address
    return forward_funds(payer_address, 0);
  }
  ;; funds from payer
  int op = in_msg~load_uint(32);
  if(op == 0xde511201) { ;; request to destroy
    return forward_funds(payee_address, -1);
  }
  if(op == 0x706c7567) { ;; plugin access
    if(last_payment + period > now()) {
      ;; new payment came too soon
      ;; result of lags in network when new request for payment was emitted
      ;; before prev one was processed and as result to payment were generated
      return forward_funds(payer_address, 0);
    }
    ;;TODO should we check and compare arrived funds with amount???
    forward_funds(payee_address, 0);
    return save_storage(payer_address, payee_address, amount, period, now(), timeout, last_request);
  }
}

() recv_external(slice in_msg) impure {
  (slice payer_address, slice payee_address, 
   int amount, int period, int last_payment,
   int timeout, int last_request) =
    load_storage();
  throw_unless(130, (last_request + timeout < now()) & (last_payment + period < now()));
  return request_subscription_payment(payer_address, amount);
}

;; Get methods

([int, int],[int, int], int, int, int, int, int) get_subscription_data() method_id {
  (slice payer_address, slice payee_address, 
   int amount, int period, int last_payment,
   int timeout, int last_request) =
    load_storage();
  (int mwc, int mad) = parse_std_addr(payer_address);
  (int dwc, int dad) = parse_std_addr(payee_address);
  return (pair(mwc, mad), pair(dwc, dad), amount, period, last_payment, timeout, last_request);
}
@tolya-yanot
Copy link
Member

@oleganza
Copy link
Author

One more thing: it would be good to support "self-destruct" for plugins. That is, if plugin can authorise removal of itself from the user's wallet. This would permit other parties to terminate their access to the wallet without user's involvement.

Examples:

  1. "One-shot delegate". If a plugin is setup with an escrow to permit access to some funds once, it would be a nice UX if the user authorized this once, and then the counterparty would unlock the funds and cleanup the setup in one step.
  2. Safety. Let's say a subscription service got their keys leaked and does not want the attacker to collect funds from their subscribers. It would be much safer for the users if the service could unilaterally terminate subscriptions without having to send out emails with warnings that no one reads.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

2 participants