Side effects and Errors¶
Smart contracts usually interact with each other and in some cases (provided some conditions) they are supposed to fail. Indigo supports side-effects and errors to handle these needs, respectively.
In the contract from the previous chapter we performed a check, based
on the parameter of the contract, and modified the
storage depending on the
result of said check.
When the check didn't pass we "reset" the
0, however, this may not
always be what we want. Instead we may only want to increment the storage when
the check passes and stop the execution entirely when it doesn't.
This chapter has an
Errors.hs file that contains
module Indigo.Tutorial.SideEffects.Errors ( errorsContract ) where import Indigo data IncrementIf = IsZero Integer | HasDigitOne Natural deriving stock (Generic, Show) deriving anyclass (IsoValue) instance ParameterHasEntrypoints IncrementIf where type ParameterEntrypointsDerivation IncrementIf = EpdPlain errorsContract :: IndigoContract IncrementIf Natural errorsContract param = defContract do case_ param $ ( #cIsZero #= checkZero , #cHasDigitOne #= checkHasDigitOne ) incrementStorage checkZero :: Var Integer -> IndigoProcedure checkZero val = defFunction do assert [mt|unacceptable zero value|] (val == 0 int) checkHasDigitOne :: Var Natural -> IndigoProcedure checkHasDigitOne val = defFunction do base <- new$ 10 nat while (val > base) do val =: val / base remainder <- new$ val % base checkSingleDigitOne remainder checkSingleDigitOne val checkSingleDigitOne :: Var Natural -> IndigoProcedure checkSingleDigitOne digit = do assert [mt|unacceptable non-one digit|] (digit == 1 nat) incrementStorage :: HasStorage Natural => IndigoProcedure incrementStorage = defFunction do storage += 1 nat storage :: HasStorage Natural => Var Natural storage = storageVar
It differs from the previous contract in two ways:
- as said we want to stop execution when a check does not succeed
- we changed the logic of the
checkHasDigitOnefrom "needs at least one digit equal to 1" to "needs all digits to be equal to 1". This is just for demonstration purposes, it could have had the same logic.
This contract, or rather its functions, use the most common error-throwing
mechanism, which is to check for a
Boolean condition and fail if it is
In such cases the
assert statement comes in handy: it expects a message for
the possible error and a
Boolean expression for the condition.
If we take a look at the
errorsContract we can see that, compared to the
result <- case_ param $
we do not want the
Boolean result from the
case_ param $
instead we let the check end in failure if necessary:
checkZero val = defFunction do assert [mt|unacceptable zero value|] (val == 0 int)
checkSingleDigitOne digit = do assert [mt|unacceptable non-one digit|] (digit == 1 nat)
Note that after the
case_ we can now just perform
incrementStorage, this is
because a failure short-circuits the execution of the contract, meaning that if
we get to this point we can be certain that the previous check did not fail:
errorsContract :: IndigoContract IncrementIf Natural errorsContract param = defContract do case_ param $ ( #cIsZero #= checkZero , #cHasDigitOne #= checkHasDigitOne ) incrementStorage
You can see how simple the usage of
assert is, by looking at the new
If you are surprised by how the message is constructed, take a look at
the types reference, this is just an
Note also that the signature for
checkZero has changed and now it is an
IndigoProcedure, because it now returns nothing instead of a
checkZero :: Var Integer -> IndigoProcedure
Keeping in mind the difference in logic for
checkHasDigitOne described above,
you should be easily able to understand how it is implemented:
checkHasDigitOne :: Var Natural -> IndigoProcedure checkHasDigitOne val = defFunction do base <- new$ 10 nat while (val > base) do val =: val / base remainder <- new$ val % base checkSingleDigitOne remainder checkSingleDigitOne val
Apart from the
assert that we show here, Indigo also supports these constructs
failWiththat has one argument: an expression for any type, this is the same behavior as Michelson's
failCustomthat expects a
Labeland an error message to compose a custom failure value
failCustom_that has only one
Labelargument, and is a specialization of the previous one. See an example on how to use this in: Custom error messages
failUnexpected_that has one argument: an expression for an error message, just like
assertCustomthat works just like
assert, but with the same inputs as
assertCustom_that works just like
assert, but with the same input as
Michelson provides three instructions to perform external
and expects the
operations you produce to be put into a list and returned with
the new storage of the contract at the end of its execution.
Indigo handles the
operations you create automatically and makes sure they are
returned as Michelson expects them to be.
So the statements it provides for the instructions above are:
Addressand takes: the
IndigoContractto create, a
Maybe KeyHashfor the optional delegate, a
Mutezfor the initial amount to take from the originating contract and the initial storage
transferTokensreturns nothing and takes: a parameter for the destination contract, a
Mutezto transfer to it from the calling contract and a
setDelegatereturns nothing and takes a
In the second file for this chapter,
SideEffects.hs, we can see an example of
a contract with side effects,
module Indigo.Tutorial.SideEffects.SideEffects ( sideEffectsContract , textKeeper ) where import Indigo sideEffectsContract :: IndigoContract (Maybe MText) Address sideEffectsContract param = defContract do deleg <- new$ none mtz <- new$ zeroMutez notInit <- new$ [mt|not initialized|] addr <- createContract textKeeper deleg mtz notInit ifSome param (initializeContract addr) (return ()) storage =: addr textKeeper :: IndigoContract MText MText textKeeper param = defContract do storageVar =: param initializeContract :: HasSideEffects => Var Address -> Var MText -> IndigoProcedure initializeContract addr msg = defFunction do mtz <- new$ zeroMutez ifSome (contract addr) (\cRef -> transferTokens msg mtz cRef) failForRef failForRef :: IndigoProcedure failForRef = failUnexpected_ @() [mt|failed to create contract reference|] storage :: HasStorage Address => Var Address storage = storageVar
As you can see this contract uses
textKeeper that we took
from a previous example and creates it with the
not initialized string.
It then decides, using
ifSome, to change its storage or not with
Note that, much like it happens for
HasStorage, functions that want to use
side effects require the constraint
HasSideEffects to be specified.
If you need a function that does both, remember that you can do so by using
the syntax in this formula:
<function_name> :: (HasStorage <storage_type>, HasSideEffects) => IndigoFunction <return_type>
On the next chapter, we will explain how to add simple documentations to our contracts: Contract documentation.
Technical details: SideEffects and Errors¶
There is not much to say about these two.
Regarding side effects, contract compilation makes sure to create a list of
Operations and assign a
Variable to it that is accessible using
At the end of the contract this list is paired with the storage.
You can check this out in the
Statements with side effects can then get this
Var and update the list it
refers to, adding a new
Operation to it.
Failure is instead handled by using the relative Lorentz instruction
and filling the rest of the resulting
Safety of using
We can safely do this because those fields are not going to ever be evaluated, due to being lazy, and so they will never actually throw an error.
You can read more about it by looking at the
As a reminder Indigo has Haddock documentation, if you are interested more in learning about its internals and exported content alike.