Permits
Implements TZIP-17 norm for fee-less operations.
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"
Storage
consumer
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> = [];
}
Type
Related
default_expiry
Default permits expiration duration set to one year (expressed in seconds).
Code
variable default_expiry : nat = 31556952
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)
}
Parameter
Returns
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
)
}
Parameters
Returns
has_expired(userp, expiry)
Code
function has_expired(up : user_permit, e : nat) : bool {
return (up.created_at + (up.expiry ? the : e) * 1s < now)
}
Parameters
Returns
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
}
}
Parameter
op
:
add(a)
to add consumera
remove(a)
to remove consumera
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
Fails with
"CONTRACT_PAUSED"
"EXPIRY_TOO_BIG"
iv
is some value greater the default_expiry
.("USER_PERMIT_NOT_FOUND", caller)
caller
is not found in permits
.("PERMIT_NOT_FOUND", (caller, p))
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
}
}
Parameter
Fails with
"INVALID_CALLER"
caller
is not owner
"CONTRACT_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
Fails with
"CONTRACT_PAUSED"
("MISSIGNED", to_sign)
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
Fails with
"INVALID_CALLER"
caller
is not a consumer
."CONTRACT_PAUSED"
"USER_PERMIT_NOT_FOUND"
signer
is not found in permits
."PERMIT_EXPIRED"
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