Skip to content

Commit

Permalink
Offchain implementation of SmartToken contracts (#13)
Browse files Browse the repository at this point in the history
* Initial nix and server boilerplate

* Offchain protocol params and directory

* Programmable Transfer

* Module refactoring, rebase & formatting

* Add endpoint for deploying protocol params

* Add loadEnv

* Fill directory derivation and redeemers

* Add test

* Programmable token issue tx builder

* Seizing tx builder

* Add initDirectoryTx + test (failing in lib/SmartTokens/LinkedList/Common.hs:214)

* Initial builders for transfer contracts

* Fix 'initDirectoryTx'

* Make a single endpoint for deployment

* Add some query endpoints

* Better type for query result

* Wst.Offchain.Endpoints.Env -> Wst.Offchain.Env, rename to OperatorEnv

* Move all env-like types to Wst.Offchain.Env

* Use classes for environment bits

* Use Env in DirectorySet, move Query module around

* Add InsertNodeArgs for insertDirectoryNode

* Issue transfer logic builder

* Add TransferLogicEnv

* Fix insert directory node test

* Add transferStablecoins tx builder

* Add seizeStablecoins tx builder

* Add issueProgrammableTokenTx endpoint

* Export issueProgrammableTokenTx

* issueProgrammableTokenTx with always yields validator

* Add query for programmable logic UTxOs

* CLI command parser

* CLI setup

* Load operator files

* Add some documentation

* Link to linked list

* Typo

* Add server implementation, better handling of Environment

* WIP issue and transfer smart tokens endpoints

* Smart token transfer unit test flow

* Sezing endpoint/unit test & fixes to transfer

* Error handling for blacklisted node

* Fix alwaysSucceeds script

* Update hash

---------

Co-authored-by: Jann Müller <[email protected]>
  • Loading branch information
amirmrad and j-mueller authored Dec 30, 2024
1 parent 0bdc09d commit 740658c
Show file tree
Hide file tree
Showing 44 changed files with 2,997 additions and 302 deletions.
56 changes: 55 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,60 @@

This is a proof-of-concept for a regulated stablecoin. It is NOT a finished product.

# Overview

The POC is based on [CIP-0143](https://github.com/colll78/CIPs/blob/patch-3/CIP-0143/README.md), instantiated with a programmable logic that checks whether the target address is blacklisted before allowing a transfer of the programmable token from one owner to another.

# Architecture

The system is designed so that all actions except the initial deployment of the programmable logic UTxOs can be performed through a web UI with browser-based wallets. The REST API therefore exposes a number of endpoints that produce fully-balanced-but-not-signed transactions. The intention is for the caller (web UI) to sign the transactions with the web-based wallet and submit them to the network. The backend uses blockfrost to query the blockchain. As a result, the server is pretty light-weight and does not even need its own database or a full cardano node.

# Usage

There is a CLI tool `wst-poc-cli` that performs the initial deployment of the system and runs the REST server. A signing key file is needed for the initial deployment but not for the operation of the server. A blockfrost token is needed for both the initial deployment.

(TO DO - document CLI operations)

# FAQs

## How is this system different from Djed?

Djed is an algorithmic stablecoin that is backed by Ada. In Djed we keep the entire reserves of the stablecoin in a UTxO that is controlled by the Djed contract. Every user of Djed can verify that the reserves exist and that there is enough Ada to pay out all Djed holders.

This POC implements a _fiat-backed stablecoin_. This means that the reserves exist in a bank account outside of the blockchain, and we have to trust the issuer of the stablecoin that every token that's been issued on-chain is backed by one USD in the bank account.

From a technical perspective, not having to manage the reserve on-chain makes the design of this POC somewhat simpler: We don't need to maintain a global state (the Djed UTxO) that all orders have to synchronise with. The challenge in this POC lies in the programmable token logic.

## How does the system scale?

The core idea of the regulated stablecoin is to run a check every time the owner of some amount of regulated tokens changes. This check is performed by the _transfer logic script_, a plutus program that consults a list of sanctioned addresses to ensure that the receiving address is not on it.

The list of sanctioned addresses is the only data structure that (a) needs to be read from by every transaction of the transfer logic script and (b) gets changed regularly during the operation of the stablecoin.

All other factors (number of scripts, script budget, max. number of transfer checks per transaction and so forth) are fixed and do not depend on the number of users.

It is important to note that the list of sanctioned addresses scales in space (number of UTxOs), but working with the data structure is done in constant time due to the way the data is laid out.

There is also no risk of UTxO congestion as the "system outputs" are used as reference inputs and not spent by user-to-user transfers. Each user-to-user transfer is processed independently.

### Sanctioned Addresses

The list of sanctioned addresses is stored on-chain as a [_linked list_](https://github.com/Anastasia-Labs/plutarch-linked-list). This means that each entry (address) in the list is represented as a single transaction output that includes the address itself as well as a pointer to the next address in lexicographical order.

When checking a transfer, the transfer logic script is provided with a single reference input containing the relevant entry in the ordered linked list.

The transfer transaction does not spend the linked list output, therefore the same linked list output can be used by many transactions in the same block and across multiple blocks.

#### How many sanctioned addresses are there?

Publicly available data on Tether (the largest fiat stablecoin) indicates that Tether has a total of [1990 sanctioned addresses](https://dune.com/phabc/usdt---banned-addresses), out of [109 million on-chain wallets](https://tether.io/news/how-many-usdt-on-chain-holders-are-there/) (Dec. 2024). This suggests that about 0.002 percent of addresses need to be blacklisted.

If our system achieved the scale of Tether then we would need about 1200 UTxOs to store the linked list. At current Ada prices this would amount to 1800 USD in min Ada UTxO deposits, an amount that will be refunded in its entirety when the linked list is deleted.

USDC, another fiat-stablecoin, currently has [264 blacklisted addresses](https://bloxy.info/txs/events_sc/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48?signature_id=257159) and 3m users, with a blacklist ratio of about 0.009 percent.

# Contributing

Bug reports and contributions are welcome!
Run the tests with `cabal test all`.

Bug reports and contributions are welcome!
14 changes: 14 additions & 0 deletions cabal.project
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,17 @@ source-repository-package
tag: 650a3435f8efbd4bf36e58768fac266ba5beede4
subdir:
src/plutarch-onchain-lib

source-repository-package
type: git
location: https://github.com/j-mueller/sc-tools
tag: e2759559324e172f12b11ab815323c48ed8922b0
subdir:
src/devnet
src/blockfrost
src/coin-selection
src/mockchain
src/optics
src/wallet
src/base
src/node-client
82 changes: 3 additions & 79 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@
};

nixpkgs.follows = "haskell-nix/nixpkgs";
iohk-nix.url = "github:input-output-hk/iohk-nix";
iohk-nix.inputs.nixpkgs.follows = "haskell-nix/nixpkgs";

# iohk-nix.url = "github:input-output-hk/iohk-nix";
# iohk-nix.inputs.nixpkgs.follows = "haskell-nix/nixpkgs";

hackage = {
url = "github:input-output-hk/hackage.nix";
flake = false;
};

CHaP = {
url = "github:input-output-hk/cardano-haskell-packages?ref=repo";
url = "github:IntersectMBO/cardano-haskell-packages?ref=repo";
flake = false;
};

Expand Down
3 changes: 2 additions & 1 deletion nix/project.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

let
sha256map = {
"https://github.com/j-mueller/sc-tools"."e2759559324e172f12b11ab815323c48ed8922b0" = "sha256-NHX+Euys+jBwKdTRJhK4XZLOOxQ+lf45T0BOroMF1m4=";
"https://github.com/colll78/plutarch-plutus"."b2379767c7f1c70acf28206bf922f128adc02f28" = "sha256-mhuW2CHxnc6FDWuMcjW/51PKuPOdYc4yxz+W5RmlQew=";
"https://github.com/input-output-hk/catalyst-onchain-libs"."650a3435f8efbd4bf36e58768fac266ba5beede4" = "sha256-NUh+l97+eO27Ppd8Bx0yMl0E5EV+p7+7GuFun1B8gRc=";
};
Expand All @@ -13,7 +14,7 @@ let
src = ../.;
name = "smart-tokens-plutarch";
compiler-nix-name = "ghc966";
index-state = "2024-10-16T00:00:00Z";
# index-state = "2024-10-16T00:00:00Z";
inputMap = {
"https://chap.intersectmbo.org/" = inputs.CHaP;
};
Expand Down
6 changes: 6 additions & 0 deletions src/exe/wst-poc-cli/Main.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module Main where

import Wst.Cli qualified

main :: IO ()
main = Wst.Cli.runMain
6 changes: 0 additions & 6 deletions src/exe/wst-poc/Main.hs

This file was deleted.

25 changes: 25 additions & 0 deletions src/lib/SmartTokens/CodeLens.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module SmartTokens.CodeLens(
_printTerm
) where

import Data.Text qualified as T
import GHC.Stack (HasCallStack)
import Plutarch (ClosedTerm)
import Plutarch.Internal qualified as PI
import Plutarch.Internal.Other (printScript)

-- TODO: Move to catalyst-libs project

-- _printTerm (communicated by Philip) just print some term as string. The term we want to print is
-- @
-- _term :: forall {s :: S}. Term s PBlacklistNode
-- _term = unsafeEvalTerm NoTracing (pconstant $ BlackListNode { key = "a", next = "b" })
-- @
-- Below, we inline the term and have it in a code lens. You can even run the code lens via Haskell
-- language server. The lens will then replace the string starting with "program ..." with exactly
-- the same string.
--
-- >>> _printTerm (pconstantData $ BlacklistNode { blnKey = "a hi", blnNext = "a" })
-- "program 1.0.0 (List [B #61206869, B #61])"
_printTerm :: HasCallStack => ClosedTerm a -> String
_printTerm term = printScript $ either (error . T.unpack) id $ PI.compile PI.NoTracing term
13 changes: 13 additions & 0 deletions src/lib/SmartTokens/Contracts/AlwaysYields.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{-| Plutus V3 script that always yields, ignoring its argument
-}
module SmartTokens.Contracts.AlwaysYields(
palwaysSucceed
) where

import Plutarch.LedgerApi.V3 (PScriptContext)
import Plutarch.Prelude (ClosedTerm, PUnit, pconstant, plam, (:-->))

{-| Validator that always succeeds
-}
palwaysSucceed :: ClosedTerm (PScriptContext :--> PUnit)
palwaysSucceed = plam (const $ pconstant ())
36 changes: 31 additions & 5 deletions src/lib/SmartTokens/Contracts/ExampleTransferLogic.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE QualifiedDo #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE TemplateHaskell #-}

module SmartTokens.Contracts.ExampleTransferLogic (
mkPermissionedTransfer,
mkFreezeAndSeizeTransfer,
BlacklistProof (..),
) where

import Plutarch.LedgerApi.V3
Expand All @@ -26,7 +30,26 @@ import Plutarch.Core.Utils
pvalidateConditions )
import Plutarch.Unsafe ( punsafeCoerce )
import SmartTokens.Types.PTokenDirectory ( PBlacklistNode, pletFieldsBlacklistNode)

import qualified PlutusTx
import Plutarch.DataRepr (DerivePConstantViaData (..))
import Plutarch.Lift (PConstantDecl, PUnsafeLiftDecl (..))

-- >>> _printTerm $ unsafeEvalTerm NoTracing (pconstant $ NonmembershipProof 1)
-- "program 1.0.0 (Constr 0 [I 1])"
data BlacklistProof
= NonmembershipProof Integer
deriving stock (Show, Eq, Generic)

PlutusTx.makeIsDataIndexed ''BlacklistProof
[('NonmembershipProof, 0)]

deriving via
(DerivePConstantViaData BlacklistProof PBlacklistProof)
instance
(PConstantDecl BlacklistProof)

-- >>> _printTerm $ unsafeEvalTerm NoTracing (mkRecordConstr PNonmembershipProof ( #nodeIdx .= pdata (pconstant 1)))
-- "program 1.0.0 (Constr 0 [I 1])"
data PBlacklistProof (s :: S)
= PNonmembershipProof
( Term
Expand All @@ -37,11 +60,14 @@ data PBlacklistProof (s :: S)
)
)
deriving stock (Generic)
deriving anyclass (PlutusType, PIsData, PEq)
deriving anyclass (PlutusType, PIsData, PEq, PShow)

instance DerivePlutusType PBlacklistProof where
type DPTStrat _ = PlutusTypeData

instance PUnsafeLiftDecl PBlacklistProof where
type PLifted PBlacklistProof = BlacklistProof

{-|
The 'mkPermissionedTransfer' is a transfer logic script that enforces that all transactions which spend the
associated programmable tokens must be signed by the specified permissioned credential.
Expand Down Expand Up @@ -86,7 +112,7 @@ mkPermissionedTransfer = plam $ \permissionedCred ctx ->
first node and lexographically less than the key of the second node (and thus if it was in the blacklist those two nodes
would not be adjacent).
- Confirms the legitimacy of both directory entries by checking the presence of the directory node currency symbol.
- For 'PNonmembershipProofTail':
- For 'PNonmembershipProofTail': FIXME: outdated
- Ensures that the witness key is greater than the tail node key in the blacklist.
- Confirms the legitimacy of the directory entry by checking the presence of the directory node currency symbol.
Expand All @@ -113,7 +139,7 @@ pvalidateWitnesses = phoistAcyclic $ plam $ \blacklistNodeCS proofs refInputs wi
-- the currency symbol is not in the blacklist
nodeKey #< witnessKey
, witnessKey #< nodeNext #|| nodeNext #== pconstant ""
-- both directory entries are legitimate, this is proven by the
-- directory entries are legitimate, this is proven by the
-- presence of the directory node currency symbol.
, phasDataCS # blacklistNodeCS # pfromData prevNodeUTxOF.value
]
Expand Down Expand Up @@ -162,4 +188,4 @@ mkFreezeAndSeizeTransfer = plam $ \blacklistNodeCS ctx -> P.do
pvalidateConditions
[ pisRewarding ctxF.scriptInfo
, pvalidateWitnesses # blacklistNodeCS # red # infoF.referenceInputs # txWitnesses
]
]
Loading

0 comments on commit 740658c

Please sign in to comment.