bitshares / bsips

BitShares Improvement Proposals and Protocols. These technical documents describe the process of updating and improving the BitShares blockchain and technical ecosystem.
https://bitshares.github.io
63 stars 86 forks source link

BSIP72: Tanks and Taps: A General Solution for Smart Contract Asset Handling #178

Open nathanielhourt opened 5 years ago

nathanielhourt commented 5 years ago

This text is now outdated.

For an up-to-date copy of BSIP 72, please see here. The following draft is preserved for historical purposes.


BSIP: TBD
Title: Tanks and Taps: A General Solution for Smart Contract Asset Handling
Authors: Nathan Hourt <nat.hourt@gmail.com>
Status: Draft
Type: Protocol
Created: 2019-06-28
Discussion: https://github.com/bitshares/bsips/issues/178

Abstract

This BSIP proposes the addition of novel, generally-applicable asset handling functionality to BitShares. These additions will support the development of a wide range of financial decentralized applications (dapps), advancing BitShares as an alternative platform to existing Turing Complete Smart Contract Platforms (TCSCPs) for dapp development. Although TCSCPs will continue to offer more flexible capabilities, the proposed infrastructure will offer a simple, ready-to-use interface to dapp developers which will facilitate more rapid development and a reduced time to market compared with generic Turing Complete platforms, which require dapps to define their own infrastructure. Furthermore, dapps utilizing the proposed infrastructure will share a consistent, easy-to-visualize asset handling model which will become increasingly familiar to users of dapps based on BitShares, giving them confidence when using BitShares dapps that they understand how their assets are being handled and what options are available to them under contract.

Context and Motivation

One of the main promises of blockchain technology is the processing and execution of "smart contracts," a formalized agreement between parties where the terms are evaluated and enforced automatically rather than relying on trusted intermediaries. Initially, there was Bitcoin, which offers a simple smart contract definition language allowing the transfer of digital tokens between cryptographic keys. Subsequently, the industry has created multiple Turing Complete Smart Contracting Platforms (TCSCPs), such as Ethereum and EOS, in order to provide more advanced smart contract definition languages which can specify any programmable algorithm as a smart contract.

BitShares was created as a financial services smart contracting platform, providing high-performance decentralized financial contracts including an asset exchange, stable-valued assets, recurring payments, and an advanced multi-signature named account system. This curated selection of contracts is developed and supported by a highly competent team of developers who are committed to the quality, security, and correctness of the supported smart contracts. These assurances of contract quality give BitShares an advantage over TCSCPs, where contracts are user-contributed with no expectation of testing or correctness standards; however, to date, neither the more flexible Turing Complete model nor the quality-assured Curated model have clearly succeeded, either in terms of developer support or user adoption.

This proposal is to implement within BitShares a general framework for smart contract asset handling, capable of supporting a great many decentralized applications (dapps) and real world smart contracts. The benefit of such a uniform asset handling framework, as opposed to the TCSCP approach of generally supporting any and all potential asset management designs, is twofold. Firstly, it provides, across all BitShares dapps, a simple and consistent representation of user funds, making it easy for users to understand and reason about the status of assets within the contract, and what options they have for moving assets through the contract. Secondly, specifying a particular structure for asset handling gives developers a clear path for implementing their dapp's asset management, reducing their task from the creation of a novel asset handling architecture for their specific dapp, to merely expressing their dapp's asset movement possibilities within an already-implemented and battle-tested framework.

With this asset handling infrastructure implemented, BitShares would enjoy significant advantages over TCSCPs for dapp development. A great deal of the complexity of designing and implementing dapps which handle user funds lies in the creation of secure mechanisms for holding funds within the contract, and the represention of these mechanisms to the user in a comprehensible fashion. By providing a simple and consistent framework for asset handling, BitShares offers developers a simpler and faster implementation path by providing sturdy and supported infrastructure for asset handling which provides users a familiar and consistent representation of the status of contract funds.

Rationale

The proposed asset handling framework is a series of asset depositories called "tanks," which have one or more withdrawal mechanisms called "taps." Tanks are simple database objects which track a balance of asset contained within them. A tank has one or more taps on it, which allow withdrawing asset from the tank. Taps are locked such that only specific authorities can open them, and may have restrictions on when or why they can be opened or how much asset can flow through them. Taps are connected to other asset depositories, such as another tank, or an account, and when opened, the asset that flows through them are deposited there. More asset may be added to a tank at any time.

All tanks will be required to have at least one tap which is capable of draining the entire tank immediately. This tap can be thought of as an emergency release valve, and will typically have an authority requiring a supermajority approval of all contract parties. This tap will typically not be connected to any destination until and unless it is used. Its purpose is to handle scenarios where real-world conditions have gone outside the bounds of scenarios planned for under contract (for example, a contract party dies or is otherwise rendered incapable of completing the contract as negotiated), and it becomes necessary to renegotiate the contract mid-flight, potentially rewriting rules for how contract funds can be handled. In such emergent scenarios, it is imperative that the contract platform have a contingency that allows contract parties to reallocate funds based upon renegotiated terms, since the prior-negotiated rules of fund allocation may no longer be usable. In most cases, if the emergency tap is used, it will be connected to a new tank with newly negotiated taps immediately before it is opened, and all funds will be drained to the new tank.

Taps are somewhat more intricate than tanks, as they define the authorities, limitations, and requirements for opening them. First, a tap specifies an authority required to open it. Next, it specifies requirements to open it, such as a requirement for a written reason for the withdrawal, or an approval from a second authority. Finally, it specifies limitations as to how much asset can flow through it once opened.

The power of this model lies in its simplicity. It mimics how money moves in real-world contracts: a balance is locked up for a purpose in advance, when exactly how that balance will be spent is not yet known. As expenditures arise, they are paid out of the balance by an authorized party. Only certain kinds of expenditures can be paid from the balance, and in some cases, a justification or review is required. Eventually, the balance may need to be topped up to support further expenditures. It is anticipated that this model will be general enough to support many smart contract use cases.

A further advantage of this model is that it is easy to visualize, both the current state of contract funds, and the effects a transaction under consideration will have. This is crucial, as it allows the development of intuitive GUIs which represent the status of contract funds to users in a readily accessible fashion, giving them confidence that they understand how their money is being handled and how they can interact with it under terms of contract.

Specifications

This proposal seeks to define five new types within the BitShares protocol, namely tank_object, tap, tap_requirement, tank_attachment, and sink. A tank_object is an object which contains a volume of asset. A tap is an object attached to a tank_object which is capable of releasing asset from the tank. A tap_requirement is a static_variant of various requirement types which can be attached to a tap to impose limits and requirements on when and why a tap can be opened, and how much asset can flow through it. A tank_attachment is a static_variant which may contain one of several structures to be stored on the tank to provide additional functionality for the tank or taps. Finally, a sink is a static_variant of various types which specify what shall be done with asset that has flowed through a tap. A tank_object has one or more taps; a tap may contain zero or more tap_requirements and is optionally connected to a sink. A tap must be connected to a sink before it can be opened.

When a tap is opened, the amount of asset to release must be known. A tap can be opened in one of two modes: to release a fixed amount of asset, or to release the maximum amount of asset available. In the former case, the amount to release is set in the transaction that opens the tap. In the latter case, the amount to release is determined by the tap_requirements, or the amount of asset remaining in the tank, whichever is lower.

A tap_requirement is attached to a tap and locks the tap until certain conditions are met. tap_requirements contain several parameters which are defined when the requirement is created, and may require arguments to be passed in the transaction when unlocking or opening the tap. Some tap_requirements are stateful; requirement state is stored by the tank_object. Initially, the following tap_requirement types are specified:

A tank_attachment is attached to a tank and provides additional functionality for the tank or associated taps. Attachments may be created to perform actions when certain events occur on the tank, track statistics, restrict what kinds of actions can be taken on the tank or who can take them, authorize updates to tank components, etc. Attachments can receive asset, but cannot store it, thus attachments which receive asset must specify a sink to deposit asset in once it is received. Tank attachments cannot directly move asset into or out of a tank; only taps can take asset out of a tank, and only sinks can put asset into a tank. Tank attachments may be stateful, and their state will be stored by the hosting tank_object. Initially, the following tank attachments are specified:

A sink provides a common interface for moving asset within Tanks and Taps structures. At a data level, a sink is very small and simple, only storing the ID of the object receiving the asset; however, sinks will specify a global interface for receiving asset and processing that deposit. The sink interface will require the source and path of the asset to be specified, and this may be used when processing the deposit to trigger events, log statistics, or detect errors. Initially, the following sink types are specified:

Asset Path Restrictions By default, tanks can receive asset at any time from any source; however, if a tank is equipped with a deposit_source_restrictor attachment, deposits to that tank are checked and required to match one of the patterns expected by the restrictor. This utilizes the information collected by the sinks that assets flow through. Presently, asset can originate from an account or a tank, and it may flow through several tank attachments before settling again in an account or a tank.

Each step of this flow is directed using a sink type, and the sink interface will record the list of sinks the asset flows through on its path from its origin to its destination. When a sink deposits to a tank equipped with a deposit_source_restrictor, the restrictor matches the full path of the deposit against a list of recognized patterns, and if the deposit path does not match any of the patterns, the deposit is rejected.

Tank Lifecycle Once created, a tank can remain in existence indefinitely and can be filled and emptied many times. It is preferred, however, that unused tanks be destroyed. There are two mechanisms by which a tank is destroyed: first is by a destructor tap, and second is by the tank_destroy operation. A destructor tap is a kind of tap which can destroy the tank after the tank is emptied, if configured to do so by the tap_open operation. Any tap can be created as a destructor tap, but the emergency tap must be a destructor tap. This is expected to be the most common method of destroying a tank, as it can be done within normal usage of the tank. Alternatively, a tank can be destroyed by using the tank_destroy operation; however, this operation requires the emergency tap authority and is therefore unlikely to be frequently used in practice.

To incentivize the destruction of tanks that are no longer being used, a deposit of core asset is required to create a new tank. This deposit is held for the lifetime of the tank, and is released when the tank is destroyed. The deposit is returned to the account which pays the fee for the operation that destroys the tank; it is not sent through a tap.

Restricted Assets Some assets specify account whitelists or blacklists, to restrict which accounts may transact in that asset. It is desired that these assets be able to utilize the Tanks and Taps infrastructure without opening the possibility of using Tanks and Taps to circumvent the restrictions on asset ownership; therefore, a restricted asset check will be enforced in three instances. First, when asset is released through a sink to an account balance, a restriction check will be performed on that account, and if the account is unauthorized to handle the asset, the transaction which opened the tap will be rejected as invalid. Second, the account paying the fee for a transaction that opens a tap must be authorized to handle the asset which is released by the tap. Third, when a sink receives asset from an account, that account must be authorized to handle the asset. With these checks in place, restricted assets can be used in conjunction with the Tanks and Taps architecture without compromising the efficacy of the asset restrictions.

Pseudocode definitions of these types are provided below:

attachment_id_type {
    /// ID of the tank hosting the attachment
    tank_id_type tank_id;
    /// ID of the attachment on the tank
    uint16 attachment_id;
}
type sink = static_variant<tank_id_type, account_id_type, attachment_id_type>;

struct unlimited_flow{};
type tap_flow_limit = static_variant<share_type, unlimited_flow>;

asset_flow_meter {
    state_type {
        /// The amount of asset that has flowed through the meter
        share_type metered_amount;
    }
    /// The type of asset which can flow through this meter
    asset_id_type asset_type;
    /// The sink which the metered asset is released to
    sink destination_sink;
}
deposit_source_restrictor {
    /// This type defines a wildcard sink type, which matches against any sink(s)
    wildcard_sink {
        /// If true, wildcard matches any number of sinks; otherwise, matches exactly one
        bool repeatable;
    }
    /// A deposit path element may be a specific sink, or a wildcard to match any sink
    type deposit_path_element = static_variant<sink, wildcard_sink>;
    type deposit_path_pattern = vector<deposit_path_element>;

    /// A list of path patterns that a deposit is checked against; if a deposit's path
    /// doesn't match any pattern, it is rejected
    vector<deposit_path_pattern> legal_deposit_paths;
    /// The sink that asset is released to after flowing through the restrictor
    sink destination_sink;
}
tap_opener {
    /// Index of the tap to open (must be on the same tank as the opener)
    uint16 tap_index;
    /// The amount to release
    tap_flow_limit release_amount;
    /// The sink that asset is released to after flowing through the opener
    sink destination_sink;
}
attachment_connect_authority {
    /// Authority that may reconnect an attachment
    authority connect_authority;
    /// Attachment which may be reconnected
    attachment_id_type attachment_id;
}

type tank_attachment = static_variant<asset_flow_meter, deposit_source_restrictor,
                                      tap_opener, attachment_connect_authority>;

immediate_flow_limit { share_type limit; }
cumulative_flow_limit {
    share_type limit;
    attachment_id_type meter_id;
}
periodic_flow_limit {
    state_type {
        /// When the limit was created, and thus, when the first period began
        time_point_sec creation_date;
    }
    /// Duration of periods in seconds
    uint32 period_duration_sec;
    /// ID of the meter tracking released funds; reset when period rolls over
    attachment_id_type meter_id;
    /// Maximum cumulative amount to release in a given period
    share_type limit;
}
time_lock {
    /// If true, the tap is initially locked
    bool start_locked;
    /// At each of these times, the tap will switch between locked and unlocked --
    /// must all be in the future
    vector<time_point_sec> lock_unlock_times;
}
minimum_tank_level {
    /// Minimum tank balance; tap cannot drain tank below this balance
    share_type minimum_level;
}
review_requirement {
    state_type {
        /// This type describes a request for withdrawal waiting for review or
        /// for redemption
        request_type {
            /// Amount requested for release -- reset if reviewer denies request
            optional<tap_flow_limit> request_amount;
            /// Optional comment about request, max 150 chars -- reset if reviewer
            /// denies request
            optional<string> request_comment;
            /// Starts false, set to true if request is approved, and back to false after
            /// a release of funds
            bool approved;
        }
        /// Number of requests made so far; used to assign request IDs
        uint16 request_counter;
        /// Map of request ID to request
        flat_map<uint16, request_type> pending_requests;
    }
    /// Authority which approves or denies requests
    authority reviewer;
}
documentation_requirement {
    /* no fields; if this requirement is present, evaluator requires a nonempty
     * documentation argument exist, but does no further verification upon it
     */
}
delay_requirement {
    state_type {
        /// This type describes a request for withdrawal waiting for its delay to pass
        request_type {
            /// When the request was made
            optional<time_point_sec> delay_period_start;
            /// Amount requested
            optional<tap_flow_limit> request_amount;
            /// Optional comment about request; max 150 chars
            optional<string> request_comment;
        }
        /// Number of requests made so far; used to assign request IDs
        uint16 request_counter;
        /// Maximum allowed number of outstanding requests; zero means no limit
        uint16 request_limit;
        /// Map of request ID to request
        flat_map<uint16, request_type> pending_requests;
    }
    /// Authority which can veto request during review period; if veto occurs,
    /// reset state values
    optional<authority> veto_authority;
    /// Period in seconds after unlock request until tap unlocks; when tap opens,
    /// all state values are reset
    uint32 delay_period_sec;
}
hash_preimage_requirement {
    /// A string of the form "HASH_ALGORITHM:HASH_HEX_BYTES"
    string hash;
    /// Size of the preimage in bytes; a preimage of a different size will be rejected
    /// If null, a matching preimage of any size will be accepted
    optional<uint16> preimage_size;
}
ticket_requirement {
    state_type {
        /// The type of the ticket that must be signed to unlock the tap
        struct ticket_type {
            /// ID of the tank containing the tap this ticket is for
            tank_id_type tank_id;
            /// Index of the tap this ticket is for
            uint16 tap_index;
            /// Maximum asset release authorized by this ticket
            tap_flow_limit max_withdrawal;
            /// Must be equal to tickets_consumed to be valid
            uint16 ticket_number;
        }
        /// Number of tickets that have been used to authorize a release of funds
        uint16 tickets_consumed;
    }
    /// Key that must sign tickets to validate them
    public_key_type ticket_signer;
}
exchange_requirement {
   /// The maximum release amount will be:
   /// meter_reading / tick_amount * release_per_tick - amount_released
    state_type {
        /// The amount of asset released so far
        share_type amount_released;
    }
    /// The ID of the meter to check
    tank_attachment_id_type meter_id;
    /// The amount to release per tick of the meter
    share_type release_per_tick;
    /// Amount of metered asset per tick
    share_type tick_amount;
}

type tap_requirement = static_variant<immediate_flow_limit, cumulative_flow_limit,
                                      periodic_flow_limit, time_lock, minimum_tank_level,
                                      review_requirement, documentation_requirement,
                                      delay_requirement, hash_preimage_requirement,
                                      ticket_requirement, exchange_requirement>;

tap {
    /// The connected sink, if present
    optional<sink> connected_sink;
    /// The authority to open the tap; if null, anyone can open the tap if they can
    /// satisfy the requirements
    optional<authority> open_authority;
    /// The authority to connect and disconnect the tap. If unset, tap must be connected
    /// on creation, and the connection cannot be later modified -- emergency tap must
    /// specify a connect_authority
    optional<authority> connect_authority;
    /// Requirements for opening this tap and releasing asset
    vector<tap_requirement> requirements;
    /// If true, this tap can be used to destroy the tank when it empties
    bool destructor_tap;
}

tank_object {
    /// Amount of asset contained in the tank
    asset contained_asset;
    /// Taps on this tank. ID 0 must be present, and must not have any tap_requirements
    flat_map<uint16, tap> taps;
    /// Counter of taps added; used to assign tap IDs
    uint16 tap_counter;
    /// Attachments on this tank
    flat_map<uint16, tank_attachment> attachments;
    /// Counter of attachments added; used to assign attachment IDs
    uint16 attachment_counter;
    /// Amount of the deposit paid to create the tank (deposit is always core asset)
    share_type deposit_amount;
}

This proposal additionally seeks to add eight operation types to the protocol:

Pseudocode definitions of these operations are provided below:

tank_create {
    /// Account that pays fee and deposit
    account_id_type fee_payer;
    /// Deposit of core asset paid to create the tank
    share_type deposit_amount;
    /// Type of asset the tank will contain
    asset_id_type tank_asset;
    /// Taps on the tank -- index 0 must be present and must have no tap_requirements
    vector<tap> taps;
    /// Attachments on the tank
    vector<tank_attachment> attachments;
}
tank_update {
    /// Account that pays fee
    account_id_type fee_payer;
    /// Authority to update tank: must match tank->taps[0].open_authority
    authority update_authority;
    /// ID of tank to update
    tank_id_type tank_to_update;

    /// IDs of taps to remove
    flat_set<uint16> taps_to_remove;
    /// New taps to add
    vector<tap> taps_to_add;

    /// IDs of attachments to remove
    flat_set<uint16> attachments to remove;
    /// New attachments to add
    vector<tank_attachment> attachments to add;
}
tank_destroy {
    /// Account that pays fee
    account_id_type fee_payer;
    /// Amount of deposit reclaimed for destroying tank
    share_type deposit_amount;
    /// Authority to destroy tank: must match tank->taps[0].open_authority
    authority destroy_authority;
    /// ID of tank to destroy; tank must be empty of asset to destroy
    tank_id_type tank_to_destroy;
}
tank_query {
    /// Account that pays the fee
    account_id_type fee_payer;
    /// Authorities required to perform the queries
    flat_set<authority> required_authorities;

    /// ID of the tank to be queried
    tank_id_type tank_id;
    /// Whether to query a tap or an attachment
    enum query_enum { tap_query, attachment_query};
    query_enum query_type;
    /// ID of the tap or attachment to query
    uint16 query_id;

    /// Key-value map of arguments
    flat_map<string, string> arguments;
}
tap_open {
    /// Account that pays the fee
    account_id_type fee_payer;
    /// Authorities required to satisfy the tap's requirements/open authority
    flat_set<authority> required_authorities;

    /// ID of the tank with the tap to be opened
    tank_id_type tank_id;
    /// Index of the tap to open
    uint16 tap_index;
    /// Amount to release from the tap
    tap_flow_limit release_amount;
    /// Total number of taps opened by this transaction, i.e. due to tap_openers
    uint16 taps_to_open;
    /// If emptying tank via a destructor tap, the deposit is returned to fee_payer
    /// Specify amount of deposit here to enable tank destruction
    optional<share_type> claimed_deposit;

    /// Key-value map of arguments
    flat_map<string, string> arguments;
}
tap_connect {
    /// Account that pays the fee
    account_id_type fee_payer;
    /// Authority to connect the tap: must match
    /// tank_id->taps[tap_index].connect_authority
    authority connect_authority;
    /// ID of the tank holding the tap to be opened
    tank_id_type tank_id;
    /// Index of the tap to be connected
    uint16 tap_index;
    /// Sink to connect the tap to; if null, tap will be disconnected
    optional<sink> sink_to_connect;
    /// Clear the tap connect authority after connecting the tap; if true,
    /// sink_to_connect must be specified
    bool clear_connect_authority;
}
account_fund_sink {
    /// Account that provides funds, and pays fee; must be authorized to handle asset
    account_id_type funding_account;
    /// Sink that the account deposits asset into
    sink destination_sink;
    /// Amount that the account is depositing into the sink
    asset amount_to_sink;
}
sink_fund_account {
    /// Account receiving funds
    account_id_type recipient;
    /// Amount the recipient received
    asset amount_received;
}

Additionally, three new committee parameters will be defined:

Tank Queries and Arguments

Many of the tap requirements and tank attachments support modes of user interaction which may or may not result in the immediate movement of asset. Those interactions which do cause movement of asset are performed using the tap_open operation, and those which do not are performed using the tank_query operation. Both of these operations will accept arguments specifying what actions should be taken on which requirements/attachments.

Initially, the following tank attachment queries will be supported:

The following tap requirement queries will be supported:

Please note that while this specification attempts to provide a sufficient level of technical detail to convey the essence of Tanks and Taps, some detail has been elided for brevity, and the final implementation may diverge from the specification in order to improve the correctness, stability, efficiency, maintainability, or functionality of the Tanks and Taps framework.

Discussion and Summary for Shareholders

The proposed modifications to the BitShares protocol will add eight new operation types, one new database object, and three new committee-controlled chain parameters. These modifications add new features, and do not modify or restrict any existing features, and therefore all currently supported use cases should be unaffected. In return, however, the new features will provide a powerful platform for financial dapp and smart contract development, and this platform can be augmented to become even more powerful in the future, requiring only minimal changes to support additional use cases. The proposed changes, in conjunction with future updates will make BitShares increasingly suitable as a financial dapp platform, notably offering dapp developers simplified development and a reduced time to market as compared with Turing Complete Smart Contract Platform alternatives which offer greater flexibility at the price of increased complexity.

Copyright

This document is created for the betterment of humanity and is hereby placed into the public domain.

thontron commented 5 years ago

Great to see ideas like this doubling-down on BTS core strength as a financial technology platform.

To create a binary conditional transfer e.g. If X pay Alice, if (- X) pay Bob. The tank would have a determined outcome. The tap may alter the flow of the tank but it will execute at some time.

The tanks run with pub keys attached to the tap correct? Would that be permanent?

Do you see 'taps' sort of like 'hops' on the lightning network?

Looking forward to more specs. Thanks again for the thought-provoking proposal.

nathanielhourt commented 5 years ago

@thontron I don't think so, but I'm not terribly familiar with lightning networks -- what is a hop exactly?

A tank is a storage of asset which is in an intermediate phase of ownership -- it is controlled by smart contract and doesn't belong to any particular individual yet. Taps are outlets which can release funds from the tank according to certain rules and limitations defined according to the contract. When a tap is opened, the funds flow to the connected sink, which currently is either an account or another tank.

If I understand lightning networks correctly, the hops are like payment channels. You can use tanks and taps to create payment channels, but this is just one structure that can be built from the tank-and-tap primitives.

Authorities are attached to taps. To implement the scenario you described, one would probably create a tank with two general purpose taps (as opposed to the emergency tap -- see Rationale paragraph 2), one connected to Alice and one connected to Bob, and the tap requirements would be set up so that which tap can be opened depends on X, which depends on the particulars of the contract in play.

christophersanborn commented 5 years ago

This is an excellent proposal with tons of potential!!

I've been trying to see what interesting yet simple "sample use-cases" I can come up with. My goal has been primarily just to explore the concept, but also to assess what can be implemented with the current draft of the spec, and what kinds of additions to the spec might be needed to support a wide variety of applications.

Towards that end, I present here one possible use case that can be implemented with two tanks and three taps. However, to work, it would require two additions to the spec as written above. If these additions can increase the utility of the concept without adding too much complexity to the implementation, then I propose that they be included in the spec.

Use Case: Matching contributions to a fund

Often times, employers who offer their employees a retirement plan will incentivize employees to contribute their own money to the plan by offering "matching funds," where the employer will kick in an equal amount as the employee, up to some monthly limit.

Here I demonstrate a T&T "contract" wherein an employer can set aside a year's worth of "matching funds" and allow the employee ("Bob" in this example) to claim those funds in monthly increments into a retirement fund IF AND ONLY IF the employee ALSO contributes an equal amount to the fund. At the end of one year, the contract allows the employer to reclaim any funds that the employee has not claimed.

Needed spec additions:

The contract assumes two specification additions not yet present in the above spec:

  1. A tank flag, zero_balance, that says a tank MUST BE EMPTY at the conclusion of a transaction. Note that the various operations within a transaction may fill or drain the tank, but if this flag is set, the tank balance MUST be zero after all operations are applied, else the transaction will fail.

  2. A tap requirement type minimum_flow_requirement, specifying that the amount withdrawn in a particular tap opening must be greater than or equal to the minimum specified in the requirement.

The contract:

The contract consists of two tanks, set up by Bob's employer. They are:

  1. An escrow tank which holds the matching funds, called the "Match Tank", funded by the employer.

  2. A "logic tank" which enforces the matching.

(Additionally there is a third tank to represent Bob's retirement fund, but this is assumed to be administered by a separate party.)

The escrow tank, or "employer match tank", has two taps (not including emergency taps). The taps, identified by [tank]:[index], are:

The logic tank starts empty, and has the zero_balance flag set, and so must always be left empty at the conclusion of a transaction. It can receive funds from the escrow tank, or from any other source. Note, however, that effectively nobody can fill the logic tank UNLESS they are also able to open a tap on the logic tank, due to the requirement that the tank balance must end at zero.

The logic tank has only one tap (aside from the emergency tap). Tap Logic:1 is connected to Bob's retirement fund, and has a requirement that the amount of the opening must be AT LEAST twice the limit that was set on the escrow tap. (Or some other multiple if the contract is for something other than a 1:1 match.)

Using the contract:

Now, for Bob to claim a matching contribution, he constructs and broadcasts a single transaction consisting of the following operations:

If the transaction succeeds, then Bob will have spent limit funds of his own money, and yet effected a 2 x limit contribution to his retirement fund.

The transaction will succeed provided Bob supplies enough of his own money to bridge the gap between however much he opens the escrow tap for, and the minimum required to open the logic tank's tap. In practice this means the amount the employer contributes cannot be more than what Bob contributes.

The zero_balance flag on the logic tank is important because it forces the contract logic to be atomic. (All the steps must occur in the same transaction.) Without the zero_balance flag, Bob could open tap Match:2 for limit, and simply leave that balance in the logic tank. A month later, he could open the tap again, and fill the logic tank with enough funds to open Logic:1 and move the funds into the Retirement Fund tank, without ever supplying his own contribution.

Variations

Contribute anytime:

In the above example, Bob is constrained to making contributions each month. If he misses a month, he misses the opportunity to claim those funds. This could be relaxed by putting two requirements instead of one on tap Match:2. The requirements would be an immediate_flow_limit of limit, and a cumulative_flow_limit of 12 x limit. Then Bob could make his monthly contributions at perhaps more convenient intervals, but no more than 12 times in total.

EDIT This actually won't work since Bob could open the escrow tap twice in a single transaction (two separate open operations) and then move through the Logic tank without contributing his own funds. A way to rectify this would be if the immediate_flow_limit is implemented as a per transaction limit rather than a per operation limit; or, if a new per_transaction_flow_limit requirement is defined.

Charitable matched donations:

Another interesting variation would be to incentivize donations to a charitable cause. In this case, the escrow tank will be called the "Donor Match" tank and will be funded by someone who wants to incentivize donations, and the Logic Tank would be connected to some charity. The immediate_flow_limit would be some reasonable discrete amount, and the authorities on the escrow tap and the logic outflow tap would be "anyone can sign." This would allow anyone to make a donation to the charity by routing the donation through the logic tank, and picking up a matching amount from the "Donor Match" tank.

nathanielhourt commented 5 years ago

Ooh, cool idea, @christophersanborn! Thanks for the write-up!

One issue I have is that I don't think we have any easy way right now to add an assertion to the end of transaction processing, such as the zero_balance flag. Currently, all operations are expected to do all their checking inline, and once the operation finishes executing, it's assumed it was successful. The ability to add assertions to the end of transaction processing to ensure that the transaction only completes if certain conditions hold after all operations process would be quite useful, but I might have an easier way to get the feature you described.

I was contemplating ways of refactoring the market engine using TNT, making orders into tanks and enforcing the matching using taps. For that, I considered an exchange_requirement as a new tap requirement. The requirement is set up like a flow meter on an otherwise unrelated asset pipeline, and it measures how much asset flows over that pipeline. It also has a price object on it with an exchange rate. When the tap is opened, it limits the outflow to the amount it metered times the exchange price.

The monitored pipeline could be set up by building a sink into the exchange_requirement that specifies where the measured asset flows out to, and then a new exchange_sink type is created which attaches to the exchange_requirement. Incoming asset flows into the exchange_sink, increments the meter, and immediately flows out through the sink on the requirement to wherever that goes.

If I'm not mistaken, this tap_requirement would also satisfy your use case. Consider your diagram, but delete the logic tank and connect the matching tank tap :2 directly to Bob's retirement tank, and add an exchange_requirement to it with the requirement's sink also connected to Bob's tank. Now, Bob deposits asset into his tank through the requirement, and that spins the meter allowing him to open match:2 and release the matching amount into his requirement tank as well. Because the meter specifies the outflow sink, Bob can't spin the meter without locking the funds in his retirement tank, but he can do it with funds from anywhere. It also supports the contribute anytime model.

The tap requirement could look like this:

exchange_requirement {
    state_type {
        /// This measures the amount of asset that has flown through the meter
        asset metered_amount;
    } 
    /// The sink that metered asset flows into; if null, flows into the meter will fail
    optional<sink> destination_sink;
    /// The exchange rate; multiply by metered_amount to calculate requirement limit
    price exchange_rate;
}

Then we'd need a new type in the sink variant, like this:

meter_sink {
    tank_id_type tank_id;
    uint16 tap_index;
    uint16 requirement_index;
}

The handler for asset flowing into that sink type would spin the meter, then release asset into the meter's destination sink.

What do you think, Christopher? Does that adequately support your use case?

litepresence commented 5 years ago

I'm excited to see this new feature on the horizon. It plays in well with BSIP 40 "custom active authority" towards the concept of "hedge fund management".

https://github.com/bitshares/bitshares-core/issues/1285 https://github.com/bitshares/bsips/blob/master/bsip-0040.md

If a tank could have both an owner and operator; such that owner has withdrawal privileges and operator has trading privileges:

1) Funds are placed into tank by owner. 2) Operator then performs multiple buy/sell/cancel operations with custom active authority. 3) Funds are removed from tank by owner using a tap.
4) The tap defines return on investment. 5) The owner gets 100% of principal remaining in tank. 6) The operator and owner split (per predefined contract terms) any gains over principal accrued through trading.

Thoughts?

nathanielhourt commented 5 years ago

What you describe may be possible with a combination of TNT and Custom Active Authorities, especially if I add the exchange tap described in the comment above yours (that's useful for doing the profit split). The trading will have to be done from an account, though, since TNT won't interface with the markets directly, at least initially. That will come in a future release. So you could make an account for the trading, with Custom Active Authority restrictions defining what trading operations can be performed. You also create a tank, and this is how asset leaves the contract; aside from trading, the account can't send asset anywhere except to the tank. The tank has taps allowing the owner to take the principal, and allowing the two to take their splits of any profits.

CryptickCryptick1 commented 5 years ago

As much as I like innovation, I feel as though I need to rain on your parade here.

First, for the employer matching funds example this is way too complicated of a solution for a simple problem.

Second, for trading portfolio or the hedge fund it is not secure.

There may be another use scenario for this idea, but I think it needs to be more secure and useful to be worth the cost.

OK so now for the long answer. Any business that is going to do a matching funds transaction needs to choose the best solution. This needs to be technically viable, practical, understandable, simple, easy, secure accountable and a bunch of other things. Any business that is wanting to do an IRA matching funds is not going to be willing to trust a new process, a complicated process and think though every possible situation. They are going to go with the quick and easy, tell the accountant to transfer the money once a month when contributions are made and proof of contributions are made. For a typical employee that is a simple 12 transactions a year, not a process worth trying to eliminate. Any process, such as payroll also has a bunch of exceptions and special situations, do I get 401K matching on retirement, sick time, time off for pregnancy, bonus pay and more. It really takes a human to make such decisions and then a human to fix them after they are not done right. As a person who was responsible for payroll, about 2 to 5% of all transactions have to be corrected. (hopefully your organization is better.)

Even explaining a process like this to all employees would be too timely and have too high of cost. Time is money. Businesses do not have time for that. People want simple and easy to understand answers. If they don’t get paid, they want to march into an office and get it fixed.

Now, I do understand this example may have been choose for illustrative purposes and there are other better use cases.

As for the Hedge Fund Discussion So much of what has been discussed –from a Hedge Fund perspective- could be done with a UIA. An asset can be sold and then the proceeds invested and traded. This can all stay transparent. I have spent a lot of time looking for a secure way to do this, however, I have not found it. The best option is multi-signature accounts like we have already. Unfortunately, as long as any one person has trading access, they can effectively steal the entirety of the money. This brings it back to a trust issue. Either you trust them or you don’t. If you trust them just give them the money, if you don’t trust them don’t give them the money.

I don’t wish to create a public primer for how to embezzle funds, but Wall Street has some pretty good examples of how this can be done. In short, if you can trade, you can run multiple accounts and orders such that you can effectively steal all the money. This tends to be a problem for those of us wanting to create a solution that avoids this.

Now I like the idea of the lightening network and I understand it has been proven secure and it works. I understand that HTLC are in the works and effectively the same thing on BitShares. This tank and tap concept seems very similar and there may be some nuances or difference that escape my attention. There may be some better uses for the concept, however, I see some very big problems with the concept- as I currently understand it. I feel better case of use examples are needed to justify the expense of development.

christophersanborn commented 5 years ago

Now, I do understand this example may have been choose for illustrative purposes and there are other better use cases.

This is exactly it. The matched-funds idea is is entirely presented as an academic exploration into what kinds of things can be done — what "elementals" of business logic can be scripted. Any real life implementation would surely require more logic, with overrides to handle the unexpected and correct mistakes, etc. (And in the specific case of matching IRA contributions... yeah, no assertion is being made here that a blockchain solution is inherently better than simple arrangements with payroll. It's just an example showing that "things can be done.")

An underlying assumption for me is that, if a certain set of elementals can be shown to work, then real-world solutions can be composed of those elementals. I probably should have been clearer at the outset of my matched-funds idea that the idea is intended as academic. But I think you can be reassured that the developer community won't make the mistake of assuming the elementals, by themselves, are sufficient drop-in replacements for real-world use cases. We're looking at the starting points here.

Now I like the idea of the lightening network and I understand it has been proven secure and it works. I understand that HTLC are in the works and effectively the same thing on BitShares. This tank and tap concept seems very similar and there may be some nuances or difference that escape my attention.

Lightning and Tanks and Taps serve very different purposes. (Although an interesting thing about Tanks and Taps is that it allows for a simplified and more flexible implementation of HTLC and related concepts, thus facilitating participation in lightning-like networks.)

I feel better case of use examples are needed to justify the expense of development.

Can't argue with this sentiment directly. I think the use case examples will come and will show the utility. Some ideas are already out there, including HTLC and Payment Channels (the latter is not written up yet though, although it is hinted at in the ticket_requirement tap requirement). These in themselves may be enough to justify it, although the aim imho of T&T is to enable and facilitate quite a bit more.

MichelSantos commented 5 years ago

This well-written proposal presents a fascinating concept. Below are some of my editorial comments.

Requires a ticket

This is the first mention of ticket. I suggest a link.

Attachments can receive asset, but cannot store it, thus attachments which receive asset must specify a sink to deposit asset in once it is received.

Can an attachment effectively be used as a tap that does not have a tap_requirement?

checks a meter

This is the first mention of meter. I suggest a link.

At most one deposit_source_restrictor may be attached to a tank

I suggest re-phrasing to something like "A tank can have at most one attached deposit_source_restrictor."

... after the current deposit settles

This is the first mention of settle. I suggest a link.

At a data level, a sink is very small and simple, only storing the ID of the object receiving the asset; however, the sink specifies a global interface for receiving asset and processing that deposit. The sink interface requires the source and path of the asset to be specified,

Where is the sink interface defined?

To incentivize the destruction of tanks that are no longer being used, a deposit of core asset is required to create a new tank. This deposit is held for the lifetime of the tank, and is released when the tank is destroyed.

This is very interesting. The vote tallying function will need to be updated if that core asset is considered to still belong to the depositor.

Third, when a sink receives asset from an account, that account must be authorized to handle the asset.

Is the only way for this to occur through the use of the account_fund_sink operation?

/// This type defines a wildcard sink type, which matches against any sink(s) wildcard_sink { /// If true, wildcard matches any number of sinks; otherwise, matches exactly one bool repeatable; }

Why repeatable?

documentation_requirement { /* no fields; if this requirement is present, evaluator requires a nonempty

  • documentation argument exist, but does no further verification upon it */ }

Argument? Is this requirement one of the arguments to tap_open and tap_unlock?

/// Authority which can veto request during review period; if veto occurs, /// reset state values

Where is state being kept?

tank_destroy { ... /// Amount of deposit reclaimed for destroying tank share_type deposit_amount;

Why? Can this amount differ from what was used when the tank was created?

tap_open and tap_unlock have /// General arguments (received by all tap_requirements) flat_set general_arguments; /// Arguments to specific tap_requirements, keyed by index of the target requirement /// on the tap flat_map<uint16, string> arguments;

Please elaborate on how this might be used by a contract author, or by the code that will interpret this

tap_open { ... /// Total number of taps opened by this transaction, i.e. due to tap_openers uint16 taps_to_open;

Which ones should be opened?

nathanielhourt commented 5 years ago

This well-written proposal presents a fascinating concept. Below are some of my editorial comments.

Requires a ticket

This is the first mention of ticket. I suggest a link.

Added a reference

Attachments can receive asset, but cannot store it, thus attachments which receive asset must specify a sink to deposit asset in once it is received.

Can an attachment effectively be used as a tap that does not have a tap_requirement?

No; added some clarification on this point.

checks a meter

This is the first mention of meter. I suggest a link.

Added a reference

At most one deposit_source_restrictor may be attached to a tank

I suggest re-phrasing to something like "A tank can have at most one attached deposit_source_restrictor."

Done, thank you.

... after the current deposit settles

This is the first mention of settle. I suggest a link.

Rephrased to "stops moving"

At a data level, a sink is very small and simple, only storing the ID of the object receiving the asset; however, the sink specifies a global interface for receiving asset and processing that deposit. The sink interface requires the source and path of the asset to be specified,

Where is the sink interface defined?

This will be formally defined by the implementation; revised the phrasing to refer to the sink interface in the future tense.

To incentivize the destruction of tanks that are no longer being used, a deposit of core asset is required to create a new tank. This deposit is held for the lifetime of the tank, and is released when the tank is destroyed.

This is very interesting. The vote tallying function will need to be updated if that core asset is considered to still belong to the depositor.

Assets in tanks, including the deposit, are not regarded as belonging to any particular account, but rather are in an 'intermediate stage of ownership' during which they are contract-controlled. Furthermore, there is no requirement that the account which claims a deposit be the same as the one that created it. Tanks can be designed so that only the creator can claim the deposit, but they don't have to be.

Third, when a sink receives asset from an account, that account must be authorized to handle the asset.

Is the only way for this to occur through the use of the account_fund_sink operation?

Yes

/// This type defines a wildcard sink type, which matches against any sink(s) wildcard_sink { /// If true, wildcard matches any number of sinks; otherwise, matches exactly one bool repeatable; }

Why repeatable?

A pattern may be designed to match against paths where several consecutive steps are unrestricted, in which case a single repeatable wildcard is more efficient than several wildcards. Additionally, a pattern may be designed to allow a section where the steps are unrestricted, and the number of unrestricted steps may vary, meaning the number of wildcards needed is unknown so a single, repeatable wildcard is necessary.

documentation_requirement { /* no fields; if this requirement is present, evaluator requires a nonempty

  • documentation argument exist, but does no further verification upon it */ }

Argument? Is this requirement one of the arguments to tap_open and tap_unlock?

It would be an argument to tap_open. See answer to question about arguments below.

/// Authority which can veto request during review period; if veto occurs, /// reset state values

Where is state being kept?

The tank will store the state for its and attachments and tap requirements.

tank_destroy { ... /// Amount of deposit reclaimed for destroying tank share_type deposit_amount;

Why? Can this amount differ from what was used when the tank was created?

The required tank deposit may be changed, so it's necessary to store it on the tank so we know what it was when the tank was created. Added missing committee-controlled chain parameter: tank_deposit_amount

tap_open and tap_unlock have /// General arguments (received by all tap_requirements) flat_set general_arguments; /// Arguments to specific tap_requirements, keyed by index of the target requirement /// on the tap flat_map<uint16, string> arguments;

Please elaborate on how this might be used by a contract author, or by the code that will interpret this

I am leaving the arguments unspecified until implementation, as their exact format and contents may be difficult to predict accurately. But to clarify, many tap requirements need some interaction from the user, requiring that data be passed to them. These argument fields are the mechanism by which that happens. Some arguments (like a "documentation" argument) are general enough that they may be used by several tap requirements, but others are specific enough to pertain only to one. This is the reason for two fields: to distinguish between these classes of argument.

tap_open { ... /// Total number of taps opened by this transaction, i.e. due to tap_openers uint16 taps_to_open;

Which ones should be opened?

The tap_open operation specifies only one tap to open; however, that tap may be connected, whether directly or indirectly, to one or more tap_opener attachments which triggers other taps to open, which may subsequently flow to other openers, causing more taps to open, and so on. This field declares in advance the total number of taps the operation will cause to be opened so the fee can be charged appropriately.

nathanielhourt commented 5 years ago

The dual arguments lists have now been replaced by a conceptually simpler, single key-value map of arguments. I've also replaced the tap_unlock operation with the more general tank_query operation which fulfills the purpose of the tap_unlock operation before, but can also be used to interact with tank attachments instead of tap requirements.

christophersanborn commented 5 years ago

tank_destroy { ... /// Amount of deposit reclaimed for destroying tank share_type deposit_amount;

Why? Can this amount differ from what was used when the tank was created?

The required tank deposit may be changed, so it's necessary to store it on the tank so we know what it was when the tank was created.

For me, this doesn't quite answer the question. Since the tank object already keeps a share_type deposit_amount, what does specifying it in the destroy operation achieve? Are there situations where the party destroying the tank would claim only a portion of the deposit? (Would the remainder be forfeit to the reserve pool?)

A very minor aside: The comment on this tank_destroy parameter briefly confused me:

/// ID of tank to destroy; must be empty to destroy tank_id_type tank_to_destroy;

(I read it as the tank_to_destroy field must be empty. Suggest phrasing: "ID of tank to destroy; tank must be empty of asset to destroy.")

nathanielhourt commented 5 years ago

For me, this doesn't quite answer the question. Since the tank object already keeps a share_type deposit_amount, what does specifying it in the destroy operation achieve? Are there situations where the party destroying the tank would claim only a portion of the deposit? (Would the remainder be forfeit to the reserve pool?)

Ahh, sorry, the reason it must be on the operation also is because all modifications to account balances should be visible in stateless observation of the operation history. This is also the motivation for the sink_fund_account virtual operation.

A very minor aside: The comment on this tank_destroy parameter briefly confused me:

/// ID of tank to destroy; must be empty to destroy tank_id_type tank_to_destroy;

(I read it as the tank_to_destroy field must be empty. Suggest phrasing: "ID of tank to destroy; tank must be empty of asset to destroy.")

Good point; I'll fix it

ryanRfox commented 5 years ago

Assigned BSIP72 to this. Please create PR based on your most recent draft.

nathanielhourt commented 5 years ago

PReq #197 added; closing issue.

abitmore commented 5 years ago

Reopening this issue for discussing potential use cases which is IMHO not appropriate in the PR.

abitmore commented 5 years ago

Just came up with an idea. Perhaps a use case of this feature is to build (somewhat decentralized and trust-less) federal gateways with the help of oracles.

The tank has a list of maintainers, E.G. in a schema like 10/15 multi-sig, who are responsible for releasing LINKED.ETH to users who deposited ETH to the contract.

The contract has a list of maintainers, who are responsible for releasing ETH to users who deposited LINKED.ETH to the tank.

For each deposit, a unique hash is generated, which is publicly visible, to be served as a proof. The maintainers' job is to "feed" the proofs between the two blockchains. When a threshold (how many maintainer have fed the same proof) is met, funds are released accordingly.

Thoughts?

Update: actually, federal gateways are already doable with current feature set of BitShares, using smart contracts will improve transparency, but may lead to higher risks due to potential bugs in the smart contracts.

MichelSantos commented 5 years ago

@abitmore Also implied in your description is that the list of maintainers are:

I think that this can work in general. I currently think that that the specification has enough functionality to enable this.

nathanielhourt commented 5 years ago

It's certainly doable, although there wouldn't be any kind of smart-contracted verification of the proofs. I suppose if there were, you could omit the multisig oracle altogether.

It might be easier to simply use an account, though? Send all of the LINKED.ETH to a multisig oracle account which is not the issuer of LINKED.ETH and process inter-chain transfers from there?

abitmore commented 5 years ago

HTLC is likely can be used to get rid of proof-feeding.

MichelSantos commented 5 years ago

HTLC is likely can be used to get rid of proof-feeding.

That's intriguing: how could we combine HTLC with account_fund_sink

sschiessl-bcp commented 4 years ago

This BSIP was to be considered to be put up for voting. Is the HTLC discussion and the BSIP itself finalized from your point of view?

nathanielhourt commented 4 years ago

This BSIP was to be considered to be put up for voting. Is the HTLC discussion and the BSIP itself finalized from your point of view?

Yes, I consider this BSIP to be finalized and ready for voting.

nathanielhourt commented 4 years ago

A complete implementation of Tanks and Taps is now available here.

sschiessl-bcp commented 4 years ago

A complete implementation of Tanks and Taps is now available here.

You are a machine.

Please go advertise your BSIP :)

nathanielhourt commented 4 years ago

Based on testing, I have decided to make a minor, but important modification to the TNT protocol. The motivation for this change is that, with features facilitating greater automation of TNT structures (most notably, the tap_opener), greater control over asset flows is needed.

While the existing deposit_source_restrictor attachment is technically adequate to provide this control, reliance upon this mechanism alone imposes onerous requirements on TNT architectures, especially on asset flows terminating in accounts, which cannot specify deposit source restrictions. I believe that the existing protocol makes proper security overly complex, and leaves it too-easily overlooked.

To alleviate these concerns, I am making the following modifications to the TNT implementation:

In effect, whereas before, asset flows were dictated entirely by the source, and structures receiving asset would receive asset from any source, and the only way for a receiving structure to restrict the sources that it could receive asset from was via the deposit_source_restrictor; now, all TNT structures which can receive asset can specify precisely which sources they will receive asset from, enabling hardened asset flow paths which cannot receive asset at unexpected locations.