packages feed

morley-upgradeable-0.3: src/Lorentz/Contracts/Upgradeable/Common/Contract.hs

-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ

{-# LANGUAGE FunctionalDependencies #-}
{-# OPTIONS_GHC -Wno-orphans #-}

{- | Template for upgradeable contract.

It provides the following features:
1. Contract with upgradeable storage format and entrypoints set.
2. Two way to upgrade the contract - one shot and entrypoint-wise.

-}
module Lorentz.Contracts.Upgradeable.Common.Contract
  ( Parameter(..)
  , UTAddress
  , UContractRef
  , PermConstraint
  , Storage
  , UpgradeableContract
  , PermanentImpl
  , InitUpgradeableContract
  , OneShotUpgradeParameters
  , DVersion (..)
  , NiceVersion
  , upgradeableContract
  , mkEmptyStorage
  , pbsContainedInRun
  , pbsContainedInRunPerm
  ) where

import Lorentz
import Prelude (Typeable)

import qualified Data.Text as T
import Fmt (Buildable(..), fmt)

import qualified Michelson.Typed as T
import Util.Instances ()
import Util.Markdown

import Lorentz.Contracts.Upgradeable.Common.Base
import Lorentz.Contracts.Upgradeable.Common.Doc
import Lorentz.UStore

{-# ANN module ("HLint: ignore Reduce duplication" :: Text) #-}

-- Types
----------------------------------------------------------------------------

-- | Parameter of upgradeable contract. It contains, among others:
--
-- 1. Entrypoint for running one of upgradeable entrypoints.
-- 2. Entrypoint for running one of permanent entrypoints, suitable e.g. for
--    implementing interfaces.
-- 3a. Entrypoint for upgrade in a single call.
-- 3b. Entrypoints for entrypoint-wise upgrade.
data Parameter (ver :: VersionKind)
  = Run (VerParam ver)
  | RunPerm (VerPermanent ver)
  | Upgrade (OneShotUpgradeParameters ver)
  | GetVersion (View () Version)
  | SetAdministrator Address

  -- Entrypoint-wise upgrades are currently not protected from version mismatch
  -- in subsequent transactions, so the user ought to be careful with them.
  -- This behavior may change in future if deemed desirable.
  | EpwBeginUpgrade ("current" :! Version, "new" :! Version)
  | EpwApplyMigration (MigrationScriptFrom (VerUStoreTemplate ver))
  | EpwSetCode SomeUContractRouter
  | EpwSetPermCode (SomePermanentImpl (VerPermanent ver))
  | EpwFinishUpgrade
  deriving stock (Generic)

deriving stock instance
  (Show (VerParam ver), Show (VerPermanent ver)) => Show (Parameter ver)

instance IsoValue (VerPermanent ver) => IsoValue (Parameter ver)

instance ( interface ~ VerInterface ver
         , UnpackUParam Buildable interface
         , Buildable (VerPermanent ver)
         ) =>
         Buildable (Parameter ver) where
  build = \case
    Run uParam ->
      case unpackUParam @Buildable @interface uParam of
        Left err -> "Run with inconsistent UParam: " <> build err
        Right (name, something) ->
          "Run " <> build name <> " with argument: " <> build something
    RunPerm permParam ->
      "Run permanent entrypoint: " <> build permParam
    Upgrade ( arg #currentVersion -> curVersion
            , arg #newVersion -> newVersion, _, _, _) ->
      "Upgrade " <> build curVersion <> " -> " <> build newVersion
    GetVersion v ->
      "GetVersion (callback to " <> build (viewCallbackTo v) <> ")"
    SetAdministrator addr ->
      "SetAdministrator " <> build addr
    EpwBeginUpgrade (arg #current -> curVersion, arg #new -> newVersion) ->
      "Begin EPW upgrade " <> build curVersion <> " -> " <> build newVersion
    EpwApplyMigration _ ->
      "Apply migration during EPW upgrade"
    EpwSetCode _ ->
      "Set code during EPW upgrade"
    EpwSetPermCode _ ->
      "Set permanent code during EPW upgrade"
    EpwFinishUpgrade ->
      "Finish EPW upgrade"

-- | Constraint on abstract set of permanent entrypoints.
type PermConstraint ver =
  -- If we want to perform calls to top-level entrypoints
  -- (including calls where we pass the whole parameter), then
  -- the following constraints are necessary
  ( NiceParameterFull (VerPermanent ver)
  , NoExplicitDefaultEntrypoint (VerPermanent ver)
  , HasAnnotation (VerPermanent ver)
  , RequireAllUniqueEntrypoints (Parameter ver)
  , Typeable ver
  , Typeable (VerInterface ver)
  , Typeable (VerUStoreTemplate ver)
  )

instance (PermConstraint ver) =>
         ParameterHasEntrypoints (Parameter ver) where
  type ParameterEntrypointsDerivation (Parameter ver) = EpdDelegate

type NiceVersion ver =
  ( Typeable (VerInterface ver), Typeable (VerUStoreTemplate ver)
  , UStoreTemplateHasDoc (VerUStoreTemplate ver)
  , TypeHasDoc (VerPermanent ver), KnownValue (VerPermanent ver)
  , HasAnnotation (VerPermanent ver), Typeable ver, WellTypedIsoValue (VerPermanent ver)
  )

instance NiceVersion ver => TypeHasDoc (Parameter ver) where
  typeDocName _ = "Global.Parameter"
  typeDocMdDescription =
    "Top-level parameter of upgradeable contract.\n\
    \Use `Run` and `RunPerm` entrypoints in order to access contract logic, \
    \other top-level entrypoints are intended solely for migrations purposes."
  typeDocMdReference tp =
    customTypeDocMdReference ("Global.Parameter", DType tp) []
  typeDocHaskellRep = homomorphicTypeDocHaskellRep
  typeDocMichelsonRep = homomorphicTypeDocMichelsonRep

-- | Parameters of one-shot upgrade.
--
-- Do not construct this value manually, consider using 'makeOneShotUpgradeParameters'.
type OneShotUpgradeParameters ver =
  ( "currentVersion" :! Version
  , "newVersion" :! Version
  , "migrationScript" :! MigrationScriptFrom (VerUStoreTemplate ver)
  , "newCode" :! Maybe SomeUContractRouter
  , "newPermCode" :! Maybe (SomePermanentImpl (VerPermanent ver))
  )

type UTAddress ver = TAddress (Parameter ver)
type UContractRef ver = ContractRef (Parameter ver)

data StorageFields (ver :: VersionKind) = StorageFields
  { code :: UContractRouter ver
  , permCode :: PermanentImpl ver
  , admin :: Address
  , currentVersion :: Version
  , paused :: Bool
  } deriving stock Generic

deriving anyclass instance (WellTypedIsoValue (VerPermanent ver)) => IsoValue (StorageFields ver)
deriving anyclass instance (HasAnnotation (VerPermanent ver)) => HasAnnotation (StorageFields ver)

data Storage (ver :: VersionKind) = Storage
  { dataMap :: VerUStore ver
  , fields :: StorageFields ver
  } deriving stock Generic

deriving anyclass instance (WellTypedIsoValue (VerPermanent ver)) => IsoValue (Storage ver)
deriving anyclass instance (HasAnnotation (VerPermanent ver)) => HasAnnotation (Storage ver)

instance NiceVersion ver => TypeHasDoc (StorageFields ver) where
  typeDocName _ = "StorageFields"
  typeDocMdDescription =
    "StorageFields of upgradeable contract.\n\
    \This type keeps general information about upgradeable \
    \contract and the logic responsible for calling entrypoints \
    \implementations kept in UStore."
  typeDocMdReference tp =
    customTypeDocMdReference ("StorageFields", DType tp) []
  typeDocHaskellRep = homomorphicTypeDocHaskellRep
  typeDocMichelsonRep = homomorphicTypeDocMichelsonRep

instance NiceVersion ver => TypeHasDoc (Storage ver) where
  typeDocName _ = "Storage"
  typeDocMdDescription =
    "Type which defines storage of the upgradeable contract.\n\
    \It contains UStore with data related to actual contract logic \
    \and fields which relate to upgradeability logic."
  typeDocMdReference tp =
    customTypeDocMdReference ("Storage", DType tp) []
  typeDocHaskellRep = homomorphicTypeDocHaskellRep
  typeDocMichelsonRep = homomorphicTypeDocMichelsonRep

-- Errors
----------------------------------------------------------------------------

-- | The reuested operation requires the contract to be running but
--   it is paused.
type instance ErrorArg "upgContractIsPaused" = ()

-- | The reuested operation requires the contract to be paused but
--   it is not.
type instance ErrorArg "upgContractIsNotPaused" = ()

-- | The provided expected current version differs from actual one.
type instance ErrorArg "upgVersionMismatch" =
  ("expectedCurrent" :! Version, "actualCurrent" :! Version)


instance Buildable (CustomError "upgContractIsPaused") where
  build (CustomError _ (_, ())) =
    "The requested operation requires the contract to be running but \
    \it is paused"

instance Buildable (CustomError "upgContractIsNotPaused") where
  build (CustomError _ (_, ())) =
    "The requested operation requires the contract to be paused but \
    \it is not."

instance Buildable (CustomError "upgVersionMismatch") where
  build (CustomError _ (_, ( arg #expectedCurrent -> expected
                           , arg #actualCurrent -> actual))) =
    "The provided expected current version " <> build expected <> " \
    \differs from actual one " <> build actual <> "."


instance CustomErrorHasDoc "upgContractIsPaused" where
  customErrClass = ErrClassActionException
  customErrDocMdCause =
    "The contract is in paused state (for migrations)."

instance CustomErrorHasDoc "upgContractIsNotPaused" where
  customErrClass = ErrClassActionException
  customErrDocMdCause =
    "The contract is not in paused state (for migrations)."

instance CustomErrorHasDoc "upgVersionMismatch" where
  customErrClass = ErrClassActionException
  customErrDocMdCause =
    "Current contract version differs from the one passed in the upgrade."

-- Doc
----------------------------------------------------------------------------

-- | Specify version if given contract.
data DVersion = DVersion Version

instance DocItem DVersion where
  docItemPos = 103
  docItemSectionName = Nothing
  docItemToMarkdown _ (DVersion (Version ver)) =
    mdSubsection "Version" (build ver)


-- | Mentions that parameter should be wrapped into 'Run' entry point.
pbsContainedInRun, pbsContainedInRunPerm :: ParamBuildingStep
(pbsContainedInRun, pbsContainedInRunPerm) =
  ( let uparam = UParamUnsafe ([mt|s|], "a")
        mich = mkMich uparam (Run @(EmptyContractVersion ()) uparam)
    in mkPbsWrapIn "Run" mich
  , let a = 999 :: Integer
        mich = mkMich a (RunPerm @(EmptyContractVersion Integer) a)
    in mkPbsWrapIn "RunPerm" mich
  )
  where
    mkMich woCtor wCtor = ParamBuilder $ \p ->
      build $
      T.replace
        (fmt . build . T.untypeValue $ toVal woCtor)
        ("(" <> fmt p <> ")")
        (fmt . build . T.untypeValue $ toVal wCtor)
        --- ^ Kinda hacky way to show how 'Run' is represented in Michelson.
        --- It should be safe though (no extra parts of text should be replaced)
        --- because we expect wCtor to be of form
        --- @{Left|Right}+ (woCtor)@


-- Initialization
----------------------------------------------------------------------------

-- Allowing custom V0 versions in functions below, not only
-- 'EmptyContractVersion' - in case if user wishes to declare his own zero
-- version identifier.

emptyCode :: (VerInterface ver ~ '[]) => UContractRouter ver
emptyCode = mkUContractRouter (drop # nil # pair)

notInitPermCode :: PermanentImpl ver
notInitPermCode = PermanentImpl $
  -- In V0 there most probably won't be a sane way to initialize permanent
  -- entrypoints implementation because storage is yet empty and its future
  -- structure depends on particular target version.
  -- Failing with text here because in practice no one should ever notice such
  -- error.
  failUsing [mt|Permanent entrypoints implementation is not yet initialized|]

mkEmptyStorage
  :: (VerInterface ver ~ '[], VerUStoreTemplate ver ~ ())
  => Address -> Storage ver
mkEmptyStorage admin = Storage
  { dataMap = mkUStore ()
  , fields = StorageFields
    { code = emptyCode
    , permCode = notInitPermCode
    , admin = admin
    , currentVersion = 0
    , paused = False
    }
  }

-- Aliases
----------------------------------------------------------------------------

type UpgradeableContract ver = Contract (Parameter ver) (Storage ver)

type InitUpgradeableContract perm = UpgradeableContract (EmptyContractVersion perm)

-- Code
----------------------------------------------------------------------------

upgradeableContract
  :: forall ver. (NiceVersion ver, NiceParameterFull (Parameter ver))
  => UpgradeableContract ver
upgradeableContract = defaultContract $ do
  doc $ DUpgradeability contractDoc
  doc $ T.DStorageType $ DType $ Proxy @(Storage ver)
  unpair
  entryCase @(Parameter ver) (Proxy @UpgradeableEntrypointsKind)
    ( #cRun /-> do
        doc $ DDescription runDoc
        dip $ do
          ensureNotPaused
          getField #dataMap
          dip $ do
            getField #fields
            toField #code; coerceUnwrap
        pair
        exec
        unpair
        dip $ setField #dataMap
        pair
    , #cRunPerm /-> do
        doc $ DDescription runPermDoc
        dip $ do
          ensureNotPaused
          getField #dataMap
        duupX @3; toField #fields; toField #permCode; coerceUnwrap
        execute
        unpair
        dip $ setField #dataMap
        pair
    , #cUpgrade /-> do
        doc $ DDescription upgradeDoc
        dip (ensureAdmin # ensureNotPaused)
        dup; dip (toField #currentVersion >> toNamed #current >> (checkVersion @ver))
        dup; dip (toField #newVersion >> toNamed #new >> updateVersion)
        getField #migrationScript; swap; dip applyMigration
        getField #newCode; swap; dip $ whenSome migrateCode
        toField #newPermCode; whenSome migratePermCode
        nil; pair
    , #cGetVersion /-> view_ $ do
        doc $ DDescription getVersionDoc
        drop @(); toField #fields; toField #currentVersion
    , #cSetAdministrator /-> do
        doc $ DDescription setAdministratorDoc
        dip (ensureAdmin # getField #fields)
        setField #admin
        setField #fields
        nil; pair
    , #cEpwBeginUpgrade /-> do
        doc $ DDescription epwBeginUpgradeDoc
        dip (ensureAdmin # ensureNotPaused)
        dup; dip (toFieldNamed #current >> (checkVersion @ver))
        toFieldNamed #new >> updateVersion
        setPaused True
        nil; pair
    , #cEpwApplyMigration /-> do
        doc $ DDescription epwApplyMigrationDoc
        dip (ensureAdmin # ensurePaused)
        applyMigration
        nil; pair
    , #cEpwSetCode /-> do
        doc $ DDescription epwSetCodeDoc
        dip (ensureAdmin # ensurePaused)
        migrateCode
        nil; pair
    , #cEpwSetPermCode /-> do
        doc $ DDescription epwSetPermCodeDoc
        dip (ensureAdmin # ensurePaused)
        migratePermCode
        nil; pair
    , #cEpwFinishUpgrade /-> do
        doc $ DDescription epwFinishUpgradeDoc
        ensureAdmin
        ensurePaused
        setPaused False
        nil; pair
    )

ensureAdmin :: (WellTypedIsoValue (VerPermanent ver)) => '[Storage ver] :-> '[Storage ver]
ensureAdmin = do
  getField #fields; toField #admin
  sender; eq
  if_ (nop) (failCustom_ #senderIsNotAdmin)

setPaused :: (WellTypedIsoValue (VerPermanent ver)) => Bool -> '[Storage ver] :-> '[Storage ver]
setPaused newState = do
  getField #fields
  push newState
  setField #paused
  setField #fields

ensurePaused :: (WellTypedIsoValue (VerPermanent ver)) => '[Storage ver] :-> '[Storage ver]
ensurePaused = do
  getField #fields; toField #paused
  if_ (nop) (failCustom_ #upgContractIsNotPaused)

ensureNotPaused :: (WellTypedIsoValue (VerPermanent ver)) => '[Storage ver] :-> '[Storage ver]
ensureNotPaused = do
  getField #fields; toField #paused
  if_ (failCustom_ #upgContractIsPaused) (nop)

checkVersion :: forall ver. (WellTypedIsoValue (VerPermanent ver)) => '["current" :! Version, Storage ver] :-> '[Storage ver]
checkVersion = do
  fromNamed #current; toNamed #expectedCurrent
  dip (getField #fields >> toField #currentVersion >> toNamed #actualCurrent)
  if keepIfArgs (#expectedCurrent ==. #actualCurrent)
  then nop
  else do pair; failCustom #upgVersionMismatch

updateVersion :: forall ver.(WellTypedIsoValue (VerPermanent ver)) =>  '["new" :! Version, Storage ver] :-> '[Storage ver]
updateVersion = do
  fromNamed #new
  dip $ getField #fields
  setField #currentVersion; setField #fields

applyMigration
  :: (WellTypedIsoValue (VerPermanent ver)) =>  '[MigrationScriptFrom (VerUStoreTemplate ver), Storage ver] :-> '[Storage ver]
applyMigration = do
  coerceUnwrap
  dip $ getField #dataMap
  checkedCoerce_
  swap
  exec
  setField #dataMap

migrateCode
  :: (WellTypedIsoValue (VerPermanent ver)) =>  '[SomeUContractRouter, Storage ver]
  :-> '[Storage ver]
migrateCode = do
  dip (getField #fields)
  checkedCoerce_
  setField #code
  setField #fields

migratePermCode
  :: (WellTypedIsoValue (VerPermanent ver)) =>  '[SomePermanentImpl (VerPermanent ver), Storage ver]
  :-> '[Storage ver]
migratePermCode = do
  dip (getField #fields)
  checkedCoerce_
  setField #permCode
  setField #fields