Safe Haskell | None |
---|---|
Language | Haskell2010 |
Extended support for the tickets feature.
This module has to be imported explicitly, it is not re-exported by Lorentz.
The primary use case for tickets: authorization tokens for specific actions.
For instance, in Pause
entrypoint a contract may accept a ticket, checking
that it is emitted from the administrator address.
The mechanism of tickets is more flexible than sender
and source
instructions:
- a ticket can be carried through an arbitrary chain of intermediate agents;
- it can permit a limited number of certain actions;
- tickets may have a restricted set of authorized actions.
More details below.
The amount of tokens in a ticket may stand for a number of allowed actions
invocations, in this case accepted tickets are checked to contain just 1
token.
This semantics makes sense, taking SPLIT_TICKET
and JOIN_TICKETS
instructions
into account, and also the fact that tickets are not dupable.
One important precaution to be kept in mind is that an emitted ticket must permit exactly one kind of action in exactly one contract. This way the ticket emitter can be sure that his ticket cannot be used in an unexpected way. We see two main approaches to this:
- Make the data carried by the ticket identify the target contract and the action that is being permitted. The contract has to verify the data in the ticket.
- Make tickets carry as little data as possible, and instead let the main authorized entity keep a single tickets emitting contract per target contract per action (the latter is optional). This way, the burden of separating tickets' area of applicability lies on that main authorized entity instead of the target contract, but actions become cheaper.
Some examples of use cases:
- An administrator may emit a ticket that permits some delegate to pause the contract. In this case, the ticket should carry a description of the pausing capability (string or enum value) and permission's expiry time.
It is also easy to share admin privileges among other participants by providing them with a ticket with a very large tokens amount. The target contract does not even need to know about many administrators being involved.
- A user may wish to delegate some actions to an intermediate service. In this case, tickets can serve as signatures made on behalf of the user's contract, with replay protection provided out-of-the-box.
Note that at the moment of writing, implicit addresses cannot serve as ticket emitters.
- The allowances mechanism known by FA1.2/FA2 may be replaced with tickets. In this case, the tokens amount in a ticket will correspond to the amount of tokens that can be spent by an operator.
This module provides helpers to cover the mentioned cases.
Synopsis
- authorizeAction :: IsoValue td => ((td ': s) :-> s') -> (TAddress emitterParam ': (Ticket td ': s)) :-> s'
- verifyTargetContract :: (TAddress selfParam ': s) :-> s
- verifyTargetContractAnd :: ((d ': s) :-> s') -> ((TAddress selfParam, d) ': s) :-> s'
- newtype STicket (action :: k) td = STicket (Ticket td)
- toSTicket :: IsoValue td => (Ticket td ': s) :-> (Address ': (td ': (STicket action td ': s)))
- coerceUnwrap :: forall a s. Unwrappable a => (a ': s) :-> (Unwrappabled a ': s)
- readSTicket :: (STicket action td ': s) :-> (ReadTicket td ': (STicket action td ': s))
- sTicketAmount :: IsoValue td => (Maybe (STicket action td) ': s) :-> (Natural ': s)
- subtractSTicket :: (IsoValue d, KnownValue (STicket action d)) => (forall s0. ErrInstr (("permitted" :! Natural, "spent" :! Natural) ': s0)) -> (Natural ': (Maybe (STicket action d) ': s)) :-> (Maybe (STicket action d) ': s)
- subtractSTicketPlain :: IsoValue d => (forall s0. ErrInstr (("permitted" :! Natural, "spent" :! Natural) ': s0)) -> (Natural ': (STicket action d ': s)) :-> (STicket action d ': s)
- addSTicket :: (STicket action d ': (Maybe (STicket action d) ': s)) :-> (Maybe (STicket action d) ': s)
- addSTicketPlain :: (STicket action d ': (STicket action d ': s)) :-> (STicket action d ': s)
- verifyTicketGeneric :: IsoValue td => (forall s0. (Natural ': s0) :-> s0) -> ((td ': s) :-> s') -> (TAddress emitterParam ': (Ticket td ': s)) :-> (Ticket td ': s')
- verifyTicketGeneric' :: IsoValue td => (forall s0. (Address ': (Address ': s0)) :-> s0) -> (forall s0. (Natural ': s0) :-> s0) -> ((td ': s) :-> s') -> (TAddress emitterParam ': (Ticket td ': s)) :-> (Ticket td ': s')
- verifyTicketer :: (Address ': (Address ': s)) :-> s
Actions authorization
authorizeAction :: IsoValue td => ((td ': s) :-> s') -> (TAddress emitterParam ': (Ticket td ': s)) :-> s' Source #
Verifies the given ticket value that permits running an action.
- Ticketer is checked to match the given address.
- Tokens amount in the ticket is checked to be equal to
1
. - Ticket data is checked with the provided handler.
In case the data contains target contract, consider using
verifyTargetContract
orverifyTargetContractAnd
.
Ticket data verification
verifyTargetContract :: (TAddress selfParam ': s) :-> s Source #
Check data in a ticket when it solely carries target contract address.
verifyTargetContractAnd :: ((d ': s) :-> s') -> ((TAddress selfParam, d) ': s) :-> s' Source #
Check data in a ticket when it carries target contract address and some other data that is to be verified with the given handler.
Tokens spending authorization
newtype STicket (action :: k) td Source #
A ticket kept in storage.
The common pattern of use here is
1. A ticket that permits spending few tokens comes in, it is partially
validated and this STicket
type is produced.
2. This type can repeatedly be used to consume up to the amount of permitted
tokens.
So in order to count allowed tokens, in storage you usually put this type,
not bare Ticket
.
The action
type parameter serves to distinguish tickets for different
actions at type level and usually just matches the value in ticket data:
- If the data in ticket is validated to be
"pause"
, then the result of validation can beSTicket "pause" ...
; This is the preferred option. - If the data in ticket has enum type, and the value is validated to be
some
PauseAction
, then validation can produceSTicket PauseAction ...
.
CAUTION: when working with this type, you must ensure that the
expected ticket emitter (the one you validate tickets against, e.g. admin
address) is constant for the lifetime of the STicket
value.
Otherwise, tickets arithmetics (provided via methods below) won't work.
So do one of
- Either never change the expected ticket emitter;
- Or, when it is changed, wipe all the
STicket
values validated against the old ticketer from storage; - Or keep ticket values in a map where key is the currently expected ticket emitter.
Note some specificity of this type - its values mostly always appear
optionally, either in a map, or in Maybe
(this is clear from the
potential use cases).
Instances
toSTicket :: IsoValue td => (Ticket td ': s) :-> (Address ': (td ': (STicket action td ': s))) Source #
Constructs an STicket
.
This also emits the ticketer address and ticket data for further use. You are oblidged to do something with both of them:
- Either validate against the expected value, you can use
verifyTicketer
,verifyTargetContract
andverifyTargetContractAnd
helpers here. - Or use as key in a map where
STicket
should be put to as a value.
This way you can be sure that arithmetics on STickets
works smoothly
and there are no, for instance, attempts to join two tickets from different
emitters.
coerceUnwrap :: forall a s. Unwrappable a => (a ': s) :-> (Unwrappabled a ': s) Source #
Specialized version of forcedCoerce_
to unwrap a haskell newtype.
Permitted tokens arithmetics
readSTicket :: (STicket action td ': s) :-> (ReadTicket td ': (STicket action td ': s)) Source #
Read contents of STicket
.
sTicketAmount :: IsoValue td => (Maybe (STicket action td) ': s) :-> (Natural ': s) Source #
Read tokens amount in STicket
.
subtractSTicket :: (IsoValue d, KnownValue (STicket action d)) => (forall s0. ErrInstr (("permitted" :! Natural, "spent" :! Natural) ': s0)) -> (Natural ': (Maybe (STicket action d) ': s)) :-> (Maybe (STicket action d) ': s) Source #
Consume given amount of tokens from the ticket, failing with given handler if amount of tokens in the ticket is insufficient.
We do not provide a ready error for this case since it will probably depend
on particular tokens that the given STicket
serves to permit.
Note that you may want to run isSome nonZero none
on the result to save
storage space.
subtractSTicketPlain :: IsoValue d => (forall s0. ErrInstr (("permitted" :! Natural, "spent" :! Natural) ': s0)) -> (Natural ': (STicket action d ': s)) :-> (STicket action d ': s) Source #
Similar to subtractSTicket
, but without Maybe
.
You may want to run nonZero
after this function call to save
storage space.
addSTicket :: (STicket action d ': (Maybe (STicket action d) ': s)) :-> (Maybe (STicket action d) ': s) Source #
Adds tokens permitted by an incoming ticket.
Useful when someone permits you to spend even more tokens than you already have been allowed to spend.
This assumes that the ticket being added has already been verified. The passed tickets must be verified against the same ticket data and ticket emitter.
addSTicketPlain :: (STicket action d ': (STicket action d ': s)) :-> (STicket action d ': s) Source #
Similar to addSTicket
, but without Maybe
.
Generic tickets verification
:: IsoValue td | |
=> (forall s0. (Natural ': s0) :-> s0) | Ticket tokens verifier |
-> ((td ': s) :-> s') | Ticket data verifier |
-> (TAddress emitterParam ': (Ticket td ': s)) :-> (Ticket td ': s') |
Generic method for verifying tickets that authorize some action.
For concrete example of use see authorizeAction
.
:: IsoValue td | |
=> (forall s0. (Address ': (Address ': s0)) :-> s0) | Ticket emitter verifier |
-> (forall s0. (Natural ': s0) :-> s0) | Ticket tokens verifier |
-> ((td ': s) :-> s') | Ticket data verifier |
-> (TAddress emitterParam ': (Ticket td ': s)) :-> (Ticket td ': s') |
Verifies a ticket.
This is an extremely generified verifyTicketGeneric
, allows providing
verifiers for all the ticket's parts. Provided just in case.
verifyTicketer :: (Address ': (Address ': s)) :-> s Source #
Generic method for verifying the ticketer.
Orphan instances
CustomErrorHasDoc "nOT_SINGLE_TICKET_TOKEN" Source # | |
CustomErrorHasDoc "nOT_TICKET_TARGET" Source # | |
CustomErrorHasDoc "wRONG_TICKETER" Source # | |