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);
}
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 ataddress
to the list of plugins.removePlugin(address)
removes the given contract.listPlugins()
returns the list of registered contracts.Wallet v4 draft
Example of subscription plugin
Originally posted by @oleganza in https://github.com/ton-blockchain/TIPs/issues/38
Originally posted by @Volimjasnu12 in https://github.com/ton-blockchain/minter-contract/issues/111
Originally posted by @Volimjasnu12 in https://github.com/ton-blockchain/ton/issues/1256