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.
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 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, 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
{
"EIP712Domain": [
{ "type": "uint256", "name": "chainId" },
{ "type": "address", "name": "verifyingContract" }
]
}
- AllowanceTransfer
{
"AllowanceTransfer": [
{ "type": "address", "name": "safe" },
{ "type": "address", "name": "token" },
{ "type": "address", "name": "to" },
{ "type": "uint96", "name": "amount" },
{ "type": "address", "name": "paymentToken" },
{ "type": "uint96", "name": "payment" },
{ "type": "uint16", "name": "nonce" },
]
}
Anyone can execute the transfer as long as they provide the 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).
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
.
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
.
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.
- 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.
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 to0
. IfresetTimeMin
is0
thespent
value will not automatically reset.
- Time span in minutes after which the
uint32 lastResetMin
- Point in time in minutes when the
spent
amount was last reset to0
. 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.
- Point in time in minutes when the
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. - 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
(aka65536
) 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.
- 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
andtokens
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.
pnpm i
pnpm test
pnpm i
pnpm build
Specify all the necessary environment variables in .env
, following the .env.sample
file.
pnpm i
pnpm run deploy <network_name>
network_name
is the name of the network you want to deploy to. It must be added to the hardhat config undernetworks
beforehand.- If the hardhat plugin cannot figure out the etherscan API url for the network, you can add it manually to
tasks/deploy_verify.ts
. Example:
await hre.run('etherscan-verify', {
forceLicense: true,
license: 'LGPL-3.0',
apiUrl: "https://api.gnosiscan.io"
})