Skip to content

Commit

Permalink
Add a chapter on TZIP 17 permits and permit-cameligo
Browse files Browse the repository at this point in the history
  • Loading branch information
aguillon committed Jan 25, 2024
1 parent a210d93 commit e392675
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 9 deletions.
3 changes: 2 additions & 1 deletion docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
[Gas Station Webapp](./webapp.md)
[SDKs](./library.md)
[API](./api.md)
[Tutorial](./tutorial.md)
[Tutorial](./tutorial.md)
[Permits](./permits.md)
138 changes: 138 additions & 0 deletions docs/src/permits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# An introduction to off-chain permits

In the [Tutorial](./tutorial.md) chapter, we show how to transfer a NFT to a smart contract
through the Gas Station. As the corresponding operation is ultimately going to get posted by the Gas
Station account, there is an issue: how is NFT contract going to allow this transfer on behalf of
the user? While FA2 contracts, which are used to implement NFTs, support the concept of *operators*
accounts acting on the behalf of the user, only the original owner of the NFT can allow a new
operator to do so. The simplest way would be to modify the NFT contract to

* make the Gas Station account a super-user of the contract, and
* let this account [register third-party contracts as
operators](https://tezostaquito.io/docs/fa2_parameters/#the-update_operators-entrypoint), which
would allow the transfer to happen

This, of course, creates a security (and, potentially, legal) issue: if the key of Marigold Gas
Station account gets stolen, then several FA 2 contracts could be compromised as well. On the other
hand, users whose operations get sponsored are not supposed to have any tez in their wallet, and
thus cannot post the `update_operator` call on-chain themselves.

What is the solution, then?

## Off-chain permits

To solve this problem in a secure way, the notion of *off-chain permits* was introduced by [TZIP
17](https://tzip.tezosagora.org/proposal/tzip-17/). It extends the FA 2 standard with a few new
entrypoints. The most interesting one, itself called `permit`, can be called by anyone, and
expects a list of authorizations for transfers signed by the owners. Those transfers are signed
off-chain: this means that the application has to ask the users for their signature through the
usual ways (e.g. a Beacon-compatible wallet), but this signature has then to be stored and/or
sent to this entrypoint by another account.

Most of the time, however, these permits can be sent in the same transaction as the call to the
other contract, as we did in the previous chapter. When a permit is registered by the contract, it
acts as a one-time authorization for a transfer to a specific address, which can either be a
contract or a implicit account. The `transfer` entrypoint has the same interface as an ordinary FA 2
contract and of course supports the same usage as before, including regular operators. This means
that regular users, who don't need their transactions to be relayed by the gas station, can always
use their assets in a normal, permissionless way.

Let's define permits: they are signed bytes, formed from 4 parameters:

* the chain identifier, such that a permit signed for a given chain (such as Ghostnet) cannot be
used on a different chain;
* the address of the permit FA2 contract, such that a permit signed for a given NFT collection
cannot be used on another one;
* a counter (nonce) defined inside the contract, such as a permit can only be used once;
* and, finally, a hash of the allowed operation, which is going to be checked when the
transfer takes place.

If you recall [the previous chapter](./tutorial.md), this byte string was computed by the library
with the following call:

```ts
const permitData = await permitContract.generatePermit({
from_: userAddress,
txs: [{
to_: RECIPIENT,
token_id,
amount: 1
}]
});
```

Indeed, it can be a little bit complicated to form by hand, and the slightest error makes the permit
fail silently.

Once it is signed by the user, the permit can be registered in the contract by calling the `permit`
entrypoint, which expects a list of parameters of the form `(public_key, signature, transfer_hash)`
where `public_key` is the user's public key, which is necessary to check the `signature`. This
`signature` is computed from the whole byte string, not just the `transfer_hash`.

i If you choose to compute permits by hand, be mindful that they are actually computed by forming
the following couple: `((chain_identifier, contract_address), (contract_counter, transfer_hash))`.
Check the documentation of the contract library that you are using to be sure.

## How to deploy a permit contract

The most up-to-date implementation of TZIP 17-style permits is [the permit-cameligo Ligo
package](https://github.com/aguillon/permit-cameligo/), which is currently maintained outside of
Ligo Package Registry website. To use it, it is recommended to clone the following repository and
use the Ligo compiler to install the dependencies:

```bash
$ git clone https://github.com/aguillon/permit-cameligo
$ cd permit-cameligo/
$ make install
$ make compile
```

Note that the Makefile assumes that you run the dockerized version of Ligo. To use another one, for
instance a local one, you can prefix the `make` commands with `ligo_compiler=ligo `. For instance:

```bash
$ ligo_compiler=ligo make install
$ ligo_compiler=ligo make compile
```

This installs the dependencies in `.ligo/`, and compiles the code to produce two files in `compiled/`.
The first of those files is a JSONized version of the second, which is ready to be deployed by the
scripts in `deploy/`. In addition to the compiled code, this script requires two files:
`deploy/metadata.json` that contains the contract's metadata, and `deploy/.env` which contains the
secret key and the RPC node.

Let's create a minimal `deploy/metadata.json` file:

```json
{
"name":"Example",
"interfaces":[
"TZIP-12"
]
}
```

Change this file according to your needs. If you just want to test the deployment script, you can
also use the pre-generated `deploy/metadata.json.dist` file and rename it to `deploy/metadata.json`.
In the same spirit, copy `deploy/.env.dist` to `deploy/.env` and edit the file to put your secret
key:

```bash
# Required: Your private key
PK=edsk...
# Required: see https://tezostaquito.io/docs/rpc_nodes/
RPC_URL=https://ghostnet.tezos.marigold.dev/
```

Finally, you should be able to

```
$ cd deploy/
$ npm i
$ npm run start
```

This workflow assumes that you're going to mint each token individually by calling the
`create_token` entrypoint. If you want to pre-mint some tokens, you need to edit the
`deploy/deploy.ts` script to start with a non-empty `token_metadata` map. The script should print
the address of the contract after origination.
13 changes: 7 additions & 6 deletions docs/src/tutorial.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Tutorial
# Developing a web application with the Gas Station

This chapter walks through a simple example of a dapp using the Gas Station. You can [try it online at this address](https://ghostnet.gas-station-nft-example.marigold.dev).

Expand Down Expand Up @@ -26,7 +26,7 @@ Let's go ! 💪

## Minting an NFT

We'll start with minting an NFT by a user. The contract we'll use is available at this address on Ghostnet: [`KT199yuNkHQKpy331A6fvWJtQ1uan9uya2jx`](https://ghostnet.tzkt.io/KT199yuNkHQKpy331A6fvWJtQ1uan9uya2jx/operations).
We'll start with minting an NFT by a user. The contract we'll use is available at this address on Ghostnet: [`KT1HUdxmgZUw21ED9gqELVvCty5d1ff41p7J`](https://ghostnet.tzkt.io/KT1HUdxmgZUw21ED9gqELVvCty5d1ff41p7J/operations).
This contract has an admin, which is the only account allowed to mint NFTs. This is the same
settings as you would have in a video game, where the game decides when to mint an NFT for a user.
In this case, the contract admin has been set to be the Gas Station account, because the `mint`
Expand All @@ -49,7 +49,8 @@ const contract = await Tezos.wallet.at(PUBLIC_PERMIT_CONTRACT);

ℹ️ The `Tezos` instance of Taquito is already initialized in the `tezos.ts` file, so it can be directly imported.

ℹ️ `PUBLIC_PERMIT_CONTRACT` is also an environment variable corresponding to the address of your NFT contract.
ℹ️ `PUBLIC_PERMIT_CONTRACT` is an environment variable corresponding to the address of your NFT
contract. It is defined in the `.env` file.

Afterward, we will forge our operation to send to the Gas Station:
```ts
Expand Down Expand Up @@ -86,7 +87,7 @@ final users do not have tez in their wallet, all the transactions are posted by

Despite this centralization, it is still possible to maintain security and non-custodiality using
permits. In this section, we call _staking_ the operation of sending an NFT to a contract. As the
user ownsthe NFT, it is appropriate to sign a permit (authorization) to perform this transfer.
user owns the NFT, it is appropriate to sign a permit (authorization) to perform this transfer.

To facilitate the development of this new feature, we will also use the TypeScript SDK (for reference, you have all the information [here](./library.md))

Expand All @@ -111,7 +112,7 @@ const permitData = await permitContract.generatePermit({
});
```
Some explanations:
- The variable `PUBLIC_STAKING_CONTRACT` contains the address of the staking contract (available at this address [`KT1MLMXwFEMcfByGbGcQ9ow3nsrQCkLbcRAu`](https://ghostnet.tzkt.io/KT1MLMXwFEMcfByGbGcQ9ow3nsrQCkLbcRAu/operations) on Ghostnet).
- The variable `PUBLIC_STAKING_CONTRACT` contains the address of the staking contract (available at this address [`KT1VVotciVbvz1SopVfoXsxXcpyBBSryQgEn`](https://ghostnet.tzkt.io/KT1VVotciVbvz1SopVfoXsxXcpyBBSryQgEn/operations) on Ghostnet).
- The `token_id` corresponds to the ID of the token you want to stake.

`permitData` then contains the hash of the permit `bytes` and the hash of transfer operation `transfer_hash`:
Expand Down Expand Up @@ -159,7 +160,7 @@ const stakingOperation = await stakingContract.methods.stake(
userAddress
).toTransferParams();
```
ℹ️ `PUBLIC_STAKING_CONTRACT` is an environment variable containing the staking contract's address.
ℹ️ `PUBLIC_STAKING_CONTRACT` is also an environment variable containing the staking contract's address.

All that remains is to send the operation to the Gas Station to have the gas fees covered:

Expand Down
2 changes: 1 addition & 1 deletion docs/src/welcome.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Welcome on Gas Station
# Marigold Gas Station documentation

## Introduction

Expand Down

0 comments on commit e392675

Please sign in to comment.