Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allowances modules (aka Withdrawal Limit) #10

Merged
merged 8 commits into from
Oct 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ node_modules/
.DS_Store
.zos.session
env/
.env
.history
coverage*
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This repository contains a collection of modules that can be used with the [Gnos
Modules
-------

- [Allowance Module](./allowances)
- [DutchX Seller Module](./dutchx_seller)
- [Recurring Transfers Module](./recurring_transfers)

Expand All @@ -18,6 +19,6 @@ License
-------
All smart contracts are released under LGPL v.3.

Contributors
Maintainers
------------
- Richard Meissner ([rmeissner](https://github.com/rmeissner))
3 changes: 3 additions & 0 deletions allowances/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
MNEMONIC=""
INFURA_TOKEN=""
NETWORK="rinkeby"
124 changes: 124 additions & 0 deletions allowances/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Allowance module

This contract is a registry of transfer allowances on a Safe that can be used by specific accounts. For this the contract needs to be enabled as a module on the Safe that holds the assets that should be transferred. The registry is written to be used with ERC20 tokens and Ether (represented by the zero address).

All transfer allowances are specific to a Safe, token and delegate. A delegate is an account that can authorize transfers (via a signature).

Note: This is not about allowances on an ERC20 token contract.

## Setting up allowances

The contract is designed as a single point registry. This way not every Safe needs to deploy their own module and it is possible that this module is shared between different Safes.

To set an allowance for a Safe it is first required that the Safe adds a **delegate**. For this a Safe transaction needs to be executed that calls `addDelegate`. This method will add the specified **delegate** for `msg.sender`, which is the Safe in case of a Safe transaction. The `addDelegate` method can be called multiple times with the same address for a **delegate** without failure.

Once a **delegate** has been enabled it is possible to set an allowance for that **delegate**. For this a Safe transaction needs to be executed that calls `setAllowance`.

To delete an allowance for a specific token a Safe transaction needs to be executed that calls `deleteAllowance`. Another way is to remove the **delegate** with a Safe transaction that calls `removeDelegate`, but this will remove all allowances for this delegate.

Note: calling `setAllowance` will not change the `spent` value of an allowance. To reset the `spent` value you need to call `resetAllowance`.

## Transfer authorization

Transfer are authorized by the delegates and must be within their allowance. To authorize a transfer the delegate needs to generate a signature. The allowance module uses the same [signature scheme as the Gnosis Safe](https://docs.gnosis.io/safe/docs/contracts_signatures/), except that the allowance module does not support contract signatures or approved hashes.

The allowance module signatures are EIP-712 based. And uses the following scheme:

- EIP712Domain
```json
{
"EIP712Domain": [
{ "type": "uint256", "name": "chainId" },
{ "type": "address", "name": "verifyingContract" }
]
}
```

- AllowanceTransfer
```json
{
"AllowanceTransfer": [
{ "type": "address", "name": "safe" },
{ "type": "address", "name": "token" },
{ "type": "uint96", "name": "amount" },
{ "type": "address", "name": "paymentToken" },
{ "type": "uint96", "name": "payment" },
{ "type": "uint16", "name": "nonce" },
]
}
```

## Transfer execution

Anyone can execute the transfer as long as they provide the [transfer authorization](#transfer-authorization) of a valid **delegate**. To execute the transfer the `executeAllowanceTransfer` method needs to be called, that checks the signature and calls the Safe to perform the transfer.

When calling the Safe there are two cases that need to be differentiated: an Ether transfer, and a token transfer.
- In case of an **Ether transfer** the module instructs the Safe to transfer the specified amount to the receiver (so no data is provided).
- In case of a **token transfer** the module instructs the Safe to call the transfer method on the token contract to transfer the specified amount to the receiver (so a value of `0` is provided).

## One time allowances

If the `resetTimeMin` time of an allowance is `0` then the allowance will not automatically renew. This means when the `delegate` has used up the allowance it is necessary to "reset" the allowance by performing a Safe transaction that calls `resetAllowance`.

## Recurring allowances
If `resetTimeMin` is set to any value greater than `0` the allowance will automatically renew after the specified amount of minutes based on the last reset time. The initial reset time can be specified with `resetBaseMin`. This can be used to set the time of day the allowance is renewed.

Specifically this means that after the specified time the `spent` amount of an allowance will be reset to `0` and therefore the allowance can be used again until `spent` is greater than the allowance `amount`.

## Refund transfer execution

It is possible to define a refund that is paid to `tx.origin`. This refund is also subject to the allowance, so if the allowance is not high enough to pay for the refund the transaction will fail.

### Considerations

- Transactions can be front-run/ executed by anyone once they are published to the mempool. A way to prevent this would be to introduce a `paymentReceiver`.
- The payment logic adds additional code and could have been removed for an initial version.

## Architecture

The contract has been designed to improve the gas usage when executing a transfer and to make it easy to interact with the contract. Because of this configuration changes (e.g. adding or removing allowance and delegates) are not optimized on gas usage.

Allowances are stored in a map called `allowances`. The key to this map is a combination of the Safe, the delegate and the token. Each allowance has the following fields:
- `uint96 amount`
- Maximum amount that can be spent
- `uint96 spent`
- Amount that has already been spent
- `uint16 resetTimeMin`
- Time span in **minutes** after which the `spent` amount will automatically reset to `0`. If `resetTimeMin` is `0` the `spent` value will not automatically reset.
- `uint32 lastResetMin`
- Point in time in **minutes** when the `spent` amount was last reset to `0`. This can be used to specify the point in time (e.g. time of a day) when the reset should happen, as each following reset will be based on last time.
- `uint16 nonce`
- Increasing nonce that is used to protect transfer confirmations (e.g. signatures) against replay attacks.

Some limits have been applied to the different fields to make sure that the overall struct fits into a single **word** of storage:
- The maximum allowance is `2^96`. Assuming a token with 18 decimals that would allow an allowance from more than 70 billion, which seems sufficient.
rmeissner marked this conversation as resolved.
Show resolved Hide resolved
- The maximum time span that can be used for the reset is `2**16` minutes. This corresponds to around 45 days, which is more than one month and should be sufficient for most use cases.
- The maximum reset time would be the unix epoch of `(2**32) * 60`. This is in the year 10136.
- The maximum numbers of transfers a delegate can do is `2**16` (aka `65536`) which should be sufficient, as it is easily possible to register a new delegate.

To make it easier to allow querying for allowances two more data structures have been added to the contract: `delegates` and `tokens`.

`delegates` stores all the delegates for a specific Safe in a double linked list. For this we also store an entry point to that double linked list in `delegatesStart`. Each delegate is identified by a `uint48` which is the first **6bytes** of the delegate address. This could theoretically cause collisions. Therfore the index points to a struct containing the `address` of the delgate, the `next` index and the `prev` index, so that it is possible to verify which address was used to get the index. In case of collisions we recommend to generate a new delegate.

`tokens` is a list that is appended when ever an allowance is set for a token for the first time. The tokens will never be removed from this list.

Using `delegates` and `tokens` it is possible to query all available allowances for a Safe. This is to avoid that there are any "hidden" allowances.

## Considerations

- If storage rent is introduced for contracts it might be possible (depending on the rent implementation) to perform a grieving attack, as anyone can register allowances.
- The information provided by `delegates` and `tokens` could be rebuilt using events and therefore these would not be necessary in the contract (this would make some consistency checks unnecessary and save gas).
- The bytes assigned to different parts of the `Allowance` struct could be optimized.

## Running tests

```bash
yarn
yarn test
```

## Compiling contracts
```bash
yarn
yarn compile
```
Loading