Hooks and helper functions

Herein hooks and helpers used in all implementations are described

Shared hooks

Declared, not implemented

//called before execution of a job
function _beforeExecute(
//job key
bytes32 jobKey_, 

//executor keeper/slasher ID
uint256 actualKeeperId_, 

//binary representation of the job to call
uint256 binJob_) internal view virtual {}

//called before initiating redemption of keeper stake
function _beforeInitiateRedeem(
//keeper ID
uint256 keeperId_) internal view virtual {}

//called after job execution succeeds
function _afterExecutionSucceeded(
//key of the executed job
bytes32 jobKey_, 

//executor keeper/slasher ID
uint256 actualKeeperId_, 

//binary representation of the executed job
uint256 binJob_) internal virtual {}

//called after job registration
function _afterRegisterJob(
//registered job key
bytes32 jobKey_) internal virtual {}

//called after depositing credits to a job (not job owner!)
function _afterDepositJobCredits(
//job key
bytes32 jobKey_) internal virtual {}

//called after withdrawing credits from a job (not job owner!)
function _afterWithdrawJobCredits(
//job key
bytes32 jobKey_) internal virtual {}

Implemented

_afterExecutionReverted

//called after reversion of the job being automated
function _afterExecutionReverted(
  //job key
  bytes32 jobKey_,
  //type of job (SELECTOR, PRE_DEFINED, RESOLVER)
  CalldataSourceType calldataSource_,
  //keeper ID
  uint256 keeperId_,
  //message with which the job reverted
  bytes memory executionResponse_
) internal virtual {
  //silence unused param warning for parameters that are not used by this implementation, but can be used by inheritors
  jobKey_;
  keeperId_;
  calldataSource_;
  
  //if no message was provided by the reverted job, revert with the corresponding message
  if (executionResponse_.length == 0) {
    revert JobCallRevertedWithoutDetails();
  } else 
  //revert with a custom message, thereby manually propagating the reversion (since reverts from low-level calls do not propagate)
  {
    assembly ("memory-safe") {
      revert(add(32, executionResponse_), mload(executionResponse_))
    }
  }
}

Shared helpers

Getters

calculateCompensationPure

//computes Flashbots compensation, is wrapped by _calculateCompensation 
function calculateCompensationPure(
    //dynamic reward part in percents of the expended gas cost
    uint256 rewardPct_,
    //fixed reward; multiplied by FIXED_PAYMENT_MULTIPLIER=1e15
    uint256 fixedReward_,
    //possibly capped (capping happens in payout block of execute_44g58pv) block gas price
    uint256 blockBaseFee_,
    //gas used
    uint256 gasUsed_
  ) public pure returns (uint256) {
    unchecked {
      //computes the total reward as the sum of a dynamic (gas-dependent) and static (gas-independent) summands
      return (gasUsed_ + _getJobGasOverhead()) * blockBaseFee_ * rewardPct_ / 100
             + fixedReward_ * FIXED_PAYMENT_MULTIPLIER;
    }
  }

getKeeperWorkerAndStake

getConfig

Gets the Agent config

function getConfig()
    external view returns (
      //minimal admissible keeper stake
      uint256 minKeeperCvp_,
      //timeout between stake withdrawal being initiated and finalised
      uint256 pendingWithdrawalTimeoutSeconds_,
      //fees that have been accrued so far
      uint256 feeTotal_,
      //fee retained from deposits made in parts per million
      uint256 feePpm_,
      //ID of the last registered keeper; in essence the total amount of historically availabe keepers in the system, since they are never removed, only deactivated
      uint256 lastKeeperId_
    )
  {
    return (
      minKeeperCvp,
      pendingWithdrawalTimeoutSeconds,
      feeTotal,
      feePpm,
      lastKeeperId
    );
  }

getKeeper

Gets Keeper information by ID

function getKeeper(uint256 keeperId_)
    external view returns (
      address admin,
      address worker,
      bool isActive,
      uint256 currentStake,
      uint256 slashedStake,
      uint256 compensation,
      uint256 pendingWithdrawalAmount,
      uint256 pendingWithdrawalEndAt
    )
  {
    pendingWithdrawalEndAt = pendingWithdrawalEndsAt[keeperId_];
    pendingWithdrawalAmount = pendingWithdrawalAmounts[keeperId_];
    compensation = compensations[keeperId_];
    slashedStake = slashedStakeOf[keeperId_];

    Keeper memory keeper = keepers[keeperId_];
    currentStake = keeper.cvpStake;
    isActive = keeper.isActive;
    worker = keeper.worker;

    admin = keeperAdmins[keeperId_];
  }

getJob

Fetches job information

function getJob(
//what job to fetch
bytes32 jobKey_
)
    external view returns (
      //owner address
      address owner,
      //pending owner address, if any (else 0)
      address pendingTransfer,
      //minimal admissible keeper stake for this job's execution (job parameter distinct from the Agent-wide parameter of a similar name)
      uint256 jobLevelMinKeeperCvp,
      //Job struct, described in the corresponding section
      Job memory details,
      //predefined calldata, if any
      bytes memory preDefinedCalldata,
      //resolver struct, described in the corresponding section
      Resolver memory resolver
    )
  {
    return (
      jobOwners[jobKey_],
      jobPendingTransfers[jobKey_],
      jobMinKeeperCvp[jobKey_],
      jobs[jobKey_],
      preDefinedCalldatas[jobKey_],
      resolvers[jobKey_]
    );
  }

getJobRaw

Fetches the binary representation of a given job, described in Job (Old)

function getJobRaw(
//key with which to fetch
bytes32 jobKey_
) public view returns (uint256 rawJob) {
    //fetch the Job struct
    Job storage job = jobs[jobKey_];
    //read the corresponding memory slot
    assembly ("memory-safe") {
      rawJob := sload(job.slot)
    }
  }

getJobKey

Gets a job key (keccak256 of the concatenation of the job's address and ID)

function getJobKey(
//address of the job
address jobAddress_, 
//ID of the job
uint256 jobId_
) public pure returns (bytes32 jobKey) {
    assembly ("memory-safe") {
      //address is a 20-byte integer value and so is padded from the left by 256-160 = 96 bits. Shift left to remove them, then store the address
      mstore(0, shl(96, jobAddress_))
      //the ID is a uint24 value, so is padded from the left by 256-24 = 232 bits. Shift left to remove them, then store the ID, obtaining a concatenation of the two values
      mstore(20, shl(232, jobId_))
      //compute the key
      jobKey := keccak256(0, 23)
    }
  }

Miscellanea

_calculateDepositFee

Computes the fee the Agent retains from a deposit made to either Job or Job owner credits

function _calculateDepositFee() internal view returns (uint256 fee, uint256 amount) {
    //since this is only used in payable functions, we can use the msg.value
    //to compute the fee from the deposited amount and the fee in parts per million
    fee = msg.value * feePpm / 1e6 /* 100% in ppm */;
    //and decrement the amount by the fee to preserve conservation of tokens
    amount = msg.value - fee;
}

_calculateCompensation

Wraps the compensation calculations

function _calculateCompensation(
    //whether the job execution call succeeded
    bool ok_,
    //job binary representation
    uint256 job_,
    //executor keeper ID
    uint256 keeperId_,
    //gas price, capped if needed
    uint256 gasPrice_,
    //gas expenditure
    uint256 gasUsed_
) internal view virtual returns (uint256) {
  //suppress unused parameter warnings for parameters not used by this implementation, but available to descendants
  ok_; 
  keeperId_; 
  //get the fixed reward from the job binary representation. It occupies four bytes (hence the rightshift by 256-32 = 224 bits) and starts at byte index 8 (hence the leftshift by 8*8 = 64 bits))
  uint256 fixedReward = (job_ << 64) >> 224;
  //get the gas expenditure dynamic reward percentage from the job binary representation. It occupies two bytes (hence the rightshift by 256-16 = 240 bits) and starts at byte index 12 (hence the leftshift by 8*12 = 96 bits))
  uint256 rewardPct = (job_ << 96) >> 240;
  //invoke the compensation computation
  return calculateCompensationPure(rewardPct, fixedReward, gasPrice_, gasUsed_);
}

RanDAO-specific hooks

RanDAO overrides many hooks, given hereafter with annotations.

_afterExecutionReverted

function _afterExecutionReverted(
  //job which reverted
  bytes32 jobKey_,
  //job type (SELECTOR, PRE_DEFINED, RESOLVER)
  CalldataSourceType calldataSource_,
  //keeper ID
  uint256 keeperId_,
  //message with which reversion occurred
  bytes memory executionResponse_
) internal override {
  //if a job is of the Resolver type and has no slashing initiated, there will be no slashing to handle, so simply revert
  if (calldataSource_ == CalldataSourceType.RESOLVER &&
    jobReservedSlasherId[jobKey_] == 0 && jobSlashingPossibleAfter[jobKey_] == 0) {
    revert SlashingNotInitiatedExecutionReverted();
  }
  
  //release the keeper unconditionally
  _releaseKeeper(jobKey_, keeperId_);
  
  //event for WS listeners
  emit ExecutionReverted(jobKey_, keeperId_, executionResponse_);
}

_beforeExecute

Called before job execution to make sure only the authorised keeper/slasher may execute

function _beforeExecute(
  //job key
  bytes32 jobKey_, 
  
  //keeper/slasher ID
  uint256 actualKeeperId_, 
  
  //job binary representation
  uint256 binJob_) internal view override 
  {
  //get the expected keeper ID (since a slasher may well execute the job under certain conditions instead)
  uint256 nextKeeperId = jobNextKeeperId[jobKey_];
  //get the interval duration from the binary job representation. It occupies three bytes (hence the rightshift by 256-24 = 232 bits) and starts at byte index 4 (hence the leftshift by 8*4 = 32 bits))
  uint256 intervalSeconds = (binJob_ << 32) >> 232;
  //get the last execution timestamp from the binary job representation. Is occupies the first four bytes, so we only need to rightshift by 256-32 = 224 bits.
  uint256 lastExecutionAt = binJob_ >> 224;

  // if interval task is called by a slasher
  if (intervalSeconds > 0 && nextKeeperId != actualKeeperId_) {
    //declare the variable to store the end of the grace period
    uint256 nextExecutionTimeoutAt;
    //copy the latest execution timestamp
    uint256 _lastExecutionAt = lastExecutionAt;
    //if the job has not been executed, initialise the last exec timestamp being equal to the job creation timestamp
    if (_lastExecutionAt == 0) {
      _lastExecutionAt = jobCreatedAt[jobKey_];
    }
    unchecked {
      //the end of the grace period is the sum of the timestamp at which execution will be made possible (_lastExecutionAt + intervalSeconds) and the grace period size, no less than 15 seconds (rdConfig.period1)
      nextExecutionTimeoutAt = _lastExecutionAt + intervalSeconds + rdConfig.period1;
    }
    // if it is to early to slash this job, only the assigned keeper can execute it, since slasher execution always leads to slashing
    if (block.timestamp < nextExecutionTimeoutAt) {
      revert OnlyNextKeeper(nextKeeperId, lastExecutionAt, intervalSeconds, rdConfig.period1, block.timestamp);
    }
    
    //if a slasher does have rights to execute the job, check that the caller is indeed the slasher assigned at the current block
    uint256 currentSlasherId = getCurrentSlasherId(jobKey_);
    if (actualKeeperId_ != currentSlasherId) {
      revert OnlyCurrentSlasher(currentSlasherId);
    }
  // if a resolver job is called by a slasher
  } else  if (intervalSeconds == 0 && nextKeeperId != actualKeeperId_) {
    //fetch the grace period end timestamp computed and set at slashing initiation
    uint256 _jobSlashingPossibleAfter = jobSlashingPossibleAfter[jobKey_];
    //if not initiated yet
    if (_jobSlashingPossibleAfter == 0) {
      revert SlashingNotInitiated();
    }
    //if the grace period has not yet elapsed
    if (_jobSlashingPossibleAfter > block.timestamp) {
      revert TooEarlyForSlashing(block.timestamp, jobSlashingPossibleAfter[jobKey_]);
    }
    //finally, check if the caller is the reserved slasher. Reserved slashers are distinct from current slashers in that they receive exclusive rights to execute the slashing they initiated, and this rights may last a good amount of blocks over which interval job slashers may change a couple of times. This is done to reward slashing initation, which is a vital part of overseeing execution of resolver jobs
    uint256 _jobReservedSlasherId = jobReservedSlasherId[jobKey_];
    if (_jobReservedSlasherId != actualKeeperId_) {
      revert OnlyReservedSlasher(_jobReservedSlasherId);
    }
  }
}

_afterDepositJobCredits

Called after job credits are deposited to handle potential keeper assignemnt.

function _afterDepositJobCredits(
//key of job for which credits have been deposited
bytes32 jobKey_) internal override {
    //if the job lacks a keeper and has enough credits from the Agent's standpoint (i.e., no less than rdConfig.jobMinCreditsFinney finneys), assign a keeper to it
    _assignNextKeeperIfRequired(jobKey_);
}

_afterWithdrawJobCredits

Called after job credit withdrawal to handle potential keeper release

function _afterWithdrawJobCredits(
//key of job for which credits were withdrawn
bytes32 jobKey_) internal override {
  //get the job's keeper
  uint256 expectedKeeperId = jobNextKeeperId[jobKey_];
  //if the job no longer has at least rdConfig.jobMinCreditsFinney finneys, release the keeper (provided there is one assigned)
  _releaseKeeperIfRequired(jobKey_, expectedKeeperId);
}

_afterExecutionSucceeded

Handles slashing logic; called after success of automated job execution.

function _afterExecutionSucceeded(
//executed job key
bytes32 jobKey_, 

//keeper/slasher id
uint256 actualKeeperId_, 

//binary job representation
uint256 binJob_) internal override {
  //get the assigned keeper ID
  uint256 expectedKeeperId = jobNextKeeperId[jobKey_];
  //release the keeper, since the job is already executed, whether by him or by slasher
  _releaseKeeper(jobKey_, expectedKeeperId);
  //get the interval duration from the binary job representation. It occupies three bytes (hence the rightshift by 256-24 = 232 bits) and starts at byte index 4 (hence the leftshift by 8*4 = 32 bits))
  uint256 intervalSeconds = (binJob_ << 32) >> 232;
  //if an interval job, zero out the slasher data. Redundant, overlaps with _releaseKeeper
  if (intervalSeconds == 0) {
    jobReservedSlasherId[jobKey_] = 0;
    jobSlashingPossibleAfter[jobKey_] = 0;
  }

  // if slashing (i.e. the actual executor is not the assigned keeper)
  if (expectedKeeperId != actualKeeperId_) {
    //get the struct of the keeper to slash
    Keeper memory eKeeper = keepers[expectedKeeperId];
    //compute the dynamic slash amount as a certain portion, no greater than 50% (due to restrictions on the rdConfig.slashingFeeBps value), of the keeper stake
    uint256 dynamicSlashAmount = eKeeper.cvpStake * uint256(rdConfig.slashingFeeBps) / 10000;
    //compute the fixed slash amount; no greater than one half of the minimal Agent-wide admissible stake by virtue of the restriction on the value of rdConfig.slashingFeeFixedCVP
    uint256 fixedSlashAmount = uint256(rdConfig.slashingFeeFixedCVP) * 1 ether;
    // Compute the total slash amount. NOTICE: totalSlashAmount can't be >= uint88.max(), since we explicitly convert to uint88 and therefore discard the extra bits (i.e. those of higher order than uint88)
    uint88 totalSlashAmount = uint88(fixedSlashAmount + dynamicSlashAmount);
    //should never come to pass thanks to the restrictions on slashingFeeBps and slashingFeeFixedCVP
    if (totalSlashAmount > eKeeper.cvpStake) {
      // Actually this block should not be reached, so this is just in case
      revert InsufficientKeeperStakeToSlash(jobKey_, expectedKeeperId, eKeeper.cvpStake, totalSlashAmount);
    }
    //slash the keeper
    keepers[expectedKeeperId].cvpStake -= totalSlashAmount;
    //reward the slasher with the slashed stake
    keepers[actualKeeperId_].cvpStake += totalSlashAmount;
    //event for WS listeners
    emit SlashIntervalJob(jobKey_, expectedKeeperId, actualKeeperId_, fixedSlashAmount, dynamicSlashAmount);
  }
  //assign next keeper to the job. Not conditional, since no change of the job's credits amount takes place in this function
  _assignNextKeeper(jobKey_);
}

_beforeInitiateRedeem

Ensures the keeper can initiate redemption of a part of his stake, i.e. has no pending jobs with execution of which stake reduction may interfere (e.g. by changing the keeper's eligibility).

function _beforeInitiateRedeem(
//keeper to check
uint256 keeperId_) internal view override {
  //make sure no jobs are pending
  _ensureCanReleaseKeeper(keeperId_);
}

_afterRegisterJob

Immediately assign the next keeper. Assignment is unconditional, but this does not beget possibilities of assigning keepers to jobs with insufficient credits because the _assignNextKeeper has an internal check for that by means of _releaseKeeperIfRequiredBinJob called with the checkAlreadyReleased flag as False, which will return True if the job/owner has insufficient credits (see Keeper assignment and release in RanDAO realisation).

function _afterRegisterJob(
//freshly registered job's key
bytes32 jobKey_) internal override {
  //assign keeper to the job
  _assignNextKeeper(jobKey_);
}

RanDAO-specific helpers

Getters

getJobsAssignedToKeeper

Gets the jobs assigned to a particular Keeper, stored in the mapping keeperLocksByJob .

function getJobsAssignedToKeeper(
//id of the keeper whose jobs to fetch
uint256 keeperId_) external view returns (bytes32[] memory jobKeys) {
    //return the pending jobs
    return keeperLocksByJob[keeperId_].values();
}

getJobsAssignedToKeeperLength

Returns the amount of jobs pending for a particular Keeper.

function getJobsAssignedToKeeperLength(
//id of the keeper whose job amount to fetch
uint256 keeperId_) external view returns (uint256) {
  //return the amount of pending jobs
  return keeperLocksByJob[keeperId_].length();
}

getCurrentSlasherId

Returns the ID of the currently assigned slasher. The returned value is distinct from a reserved slasher and corresponds to the ID of the slasher chosen according to the algorithm described in Copy of Slashing.

function getCurrentSlasherId(
//key of the job for which to produce the slasher
bytes32 jobKey_) public view returns (uint256) {
  //returns (blocknumber/rdConfig.slashingEpochBlocks+jobKey)%totalActiveKeepers
  return getSlasherIdByBlock(block.number, jobKey_);
}

getActiveKeepersLength

Returns the amount of currently active keepers.

function getActiveKeepersLength() public view returns (uint256) {
  return activeKeepers.length();
}

getActiveKeepers

Returns the list of currently active keepers (IDs)

function getActiveKeepers() public view returns (uint256[] memory) {
  return activeKeepers.values();
}

getSlasherIdByBlock

Provides a way to obtain historical data on assigned slasher IDs (returns ID assigned for a given job key at a given block).

function getSlasherIdByBlock(
//block number at which to get the ID
uint256 blockNumber_, 

//job key for which to get the ID
bytes32 jobKey_) public view returns (uint256) {
  //get the total amount of active keepers
  uint256 totalActiveKeepers = activeKeepers.length();
  //obtain the index of the slasher. Remember that division in Solidity always rounds down to the nearest integer 
  uint256 index = ((blockNumber_ / rdConfig.slashingEpochBlocks + uint256(jobKey_)) % totalActiveKeepers);
  //fetch ID from the enumerable set by index
  return activeKeepers.at(index);
}

Miscellanea

checkCouldBeExecuted

An always-reverting subroutine for checking executability of a certain function, where executability is flagged by different revert messages. Reversion is necessary to make sure no state changes incurred by successful execution persist.

function checkCouldBeExecuted(
//address of the job to check
address jobAddress_, 

//calldata to pass for a check
bytes memory jobCalldata_) external {
  //perform a low-level call to the address with the calldata. Low-level call is used to avoid propagation of reversions, since we desire to employ custom revert messages
  (bool ok, bytes memory result) = jobAddress_.call(jobCalldata_);
  //job executable
  if (ok) {
    revert JobCheckCanBeExecuted();
  } else 
  //job not executable
  {
    revert JobCheckCanNotBeExecuted(result);
  }
}

getPseudoRandom

Gets the pseudo-random number corresponding to the RanDAO realisation; stored in block.difficulty field for backward compatibility since the advent of PoS.

function _getPseudoRandom() internal view returns (uint256) {
  return block.difficulty;
}

_checkBaseFee

Overrides the Flashbots implementation function of the same name that was initially used to make sure the base fee used for computing rewards is not in excess of what was set as maximum by the job. Since in RanDAO realisation the keepers have no choice in whether to execute the job (in Flashbots realisation, they could control whether the _checkBaseFee function would produce and propagate a revert in case of the base fee being capped), the Job owner is bereft of his ability to set a maximal fee, and so to preserve backward compatilibity the function is overridden to always return the maximal value of the type.

function _checkBaseFee(uint256 
//job for which to fetch the fee
binJob_, 

//job config byte
uint256 cfg_) internal pure override returns (uint256) {
    //suppress unused parameter warning
    binJob_;
    cfg_;
    
    //return maximal possible value for outlined reasons
    return type(uint256).max;
}

_calculateCompensation

Overrides the eponymous function in the Flashbots reaslisation due to the change in the algorithm.

function _calculateCompensation(
  //whether the call was a success
  bool ok_,
  //job binary representation
  uint256 job_,
  //executor (keeper/slasher) ID
  uint256 keeperId_,
  //gas price, now capped only by type(uint256).max
  uint256 gasPrice_,
  //total gas expended
  uint256 gasUsed_
) internal view override returns (uint256) {
  //if a call reverted, at leat compensate the gas expenditure, since we no longer have the costless reversions of Flashbots. Mind that all checks made before the Payout block in the execute_44g58pv must still be passed in order to reach this calculation.
  if (!ok_) {
    return gasUsed_ * gasPrice_;
  }
  
  //suppress unused parameter warning
  job_; 
  
  //get Agent config
  RandaoConfig memory _rdConfig = rdConfig;

  //get executor stake
  uint256 stake = keepers[keeperId_].cvpStake;
  //get the job's cap on the keeper stake for the purposes of computing reward (introduced to prevent excessive payouts to keepers with a very great stake). This value occupies the place of Flashbots' fixedReward in the binary layout and is obtained just like it: leftshift by 64 bits (the value starts at the eighth byte) and rightshift by 224 bits (the value is comprised of four bytes). 
  uint256 _jobMaxCvpStake = ((job_ << 64) >> 224) * 1 ether;
  //cap the stake for computational purposes if needed
  if (_jobMaxCvpStake > 0  && _jobMaxCvpStake < stake) {
    stake = _jobMaxCvpStake;
  }
  //consider also the Agent-wide stake cap, if one is set
  if (_rdConfig.agentMaxCvpStake > 0 && _rdConfig.agentMaxCvpStake < stake) {
    stake = _rdConfig.agentMaxCvpStake;
  }
  //compute the reward as a sum of fixed (set portion of the capped stake, no greater than the entirety of the capped stake (since the lowest admissible value of stakeDivisor is 1) and no lesser than 1/(2^32-1) of the capped stake (by means of the value being stored in uint32), effectively being zero) and dynamic (a certain preset percetnage, which may exceed 100% and is capped from above by 655.35%, of the total gas expenditure) portions
  return (gasPrice_ * gasUsed_ * _rdConfig.jobCompensationMultiplierBps / 10000) +
    (stake / _rdConfig.stakeDivisor);
}

Last updated