diff --git a/NodeFactory/stable-coin/.gitignore b/NodeFactory/stable-coin/.gitignore new file mode 100644 index 000000000..db3002230 --- /dev/null +++ b/NodeFactory/stable-coin/.gitignore @@ -0,0 +1,23 @@ +dist +dist-* +cabal-dev +*.o +*.hi +*.hie +*.chi +*.chs.h +*.dyn_o +*.dyn_hi +.hpc +.hsenv +.cabal-sandbox/ +cabal.sandbox.config +*.prof +*.aux +*.hp +*.eventlog +.stack-work/ +cabal.project.local +cabal.project.local~ +.HTF/ +.ghc.environment.* \ No newline at end of file diff --git a/NodeFactory/stable-coin/LICENSE b/NodeFactory/stable-coin/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/NodeFactory/stable-coin/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NodeFactory/stable-coin/README.md b/NodeFactory/stable-coin/README.md new file mode 100644 index 000000000..452d2b97c --- /dev/null +++ b/NodeFactory/stable-coin/README.md @@ -0,0 +1,59 @@ + +# Node Factory - Crypto backed stable coin on Cardano + +### Introduction + +The idea of this use case is to create a stable coin pegged to the US dollar within the Plutus partnership program. In this case, the stable coin is backed by the over-collateralization of ADA. For example, if a user wants to mint 100$ worth of stable coin he/she will need to lock at least 150% of the issued value as ADA (the price of ADA is provided from the oracle at the moment of minting the stable coin). +## Setting up + +- Install [nix](https://nixos.org/download.html) +- Clone and setup [plutus repo](https://github.com/input-output-hk/plutus) following README instructions (pay attention on setting binary cache) + +## Starting example + +1. Start `nix-shell` + +*Make sure that plutus is set up - and run nix-shell inside cloned Plutus repo* + +1. Build the PAB executable: + +``` +cabal build plutus-starter-pab +``` + +2. Run the PAB binary: + +``` +cabal run plutus-starter-pab +```` + +This will then start up the server on port 8080. The devcontainer process will then automatically expose this port so that you can connect to it from any terminal (it doesn't have to be a terminal running in the devcontainer). + +## Documentation + +### Opening a vault (minting stable coin) + +If a user wants to mint new stable coins, he/she needs to open a new “vault”. This vault will hold ADA collateral that must be at least 150% of minted stable coin value at the moment of minting. In practical use cases, users will always want to lock more than 150% of value in collateral as this is the threshold for liquidation (if the collateral value falls under 150% of the value it is eligible for liquidation). The current value of ADA on the market will be provided through an oracle. + +Based on the price of ADA (PADA) and the amount of stable coins that the user wants to mint (ASC), the user will need to send at least a minimal amount of ADA (Amin) to open the vault. + +Collateral will be sent to the created vault’s script address, which will also record information on the sender and the size of the collateral. This will be used as input on closing or liquidating the vault. + +Let’s say that Alice wants to mint 100 stable coins (SC) and she has decided to lock 200% of the minted value as collateral. As the current price of 1 ADA is 2$, she will send 100 ADA when opening the vault and receive 100 SC. This process is represented in the following schema. + +![alt text](static/open_vault.png) +### Closing a vault (burning the stable coin) + +When Alice wants to close her opened vault and return the collateral, closing action must be invoked. Here Alice sends the same amount of stable coin that was originally minted together with the vault UTXO - vault is closed and the user receives original collateral back. It is important to note that this action is possible only if the liquidation criteria haven’t been met. This process is represented in the following schema. + +![alt text](static/close_vault.png) + +### Vault liquidation + +Liquidation is an action that can be executed only if the current value of vault collateral has fallen below the defined 150% value of the minted stable coin. In this case, anybody is eligible to execute the liquidation process if he/she has enough stable coins to close the vault. + +We will continue with our example where Alice minted 100 SC for 100 ADA when the price of 1 ADA was 2$, and the collateral value was 200% of the minted stable coin value. + +Currently, the price of one ADA is 1.4$, and the value of the collateral is 140% of the minted stable coin value. This makes Alice’s vault eligible for liquidation. Bob decides that he has some spare stable coin and wants to liquidate Alice’s vault as he is incentivized by the reward for doing that. He will send the same amount of stable coin that Alice originally minted to close the vault. Locked ADA collateral is firstly used to pay out Bob for providing stable coins needed to close the vault. As an incentive for executing liquidation Bob will get 8% (an arbitrary parameter defined by the protocol) more value back. In the example below, as Bob provided 100 SC having a value of 100$, he will get 77.14 ADA back that at the moment of liquidation has a value of 108$. In total, Bob has earned 8$ in total for providing 100$ worth of stable coins (8%). The rest of the collateral that’s remaining will be returned to Alice. + +![alt text](static/liquidate_vault.png) diff --git a/NodeFactory/stable-coin/app/oracle-client.hs b/NodeFactory/stable-coin/app/oracle-client.hs new file mode 100644 index 000000000..06c07e23b --- /dev/null +++ b/NodeFactory/stable-coin/app/oracle-client.hs @@ -0,0 +1,59 @@ +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} + +module Main + ( main + ) where + +import Control.Concurrent +import Control.Monad (when) +import Control.Monad.IO.Class (MonadIO (..)) +import Data.ByteString (ByteString) +import Data.ByteString.Char8 (unpack) +import Data.Proxy (Proxy (..)) +import Data.Text (pack) +import Data.UUID +import Network.HTTP.Req +import Text.Regex.TDFA + +main :: IO () +main = do + uuid <- read <$> readFile "oracle.cid" + putStrLn $ "oracle contract instance id: " ++ show uuid + go uuid Nothing + where + go :: UUID -> Maybe Integer -> IO a + go uuid m = do + x <- getExchangeRate + let y = Just x + when (m /= y) $ + updateOracle uuid x + threadDelay 5_000_000 + go uuid y + +updateOracle :: UUID -> Integer -> IO () +updateOracle uuid x = runReq defaultHttpConfig $ do + v <- req + POST + (http "127.0.0.1" /: "api" /: "new" /: "contract" /: "instance" /: pack (show uuid) /: "endpoint" /: "update") + (ReqBodyJson x) + (Proxy :: Proxy (JsonResponse ())) + (port 8080) + liftIO $ putStrLn $ if responseStatusCode v == 200 + then "updated oracle to " ++ show x + else "error updating oracle" + +getExchangeRate :: IO Integer +getExchangeRate = runReq defaultHttpConfig $ do + v <- req + GET + (https "coinmarketcap.com" /: "currencies" /: "cardano") + NoReqBody + bsResponse + mempty + let priceRegex = "priceValue___11gHJ\">\\$([\\.0-9]*)" :: ByteString + (_, _, _, [bs]) = responseBody v =~ priceRegex :: (ByteString, ByteString, ByteString, [ByteString]) + d = read $ unpack bs :: Double + x = round $ 1_000_000 * d + liftIO $ putStrLn $ "queried exchange rate: " ++ show d + return x \ No newline at end of file diff --git a/NodeFactory/stable-coin/app/oracle-pab.hs b/NodeFactory/stable-coin/app/oracle-pab.hs new file mode 100644 index 000000000..6858e68aa --- /dev/null +++ b/NodeFactory/stable-coin/app/oracle-pab.hs @@ -0,0 +1,123 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +module Main + ( main + ) where + +import Control.Monad (forM_, void, when) +import Control.Monad.Freer (Eff, Member, interpret, type (~>)) +import Control.Monad.Freer.Error (Error) +import Control.Monad.Freer.Extras.Log (LogMsg) +import Control.Monad.IO.Class (MonadIO (..)) +import Data.Aeson (FromJSON, Result (..), fromJSON) +import Data.Monoid (Last (..)) +import Data.Text (Text, pack) +import Ledger +import Ledger.Constraints +import qualified Ledger.Value as Value +import Plutus.Contract hiding (when) +import Plutus.PAB.Effects.Contract (ContractEffect (..)) +import Plutus.PAB.Effects.Contract.Builtin (Builtin, SomeBuiltin (..), type (.\\), endpointsToSchemas, handleBuiltin) +import Plutus.PAB.Monitoring.PABLogMsg (PABMultiAgentMsg) +import Plutus.PAB.Simulator (SimulatorEffectHandlers) +import qualified Plutus.PAB.Simulator as Simulator +import Plutus.PAB.Types (PABError (..)) +import qualified Plutus.PAB.Webserver.Server as PAB.Server +import qualified Plutus.Contracts.Currency as Currency + +import Wallet.Emulator.Types (Wallet (..), walletPubKey) +import Wallet.Types (ContractInstanceId (..)) + +import qualified NodeFactory.Plutus.Contracts.Oracle.Core as Oracle +import NodeFactory.Plutus.Contracts.Oracle.PAB (OracleContracts (..)) +import qualified NodeFactory.Plutus.Contracts.Oracle.Swap as Oracle + +main :: IO () +main = void $ Simulator.runSimulationWith handlers $ do + Simulator.logString @(Builtin OracleContracts) "Starting Oracle PAB webserver. Press enter to exit." + shutdown <- PAB.Server.startServerDebug + + cidInit <- Simulator.activateContract (Wallet 1) Init + cs <- waitForLast cidInit + _ <- Simulator.waitUntilFinished cidInit + + cidOracle <- Simulator.activateContract (Wallet 1) $ Oracle cs + liftIO $ writeFile "oracle.cid" $ show $ unContractInstanceId cidOracle + oracle <- waitForLast cidOracle + + forM_ wallets $ \w -> + when (w /= Wallet 1) $ do + cid <- Simulator.activateContract w $ Swap oracle + liftIO $ writeFile ('W' : show (getWallet w) ++ ".cid") $ show $ unContractInstanceId cid + + void $ liftIO getLine + shutdown + +waitForLast :: FromJSON a => ContractInstanceId -> Simulator.Simulation t a +waitForLast cid = + flip Simulator.waitForState cid $ \json -> case fromJSON json of + Success (Last (Just x)) -> Just x + _ -> Nothing + +wallets :: [Wallet] +wallets = [Wallet i | i <- [1 .. 5]] + +usdt :: TokenName +usdt = "USDT" + +oracleParams :: CurrencySymbol -> Oracle.OracleParams +oracleParams cs = Oracle.OracleParams + { Oracle.opFees = 1_000_000 + , Oracle.opSymbol = cs + , Oracle.opToken = usdt + } + +handleOracleContracts :: + ( Member (Error PABError) effs + , Member (LogMsg (PABMultiAgentMsg (Builtin OracleContracts))) effs + ) + => ContractEffect (Builtin OracleContracts) + ~> Eff effs +handleOracleContracts = handleBuiltin getSchema getContract where + getSchema = \case + Init -> endpointsToSchemas @Empty + Oracle _ -> endpointsToSchemas @(Oracle.OracleSchema .\\ BlockchainActions) + Swap _ -> endpointsToSchemas @(Oracle.SwapSchema .\\ BlockchainActions) + getContract = \case + Init -> SomeBuiltin initContract + Oracle cs -> SomeBuiltin $ Oracle.runOracle $ oracleParams cs + Swap oracle -> SomeBuiltin $ Oracle.swap oracle + +handlers :: SimulatorEffectHandlers (Builtin OracleContracts) +handlers = + Simulator.mkSimulatorHandlers @(Builtin OracleContracts) [] + $ interpret handleOracleContracts + +initContract :: Contract (Last CurrencySymbol) BlockchainActions Text () +initContract = do + ownPK <- pubKeyHash <$> ownPubKey + cur <- + mapError (pack . show) + (Currency.forgeContract ownPK [(usdt, fromIntegral (length wallets) * amount)] + :: Contract (Last CurrencySymbol) BlockchainActions Currency.CurrencyError Currency.OneShotCurrency) + let cs = Currency.currencySymbol cur + v = Value.singleton cs usdt amount + forM_ wallets $ \w -> do + let pkh = pubKeyHash $ walletPubKey w + when (pkh /= ownPK) $ do + tx <- submitTx $ mustPayToPubKey pkh v + awaitTxConfirmed $ txId tx + tell $ Last $ Just cs + where + amount :: Integer + amount = 100_000_000 \ No newline at end of file diff --git a/NodeFactory/stable-coin/app/swap-client.hs b/NodeFactory/stable-coin/app/swap-client.hs new file mode 100644 index 000000000..dabb1a813 --- /dev/null +++ b/NodeFactory/stable-coin/app/swap-client.hs @@ -0,0 +1,122 @@ +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Main + ( main + ) where + +import Control.Concurrent +import Control.Exception +import Control.Monad.IO.Class (MonadIO (..)) +import Data.Aeson (Result (..), fromJSON) +import Data.Monoid (Last (..)) +import Data.Proxy (Proxy (..)) +import Data.Text (pack) +import Data.UUID +import Ledger.Value (flattenValue) +import Network.HTTP.Req +import Plutus.PAB.Events.ContractInstanceState (PartiallyDecodedResponse (..)) +import Plutus.PAB.Webserver.Types +import System.Environment (getArgs) +import System.IO +import Text.Read (readMaybe) + +import NodeFactory.Plutus.Contracts.Oracle.PAB (OracleContracts) + +main :: IO () +main = do + [i :: Int] <- map read <$> getArgs + uuid <- read <$> readFile ('W' : show i ++ ".cid") + hSetBuffering stdout NoBuffering + putStrLn $ "swap contract instance id for Wallet " ++ show i ++ ": " ++ show uuid + go uuid + where + go :: UUID -> IO a + go uuid = do + cmd <- readCommand + case cmd of + Offer amt -> offer uuid amt + Retrieve -> retrieve uuid + Use -> use uuid + Funds -> getFunds uuid + go uuid + + readCommand :: IO Command + readCommand = do + putStr "enter command (Offer amt, Retrieve, Use or Funds): " + s <- getLine + maybe readCommand return $ readMaybe s + +data Command = Offer Integer | Retrieve | Use | Funds + deriving (Show, Read, Eq, Ord) + +getFunds :: UUID -> IO () +getFunds uuid = handle h $ runReq defaultHttpConfig $ do + v <- req + POST + (http "127.0.0.1" /: "api" /: "new" /: "contract" /: "instance" /: pack (show uuid) /: "endpoint" /: "funds") + (ReqBodyJson ()) + (Proxy :: Proxy (JsonResponse ())) + (port 8080) + if responseStatusCode v /= 200 + then liftIO $ putStrLn "error getting funds" + else do + w <- req + GET + (http "127.0.0.1" /: "api" /: "new" /: "contract" /: "instance" /: pack (show uuid) /: "status") + NoReqBody + (Proxy :: Proxy (JsonResponse (ContractInstanceClientState OracleContracts))) + (port 8080) + liftIO $ putStrLn $ case fromJSON $ observableState $ cicCurrentState $ responseBody w of + Success (Last (Just f)) -> "funds: " ++ show (flattenValue f) + _ -> "error decoding state" + where + h :: HttpException -> IO () + h _ = threadDelay 1_000_000 >> getFunds uuid + +offer :: UUID -> Integer -> IO () +offer uuid amt = handle h $ runReq defaultHttpConfig $ do + v <- req + POST + (http "127.0.0.1" /: "api" /: "new" /: "contract" /: "instance" /: pack (show uuid) /: "endpoint" /: "offer") + (ReqBodyJson amt) + (Proxy :: Proxy (JsonResponse ())) + (port 8080) + liftIO $ putStrLn $ if responseStatusCode v == 200 + then "offered swap of " ++ show amt ++ " lovelace" + else "error offering swap" + where + h :: HttpException -> IO () + h _ = threadDelay 1_000_000 >> offer uuid amt + +retrieve :: UUID -> IO () +retrieve uuid = handle h $ runReq defaultHttpConfig $ do + v <- req + POST + (http "127.0.0.1" /: "api" /: "new" /: "contract" /: "instance" /: pack (show uuid) /: "endpoint" /: "retrieve") + (ReqBodyJson ()) + (Proxy :: Proxy (JsonResponse ())) + (port 8080) + liftIO $ putStrLn $ if responseStatusCode v == 200 + then "retrieved swaps" + else "error retrieving swaps" + where + h :: HttpException -> IO () + h _ = threadDelay 1_000_000 >> retrieve uuid + +use :: UUID -> IO () +use uuid = handle h $ runReq defaultHttpConfig $ do + v <- req + POST + (http "127.0.0.1" /: "api" /: "new" /: "contract" /: "instance" /: pack (show uuid) /: "endpoint" /: "use") + (ReqBodyJson ()) + (Proxy :: Proxy (JsonResponse ())) + (port 8080) + liftIO $ putStrLn $ if responseStatusCode v == 200 + then "used swap" + else "error using swap" + where + h :: HttpException -> IO () + h _ = threadDelay 1_000_000 >> use uuid + diff --git a/NodeFactory/stable-coin/cabal.project b/NodeFactory/stable-coin/cabal.project new file mode 100644 index 000000000..062ecfc4c --- /dev/null +++ b/NodeFactory/stable-coin/cabal.project @@ -0,0 +1,195 @@ +index-state: 2021-04-13T00:00:00Z + +packages: ./. + +-- You never, ever, want this. +write-ghc-environment-files: never + +-- Always build tests and benchmarks. +tests: true +benchmarks: true + +source-repository-package + type: git + location: https://github.com/input-output-hk/plutus.git + subdir: + freer-extras + playground-common + plutus-chain-index + plutus-core + plutus-contract + plutus-ledger + plutus-ledger-api + plutus-pab + plutus-tx + plutus-tx-plugin + plutus-use-cases + prettyprinter-configurable + quickcheck-dynamic + word-array + tag: 81ba78edb1d634a13371397d8c8b19829345ce0d + +-- The following sections are copied from the 'plutus' repository cabal.project at the revision +-- given above. +-- This is necessary because the 'plutus' libraries depend on a number of other libraries which are +-- not on Hackage, and so need to be pulled in as `source-repository-package`s themselves. Make sure to +-- re-update this section from the template when you do an upgrade. + +-- This is also needed so evenful-sql-common will build with a +-- newer version of persistent. See stack.yaml for the mirrored +-- configuration. +package eventful-sql-common + ghc-options: -XDerivingStrategies -XStandaloneDeriving -XUndecidableInstances -XDataKinds -XFlexibleInstances -XMultiParamTypeClasses + +allow-newer: + -- Pins to an old version of Template Haskell, unclear if/when it will be updated + size-based:template-haskell + + -- The following two dependencies are needed by plutus. + , eventful-sql-common:persistent + , eventful-sql-common:persistent-template + , ouroboros-consensus-byron:formatting + , beam-core:aeson + , beam-sqlite:aeson + , beam-sqlite:dlist + , beam-migrate:aeson + +constraints: + -- big breaking change here, inline-r doens't have an upper bound + singletons < 3.0 + -- breaks eventful even more than it already was + , persistent-template < 2.12 + -- bizarre issue: in earlier versions they define their own 'GEq', in newer + -- ones they reuse the one from 'some', but there isn't e.g. a proper version + -- constraint from dependent-sum-template (which is the library we actually use). + , dependent-sum > 0.6.2.0 + +-- See the note on nix/pkgs/default.nix:agdaPackages for why this is here. +-- (NOTE this will change to ieee754 in newer versions of nixpkgs). +extra-packages: ieee, filemanip + +-- Drops an instance breaking our code. Should be released to Hackage eventually. +source-repository-package + type: git + location: https://github.com/Quid2/flat.git + tag: 95e5d7488451e43062ca84d5376b3adcc465f1cd + +-- Needs some patches, but upstream seems to be fairly dead (no activity in > 1 year) +source-repository-package + type: git + location: https://github.com/shmish111/purescript-bridge.git + tag: 6a92d7853ea514be8b70bab5e72077bf5a510596 + +source-repository-package + type: git + location: https://github.com/shmish111/servant-purescript.git + tag: a76104490499aa72d40c2790d10e9383e0dbde63 + +source-repository-package + type: git + location: https://github.com/input-output-hk/cardano-crypto.git + tag: ce8f1934e4b6252084710975bd9bbc0a4648ece4 + +source-repository-package + type: git + location: https://github.com/input-output-hk/cardano-base + tag: a715c7f420770b70bbe95ca51d3dec83866cb1bd + subdir: + binary + binary/test + slotting + cardano-crypto-class + cardano-crypto-praos + cardano-crypto-tests + strict-containers + +source-repository-package + type: git + location: https://github.com/input-output-hk/cardano-prelude + tag: fd773f7a58412131512b9f694ab95653ac430852 + subdir: + cardano-prelude + cardano-prelude-test + +source-repository-package + type: git + location: https://github.com/input-output-hk/ouroboros-network + tag: e50613562d6d4a0f933741fcf590b0f69a1eda67 + subdir: + typed-protocols + typed-protocols-examples + ouroboros-network + ouroboros-network-testing + ouroboros-network-framework + ouroboros-consensus + ouroboros-consensus-byron + ouroboros-consensus-cardano + ouroboros-consensus-shelley + io-sim + io-sim-classes + network-mux + +source-repository-package + type: git + location: https://github.com/input-output-hk/iohk-monitoring-framework + tag: 34abfb7f4f5610cabb45396e0496472446a0b2ca + subdir: + iohk-monitoring + tracer-transformers + contra-tracer + plugins/backend-aggregation + plugins/backend-ekg + plugins/backend-monitoring + plugins/backend-trace-forwarder + plugins/scribe-systemd + +source-repository-package + type: git + location: https://github.com/input-output-hk/cardano-ledger-specs + tag: a3ef848542961079b7cd53d599e5385198a3035c + subdir: + byron/chain/executable-spec + byron/crypto + byron/crypto/test + byron/ledger/executable-spec + byron/ledger/impl + byron/ledger/impl/test + semantics/executable-spec + semantics/small-steps-test + shelley/chain-and-ledger/dependencies/non-integer + shelley/chain-and-ledger/executable-spec + shelley/chain-and-ledger/shelley-spec-ledger-test + shelley-ma/impl + cardano-ledger-core + alonzo/impl + +-- A lot of plutus dependencies have to be synchronized with the dependencies of +-- cardano-node. If you update cardano-node, please make sure that all dependencies +-- of cardano-node are also updated. +source-repository-package + type: git + location: https://github.com/input-output-hk/cardano-node.git + tag: b3cabae6b3bf30a0b1b4e78bc4b67282dabad0a6 + subdir: + cardano-api/test + cardano-api + cardano-node + cardano-cli + cardano-config + +source-repository-package + type: git + location: https://github.com/input-output-hk/Win32-network + tag: 94153b676617f8f33abe8d8182c37377d2784bd1 + +source-repository-package + type: git + location: https://github.com/input-output-hk/hedgehog-extras + tag: 8bcd3c9dc22cc44f9fcfe161f4638a384fc7a187 + +-- The following dependencies are not mirrored in the +-- stack.yaml file, but they are needed regardless by cabal. +source-repository-package + type: git + location: https://github.com/input-output-hk/goblins + tag: cde90a2b27f79187ca8310b6549331e59595e7ba \ No newline at end of file diff --git a/NodeFactory/stable-coin/hie.yaml b/NodeFactory/stable-coin/hie.yaml new file mode 100644 index 000000000..04cd24395 --- /dev/null +++ b/NodeFactory/stable-coin/hie.yaml @@ -0,0 +1,2 @@ +cradle: + cabal: diff --git a/NodeFactory/stable-coin/plutus-stable-coin.cabal b/NodeFactory/stable-coin/plutus-stable-coin.cabal new file mode 100644 index 000000000..c7c9ab107 --- /dev/null +++ b/NodeFactory/stable-coin/plutus-stable-coin.cabal @@ -0,0 +1,103 @@ +cabal-version: 2.4 +name: plutus-stable-coin +version: 0.1.0.0 +license: Apache-2.0 +license-files: LICENSE +author: NodeFactory +maintainer: mak@nodefactory.io + +flag defer-plugin-errors + description: + Defer errors from the plugin, useful for things like Haddock that can't handle it. + default: False + manual: True + +common lang + default-language: Haskell2010 + default-extensions: ExplicitForAll ScopedTypeVariables + DeriveGeneric StandaloneDeriving DeriveLift + GeneralizedNewtypeDeriving DeriveFunctor DeriveFoldable + DeriveTraversable + ghc-options: -Wall -Wnoncanonical-monad-instances + -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wredundant-constraints -Widentities + -- See Plutus Tx readme + -fobject-code -fno-ignore-interface-pragmas -fno-omit-interface-pragmas + if flag(defer-plugin-errors) + ghc-options: -fplugin-opt PlutusTx.Plugin:defer-errors + +library + hs-source-dirs: src + exposed-modules: + -- NodeFactory.Plutus.Contracts.Oracle.Core + -- NodeFactory.Plutus.Contracts.Oracle.Funds + -- NodeFactory.Plutus.Contracts.Oracle.PAB + -- NodeFactory.Plutus.Contracts.Oracle.Swap + NodeFactory.Plutus.Contracts.StableCoin.Types + NodeFactory.Plutus.Contracts.StableCoin.OnChain + NodeFactory.Plutus.Contracts.StableCoin.OffChain + NodeFactory.Plutus.Contracts.Currency + build-depends: aeson + , base ^>=4.14.1.0 + , containers + , data-default + , freer-extras + , playground-common + , plutus-contract + , plutus-ledger + , plutus-ledger-api + , plutus-tx-plugin + , plutus-tx + , plutus-use-cases + , lens + , prettyprinter + , text + default-language: Haskell2010 + default-extensions: ExplicitForAll ScopedTypeVariables + DeriveGeneric StandaloneDeriving DeriveLift + GeneralizedNewtypeDeriving DeriveFunctor DeriveFoldable + DeriveTraversable + ghc-options: -Wall -fobject-code -fno-ignore-interface-pragmas + -fno-omit-interface-pragmas -fno-strictness -fno-spec-constr + -fno-specialise -Wnoncanonical-monad-instances + -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wredundant-constraints -Widentities + +executable oracle-pab + main-is: oracle-pab.hs + hs-source-dirs: app + ghc-options: -Wall -threaded + build-depends: aeson + , base ^>= 4.14.1.0 + , freer-extras + , freer-simple + , plutus-contract + , plutus-ledger + , plutus-pab + , plutus-stable-coin + , plutus-use-cases + , text + +executable oracle-client + main-is: oracle-client.hs + hs-source-dirs: app + ghc-options: -Wall + build-depends: base ^>= 4.14.1.0 + , bytestring + , regex-tdfa ^>= 1.3.1.0 + , req ^>= 3.9.0 + , text + , uuid + +executable swap-client + main-is: swap-client.hs + hs-source-dirs: app + ghc-options: -Wall + build-depends: aeson + , base ^>= 4.14.1.0 + , plutus-ledger + , plutus-pab + , plutus-stable-coin + , req ^>= 3.9.0 + , text + , uuid diff --git a/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/Currency.hs b/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/Currency.hs new file mode 100644 index 000000000..d89a76e5c --- /dev/null +++ b/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/Currency.hs @@ -0,0 +1,205 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE MonoLocalBinds #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE ViewPatterns #-} +{-# OPTIONS_GHC -fno-ignore-interface-pragmas #-} +-- | Implements a custom currency with a minting policy that allows +-- the minting of a fixed amount of units. +module NodeFactory.Plutus.Contracts.Currency( + OneShotCurrency(..) + , CurrencySchema + , CurrencyError(..) + , AsCurrencyError(..) + , curPolicy + -- * Actions etc + , mintContract + , mintedValue + , currencySymbol + -- * Simple minting policy currency + , SimpleMPS(..) + , mintCurrency + -- * Creating thread tokens + , createThreadToken + ) where + +import Control.Lens +import Plutus.Contracts.PubKey (AsPubKeyError (..), PubKeyError) +import qualified Plutus.Contracts.PubKey as PK +import PlutusTx.Prelude hiding (Monoid (..), Semigroup (..)) + +import Plutus.Contract as Contract + +import Ledger (CurrencySymbol, PubKeyHash, TxId, TxOutRef (..), pubKeyHash, + scriptCurrencySymbol, txId) +import qualified Ledger.Ada as Ada +import qualified Ledger.Constraints as Constraints +import qualified Ledger.Contexts as V +import Ledger.Scripts +import qualified PlutusTx +import qualified Ledger.Typed.Scripts as Scripts +import Ledger.Value (AssetClass, TokenName, Value) +import qualified Ledger.Value as Value + +import Data.Aeson (FromJSON, ToJSON) +import qualified Data.Map as Map +import Data.Semigroup (Last (..)) +import GHC.Generics (Generic) +import qualified PlutusTx.AssocMap as AssocMap +import Prelude (Semigroup (..)) +import qualified Prelude as Haskell +import Schema (ToSchema) + +{-# ANN module ("HLint: ignore Use uncurry" :: Haskell.String) #-} + +-- | A currency that can be created exactly once +data OneShotCurrency = OneShotCurrency + { curRefTransactionOutput :: (TxId, Integer) + -- ^ Transaction input that must be spent when + -- the currency is minted. + , curAmounts :: AssocMap.Map TokenName Integer + -- ^ How many units of each 'TokenName' are to + -- be minted. + } + deriving stock (Generic, Haskell.Show, Haskell.Eq) + deriving anyclass (ToJSON, FromJSON) +PlutusTx.makeLift ''OneShotCurrency + +currencyValue :: CurrencySymbol -> OneShotCurrency -> Value +currencyValue s OneShotCurrency{curAmounts = amts} = + let + values = map (\(tn, i) -> (Value.singleton s tn i)) (AssocMap.toList amts) + in fold values + +mkCurrency :: TxOutRef -> [(TokenName, Integer)] -> OneShotCurrency +mkCurrency (TxOutRef h i) amts = + OneShotCurrency + { curRefTransactionOutput = (h, i) + , curAmounts = AssocMap.fromList amts + } + +validate :: OneShotCurrency -> () -> V.ScriptContext -> Bool +validate c@(OneShotCurrency (refHash, refIdx) _) _ ctx@V.ScriptContext{V.scriptContextTxInfo=txinfo} = + let + -- see note [Obtaining the currency symbol] + ownSymbol = V.ownCurrencySymbol ctx + + minted = V.txInfoForge txinfo + expected = currencyValue ownSymbol c + + -- True if the pending transaction mints the amount of + -- currency that we expect + mintOK = + let v = expected == minted + in traceIfFalse "Value minted different from expected" v + + -- True if the pending transaction spends the output + -- identified by @(refHash, refIdx)@ + txOutputSpent = + let v = V.spendsOutput txinfo refHash refIdx + in traceIfFalse "Pending transaction does not spend the designated transaction output" v + + in mintOK && txOutputSpent + +curPolicy :: OneShotCurrency -> MintingPolicy +curPolicy cur = mkMintingPolicyScript $ + $$(PlutusTx.compile [|| \c -> Scripts.wrapMintingPolicy (validate c) ||]) + `PlutusTx.applyCode` + PlutusTx.liftCode cur + +{- note [Obtaining the currency symbol] +The currency symbol is the address (hash) of the validator. That is why +we can use 'Ledger.scriptAddress' here to get the symbol in off-chain code, +for example in 'mintedValue'. +Inside the validator script (on-chain) we can't use 'Ledger.scriptAddress', +because at that point we don't know the hash of the script yet. That +is why we use 'V.ownCurrencySymbol', which obtains the hash from the +'PolicyCtx' value. +-} + +-- | The 'Value' minted by the 'OneShotCurrency' contract +mintedValue :: OneShotCurrency -> Value +mintedValue cur = currencyValue (currencySymbol cur) cur + +currencySymbol :: OneShotCurrency -> CurrencySymbol +currencySymbol = scriptCurrencySymbol . curPolicy + +data CurrencyError = + CurPubKeyError PubKeyError + | CurContractError ContractError + deriving stock (Haskell.Eq, Haskell.Show, Generic) + deriving anyclass (ToJSON, FromJSON) + +makeClassyPrisms ''CurrencyError + +instance AsContractError CurrencyError where + _ContractError = _CurContractError + +instance AsPubKeyError CurrencyError where + _PubKeyError = _CurPubKeyError + +-- | @mint [(n1, c1), ..., (n_k, c_k)]@ creates a new currency with +-- @k@ token names, minting @c_i@ units of each token @n_i@. +-- If @k == 0@ then no value is minted. A one-shot minting policy +-- script is used to ensure that no more units of the currency can +-- be minted afterwards. +mintContract + :: forall w s e. + ( AsCurrencyError e + ) + => PubKeyHash + -> [(TokenName, Integer)] + -> Contract w s e OneShotCurrency +mintContract pk amounts = mapError (review _CurrencyError) $ do + (txOutRef, txOutTx, pkInst) <- PK.pubKeyContract pk (Ada.lovelaceValueOf 1) + let theCurrency = mkCurrency txOutRef amounts + curVali = curPolicy theCurrency + lookups = Constraints.mintingPolicy curVali + <> Constraints.otherScript (Scripts.validatorScript pkInst) + <> Constraints.unspentOutputs (Map.singleton txOutRef txOutTx) + let mintTx = Constraints.mustSpendScriptOutput txOutRef unitRedeemer + <> Constraints.mustMintValue (mintedValue theCurrency) + tx <- submitTxConstraintsWith @Scripts.Any lookups mintTx + _ <- awaitTxConfirmed (txId tx) + pure theCurrency + +-- | Minting policy for a currency that has a fixed amount of tokens issued +-- in one transaction +data SimpleMPS = + SimpleMPS + { tokenName :: TokenName + , amount :: Integer + } + deriving stock (Haskell.Eq, Haskell.Show, Generic) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +type CurrencySchema = + Endpoint "Create native token" SimpleMPS + +-- | Use 'mintContract' to create the currency specified by a 'SimpleMPS' +mintCurrency + :: Contract (Maybe (Last OneShotCurrency)) CurrencySchema CurrencyError OneShotCurrency +mintCurrency = do + SimpleMPS{tokenName, amount} <- endpoint @"Create native token" + ownPK <- pubKeyHash <$> ownPubKey + cur <- mintContract ownPK [(tokenName, amount)] + tell (Just (Last cur)) + pure cur + +-- | Create a thread token for a state machine +createThreadToken :: forall s w. Contract w s CurrencyError AssetClass +createThreadToken = do + ownPK <- pubKeyHash <$> ownPubKey + let tokenName :: TokenName = "thread token" + s <- mintContract ownPK [(tokenName, 1)] + pure $ Value.assetClass (currencySymbol s) tokenName \ No newline at end of file diff --git a/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/Oracle/Core.hs b/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/Oracle/Core.hs new file mode 100644 index 000000000..88ff4322e --- /dev/null +++ b/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/Oracle/Core.hs @@ -0,0 +1,207 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +module NodeFactory.Plutus.Contracts.Oracle.Core + ( Oracle (..) + , OracleRedeemer (..) + , oracleTokenName + , oracleValue + , oracleAsset + , oracleInst + , oracleValidator + , oracleAddress + , OracleSchema + , OracleParams (..) + , runOracle + , findOracle + ) where + +import Control.Monad hiding (fmap) +import Data.Aeson (FromJSON, ToJSON) +import qualified Data.Map as Map +import Data.Monoid (Last (..)) +import Data.Text (Text, pack) +import GHC.Generics (Generic) +import Plutus.Contract as Contract hiding (when) +import qualified PlutusTx +import PlutusTx.Prelude hiding (Semigroup(..), unless) +import Ledger hiding (singleton) +import Ledger.Constraints as Constraints +import qualified Ledger.Typed.Scripts as Scripts +import Ledger.Value as Value +import Ledger.Ada as Ada +import Plutus.Contracts.Currency as Currency +import Prelude (Semigroup (..), String, show) +import qualified Prelude as Prelude + +-- contract param structure +data Oracle = Oracle + { oSymbol :: !CurrencySymbol -- currency symbol of NFT + , oOperator :: !PubKeyHash -- owner of the oracle + , oFee :: !Integer -- fees in lovelace + , oAsset :: !AssetClass -- target of the oracle (US dollar) + } deriving (Prelude.Show, Generic, FromJSON, ToJSON, Prelude.Eq, Prelude.Ord) + +PlutusTx.makeLift ''Oracle + +data OracleRedeemer = Update | Use -- two availabe operations + deriving Prelude.Show + +PlutusTx.unstableMakeIsData ''OracleRedeemer + +-- HELPER FUNCTIONS + +{-# INLINABLE oracleTokenName #-} -- use empty string for token name +oracleTokenName :: TokenName +oracleTokenName = TokenName emptyByteString + +{-# INLINABLE oracleAsset #-} +oracleAsset :: Oracle -> AssetClass -- identify asset class if oracle provided +oracleAsset oracle = AssetClass (oSymbol oracle, oracleTokenName) + +{-# INLINABLE oracleValue #-} -- convert transaction output datum to integer +oracleValue :: TxOut -> (DatumHash -> Maybe Datum) -> Maybe Integer +oracleValue o f = do + dh <- txOutDatum o + Datum d <- f dh + PlutusTx.fromData d + +{-# INLINABLE mkOracleValidator #-} -- validator functions +mkOracleValidator :: Oracle -> Integer -> OracleRedeemer -> ScriptContext -> Bool +mkOracleValidator oracle x r ctx = + traceIfFalse "token missing from input" inputHasToken && + traceIfFalse "token missing from output" outputHasToken && + case r of + Update -> traceIfFalse "operator signature missing" (txSignedBy info $ oOperator oracle) && + traceIfFalse "invalid output datum" validOutputDatum + Use -> traceIfFalse "oracle value changed" (outputDatum == Just x) && + traceIfFalse "fees not paid" feesPaid + where + info :: TxInfo -- extract transaction info from context + info = scriptContextTxInfo ctx + + ownInput :: TxOut -- get oracle input + ownInput = case findOwnInput ctx of + Nothing -> traceError "oracle input missing" + Just i -> txInInfoResolved i + + inputHasToken :: Bool -- check if input contains nft token + inputHasToken = assetClassValueOf (txOutValue ownInput) (oracleAsset oracle) == 1 + + ownOutput :: TxOut -- get oracle output + ownOutput = case getContinuingOutputs ctx of + [o] -> o + _ -> traceError "expected exactly one oracle output" + + outputHasToken :: Bool -- check if output contains nft token + outputHasToken = assetClassValueOf (txOutValue ownOutput) (oracleAsset oracle) == 1 + + outputDatum :: Maybe Integer -- get data from output datum + outputDatum = oracleValue ownOutput (`findDatum` info) + + validOutputDatum :: Bool -- check if type of data in output datum valid + validOutputDatum = isJust outputDatum + + feesPaid :: Bool -- check if user paid fees + feesPaid = + let + inVal = txOutValue ownInput + outVal = txOutValue ownOutput + in + outVal `geq` (inVal <> Ada.lovelaceValueOf (oFee oracle)) + +data Oracling +instance Scripts.ValidatorTypes Oracling where + type instance DatumType Oracling = Integer + type instance RedeemerType Oracling = OracleRedeemer + +oracleInst :: Oracle -> Scripts.TypedValidator Oracling +oracleInst oracle = Scripts.mkTypedValidator @Oracling + ($$(PlutusTx.compile [|| mkOracleValidator ||]) `PlutusTx.applyCode` PlutusTx.liftCode oracle) + $$(PlutusTx.compile [|| wrap ||]) + where + wrap = Scripts.wrapValidator @Integer @OracleRedeemer + +oracleValidator :: Oracle -> Validator +oracleValidator = Scripts.validatorScript . oracleInst + +oracleAddress :: Oracle -> Ledger.Address +oracleAddress = scriptAddress . oracleValidator + +data OracleParams = OracleParams -- parans fir starting oracle + { opFees :: !Integer + , opSymbol :: !CurrencySymbol + , opToken :: !TokenName + } deriving (Prelude.Show, Generic, FromJSON, ToJSON) + +startOracle :: forall w s. HasBlockchainActions s => OracleParams -> Contract w s Text Oracle +startOracle op = do + pkh <- pubKeyHash <$> Contract.ownPubKey + -- mint NFT using forgeContract function (provide owner pkh and array of nft-s that are going to be minted) + -- use mapError to convert String error to Text error + osc <- mapError (pack . show) (forgeContract pkh [(oracleTokenName, 1)] :: Contract w s CurrencyError OneShotCurrency) + let cs = Currency.currencySymbol osc + oracle = Oracle + { oSymbol = cs + , oOperator = pkh + , oFee = opFees op + , oAsset = AssetClass (opSymbol op, opToken op) + } + logInfo @String $ "started oracle " ++ show oracle + return oracle + +updateOracle :: forall w s. HasBlockchainActions s => Oracle -> Integer -> Contract w s Text () +updateOracle oracle x = do + m <- findOracle oracle -- check if oracle exists + let c = Constraints.mustPayToTheScript x $ assetClassValue (oracleAsset oracle) 1 + case m of + Nothing -> do -- oracle nft not created + ledgerTx <- submitTxConstraints (oracleInst oracle) c + awaitTxConfirmed $ txId ledgerTx + logInfo @String $ "set initial oracle value to " ++ show x + Just (oref, o, _) -> do -- update existing oracle nft value + let lookups = Constraints.unspentOutputs (Map.singleton oref o) <> + -- Constraints.scriptInstanceLookups (oracleInst oracle) <> TODO FIX THIS + Constraints.otherScript (oracleValidator oracle) + tx = c <> Constraints.mustSpendScriptOutput oref (Redeemer $ PlutusTx.toData Update) + -- fees auto send because of inbalance + ledgerTx <- submitTxConstraintsWith @Oracling lookups tx + awaitTxConfirmed $ txId ledgerTx + logInfo @String $ "updated oracle value to " ++ show x + +-- +findOracle :: forall w s. HasBlockchainActions s => Oracle -> Contract w s Text (Maybe (TxOutRef, TxOutTx, Integer)) +findOracle oracle = do + utxos <- Map.filter f <$> utxoAt (oracleAddress oracle) + return $ case Map.toList utxos of + [(oref, o)] -> do + x <- oracleValue (txOutTxOut o) $ \dh -> Map.lookup dh $ txData $ txOutTxTx o + return (oref, o, x) + _ -> Nothing + where + f :: TxOutTx -> Bool + f o = assetClassValueOf (txOutValue $ txOutTxOut o) (oracleAsset oracle) == 1 + +type OracleSchema = BlockchainActions .\/ Endpoint "update" Integer + +runOracle :: OracleParams -> Contract (Last Oracle) OracleSchema Text () +runOracle op = do + oracle <- startOracle op + tell $ Last $ Just oracle + go oracle + where + go :: Oracle -> Contract (Last Oracle) OracleSchema Text a + go oracle = do + x <- endpoint @"update" + updateOracle oracle x + go oracle \ No newline at end of file diff --git a/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/Oracle/Funds.hs b/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/Oracle/Funds.hs new file mode 100644 index 000000000..88d62359f --- /dev/null +++ b/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/Oracle/Funds.hs @@ -0,0 +1,41 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +module NodeFactory.Plutus.Contracts.Oracle.Funds + ( ownFunds + , ownFunds' + ) where + +import Control.Monad hiding (fmap) +import qualified Data.Map as Map +import Data.Monoid (Last (..)) +import Data.Text (Text) +import Plutus.Contract as Contract hiding (when) +import PlutusTx.Prelude hiding ((<$>)) +import Prelude ((<$>)) +import Ledger hiding (singleton) +import Ledger.Value as Value + +ownFunds :: HasBlockchainActions s => Contract w s Text Value +ownFunds = do + pk <- ownPubKey + utxos <- utxoAt $ pubKeyAddress pk + let v = mconcat $ Map.elems $ txOutValue . txOutTxOut <$> utxos + -- logInfo @String $ "own funds: " ++ show (Value.flattenValue v) + return v + +ownFunds' :: Contract (Last Value) BlockchainActions Text () +ownFunds' = do + handleError logError $ ownFunds >>= tell . Last . Just + void $ Contract.waitNSlots 1 + ownFunds' \ No newline at end of file diff --git a/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/Oracle/PAB.hs b/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/Oracle/PAB.hs new file mode 100644 index 000000000..d721a3a6d --- /dev/null +++ b/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/Oracle/PAB.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} + +module NodeFactory.Plutus.Contracts.Oracle.PAB + ( OracleContracts (..) + ) where + +import Data.Aeson (FromJSON, ToJSON) +import Data.Text.Prettyprint.Doc (Pretty (..), viaShow) +import GHC.Generics (Generic) +import Ledger + +import qualified NodeFactory.Plutus.Contracts.Oracle.Core as Oracle + +data OracleContracts = Init | Oracle CurrencySymbol | Swap Oracle.Oracle + deriving (Eq, Ord, Show, Generic, FromJSON, ToJSON) + +instance Pretty OracleContracts where + pretty = viaShow \ No newline at end of file diff --git a/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/Oracle/Swap.hs b/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/Oracle/Swap.hs new file mode 100644 index 000000000..12d65e649 --- /dev/null +++ b/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/Oracle/Swap.hs @@ -0,0 +1,230 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +module NodeFactory.Plutus.Contracts.Oracle.Swap + ( SwapSchema + , swap + ) where + +import Control.Monad hiding (fmap) +import Data.List (find) +import qualified Data.Map as Map +import Data.Maybe (mapMaybe) +import Data.Monoid (Last (..)) +import Data.Text (Text) +import Plutus.Contract as Contract hiding (when) +import qualified PlutusTx +import PlutusTx.Prelude hiding (Semigroup(..), (<$>), unless, mapMaybe, find) +import Ledger hiding (singleton) +import Ledger.Constraints as Constraints +import qualified Ledger.Typed.Scripts as Scripts +import Ledger.Ada as Ada hiding (divide) +import Ledger.Value as Value +import Prelude (Semigroup (..), String, show, (<$>)) + +import NodeFactory.Plutus.Contracts.Oracle.Core +import NodeFactory.Plutus.Contracts.Oracle.Funds + +{-# INLINABLE price #-} +price :: Integer -> Integer -> Integer +price lovelace exchangeRate = (lovelace * exchangeRate) `divide` 1000000 + +{-# INLINABLE lovelaces #-} +lovelaces :: Value -> Integer +lovelaces = Ada.getLovelace . Ada.fromValue + +{-# INLINABLE mkSwapValidator #-} +mkSwapValidator :: Oracle -> Address -> PubKeyHash -> () -> ScriptContext -> Bool +mkSwapValidator oracle addr pkh () ctx = + txSignedBy info pkh || + (traceIfFalse "expected exactly two script inputs" hasTwoScriptInputs && + traceIfFalse "price not paid" sellerPaid) + + where + info :: TxInfo + info = scriptContextTxInfo ctx + + oracleInput :: TxOut + oracleInput = + let + -- all inputs that sit at addr + ins = [ o + | i <- txInfoInputs info + , let o = txInInfoResolved i + , txOutAddress o == addr + ] + in + case ins of + [o] -> o + _ -> traceError "expected exactly one oracle input" + + oracleValue' = case oracleValue oracleInput (`findDatum` info) of + Nothing -> traceError "oracle value not found" + Just x -> x + + hasTwoScriptInputs :: Bool + hasTwoScriptInputs = + let + xs = filter (isJust . toValidatorHash . txOutAddress . txInInfoResolved) $ txInfoInputs info + in + length xs == 2 + + minPrice :: Integer + minPrice = + let + lovelaceIn = case findOwnInput ctx of + Nothing -> traceError "own input not found" + Just i -> lovelaces $ txOutValue $ txInInfoResolved i + in + price lovelaceIn oracleValue' + + sellerPaid :: Bool + sellerPaid = + let + pricePaid :: Integer + pricePaid = assetClassValueOf (valuePaidTo info pkh) (oAsset oracle) + in + pricePaid >= minPrice + +data Swapping +instance Scripts.ScriptType Swapping where + type instance DatumType Swapping = PubKeyHash + type instance RedeemerType Swapping = () + +swapInst :: Oracle -> Scripts.ScriptInstance Swapping +swapInst oracle = Scripts.validator @Swapping + ($$(PlutusTx.compile [|| mkSwapValidator ||]) + `PlutusTx.applyCode` PlutusTx.liftCode oracle + `PlutusTx.applyCode` PlutusTx.liftCode (oracleAddress oracle)) + $$(PlutusTx.compile [|| wrap ||]) + where + wrap = Scripts.wrapValidator @PubKeyHash @() + +swapValidator :: Oracle -> Validator +swapValidator = Scripts.validatorScript . swapInst + +swapAddress :: Oracle -> Ledger.Address +swapAddress = scriptAddress . swapValidator + +-- OFFCHAIN + +offerSwap :: forall w s. HasBlockchainActions s => Oracle -> Integer -> Contract w s Text () +offerSwap oracle amt = do + pkh <- pubKeyHash <$> Contract.ownPubKey + let tx = Constraints.mustPayToTheScript pkh $ Ada.lovelaceValueOf amt + ledgerTx <- submitTxConstraints (swapInst oracle) tx + awaitTxConfirmed $ txId ledgerTx + logInfo @String $ "offered " ++ show amt ++ " lovelace for swap" + +findSwaps :: HasBlockchainActions s => Oracle -> (PubKeyHash -> Bool) -> Contract w s Text [(TxOutRef, TxOutTx, PubKeyHash)] +findSwaps oracle p = do + utxos <- utxoAt $ swapAddress oracle + return $ mapMaybe g $ Map.toList utxos + where + f :: TxOutTx -> Maybe PubKeyHash + f o = do + dh <- txOutDatumHash $ txOutTxOut o + (Datum d) <- Map.lookup dh $ txData $ txOutTxTx o + PlutusTx.fromData d + + g :: (TxOutRef, TxOutTx) -> Maybe (TxOutRef, TxOutTx, PubKeyHash) + g (oref, o) = do + pkh <- f o + guard $ p pkh + return (oref, o, pkh) + +retrieveSwaps :: HasBlockchainActions s => Oracle -> Contract w s Text () +retrieveSwaps oracle = do + pkh <- pubKeyHash <$> ownPubKey + xs <- findSwaps oracle (== pkh) + case xs of + [] -> logInfo @String "no swaps found" + _ -> do + let lookups = Constraints.unspentOutputs (Map.fromList [(oref, o) | (oref, o, _) <- xs]) <> + Constraints.otherScript (swapValidator oracle) + tx = mconcat [Constraints.mustSpendScriptOutput oref $ Redeemer $ PlutusTx.toData () | (oref, _, _) <- xs] + ledgerTx <- submitTxConstraintsWith @Swapping lookups tx + awaitTxConfirmed $ txId ledgerTx + logInfo @String $ "retrieved " ++ show (length xs) ++ " swap(s)" + +useSwap :: forall w s. HasBlockchainActions s => Oracle -> Contract w s Text () +useSwap oracle = do + funds <- ownFunds + let amt = assetClassValueOf funds $ oAsset oracle + logInfo @String $ "available assets: " ++ show amt + + m <- findOracle oracle + case m of + Nothing -> logInfo @String "oracle not found" + Just (oref, o, x) -> do + logInfo @String $ "found oracle, exchange rate " ++ show x + pkh <- pubKeyHash <$> Contract.ownPubKey + swaps <- findSwaps oracle (/= pkh) + case find (f amt x) swaps of + Nothing -> logInfo @String "no suitable swap found" + Just (oref', o', pkh') -> do + let v = txOutValue (txOutTxOut o) <> lovelaceValueOf (oFee oracle) + p = assetClassValue (oAsset oracle) $ price (lovelaces $ txOutValue $ txOutTxOut o') x + lookups = Constraints.otherScript (swapValidator oracle) <> + Constraints.otherScript (oracleValidator oracle) <> + Constraints.unspentOutputs (Map.fromList [(oref, o), (oref', o')]) + tx = Constraints.mustSpendScriptOutput oref (Redeemer $ PlutusTx.toData Use) <> + Constraints.mustSpendScriptOutput oref' (Redeemer $ PlutusTx.toData ()) <> + Constraints.mustPayToOtherScript + (validatorHash $ oracleValidator oracle) + (Datum $ PlutusTx.toData x) + v <> + Constraints.mustPayToPubKey pkh' p + ledgerTx <- submitTxConstraintsWith @Swapping lookups tx + awaitTxConfirmed $ txId ledgerTx + logInfo @String $ "made swap with price " ++ show (Value.flattenValue p) + where + getPrice :: Integer -> TxOutTx -> Integer + getPrice x o = price (lovelaces $ txOutValue $ txOutTxOut o) x + + f :: Integer -> Integer -> (TxOutRef, TxOutTx, PubKeyHash) -> Bool + f amt x (_, o, _) = getPrice x o <= amt + +type SwapSchema = + BlockchainActions + .\/ Endpoint "offer" Integer + .\/ Endpoint "retrieve" () + .\/ Endpoint "use" () + .\/ Endpoint "funds" () + +swap :: Oracle -> Contract (Last Value) SwapSchema Text () +swap oracle = (offer `select` retrieve `select` use `select` funds) >> swap oracle + where + offer :: Contract (Last Value) SwapSchema Text () + offer = h $ do + amt <- endpoint @"offer" + offerSwap oracle amt + + retrieve :: Contract (Last Value) SwapSchema Text () + retrieve = h $ do + endpoint @"retrieve" + retrieveSwaps oracle + + use :: Contract (Last Value) SwapSchema Text () + use = h $ do + endpoint @"use" + useSwap oracle + + funds :: Contract (Last Value) SwapSchema Text () + funds = h $ do + endpoint @"funds" + v <- ownFunds + tell $ Last $ Just v + + h :: Contract (Last Value) SwapSchema Text () -> Contract (Last Value) SwapSchema Text () + h = handleError logError \ No newline at end of file diff --git a/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/StableCoin/OffChain.hs b/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/StableCoin/OffChain.hs new file mode 100644 index 000000000..9dc50513b --- /dev/null +++ b/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/StableCoin/OffChain.hs @@ -0,0 +1,282 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +module NodeFactory.Plutus.Contracts.StableCoin.OffChain + ( vaultStateCoinFromUniswapCurrency + , CreateParams (..) + , CloseParams (..) + , StableCoinUserSchema, UserContractState (..) + , StableCoinOwnerSchema + , start, create, pools + , ownerEndpoint, userEndpoints + ) where + +import Control.Monad hiding (fmap) +import qualified Data.Map as Map +import Data.Monoid (Last (..)) +import Data.Proxy (Proxy (..)) +import Data.Text (Text, pack) +import Data.Void (Void, absurd) +import Ledger hiding (singleton) +import Ledger.Constraints as Constraints +import qualified Ledger.Typed.Scripts as Scripts +import Playground.Contract +import Plutus.Contract +import qualified NodeFactory.Plutus.Contracts.Currency as Currency +import NodeFactory.Plutus.Contracts.StableCoin.OnChain (mkStableCoinValidator, validateStableCoinForging) +import NodeFactory.Plutus.Contracts.StableCoin.Types +import qualified PlutusTx +import PlutusTx.Prelude hiding (Semigroup (..), dropWhile, flip, unless) +import Prelude as Haskell (Int, Semigroup (..), String, div, dropWhile, flip, show, + (^)) +import Text.Printf (printf) + +data StableCoining +instance Scripts.ValidatorTypes StableCoining where + type instance RedeemerType StableCoining = StableCoinAction + type instance DatumType StableCoining = StableCoinDatum + +type StableCoinOwnerSchema = Endpoint "start" () + +-- | Schema for the endpoints for users of stable coin. +type StableCoinUserSchema = + Endpoint "create" CreateParams + -- .\/ Endpoint "close" CloseParams + .\/ Endpoint "funds" () + -- .\/ Endpoint "liquidate" LiquidateParams + .\/ Endpoint "stop" () + +-- | Type of the StableCoin user contract state. +data UserContractState = + Funds Value + | Created + -- | Closed + | Stopped + deriving (Show, Generic, FromJSON, ToJSON) + + +scTokenName, vaultStateTokenName, scName :: TokenName +scTokenName = "StableCoin" +vaultStateTokenName = "Vault State" +scName = "USDc" + +scInstance :: StableCoin -> Scripts.TypedValidator StableCoining +scInstance sc = Scripts.mkTypedValidator @StableCoining + ($$(PlutusTx.compile [|| mkStableCoinValidator ||]) + `PlutusTx.applyCode` PlutusTx.liftCode sc + `PlutusTx.applyCode` PlutusTx.liftCode c) + $$(PlutusTx.compile [|| wrap ||]) + where + c :: Coin VaultState + c = vaultStateCoin sc + + wrap = Scripts.wrapValidator @StableCoinDatum @StableCoinAction + +scScript :: StableCoin -> Validator +scScript = Scripts.validatorScript . scInstance + +scAddress :: StableCoin -> Ledger.Address +scAddress = Ledger.scriptAddress . scScript + +stablecoin :: CurrencySymbol -> StableCoin +stablecoin cs = StableCoin {sCoin = scoin cs, scStablecoinTokenName = scName} + where + scoin :: CurrencySymbol -> Coin SC + scoin cs = mkCoin cs scTokenName + +stableCoinPolicy :: StableCoin -> MintingPolicy +stableCoinPolicy sc = mkMintingPolicyScript $ + $$(PlutusTx.compile [|| \u t -> Scripts.wrapMintingPolicy (validateStableCoinForging u t) ||]) + `PlutusTx.applyCode` PlutusTx.liftCode sc + `PlutusTx.applyCode` PlutusTx.liftCode vaultStateTokenName + +stableCoinCurrency :: StableCoin -> CurrencySymbol +stableCoinCurrency = scriptCurrencySymbol . stableCoinPolicy + +vaultStateCoin :: StableCoin -> Coin VaultState +vaultStateCoin = flip mkCoin vaultStateTokenName . stableCoinCurrency + +-- vaultStateCoinFromUniswapCurrency :: CurrencySymbol -- ^ The currency identifying the StableCoin instance. +-- -> Coin VaultState +-- vaultStateCoinFromUniswapCurrency = vaultStateCoin . stablecoin + +-- | Parameters for the @create@-endpoint, which creates a new vault. +data CreateParams = CreateParams + { crAmount :: Integer + , crOwner :: PubKeyHash + } deriving (Show, Generic, ToJSON, FromJSON, ToSchema) + +-- | Parameters for the @close@-endpoint, which closes a vault. +data CloseParams = CloseParams + { clOwner :: PubKeyHash + , clAmount :: Integer + } deriving (Show, Generic, ToJSON, FromJSON, ToSchema) + +start :: forall w s. Contract w s Text StableCoin +start = do + pkh <- pubKeyHash <$> ownPubKey + cs <- fmap Currency.currencySymbol $ + mapError (pack . show @Currency.CurrencyError) $ + Currency.mintContract pkh [(scTokenName, 1)] + let c = mkCoin cs scTokenName + sc = stablecoin cs + inst = scInstance sc + tx = mustPayToTheScript (Factory []) $ unitValue c + ledgerTx <- submitTxConstraints inst tx + void $ awaitTxConfirmed $ txId ledgerTx + + logInfo @String $ printf "started StableCoin %s at address %s" (show sc) (show $ scAddress sc) + return sc + +-- | Creates stable coin vault +create :: forall w s. StableCoin -> CreateParams -> Contract w s Text () +create sc CreateParams{..} = do + when (crAmount <= 0) $ throwError "Amount of stable coin must be positive" + (oref, o, vs) <- findStableCoinFactory sc + let v = StableCoinVault {owner = crOwner, amount = crAmount} + let scInst = scInstance sc + scScript = scScript sc + scDat1 = Factory $ v : vs + scDat2 = Vault v + vsC = vaultStateCoin sc + lC = mkCoin (stableCoinCurrency sc) $ scStablecoinTokenName sc + scVal = unitValue $ sCoin sc + vVal = valueOf unitValue vsC + + lookups = Constraints.typedValidatorLookups scInst <> + Constraints.otherScript scScript <> + Constraints.mintingPolicy (stableCoinPolicy sc) <> + Constraints.unspentOutputs (Map.singleton oref o) + + tx = Constraints.mustPayToTheScript scDat1 scVal <> + Constraints.mustPayToTheScript scDat2 vVal <> + Constraints.mustMintValue (unitValue vsC <> valueOf lC) <> + Constraints.mustSpendScriptOutput oref (Redeemer $ PlutusTx.toData $ Create v) + + ledgerTx <- submitTxConstraintsWith lookups tx + void $ awaitTxConfirmed $ txId ledgerTx + + logInfo $ "created stable coin vault: " ++ show v + +-- | Closes a stable coin vault +close :: forall w s. StableCoin -> CloseParams -> Contract w s Text () +close sc CloseParams{..} = do + vs <- findStableCoinFactory sc + v <- findStableCoinVault + pkh <- pubKeyHash <$> ownPubKey + let scInst = scInstance sc + scScript = scScript sc + usDat = Factory $ filter (/= v) vs + usC = sCoin sc + vsC = vaultStateCoin sc + lC = mkCoin (stableCoinCurrency sc) $ lpTicker v + scVal = unitValue usC + psVal = unitValue vsC + lVal = valueOf lC liquidity + redeemer = Redeemer $ PlutusTx.toData Close + + lookups = Constraints.typedValidatorLookups scInst <> + Constraints.otherScript scScript <> + Constraints.mintingPolicy (stableCoinPolicy sc) <> + Constraints.ownPubKeyHash pkh + + tx = Constraints.mustPayToTheScript usDat scVal <> + Constraints.mustMintValue (negate $ psVal <> lVal) <> + Constraints.mustIncludeDatum (Datum $ PlutusTx.toData $ Vault v) + + ledgerTx <- submitTxConstraintsWith lookups tx + void $ awaitTxConfirmed $ txId ledgerTx + + logInfo $ "closed stable coin vault: " ++ show v + +-- | Gets the caller's funds. +funds :: forall w s. Contract w s Text Value +funds = do + pkh <- pubKeyHash <$> ownPubKey + os <- map snd . Map.toList <$> utxoAt (pubKeyHashAddress pkh) + return $ mconcat [txOutValue $ txOutTxOut o | o <- os] + +getStableCoinDatum :: TxOutTx -> Contract w s Text StableCoinDatum +getStableCoinDatum o = case txOutDatumHash $ txOutTxOut o of + Nothing -> throwError "datumHash not found" + Just h -> case Map.lookup h $ txData $ txOutTxTx o of + Nothing -> throwError "datum not found" + Just (Datum e) -> case PlutusTx.fromData e of + Nothing -> throwError "datum has wrong type" + Just d -> return d + +findStableCoinInstance :: forall a b w s. StableCoin -> Coin b -> (StableCoinDatum -> Maybe a) -> Contract w s Text (TxOutRef, TxOutTx, a) +findStableCoinInstance sc c f = do + let addr = scAddress sc + logInfo @String $ printf "looking for StableCoin instance at address %s containing coin %s " (show addr) (show c) + utxos <- utxoAt addr + go [x | x@(_, o) <- Map.toList utxos, isUnity (txOutValue $ txOutTxOut o) c] + where + go [] = throwError "StableCoin instance not found" + go ((oref, o) : xs) = do + d <- getStableCoinDatum o + case f d of + Nothing -> go xs + Just a -> do + logInfo @String $ printf "found StableCoin instance with datum: %s" (show d) + return (oref, o, a) + +findStableCoinFactory :: forall w s. StableCoin -> Contract w s Text (TxOutRef, TxOutTx, [StableCoinVault]) +findStableCoinFactory sc@StableCoin{..} = findStableCoinInstance sc sCoin $ \case + Factory vs -> Just vs + Vault _ -> Nothing + +findStableCoinVault :: forall w s. StableCoin -> StableCoinVault -> Contract w s Text (TxOutRef, TxOutTx, StableCoinVault) +findStableCoinVault sc v = findStableCoinInstance sc (vaultStateCoin sc) $ \case + Vault v' + | v' == v -> Just v + _ -> Nothing + +ownerEndpoint :: Contract (Last (Either Text StableCoin)) Plutus.Contract.Empty ContractError () +ownerEndpoint = do + e <- mapError absurd $ runError start + tell $ Last $ Just e + -- void $ waitNSlots 10 + +userEndpoints :: StableCoin -> Contract (Last (Either Text UserContractState)) StableCoinUserSchema Void () +userEndpoints sc = + stop + `select` + ((f (Proxy @"create") (const Created) create `select` + f (Proxy @"close") (const Closed) close `select` + f (Proxy @"funds") Funds (\_us () -> funds)) >> userEndpoints sc) + where + f :: forall l a p. + (HasEndpoint l p StableCoinUserSchema, FromJSON p) + => Proxy l + -> (a -> UserContractState) + -> (StableCoin -> p -> Contract (Last (Either Text UserContractState)) StableCoinUserSchema Text a) + -> Contract (Last (Either Text UserContractState)) StableCoinUserSchema Void () + f _ g c = do + e <- runError $ do + p <- endpoint @l + c sc p + tell $ Last $ Just $ case e of + Left err -> Left err + Right a -> Right $ g a + + stop :: Contract (Last (Either Text UserContractState)) StableCoinUserSchema Void () + stop = do + e <- runError $ endpoint @"stop" + tell $ Last $ Just $ case e of + Left err -> Left err + Right () -> Right Stopped \ No newline at end of file diff --git a/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/StableCoin/OnChain.hs b/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/StableCoin/OnChain.hs new file mode 100644 index 000000000..8582d3965 --- /dev/null +++ b/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/StableCoin/OnChain.hs @@ -0,0 +1,164 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# options_ghc -fno-strictness #-} +{-# options_ghc -fno-specialise #-} + +module NodeFactory.Plutus.Contracts.StableCoin.OnChain + ( mkStableCoinValidator + , validateStableCoinForging + ) where + +import Ledger +import Ledger.Ada (Ada) +import qualified Ledger.Ada as Ada +import Ledger.Constraints.OnChain as Constraints +import Ledger.Constraints.TxConstraints as Constraints +import Ledger.Value (AssetClass (..), symbols) +import NodeFactory.Plutus.Contracts.StableCoin.Types +import qualified PlutusTx +import PlutusTx.Prelude + +{-# INLINABLE findOwnInput' #-} +findOwnInput' :: ScriptContext -> TxInInfo +findOwnInput' ctx = fromMaybe (error ()) (findOwnInput ctx) + +{-# INLINABLE valueWithin #-} +valueWithin :: TxInInfo -> Value +valueWithin = txOutValue . txInInfoResolved + +{-# INLINABLE validateCreate #-} +-- | Validates the creation of the stable coin vault. Conditions: +-- +-- 1,2. Check that stable coin factory utxo is in input and output +-- 3. Check that we are creating new vault +-- 4. Check that one vault state coin has been forged +-- 5. Check that more than minimum amount of lovelace has been sent +-- 6. Check that enough lovelace has been sent to mint stablecoin +-- 7. Check if appropriate amount of stablecoin has been minted +-- 8. Check oracle input/output +validateCreate :: StableCoin + -> Coin VaultState + -> [StableCoinVault] + -> StableCoinVault + -> ScriptContext + -> Bool +validateCreate StableCoin{..} c vs v@StableCoinVault{..} ctx = + traceIfFalse "StableCoin coin not present" (isUnity (valueWithin $ findOwnInput' ctx) sCoin) && -- 1 + Constraints.checkOwnOutputConstraint ctx (OutputConstraint (Factory $ v : vs) $ unitValue sCoin) && -- 2 + all (/= v) vs && -- 3 + isUnity forged c && -- 4 + traceIfFalse "Less than minimum" (amountOfAdaInInput > minimumLovelaceAmount) && -- 5 + traceIfFalse "Not enough ada sent" (requiredAmountOfAda amount <= amountOfAdaInInput) && -- 6 + (unAmount (amountOf forged stableCoin') == amount) -- 7 + -- 8 TODO - Check oracle input/output + where + info :: TxInfo + info = scriptContextTxInfo ctx + + forged :: Value + forged = txInfoForge $ scriptContextTxInfo ctx + + adaValueIn :: Value -> Integer + adaValueIn v = Ada.getLovelace (Ada.fromValue v) + + -- Maybe define as const outside validator + minimumLovelaceAmount = 10 + + amountOfAdaInInput = adaValueIn (valueWithin $ findOwnInput' ctx) + + requiredAmountOfAda :: Integer -> Integer + requiredAmountOfAda a = a * 1 -- TODO use value from oracle + + stableCoin' :: Coin USDc + stableCoin' = let AssetClass (cs, _) = unCoin c in mkCoin cs $ scStablecoinTokenName + + -- oracleInput :: TxOut + -- oracleInput = + -- let + -- -- all inputs that sit at addr + -- ins = [ o + -- | i <- txInfoInputs info + -- , let o = txInInfoResolved i + -- , txOutAddress o == addr + -- ] + -- in + -- case ins of + -- [o] -> o + -- _ -> traceError "expected exactly one oracle input" + + -- oracleValue' = case oracleValue oracleInput (`findDatum` info) of + -- Nothing -> traceError "oracle value not found" + -- Just x -> x + + -- isAmountCollateralized :: Integer -> Integer -> Bool + -- isAmountCollateralized lovelace oracleValue = oracleValue * lovelace > amount + + -- hasEnoughLovelace :: Bool + -- hasEnoughLovelace = + -- let + -- lovelaceIn = case findOwnInput ctx of + -- Nothing -> traceError "own input not found" + -- Just i -> lovelaces $ txOutValue $ txInInfoResolved i + -- in + -- isAmountCollateralized lovelaceIn oracleValue' + + +{-# INLINABLE validateCloseVault #-} +-- | Validates the closing of the stable coin vault. Conditions: +-- +validateCloseVault :: StableCoin + -> ScriptContext + -> Bool +validateCloseVault sc ctx = hasFactoryInput + where + info :: TxInfo + info = scriptContextTxInfo ctx + + hasFactoryInput :: Bool + hasFactoryInput = + traceIfFalse "Stable coin factory input expected" $ + isUnity (valueSpent info) (sCoin sc) + +-- TODO +-- {-# INLINABLE validateLiquidateVault #-} +-- validateLiquidateVault :: StableCoin... + +mkStableCoinValidator :: StableCoin + -> Coin VaultState + -> StableCoinDatum + -> StableCoinAction + -> ScriptContext + -> Bool +mkStableCoinValidator sc c (Factory vs) (Create v) ctx = validateCreate sc c vs v ctx -- case: create vault +mkStableCoinValidator sc _ (Vault _) Close ctx = validateCloseVault sc ctx -- case: close vault +mkStableCoinValidator _ _ _ _ _ = False -- case: default +-- TODO case: liquidate vault + +{-# INLINABLE validateStableCoinForging #-} +validateStableCoinForging :: StableCoin -> TokenName -> () -> ScriptContext -> Bool +validateStableCoinForging StableCoin{..} tn _ ctx + = case [ i + | i <- txInfoInputs $ scriptContextTxInfo ctx + , let v = valueWithin i + , isUnity v sCoin || isUnity v lpC + ] of + [_] -> True + [_, _] -> True + _ -> traceError "pool state forging without StableCoin input" + where + lpC :: Coin sUSD + lpC = mkCoin (ownCurrencySymbol ctx) tn \ No newline at end of file diff --git a/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/StableCoin/Types.hs b/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/StableCoin/Types.hs new file mode 100644 index 000000000..c33dbcb56 --- /dev/null +++ b/NodeFactory/stable-coin/src/NodeFactory/Plutus/Contracts/StableCoin/Types.hs @@ -0,0 +1,132 @@ + +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# options_ghc -Wno-redundant-constraints #-} +{-# options_ghc -fno-strictness #-} +{-# options_ghc -fno-specialise #-} + + +module NodeFactory.Plutus.Contracts.StableCoin.Types + where + +import Ledger +import Ledger.Value (AssetClass (..), assetClass, assetClassValue, assetClassValueOf) +import Playground.Contract (FromJSON, Generic, ToJSON, ToSchema) +import qualified PlutusTx +import PlutusTx.Prelude +import qualified Prelude as Haskell +import Text.Printf (PrintfArg) +import Ledger.Ada as Ada + +-- | SC-state coin token +data SC = SC +PlutusTx.makeIsDataIndexed ''SC [('SC, 0)] +PlutusTx.makeLift ''SC + +-- | Vault-state coin token +data VaultState = VaultState +PlutusTx.makeIsDataIndexed ''VaultState [('VaultState, 0)] +PlutusTx.makeLift ''VaultState + +-- | USDc coin token +data USDc = USDc +PlutusTx.makeIsDataIndexed ''USDc [('USDc, 0)] +PlutusTx.makeLift ''USDc + +-- | A single 'AssetClass'. +newtype Coin a = Coin { unCoin :: AssetClass } + deriving stock (Haskell.Show, Generic) + deriving newtype (ToJSON, FromJSON, ToSchema, Eq, Haskell.Eq, Haskell.Ord) +PlutusTx.makeIsDataIndexed ''Coin [('Coin, 0)] +PlutusTx.makeLift ''Coin + +-- | Likewise for 'Integer'. +newtype Amount a = Amount { unAmount :: Integer } + deriving stock (Haskell.Show, Generic) + deriving newtype (ToJSON, FromJSON, ToSchema, Eq, Ord, PrintfArg) + deriving newtype (Haskell.Eq, Haskell.Ord, Haskell.Num) + deriving newtype (AdditiveGroup, AdditiveMonoid, AdditiveSemigroup, MultiplicativeSemigroup) +PlutusTx.makeIsDataIndexed ''Amount [('Amount, 0)] +PlutusTx.makeLift ''Amount + +{-# INLINABLE valueOf #-} +valueOf :: Coin a -> Amount a -> Value +valueOf c a = assetClassValue (unCoin c) (unAmount a) + +{-# INLINABLE valueFrom #-} +valueFrom :: Coin a -> Value +valueFrom c = assetClassValue (unCoin c) 0 + +{-# INLINABLE unitValue #-} +unitValue :: Coin a -> Value +unitValue c = valueOf c 1 + +{-# INLINABLE isUnity #-} +isUnity :: Value -> Coin a -> Bool +isUnity v c = amountOf v c == 1 + +{-# INLINABLE amountOf #-} +amountOf :: Value -> Coin a -> Amount a +amountOf v = Amount . assetClassValueOf v . unCoin + +{-# INLINABLE mkCoin #-} +mkCoin :: CurrencySymbol -> TokenName -> Coin a +mkCoin c = Coin . assetClass c + +{-# INLINABLE lovelaces #-} +lovelaces :: Value -> Integer +lovelaces = Ada.getLovelace . Ada.fromValue + +data StableCoin = StableCoin + { sCoin :: Coin SC + , scStablecoinTokenName :: TokenName +-- , oracle :: Oracle + } deriving stock (Haskell.Show, Generic) + deriving anyclass (ToJSON, FromJSON, ToSchema) +PlutusTx.makeIsDataIndexed ''StableCoin [('StableCoin, 0)] +PlutusTx.makeLift ''StableCoin + +instance Eq StableCoin where + {-# INLINABLE (==) #-} + x == y = (sCoin x == sCoin y && scStablecoinTokenName x == scStablecoinTokenName y) + +data StableCoinVault = StableCoinVault + { owner :: !PubKeyHash -- owner of the of the vault + , amount :: !Integer -- amount of Stable locked in vault + } deriving (Haskell.Show, Generic, ToJSON, FromJSON, ToSchema) +PlutusTx.makeIsDataIndexed ''StableCoinVault [('StableCoinVault, 0)] +PlutusTx.makeLift ''StableCoinVault + +instance Eq StableCoinVault where + {-# INLINABLE (==) #-} + x == y = (owner x == owner y && amount x == amount y) + +-- Actions that can be executed on vault +data StableCoinAction = Create StableCoinVault | Close | Liquidate + deriving Haskell.Show +PlutusTx.makeIsDataIndexed ''StableCoinAction [ ('Create, 0) + , ('Close, 1) + , ('Liquidate, 2) + ] +PlutusTx.makeLift ''StableCoinAction + +data StableCoinDatum = + Factory [StableCoinVault] + | Vault StableCoinVault + deriving stock (Haskell.Show) + +PlutusTx.unstableMakeIsData ''StableCoinDatum --[ ('Factory, 0) + --, ('Vault, 1) + --] +PlutusTx.makeLift ''StableCoinDatum diff --git a/NodeFactory/stable-coin/static/close_vault.png b/NodeFactory/stable-coin/static/close_vault.png new file mode 100644 index 000000000..1be5c6e15 Binary files /dev/null and b/NodeFactory/stable-coin/static/close_vault.png differ diff --git a/NodeFactory/stable-coin/static/liquidate_vault.png b/NodeFactory/stable-coin/static/liquidate_vault.png new file mode 100644 index 000000000..80d98940a Binary files /dev/null and b/NodeFactory/stable-coin/static/liquidate_vault.png differ diff --git a/NodeFactory/stable-coin/static/open_vault.png b/NodeFactory/stable-coin/static/open_vault.png new file mode 100644 index 000000000..71d7d0b86 Binary files /dev/null and b/NodeFactory/stable-coin/static/open_vault.png differ