RIF Scheduler - Contracts
Repo: rsksmart/rif-scheduler-contracts
Each service provider should deploy a smart contract to manage the plans, receive payments for the service and execute the requested transactions.
The smart contract has four modules:
- Admin: The Service Provider can create new plans and change withdraw account
- Purchasing: The requestors can use these methods to pay for the service using tokens
- Scheduling: Requestors submit the transactions they need to execute and when
- Execution: Service Provider executes transactions and receives payment
See this guide about how to run a scheduler on your own.
Admin
The Service Provider can set up different plans. The plans must specify the:
- Price per execution,
- The time window (in seconds) within a scheduled execution should run.
- The maximum amount of gas the internal call can spend
The price of the plan strongly depends on these last two values We recommend a value more than 15 seconds. This value will depend strongly on the service configuration. For example, if the time window for a plan is set to 100, then this following condition should apply, otherwise the execution will not performed:
scheduled time
- 10 <= execution time
<= scheduled time
+ 10
- The maximum gas that an schedule execution could spend.
- The address of the token used to purchase executions. If the address
0x0
passed as parameter the contract assumes that the executions will be paid with RBTC.
Plan[] public plans;
function addPlan(uint256 price, uint256 window, uint256 gasLimit, IERC677 token) external onlyProvider
function removePlan(uint256 plan) external onlyProvider
The payee is the account that receives the tokens after execution.
The Service Provider can change this account by calling setPayee
.
address public payee
function setPayee(address payee_) external onlyProvider
Purchasing
Purchasing executions of a plan can be done in three ways:
- ERC-20: Use
purchase
after performing a ERC-20approve
- ERC-677: Use
transferAndCall
with ABI encoded(uint256 plan, uint256 quantity)
- RBTC: Use
purchase
sending an RBTC value. This only available for plans having set the token address to0x0
In all of them, the requestor can choose the amount of transaction executions that they want to purchase at the given price.
function purchase(uint256 plan, uint256 quantity) external payable
Scheduling
The executions have an id. Use this hash function to calculate it:
abi.encode(execution.requestor, execution.plan, execution.to, execution.data, execution.timestamp, execution.value)
Submit new transactions to be executed by the Service Provider. Scheduling can be done only after purchasing a plan. When scheduling you must specify this fields:
plan
: The plan id the requestor wants to use for the executionto
: The receiver of the scheduled transactiondata
: The data field of the transactiontimestamp
: When it must be executed, considering the window
function schedule(uint256 plan, address to, bytes calldata data, uint256 timestamp) external payable
It is possible to schedule multiple transactions at the same time by calling:
batchSchedule(bytes[] calldata data) external payable
This function receives an array of ABI encoded transactions to be executed and the total RBTC required by them, if necessary. Each element of the transaction array should be encoded as follows:
abi.encode(['uint256', 'address', 'bytes', 'uint256', 'uint256'], [execution.plan, execution.to, execution.data, execution.timestamp, execution.value])
To cancel a scheduling before its execution use:
function cancelScheduling(bytes32 id) external
When the scheduled transactions are cancelled, the contract refunds any value transferred during the schedule and increases the plan balance for the requestor.
Please note that it is only possible to cancel Scheduled
and Overdue
transaction executions.
Retrieving scheduled transactions data
Query the state of an execution at any time. State machine is defined as:
Nonexistent -> Scheduled
-- Nonexistent status is returned by the contract when asking for an execution that has not been registered to the blockchain. This is never assigned. The first valid state isScheduled
.Scheduled -> Cancelled
-- requestor cancelled executionScheduled -> ExecutionSuccessful
-- call was executed in the given time and did not failScheduled -> ExecutionFailed
-- call was executed in the given time but failedScheduled -> Overdue
-- execution window has passed, expected earlierOverdue -> Refunded
-- refund for overdue execution paid
enum ExecutionState { Nonexistent, Scheduled, ExecutionSuccessful, ExecutionFailed, Overdue, Refunded, Cancelled }
function getState(bytes32 id) public view returns (ExecutionState)
Retrieve the data for a particular scheduled transaction execution using its id (computed as described above).
function getExecutionById(bytes32 id) public view returns (Execution memory execution)
To get the number of transactions for a requestor use:
function executionsByRequestorCount(address requestor) external view returns (uint256)
and list them with:
function getExecutionsByRequestor(
address requestor,
uint256 fromIndex,
uint256 toIndex
)
Besides the requestor address, this function also requires fromIndex
and toIndex
to enable pagination.
Execution
The execution is performed by the Service Providers.
The Service Providers performs the execution of the scheduled transaction by submitting it between transaction.timestamp - plan.window
and transaction.timestamp + plan.window
.
function execute(bytes32 id) external nonReentrant
This performs execution in this way:
(bool success, bytes memory result) = payable(execution.to).call{ gas: plan.gas, value: execution.value }(execution.data);
After execution is completed, the Service Provider will receive the payment for the execution.
If the transaction is submitted before time window, the execution will be rejected and it will remain scheduled. If it is submitted after the time window, it won't be executed and its state will be Overdue.
In any case the Service Provider is not responsible for the successful execution of the submitted transaction. It will only guarantee that it is submitted as scheduled.
Refunding overdue executions
The requestor can request the refund of the overdue scheduled transactions (those that were not executed on time, inside the execution window). To claim the refund, the requestor should call the following method:
function requestExecutionRefund(bytes32 id) external
This will set the requested execution as refunded and the remaining executions for thar plan will be increased by one. Also if there was some value transferred during the scheduling, it will be returned to the requestor.
Refunding plans
The following steps describe the procedure that should be followed in the case that the user provider stops offering the scheduling service. This allows users to get a refund for their available balance:
- The service provider should pause the contract using the method
pause()
. This blocks all the plan purchases, transaction schedules, and executions. Nevertheless this operation does not affect any state or balance, and may be reverted callingunpause()
. - Once the contract is paused, each requestor should cancel all the pending executions (Scheduled or Overdue), by calling
cancelScheduling(bytes32 id)
as described above. This will cancel the schedule and increase the available executions. - Then, each requestor should call the method
requestPlanRefund(uint256 plan)
for each plan that they purchased to receive a transfer for the value of the remaining balance. Please notice that this is only available while the contract is paused.
Steps 2 and 3 can be repeated as many times as needed. These can also be combined in a single contract call using the multicall
method.
Multicall
The contract allows one to combine multiple calls into a single one using this method.
multicall(bytes[] calldata data, bool revertIfFails)
This method receives:
- calldata: an array of encoded contract calls to be executed
- revertIfFails: if true when a transaction in calldata is executed and it reverts, all the transactions will be reverted. Otherwise, it will continue executing the following transactions.
WARNING: using
revertIfFails
set totrue
can incur in DoS attackas: by spamming one transaction, all the others will fail. In the other hand, usingrevertIfFails
set tofalse
will only allow to retrieve the index of the calls that failed.