SIP 79: Deferred Transaction Gas Tank Source

AuthorAnton Jurisevic, Clément Balestrat, Clinton Ennis
Discussions-To<Create a new thread on https://research.synthetix.io and drop the link here>
StatusWIP
Created2020-08-19

Simple Summary

A persistent gas tank allows users to submit transactions to be executed at a later time by keepers, paying those keepers out of the user’s balance.

Abstract

A new GasTank contract will be deployed that does the following:

  • Holds a balance of ether for users to pay for deferred transactions
  • Allows users to deposit and withdraw their balance
  • Listens to current gas prices from an oracle
  • Permits approved contracts to spend ether out of the user’s balance for an execution at the current gas price, including a configurable keeper fee
  • Allows users to set a maximum gas price they are willing to spend for a transaction
  • Enables users to delegate privileged operations to other accounts

Motivation

There are a number of proposed operations in the Synthetix ecosystem which require transactions to be executed after a delay. Such operations include:

  • Limit orders and other future triggered orders
  • Futures contract order confirmations
  • Fee reclamation settlements

Users cannot be expected to monitor prices for limit orders, or tediously execute the transaction sequences required by frontrunning protection. This gas tank mechanism allows keepers to execute such deferred transactions and be reimbursed for the gas cost of executing them. It is intended that this will significantly reduce UX friction for users, as they will not have to execute these advanced operations themselves. Having a balance in the gas tank will not be required for standard exchange operations that do not need it.

Specification

Overview

Any operation that needs to be deferred and executed by a keeper must measure its own gas consumption, reporting this quantity to the gas tank contract at the end of its execution. The gas tank will then consult the latest fast gas price from Chainlink, ensure that this does not exceed the user’s configured maximum gas price, and reimburse the keeper from the user’s balance, along with a fee to incentivise the execution.

Rationale

The gas tank addresses the question of incentivising keepers to perform actions for users that it would otherwise be too inconvenient for them to perform for themelves. Several different incentive schemes were considered.

SNX incentives

A gas tank could be avoided altogether by minting SNX to incentivise keepers, but this runs into several issues:

  • the responsiveness of keepers is influenced by volatility in the SNX price
  • the incentive level has to be set carefully to prevent reward farming
  • There are potentially macro-economic consequences, as SNX supply expansion depends upon system demand

Synth incentives

Users could deposit synths in their gas tank instead, but this would add additional gas cost to transactions, and necessitate keepers to exchange their earnings. If keeper incentives are paid in ether, then they need never top up their own balances in order to continue operating.

Technical Specification

Least Privilege

Only contracts that absolutely need it should have the ability to invoke this functionality, and therefore the gas tank contract should verify that those attempting to spend user’s ether are the correct contracts known to the AddressResolver. Therefore, this contract will need to inherit MixinResolver.

Upgradeability

The gas tank contract should not operate if the system is suspended, and should itself be pausable for upgrades. For the same reason, it should have a separated state contract that holds user balances and ether; the two contracts should retrieve each other’s address through the AddressResolver.

Execution Fee

Each execution by a keeper will be incentivised by flat SCCP-configurable keeper fee, stored as a global system setting. This should be retrievable from the by a new SystemSettings.keeperFee() function. This will return the USD value of ether to be awarded to keepers, and should generally be kept as low as possible while still being incentivising for keepers to operate.

Delegation

In order to support delegation of gas tank management, the DelegateApprovals contract will need to be updated with a new canManageGasTankFor function.


Function API

isApprovedContract

Signature: function isApprovedContract(bytes32 contractName) returns (bool isApproved)

Returns true if and only if the provided contract is approved to spend gas for deferred transactions.

approveContract

Signature: function approveContract(bytes32 contractName, bool approve) external

Allows a contract to be approved or disapproved to spend gas for deferred transactions.

This function should revert if either:

  • contractName is not an identifier recognised by the AddressResolver.
  • msg.sender is not the contract owner.

balanceOf

Signature: function balanceOf(address account) external view returns (uint balance)

Returns the remaining deposited ether in a given account.

depositEtherOnBehalf

Signature: function depositEtherOnBehalf(address account, uint value) external payable

Increases an account’s ether balance. This function should revert if either:

  • tx.value != value
  • account != msg.sender && !DelegateApprovals.canManageGasTankFor(account, msg.sender)

depositEther

Signature: function depositEther(uint value) external payable

Equivalent to depositEtherOnBehalf(msg.sender, value).

withdrawEtherOnBehalf

Signature: function withdrawEtherOnBehalf(address account, address payable recipient, uint value) external

Reduces an account’s ether balance, remitting the withdrawn ether to the recipient address. This function should revert if:

  • balanceOf(msg.sender) < value
  • account != msg.sender && !DelegateApprovals.canManageGasTankFor(account, msg.sender)

withdrawEther

Signature: function withdrawEther(address payable recipient, uint value) external

Equivalent to withdrawEtherOnBehalf(msg.sender, recipient, value).

maxGasPriceOf

Signature: function maxGasPriceOf(address account) external view returns (uint maxGasPriceWei)

Returns the account’s configured maximum gas price in wei.

setMaxGasPriceOnBehalf

Signature: function setMaxGasPriceOnBehalf(address account, uint maxGasPriceWei) external

Allows a user to set the maximum gas price that they are willing to pay for any deferred transaction.

This should revert if:

  • account != msg.sender && !DelegateApprovals.canManageGasTankFor(account, msg.sender)

setMaxGasPrice

Signature: function setMaxGasPrice(uint maxGasPriceWei) external

Equivalent to setMaxGasPriceOnBehalf(msg.sender, maxGasPriceWei)

currentGasPrice

Signature: function currentGasPrice() external view returns (uint currentGasPriceWei)

Fetches the current fast gas price from Chainlink’s Fast Gas / Gwei aggregation, returning it as a quantity of wei.

currentEtherPrice

Signature: function currentEtherPrice() external view returns (uint currentEtherPrice)

Fetches the current ether price from Chainlink’s ETH / USD aggregation.

executionCost

Signature: function executionCost(uint gas) external returns (uint etherCost)

Returns the cost in ether to spend a given quantity of gas at the current gas price, plus the keeper fee, plus the execution cost of an invocation of the spendGas function. That is, this returns (gas + cost(spendGas)) * currentGasPrice() + SystemSettings.keeperFee() / currentEtherPrice().

payGas

Signature: function payGas(address spender, address payable recipient, uint gas) external returns (uint etherSpent)

Allows privileged system smart contracts to reimburse an executing address for executions of a spender’s deferred transactions at the current fast gas price.

Provided there is sufficient balance in the spender’s account, and the transaction was executed at a gas price in the correct range, executionCost(gas) of ether will be transferred to the recipient address, and deducted from the spender’s balance.

This function should revert if any of the following is satisfied:

  • msg.sender is not an approved contract
  • balanceOf(spender) < executionCost(gas)
  • tx.gasprice < currentGasPrice()
  • maxGasPriceOf(spender) < tx.gasprice
  • The function call is reentrant

Event API

ContractApproved

Signature: event ContractApproved(bytes32 contractName, bool approved)

Records that a contract was approved or disapproved to spend gas.

EtherDeposited

Signature: event EtherDeposited(address payable indexed spender, uint value)

Records that a user deposited ether in their gas tank.

EtherWithdrawn

Signature: event EtherWithdrawn(address indexed spender, address payable indexed recipient, uint value)

Records that a user withdrew ether from their gas tank.

EtherSpent

Signature: event EtherSpent(address indexed spender, address payable indexed recipient, uint value, uint gasPrice)

Records that an executor was transferred a value of ether by a spender to reimburse the the execution of a deferred transaction at a certain gas price.

MaxGasPriceSet

Signature: event MaxGasPriceSet(address indexed account, uint maxGasPriceWei)

Records that an account set its max acceptable gas price in wei.


Test Cases

See implementation.

Configurable Values (Via SCCP)

Value Type Description
keeperFee uint The usd value of ether to pay keepers when they execute a gas-reimbursed deferred transaction

Copyright and related rights waived via CC0.