diff --git a/kip-0009/kip-0009.md b/kip-0009/kip-0009.md new file mode 100644 index 0000000..e75a413 --- /dev/null +++ b/kip-0009/kip-0009.md @@ -0,0 +1,53 @@ +--- +KIP: 0009 +Title: restricted-fungible-v1 +Author: Stuart Popejoy @sirlensalot +Status: Draft +Type: Standard +Category: Chainweb +Created: 2021-02-02 +--- + +## Abstract + +Define and implement a Pact interface to specify restrictions on transfer of a fungible token. + +## Motivation + +As seen in ERC-1404, certain use cases require restriction of transfer of a fungible token. This +interface is modeled closely on ERC-1404 as an open-ended "descriptive" API that implies the +restriction must be honored in `transfer` and related functions. + +## Rationale + +### One function to rule them all + +The only particularity worth mentioning is the simplification from ERC-1404 from two functions to +one. There is no need for a descriptive function as the equivalent of `detectTransferRestriction` +can return a `string` in Pact (Solidity lacks mechanisms for string comparison), and the lack of +a standard on what integer values in ERC-1404 might mean. + +### Specific to `fungible-v1` + +As opposed to a generic `restricted-v1`, that could e.g. apply to non-fungible tokens like KIP-0008/`non-fungible-v1`, +this follows ERC-1404 closely in including the _amount_ in the restriction API, as opposed to focusing +only on accounts. This makes it incompatible with `non-fungible-v1` which uses a `string` identifier +for NFTs. + +Note that Pact is a "mixin" language, so there is no concept of this "extending" `fungible-v2`. Instead, +implementing fungibles will implement _both_ `fungible-v2` and `restricted-fungible-v1`. + + +## Backwards Compatibility + +This presents no conflicts with its intended interop with `fungible-v2` (or `fungible-v1`). + +## Specification + +- Interface: [restricted-fungible-v1.pact](pact/restricted-fungible-v1.pact) +- Reference implementation: [restricted-fungible-v1-reference.pact](pact/restricted-fungible-v1-reference.pact) +- Test: [restricted-fungible-v1.repl](pact/restricted-fungible-v1.repl) + +## References +* ERC-1404: +* fungible-v2: diff --git a/kip-0009/pact/include/fungible-util.pact b/kip-0009/pact/include/fungible-util.pact new file mode 100644 index 0000000..70b015b --- /dev/null +++ b/kip-0009/pact/include/fungible-util.pact @@ -0,0 +1,45 @@ + +(namespace (read-msg 'ns)) + +(module fungible-util GOVERNANCE + + (defcap GOVERNANCE () + (enforce-guard (keyset-ref-guard 'swap-ns-admin))) + + (defun enforce-valid-amount + ( precision:integer + amount:decimal + ) + (enforce (> amount 0.0) "Positive non-zero amount") + (enforce-precision precision amount) + ) + + (defun enforce-valid-account (account:string) + (enforce (> (length account) 2) "minimum account length") + ) + + (defun enforce-precision + ( precision:integer + amount:decimal + ) + (enforce + (= (floor amount precision) amount) + "precision violation") + ) + + (defun enforce-valid-transfer + ( sender:string + receiver:string + precision:integer + amount:decimal) + (enforce (!= sender receiver) + "sender cannot be the receiver of a transfer") + (enforce-valid-amount precision amount) + (enforce-valid-account sender) + (enforce-valid-account receiver) + ) + + + + +) diff --git a/kip-0009/pact/include/fungible-v2.pact b/kip-0009/pact/include/fungible-v2.pact new file mode 100644 index 0000000..a8dd0b7 --- /dev/null +++ b/kip-0009/pact/include/fungible-v2.pact @@ -0,0 +1,135 @@ +(interface fungible-v2 + + " Standard for fungible coins and tokens as specified in KIP-0002. " + + ; ---------------------------------------------------------------------- + ; Schema + + (defschema account-details + @doc "Schema for results of 'account' operation." + @model [ (invariant (!= "" sender)) ] + + account:string + balance:decimal + guard:guard) + + + ; ---------------------------------------------------------------------- + ; Caps + + (defcap TRANSFER:bool + ( sender:string + receiver:string + amount:decimal + ) + @doc " Managed capability sealing AMOUNT for transfer from SENDER to \ + \ RECEIVER. Permits any number of transfers up to AMOUNT." + @managed amount TRANSFER-mgr + ) + + (defun TRANSFER-mgr:decimal + ( managed:decimal + requested:decimal + ) + @doc " Manages TRANSFER AMOUNT linearly, \ + \ such that a request for 1.0 amount on a 3.0 \ + \ managed quantity emits updated amount 2.0." + ) + + ; ---------------------------------------------------------------------- + ; Functionality + + + (defun transfer:string + ( sender:string + receiver:string + amount:decimal + ) + @doc " Transfer AMOUNT between accounts SENDER and RECEIVER. \ + \ Fails if either SENDER or RECEIVER does not exist." + @model [ (property (> amount 0.0)) + (property (!= sender "")) + (property (!= receiver "")) + (property (!= sender receiver)) + ] + ) + + (defun transfer-create:string + ( sender:string + receiver:string + receiver-guard:guard + amount:decimal + ) + @doc " Transfer AMOUNT between accounts SENDER and RECEIVER. \ + \ Fails if SENDER does not exist. If RECEIVER exists, guard \ + \ must match existing value. If RECEIVER does not exist, \ + \ RECEIVER account is created using RECEIVER-GUARD. \ + \ Subject to management by TRANSFER capability." + @model [ (property (> amount 0.0)) + (property (!= sender "")) + (property (!= receiver "")) + (property (!= sender receiver)) + ] + ) + + (defpact transfer-crosschain:string + ( sender:string + receiver:string + receiver-guard:guard + target-chain:string + amount:decimal + ) + @doc " 2-step pact to transfer AMOUNT from SENDER on current chain \ + \ to RECEIVER on TARGET-CHAIN via SPV proof. \ + \ TARGET-CHAIN must be different than current chain id. \ + \ First step debits AMOUNT coins in SENDER account and yields \ + \ RECEIVER, RECEIVER_GUARD and AMOUNT to TARGET-CHAIN. \ + \ Second step continuation is sent into TARGET-CHAIN with proof \ + \ obtained from the spv 'output' endpoint of Chainweb. \ + \ Proof is validated and RECEIVER is credited with AMOUNT \ + \ creating account with RECEIVER_GUARD as necessary." + @model [ (property (> amount 0.0)) + (property (!= sender "")) + (property (!= receiver "")) + (property (!= sender receiver)) + (property (!= target-chain "")) + ] + ) + + (defun get-balance:decimal + ( account:string ) + " Get balance for ACCOUNT. Fails if account does not exist." + ) + + (defun details:object{account-details} + ( account: string ) + " Get an object with details of ACCOUNT. \ + \ Fails if account does not exist." + ) + + (defun precision:integer + () + "Return the maximum allowed decimal precision." + ) + + (defun enforce-unit:bool + ( amount:decimal ) + " Enforce minimum precision allowed for transactions." + ) + + (defun create-account:string + ( account:string + guard:guard + ) + " Create ACCOUNT with 0.0 balance, with GUARD controlling access." + ) + + (defun rotate:string + ( account:string + new-guard:guard + ) + " Rotate guard for ACCOUNT. Transaction is validated against \ + \ existing guard before installing new guard. " + ) + +) diff --git a/kip-0009/pact/restricted-fungible-v1-reference.pact b/kip-0009/pact/restricted-fungible-v1-reference.pact new file mode 100644 index 0000000..f57ecc1 --- /dev/null +++ b/kip-0009/pact/restricted-fungible-v1-reference.pact @@ -0,0 +1,181 @@ +(namespace (read-msg 'ns)) + +(module restricted-fungible-v1-reference GOVERNANCE + + (implements fungible-v2) + (implements restricted-fungible-v1) + (use fungible-util) + + (defschema entry + balance:decimal + guard:guard) + + (deftable ledger:{entry}) + + (defcap GOVERNANCE () true) + + (defcap DEBIT (sender:string) + (enforce-guard (at 'guard (read ledger sender)))) + + (defcap CREDIT (receiver:string) true) + + (defcap TRANSFER:bool + ( sender:string + receiver:string + amount:decimal + ) + @managed amount TRANSFER-mgr + (enforce-valid-transfer sender receiver (precision) amount) + (let ((r (detect-transfer-restriction sender receiver amount))) + (enforce (= r "") (format "Transfer restriction detected: {}" [r]))) + (compose-capability (DEBIT sender)) + (compose-capability (CREDIT receiver)) + ) + + (defun TRANSFER-mgr:decimal + ( managed:decimal + requested:decimal + ) + + (let ((newbal (- managed requested))) + (enforce (>= newbal 0.0) + (format "TRANSFER exceeded for balance {}" [managed])) + newbal) + ) + + (defconst MINIMUM_PRECISION 12) + + (defun enforce-unit:bool (amount:decimal) + (enforce-precision (precision) amount)) + + (defun create-account:string + ( account:string + guard:guard + ) + (enforce-valid-account account) + (insert ledger account + { "balance" : 0.0 + , "guard" : guard + }) + ) + + (defun get-balance:decimal (account:string) + (at 'balance (read ledger account)) + ) + + (defun details:object{fungible-v2.account-details} + ( account:string ) + (with-read ledger account + { "balance" := bal + , "guard" := g } + { "account" : account + , "balance" : bal + , "guard": g }) + ) + + (defun rotate:string (account:string new-guard:guard) + (with-read ledger account + { "guard" := old-guard } + + (enforce-guard old-guard) + + (update ledger account + { "guard" : new-guard })) + ) + + + (defun fund:string (account:string amount:decimal) + (with-capability (CREDIT account) + (credit account + (at 'guard (read ledger account)) + amount)) + ) + + (defun precision:integer () + MINIMUM_PRECISION) + + (defun transfer:string (sender:string receiver:string amount:decimal) + + (enforce (!= sender receiver) + "sender cannot be the receiver of a transfer") + (enforce-valid-transfer sender receiver (precision) amount) + + (with-capability (TRANSFER sender receiver amount) + (debit sender amount) + (with-read ledger receiver + { "guard" := g } + (credit receiver g amount)) + ) + ) + + (defun transfer-create:string + ( sender:string + receiver:string + receiver-guard:guard + amount:decimal ) + + (enforce (!= sender receiver) + "sender cannot be the receiver of a transfer") + (enforce-valid-transfer sender receiver (precision) amount) + + (with-capability (TRANSFER sender receiver amount) + (debit sender amount) + (credit receiver receiver-guard amount)) + ) + + (defun debit:string (account:string amount:decimal) + + (require-capability (DEBIT account)) + (with-read ledger account + { "balance" := balance } + + (enforce (<= amount balance) "Insufficient funds") + + (update ledger account + { "balance" : (- balance amount) } + )) + ) + + + (defun credit:string (account:string guard:guard amount:decimal) + + (require-capability (CREDIT account)) + (with-default-read ledger account + { "balance" : 0.0, "guard" : guard } + { "balance" := balance, "guard" := retg } + ; we don't want to overwrite an existing guard with the user-supplied one + (enforce (= retg guard) + "account guards do not match") + + (write ledger account + { "balance" : (+ balance amount) + , "guard" : retg + }) + )) + + + (defpact transfer-crosschain:string + ( sender:string + receiver:string + receiver-guard:guard + target-chain:string + amount:decimal ) + (step (enforce false "cross chain not supported")) + ) + + (defun detect-transfer-restriction:string + ( sender:string + receiver:string + amount:decimal ) + @doc "Hard-coded test implementation" + (if (= sender "Bob") + (if (= receiver "Alice") + (if (> amount 10.0) "Transfer restriction Bob->Alice > 10.0" + "") + "") + "") + ) + +) + +(create-table ledger) diff --git a/kip-0009/pact/restricted-fungible-v1.pact b/kip-0009/pact/restricted-fungible-v1.pact new file mode 100644 index 0000000..5d71ec4 --- /dev/null +++ b/kip-0009/pact/restricted-fungible-v1.pact @@ -0,0 +1,22 @@ +(namespace (read-msg 'ns)) + +(interface restricted-fungible-v1 + + "KIP-0009 Restricted Fungible standard, to be used with `fungible-v2` tokens." + + (defun detect-transfer-restriction:string + ( sender:string + receiver:string + amount:decimal ) + @doc " Implements restriction logic of their token transfers from SENDER to RECEIVER for AMOUNT. \ + \ Returns a `string` value explaining or codifying reason for restriction, \ + \ or `\"\"` (empty string) for success. \ + \ A `fungible-v2` MUST call this function in `transfer`, `transfer-create`, and \ + \ `transfer-crosschain`. The function also allows a 3rd party to test the expected \ + \ outcome of a transfer." + @model [ (!= sender "") + (!= receiver "") + (> amount 0.0) ] + ) + +) diff --git a/kip-0009/pact/restricted-fungible-v1.repl b/kip-0009/pact/restricted-fungible-v1.repl new file mode 100644 index 0000000..7008dad --- /dev/null +++ b/kip-0009/pact/restricted-fungible-v1.repl @@ -0,0 +1,44 @@ + +(begin-tx) +(load "include/fungible-v2.pact") +(env-data { 'ns: "test" }) +(define-namespace "test" (sig-keyset) (sig-keyset)) +(load "include/fungible-util.pact") +(load "restricted-fungible-v1.pact") +(load "restricted-fungible-v1-reference.pact") +(env-data { 'bob: ["Bob"], 'alice: ["Alice"], 'carol: ["Carol"]}) +(create-account "Bob" (read-keyset 'bob)) +(create-account "Alice" (read-keyset 'alice)) +(create-account "Carol" (read-keyset 'carol)) +(test-capability (CREDIT "Bob")) +(fund "Bob" 100.0) +(commit-tx) + +(begin-tx) +(use test.restricted-fungible-v1-reference) +(env-sigs + [ { 'key: "Bob", 'caps: + [ (TRANSFER "Bob" "Alice" 12.0) + (TRANSFER "Bob" "Carol" 12.0)]} + ]) + +(expect "Bob -> Carol 12.0 success" + "Write succeeded" + (transfer "Bob" "Carol" 12.0)) +(expect-failure "Bob -> Alice 12.0 failure, transfer" + "Transfer restriction detected" + (transfer "Bob" "Alice" 11.0)) +(expect-failure "Bob -> Alice 12.0 failure, transfer-create" + "Transfer restriction detected" + (transfer-create "Bob" "Alice" (read-keyset 'alice) 11.0)) +(rollback-tx) + +(begin-tx) +(use test.restricted-fungible-v1-reference) +(env-sigs + [ { 'key: "Bob", 'caps: + [ (TRANSFER "Bob" "Alice" 1.0)]} + ]) +(expect "Bob -> Alice 1.0 success" + "Write succeeded" + (transfer "Bob" "Alice" 1.0))