Slashing

Overview

A big headache of any automated execution protocol is to ensure that all tasks are executed timely and reliably - either for interval tasks (that should be executed every once in a predefined time interval) or for resolver tasks (that are executed based on some complex external logic, e.g., a certain amount of tokens is accrued as a reward and should be claimed and restaked).

The goal here is to make partaking in automatic execution economically interesting (provide reasonable rewards for each correctly executed task) and, simultaneously, punish keepers which provide unreliable results.

Moreover, it is a puzzle to make the process fully decentralized and economically incentivized for those participants who observe and supervise the behavior of keepers.

PowerAgent solves both problems by introducing a slasher role and a mechanism of slashing.

Slashing is a process of taking away a certain part of a keeper’s stake once they misbehave in any way: either not execute the task on time or fail during execution. Before slashing algorithm ensures that the failure occurred on the keeper’s side, and not on the side of the contract being executed.

Slasher, accordingly, is a keeper which is assigned the role of overseer. Their task is to constantly challenge keepers and try to catch them on low-quality execution (or non-execution) of a task. To incentivize the slasher, they receive all the penalty collected from the punished keeper.

Specification

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

Flashbots

Flashbots realization naturally doesn’t need the slashing as a decentralized mechanism: the rally of keepers trying to overbid each other in a Gas War 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 reverted.

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 to
    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_
    ) something
{
...
}

RanDAO

RanDAO realization uses a random number generator to pre-assign both keepers and slashers for the next execution of a given task. The slashing then is divided into several logical steps: assign a slasher, initiate slashing, slash.

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 modulo remainder from the following expression:

(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.

So, for each block a single unique slasher has a chance to slash a negligent keeper.

function getSlasherIdByBlock(
    
    //block number to get slasher id for
    uint256 block.number,
    
    //job key of the job to get 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);
    
    return activeKeepers.at(index);
}

Initiate slashing

To initiate slashing, a current slasher tries to perform execution of a given task. The initiation is handled by a set of hooks, see link.

Initiation is needed for resolver type jobs, where the exact timestamp of execution is not known a priori.

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, both try to execute the target function. If the slasher is the one to do it first, the slashing is initiated. This means, a preconfigured time interval is given for a keeper, during which the keeper can execute the task and not be punished. If they fail to do this during this period, the slashing may commence. If slashing is not commenced, it may be re-initiated after a different period 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 the job is resolver or not
    bool useResolver_,
    
    //job execution calldata 
    bytes memory jobCalldata_
    ) external
{
...
}

Slash

If the keeper could not properly execute the task, the slasher is allowed to execute the task instead of the failed keeper. After the execution in the _afterSuccessfulExecution hook the keeper’s CVP stake is reduced and the slasher CVP stake is increased by the same amount.

Last updated