Copy of Slashing

Overview

One prominent issue any attempt at decentralised execution automation must tackle is that of incentivising task execution in a timely and reliable manner for conditionals of arbitrary logical complexity. This is achieved by means of combining rewards for execution with punishments for failure thereof. Do refer to Copy of Task Reward and Gas Compensation for details on computation and payment of rewards; hereafter punishment is given ample consideration.

Since the protocol needs to stay decentralised, and we wish not to offload the role of an overseer to any external party, it is necessary to devise a method and an incentive for keepers to not only execute tasks, but also ensure execution thereof by others. In PowerAgent, this is done by introducing the concept of a Slasher and the corresponding process of slashing.

Slashing is defined as confiscation of a portion of a Keeper's stake by an entity acting as a Slasher. Slashing may be brought about by failure to execute tasks or other theoretically arbitrarily configurable conditions. The role of a Slaher may be filled by either the Agent owner (as is the case for the Flashbots realisation) or some other entity (e.g. other Keepers, as is the case for the RanDAO realisation). Naturally, a process must be introduced to make sure wrongful slashes do not happen (e.g. a Keeper is not slashed for failing to execute a task which reverts at attempts to do so). Hence a natural way to incentivise Keeper Slashers arises: if they are rewarded for slashing by the amount confiscated or a portion thereof, they would naturally seek to maximise their slashing volume and thereby exert effective control on the Keepers, automatically providing efficient economic disincentive for malicious conduct.

Moreover, if the Slasher is also granted the opportunity to execute the contract instead of the failed Keeper, a natural way to generalise the slashing process to also provide additional execution security to Job owners arises, and the incentive to perform the important duty of a Slasher is accordingly increased, since now the slashing reward is augmented by the execution reward.

Specification

The details of slashing mechanism implementation depend on the realization of the PowerAgent.

Flashbots

Flashbots realization doesn’t need an imbedded decentralised slashing mechanism: the auction format of the (Flashbots mempool) Gas Wars implies that there always will be enough actors ready to execute the task, and one of them will be successful and included by the Flashbots, others suffering revertion. In other words, there is theoretically no need to safeguard against task non-execution due to non-exclusivity of possible executors and therefore a glut thereof.

This realization has a fallback slashing method which can be called only by the PowerAgent contract owner, aka manual slashing.

function slash(
    //id of the keeper to be slashed
    uint256 keeperId_,
    
    //address to which to transfer the slashed stake
    address to_,
    
    //Amount of CVP to slash from the current keeper's stake
    uint256 currentAmount_,
    
    //Amount of CVP to slash from the pending withdrawal keeper balance
    uint256 pendingAmount_
    ) 
{
...
}

For the sake of completeness, we briefly outline the slashing algorithm elucidated beforehand in Keeper staking, withdrawal, and deactivation. The values of currentAmount_ and pendingAmount_ are subtracted from the current stake of the keeper and his stake pending withdrawal, respectively. The latter value is simply subtracted, while the former is afterwards added to the mapping slashedStakeOf at an entry corresponding to this keeper's ID. In effect, both types of the slashed token no longer belong to the Keeper in question, since their totality is transferred to the specified address immediately after decrements are performed, but the totality slashed from the current amount is left accessible as a variable so that derivative implementations could use knowledge on the accrued slashed stake. A means of zeroing the value of slashedStakeOf is provided by the stake withdrawal function, where one needs to compensate all the slashed stake before withdrawing any actual tokens. In the core Flashbots realisation contract, this is a mere formality, only requiring addition of a number to the specified withdrawal amount without actually losing the Keeper any extra tokens, but derivative implementations could well define additional restrictions and impose extra punishment via the overridable _beforeInitiateRedeem hook invoked in the redemption initiation functions.

RanDAO

RanDAO realization uses a random number generator (outsourced to RanDAO, whence the name of the realisation is derived, and used in the form of the block difficulty parameter, which since the transition from PoW to PoS corresponds to the block's RanDAO reveal) to pre-assign both keepers and slashers for the next execution of a given task. Then we can obtain the following algorithms of slashing:

Interval jobs
  1. Obtain current slasher ID by job

  2. Execute the job the keeper failed to

  3. Slash the keeper

Resolver jobs
  1. Obtain current slasher ID by job

  2. Initiate slashing

  3. Execute the job the keeper failed to

  4. Slash the keeper

Note that since the slashers necessarily execute the job the keeper failed to (as a precaution against nonexecution), they must satisfy the job's demands regarding minimal stake to be able to do so. Assertions to such effect are in place.

Mind that although it is ensured that the assigned keeper for each job has sufficient stake, the same does not necessarily hold for the assigned slasher. For this reason, it is perfectly possible for a slasher to lack the ability to actually slash, which may cause delays a few blocks long (as the keeper stake obedience check is in the non-overridable function execute_44g58pv).

The sole distinction in these algorithms is the presence of slashing initiation in the Resolver case. The cause for this is the fact that the timestamp at which execution is made possible is known exactly a priori for interval jobs by definition, but for resolver jobs is is defined by essentially arbitrary logic. Slashing is initiated whenever a corresponding function is called by the assigned slasher who can execute the underlying function being automated at the timestamp of initiation. This timestamp is taken to be the first one at which job execution is made possible for the purposes of computing grace and admissibility periods. This estimate is rather tight at discrete time and rational slashers (i.e. slashers acting as optimiser agents).

Assign a slasher

A slasher for a given task is assigned from a list of active keepers on a per-block basis by the function getSlasherIdByBlock() using the following expression with projection onto a finite ring (not necessarily a field since the keeper amount is quite arbitrary) Z/kZZ/kZ, where kk is the cardinality of the active keeper set, by means of modular arithmetic:

(blockNumber_rdConfig.slashingEpochBlocks+uint256(jobKey_))%totalActiveKeepers(\frac{blockNumber\_}{rdConfig.slashingEpochBlocks} + uint256(jobKey\_)) \hspace{2mm} \% \hspace{2mm} totalActiveKeepers

The keeper from the active list chosen by this index becomes a slasher for this particular job (since keys are unique, the mapping from slashers to jobs is injective, though the converse is generally false due to difference in set cardinalities).

So, for each block a single unique (with some caveats) slasher has a chance to slash a negligent keeper.

A deeper dissection of the choice algorithm

The first summand blockNumber_rdConfig.slashingEpochBlocks\frac{blockNumber\_}{rdConfig.slashingEpochBlocks} provides some permanence to slasher selection, ensuring that for rdConfig.slashingEpochBlocksrdConfig.slashingEpochBlocks this term is the same (since Solidity division returns an integer quotient with rounding to the nearest lower integer). The second summand ensures that at each block, slashers assigned to different jobs will be different. Since job keys are unique, this can indeed be satisfied by adding said keys. Finally, the modular operation has the meaning of ensuring the index thus obtained is within the set of possible active keeper indices. In theory, during each slashing epoch, the slasher assignements are invariant, but in actuality some variance will be observed due to changes in job and keeper pools.

function getSlasherIdByBlock(
    
    //block number to get slasher id for
    uint256 block.number,
    
    //job key (keccak256 of a concatenation of job address and ID) of the job to assign a slasher for
    bytes32 jobKey_
    ) public view returns (uint256)
    
{
    //total number of all active keepers
    uint256 totalActiveKeepers = activeKeepers.length();
    
    //index of the keeper to choose as a slasher
    uint256 index = ((blockNumber_ / rdConfig.slashingEpochBlocks + uint256(jobKey_)) % totalActiveKeepers);
    
    //fetch the keeper at a chosen index
    return activeKeepers.at(index);
}

Resolver job slashing initiation

To initiate slashing, a current slasher invokes the function initiateSlashing once he is able to execute the task (checked off-chain by means of the supplied resolver). The initiation is handled by a set of hooks, see link, that ensure executability of the job and onset of a number of conditions, such as the slasher being indeed the slasher assigned at this time. Superior presentation of logic entailed is available at the Hooks page.

Initiation is needed for resolver type jobs, where the exact timestamp of execution is not known a priori. Interval jobs are instead slashed directly.

The idea behind initiation is as follows: for a resolver type job, both the keeper and the slasher constantly virtually execute the resolver contract on their nodes, waiting for the resolver to return canExecute=True (meaning that the execution is made possible and should be done).

As soon as this happens, the keeper tries to execute the function, and the slasher tries to initiate slashing. If the slasher initiates before the keeper is able to execute, the slashing is initiated. This means that a preconfigured time interval (grace period defined by rdConfig.period1) is assigned during which a keeper can execute the task, but cannot be slashed. If they fail to do this during this period, the slashing may commence, with the job being executed by the slasher instead. If slashing is not commenced, it may be re-initiated after a different period (admissibility period given by rdConfig.period2) elapses.

function initiateSlashing(
    //address of the job to initiate slashing for
    address jobAddress_,
    
    //job id of the job to initiate slashing for
    uint256 jobId_,
    
    //id of the keeper who initiates slashing
    uint256 slasherKeeperId_,
    
    //whether to use the resolver for checking executability of the job at initiation
    bool useResolver_,
    
    //job execution calldata 
    bytes memory jobCalldata_
    ) external
{
...
}

Slash initiation event

event InitiateSlashing(
    //key of the job for which slashing is initiated
    bytes32 indexed jobKey,
    //id of the keeper reserved as slasher for this job. Slasher reservation is a unique feature of the resolver jobs, required since the current keeper need not necessarily coincide with the keeper who initiated slashing (as period1 is at least one block long), and it is desirable to reward the vigilant slasher with protection of his ability to perform slashing
    uint256 indexed slasherKeeperId,
    //whether resolver was used to check executability
    bool useResolver,
    //timestampt at which the grace period elapses
    uint256 jobSlashingPossibleAfter
  );

Slash

If the keeper could not properly execute the task in time (i.e. the current time is not in excess of nextExecutionTimeoutAt = _lastExecutionAt + intervalSeconds + rdConfig.period1 for interval jobs or jobSlashingPossibleAfter[jobKey_] = initiation_timestamp+rdConfig.period1 for resolver jobs) , the slasher is allowed to execute the task instead of the failed keeper. Refer to the hooks page Hooks and helper functions to obtain superior clarity regarding the checks performed. After successful execution, in the _afterSuccessfulExecution hook the keeper’s CVP stake is reduced and the slasher CVP stake is increased by the same amount.

By this process, execution is secured, and unreliable behaviour is punished.

Re-initiation

Should the slasher fail to execute the job as well, either due to negligence or because his stake is not sufficient to do so, another slasher may re-initiate the slashing once the admissibility period expires.

Slashing amount

Flashbots

Since Flashbots slashing is manual, slashing amounts are fully determined by the Agent owner and are only bounded by the totality of the keeper's stake (due to reverts at setting negative values for uint variables).

RanDAO

In the RanDAO realisation, slashing consists of two components: fixed and dynamic, where fixed component is independent of the keeper stake, and the dynamic component comprises a pre-defined portion thereof. Both the fixed slashing fee and the dynamic slashing fee in basis points of the stake are Agent-wide parameters controlled by setting rdConfig. The formulae are as follows:

fixedSlashAmount=rdConfig.slashingFeeFixedCVPfixedSlashAmount = rdConfig.slashingFeeFixedCVP
dynamicSlashAmount=Keeper.cvpStake∗uint256(rdConfig.slashingFeeBps)/10000dynamicSlashAmount = Keeper.cvpStake * uint256(rdConfig.slashingFeeBps) / 10000
totalSlashAmount=fixedSlashAmount+dynamicSlashAmounttotalSlashAmount = fixedSlashAmount + dynamicSlashAmount

Since the fixed CVP slashing fee is capped at one half of the minimal CVP stake admissible by the Agent, and the dynamic CVP slashing fee is capped at 50% (5000 basis points), no keeper at no point in time can be slashed for more than 100% of his stake.

Last updated