Skip to main content

Permits

Implements TZIP-17 norm for fee-less operations.

info

The Permits contract presented below have been audited by Inference AG as part of the FA 2 contracts. The full audit report may be found here.

Repository


Author


Norm


Templates

Disclaimers

Permits

User permits are stored as a (blake2b) hash of the transaction data (e.g. source, destination, token_id, and amount for a FA2 token transfer). These hashes of registered permits may be guessed e.g. based on previously performed transactions and the corresponding permit can be consumed. Thus, users of permits have to be aware that they should never register a permit of a transaction which they actually do not want to be executed. Users of permits have to think about registered permits as when these transactions were already executed.

Counter

The permit implementation is using a counter to protect against signature reply attacks. Thus, each permit and one-step permit using the check entrypoint have to use the counter value in order to create the hash which is signed by the user.

However, due to this counter variable permits have also to be strictly sent in the order of expected counter values to the permit contract. If not sent in this order, the permit is rejected and permits have to be resend.

Gas exhaustion

Admin of these template contracts should get familiar with the smart contract code and assess whether the template is appropriate for their purpose. With regards to the template please especially note that:

  • only a few consumer should be registered as “consumers”, since the more consumer you registered the more gas is used with each call. In the case the list of consumers gets huge, the entire contract could even break.
  • this template limits the size of metadata. This limit is basically given by the blockchain’s constant max_operation_data_length

Errors

constant EXPIRY_TOO_BIG        : string = "EXPIRY_TOO_BIG"
constant USER_PERMIT_NOT_FOUND : string = "USER_PERMIT_NOT_FOUND"
constant PERMIT_NOT_FOUND : string = "PERMIT_NOT_FOUND"
constant MISSIGNED : string = "MISSIGNED"
constant PERMIT_EXPIRED : string = "PERMIT_EXPIRED"

constant string

Storage

consumer

Code


asset consumer {
cid : address
}

asset address

Type


Related

permits

Code


record user_permit {
expiry : option<nat>;
created_at : date;
}

asset permits to big_map {
user_address : address;
counter : nat = 0;
user_expiry : option<nat> = none;
user_permits : map<bytes, user_permit> = [];
}

record option nat date asset address map bytes

Type


Related

default_expiry

Default permits expiration duration set to one year (expressed in seconds).

Code


variable default_expiry : nat = 31556952

variable duration

Type


Related

Functions

get_default_expiry(addr)

Code


function get_default_expiry(addr : address) : nat {
return (permits[addr] ? (the.user_expiry ? the : default_expiry) : default_expiry)
}

function address nat ?:

Parameter


addr :

Address to get expiry of

Returns


addr's expiry or default expiry if not found

Fails with

does not fail


Related

get_expiry(addr, permitkey)

Code


function get_expiry(addr : address, permit_key : bytes) : nat {
return (permits[addr] ? let d = (the.user_expiry ? the : default_expiry) in
(the.user_permits[permit_key] ?
(the.expiry ? the : default_expiry) : d) :
default_expiry
)
}

function address bytes nat [] ?: let []

Parameters


addr :

Address to get expiry of


permit_key :

Permit key

Returns


addr's permit permit_key expiry or default expiry if not found

Fails with

does not fail


Related

has_expired(userp, expiry)

Code


function has_expired(up : user_permit, e : nat) : bool {
return (up.created_at + (up.expiry ? the : e) * 1s < now)
}

function bool + ?: * < now

Parameters


User permit


e :

Expiry

Returns

Fails with

does not fail


Related

Entrypoints

manage_consumer(op)

Code


enum consumer_op =
| add<address>
| remove<address>

entry manage_consumer(op : consumer_op) {
called by owner
effect {
match op with
| add(a) -> consumer.add({ a })
| remove(a) -> consumer.remove(a)
end
}
}

enum address entry called by effect match add remove

Parameter


Consumer operation specification:
  • add(a) to add consumer a
  • remove(a) to remove consumer a

Fails with

does not fail


Related

set_expiry(v, p)

Code


entry set_expiry(iv : option<nat>, ip : option<bytes>) {
constant {
caller_permit ?is permits[caller] otherwise (USER_PERMIT_NOT_FOUND, caller);
}
require {
r1: is_not_paused();
r2: iv ? the < default_expiry : true otherwise EXPIRY_TOO_BIG;
}
effect {
match ip with
| some(p) -> begin
const permit_key = blake2b(p);
if (iv ? the > 0 : true) then begin
const up : user_permit ?=
caller_permit.user_permits[permit_key] : (PERMIT_NOT_FOUND, (caller, p));
permits[caller].user_permits.update(permit_key, some({ up with expiry = iv }))
end else begin
permits[caller].user_permits.remove(permit_key)
end
end
| none -> permits.update(caller, { user_expiry = iv })
end
}
}

entry require effect const ?= [] match begin if ?: [] update remove

Parameters


User permit


Expiry

Fails with

"CONTRACT_PAUSED"

When contract is paused.


"EXPIRY_TOO_BIG"

When iv is some value greater the default_expiry.


("USER_PERMIT_NOT_FOUND", caller)

When caller is not found in permits.


("PERMIT_NOT_FOUND", (caller, p))

When ip is some value of a permit key not found in caller's permits.


Related

set_default_expiry(v)

Code


entry set_default_expiry(v : nat) {
called by owner
require { p3: is_not_paused() }
effect {
default_expiry := v
}
}

entry called by require effect :=

Parameter


v :

New default expiry value

Fails with

"INVALID_CALLER"

When caller is not owner


"CONTRACT_PAUSED"

When contract is paused.


Related

permit(pk, sig, data)

Code


entry permit(signer : key, sig : signature, data : bytes) {
constant {
permit_key is blake2b(data);
user is key_to_address(signer);
usr_permit is permits[user] ?
(the.counter, the.user_permits) :
(0, make_map<bytes, user_permit>([]));
lcounter is usr_permit[0];
luser_permits is usr_permit[1];
to_sign is pack(((self_address, self_chain_id), (lcounter, data)));
usr_expiry is get_default_expiry(user);
}
require {
p4: is_not_paused();
p5: check_signature(signer, sig, to_sign) otherwise (MISSIGNED, to_sign)
}
effect {
permits.add_update(user, {
counter += 1;
user_permits = put(luser_permits, permit_key, {
expiry = some(usr_expiry);
created_at = now
})
});
for (k, v) in permits[user].user_permits do
if has_expired(v, usr_expiry)
then permits[user].user_permits.remove(k)
done
}
}

entry key signature bytes constant key_to_address [] ?: make_map [] pack self_address self_chain_id require check_signature effect add_update put some now for if remove

Parameters


pk :

Public key that signed data


sig :

Signed data by pk.


data :

Permit data.

Fails with

"CONTRACT_PAUSED"

When contract is paused.


("MISSIGNED", to_sign)

When sign is not obtained from data.


Related

consume(from, data, err)

Code


entry consume(user : address, data: bytes, err: string) {
called by consumer
constant {
permit_key is blake2b(data);
signer_expiry is get_expiry(user, permit_key);
lpermit ?is permits[user] otherwise err;
luser_permits ?is lpermit.user_permits[permit_key] otherwise err;
}
require {
p6: is_not_paused()
}
fail if {
p7 : has_expired(luser_permits, signer_expiry) with (PERMIT_EXPIRED, ((luser_permits.created_at + (luser_permits.expiry ? the : signer_expiry) * 1s)))
}
effect {
permits[user].user_permits.remove(permit_key)
}
}

entry address bytes string called by constant blake2b [] fail if remove

Parameters


%from :

Public key that signed data


sig :

Signed data by pk.


data :

Permit data.

Fails with

"INVALID_CALLER"

When caller is not a consumer.


"CONTRACT_PAUSED"

When contract is paused.


"USER_PERMIT_NOT_FOUND"

When signer is not found in permits.


"PERMIT_EXPIRED"

When signer's permit has expired.


Related

check(signer, sig, data)

Code


entry check(signer : key, sig : signature, data : bytes) {
called by consumer
constant {
user is key_to_address(signer);
lcounter is permits[user] ? the.counter : 0;
to_sign is pack(((self_address, self_chain_id), (lcounter, data)));
}
require {
p8: is_not_paused();
p9: check_signature(signer, sig, to_sign) otherwise (MISSIGNED, to_sign)
}
effect {
permits.add_update(user, { counter = (lcounter + 1)});
}
}

entry key signature bytes called by constant key_to_address [] ?: pack self_address blake2b require check_signature effect add_update

Parameters


%from :

Public key that signed data


sig :

Signed data by pk.


data :

Permit data.

Fails with

"INVALID_CALLER"

When caller is not a consumer


"CONTRACT_PAUSED"

When contract is paused.


("MISSIGNED", to_sign)

When sign is not obtained from data.


Related