Herein hooks and helpers used in all implementations are described
Shared hooks
Declared, not implemented
//called before execution of a jobfunction_beforeExecute(//job keybytes32 jobKey_,//executor keeper/slasher IDuint256 actualKeeperId_,//binary representation of the job to calluint256 binJob_) internalviewvirtual {}//called before initiating redemption of keeper stakefunction_beforeInitiateRedeem(//keeper IDuint256 keeperId_) internalviewvirtual {}//called after job execution succeedsfunction_afterExecutionSucceeded(//key of the executed jobbytes32 jobKey_,//executor keeper/slasher IDuint256 actualKeeperId_,//binary representation of the executed jobuint256 binJob_) internalvirtual {}//called after job registrationfunction_afterRegisterJob(//registered job keybytes32 jobKey_) internalvirtual {}//called after depositing credits to a job (not job owner!)function_afterDepositJobCredits(//job keybytes32 jobKey_) internalvirtual {}//called after withdrawing credits from a job (not job owner!)function_afterWithdrawJobCredits(//job keybytes32 jobKey_) internalvirtual {}
Implemented
_afterExecutionReverted
//called after reversion of the job being automatedfunction_afterExecutionReverted(//job keybytes32 jobKey_,//type of job (SELECTOR, PRE_DEFINED, RESOLVER)CalldataSourceType calldataSource_,//keeper IDuint256 keeperId_,//message with which the job revertedbytesmemory executionResponse_) internalvirtual {//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 messageif (executionResponse_.length ==0) {revertJobCallRevertedWithoutDetails(); } 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 functioncalculateCompensationPure(//dynamic reward part in percents of the expended gas costuint256 rewardPct_,//fixed reward; multiplied by FIXED_PAYMENT_MULTIPLIER=1e15uint256 fixedReward_,//possibly capped (capping happens in payout block of execute_44g58pv) block gas priceuint256 blockBaseFee_,//gas useduint256 gasUsed_ ) publicpurereturns (uint256) {unchecked {//computes the total reward as the sum of a dynamic (gas-dependent) and static (gas-independent) summandsreturn (gasUsed_ +_getJobGasOverhead()) * blockBaseFee_ * rewardPct_ /100+ fixedReward_ * FIXED_PAYMENT_MULTIPLIER; } }
getKeeperWorkerAndStake
functiongetKeeperWorkerAndStake(//keeper ID for which to fetch stake and workeruint256 keeperId_externalviewreturns (//keeper's worker addressaddress worker,//keeper's stakeuint256 currentStake,//keeper's activation statusbool isActive ) {//fetch the keeper struct with this ID Keeper memory keeper = keepers[keeperId_];//return the necessary datareturn ( keeper.worker, keeper.cvpStake, keeper.isActive ); }
getConfig
Gets the Agent config
functiongetConfig()externalviewreturns (//minimal admissible keeper stakeuint256 minKeeperCvp_,//timeout between stake withdrawal being initiated and finaliseduint256 pendingWithdrawalTimeoutSeconds_,//fees that have been accrued so faruint256 feeTotal_,//fee retained from deposits made in parts per millionuint256 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 ); }
functiongetJob(//what job to fetchbytes32 jobKey_)externalviewreturns (//owner addressaddress 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 sectionJobmemory details,//predefined calldata, if anybytesmemory preDefinedCalldata,//resolver struct, described in the corresponding sectionResolvermemory 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)
functiongetJobRaw(//key with which to fetchbytes32 jobKey_) publicviewreturns (uint256 rawJob) {//fetch the Job struct Job storage job = jobs[jobKey_];//read the corresponding memory slotassembly ("memory-safe") { rawJob :=sload(job.slot) } }
getJobKey
Gets a job key (keccak256 of the concatenation of the job's address and ID)
functiongetJobKey(//address of the jobaddress jobAddress_,//ID of the jobuint256 jobId_) publicpurereturns (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() internalviewreturns (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 succeededbool ok_,//job binary representationuint256 job_,//executor keeper IDuint256 keeperId_,//gas price, capped if neededuint256 gasPrice_,//gas expenditureuint256 gasUsed_) internalviewvirtualreturns (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 computationreturncalculateCompensationPure(rewardPct, fixedReward, gasPrice_, gasUsed_);}
RanDAO-specific hooks
RanDAO overrides many hooks, given hereafter with annotations.
_afterExecutionReverted
function_afterExecutionReverted(//job which revertedbytes32 jobKey_,//job type (SELECTOR, PRE_DEFINED, RESOLVER)CalldataSourceType calldataSource_,//keeper IDuint256 keeperId_,//message with which reversion occurredbytesmemory executionResponse_) internaloverride { //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) {revertSlashingNotInitiatedExecutionReverted(); }//release the keeper unconditionally_releaseKeeper(jobKey_, keeperId_);//event for WS listenersemitExecutionReverted(jobKey_, keeperId_, executionResponse_);}
_beforeExecute
Called before job execution to make sure only the authorised keeper/slasher may execute
function_beforeExecute(//job keybytes32 jobKey_,//keeper/slasher IDuint256 actualKeeperId_,//job binary representationuint256 binJob_) internalviewoverride {//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 slasherif (intervalSeconds >0&& nextKeeperId != actualKeeperId_) {//declare the variable to store the end of the grace perioduint256 nextExecutionTimeoutAt;//copy the latest execution timestampuint256 _lastExecutionAt = lastExecutionAt;//if the job has not been executed, initialise the last exec timestamp being equal to the job creation timestampif (_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) {revertOnlyNextKeeper(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) {revertOnlyCurrentSlasher(currentSlasherId); }// if a resolver job is called by a slasher } elseif (intervalSeconds ==0&& nextKeeperId != actualKeeperId_) {//fetch the grace period end timestamp computed and set at slashing initiationuint256 _jobSlashingPossibleAfter = jobSlashingPossibleAfter[jobKey_];//if not initiated yetif (_jobSlashingPossibleAfter ==0) {revertSlashingNotInitiated(); }//if the grace period has not yet elapsedif (_jobSlashingPossibleAfter > block.timestamp) {revertTooEarlyForSlashing(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_) {revertOnlyReservedSlasher(_jobReservedSlasherId); } }}
_afterDepositJobCredits
Called after job credits are deposited to handle potential keeper assignemnt.
function_afterDepositJobCredits(//key of job for which credits have been depositedbytes32 jobKey_) internaloverride { //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 withdrawnbytes32 jobKey_) internaloverride {//get the job's keeperuint256 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 keybytes32 jobKey_,//keeper/slasher iduint256 actualKeeperId_,//binary job representationuint256 binJob_) internaloverride {//get the assigned keeper IDuint256 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 _releaseKeeperif (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) *1ether; // 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 slashingFeeFixedCVPif (totalSlashAmount > eKeeper.cvpStake) {// Actually this block should not be reached, so this is just in caserevertInsufficientKeeperStakeToSlash(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 listenersemitSlashIntervalJob(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 checkuint256 keeperId_) internalviewoverride {//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 keybytes32 jobKey_) internaloverride {//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 .
functiongetJobsAssignedToKeeper(//id of the keeper whose jobs to fetchuint256 keeperId_) externalviewreturns (bytes32[] memory jobKeys) {//return the pending jobsreturn keeperLocksByJob[keeperId_].values();}
getJobsAssignedToKeeperLength
Returns the amount of jobs pending for a particular Keeper.
functiongetJobsAssignedToKeeperLength(//id of the keeper whose job amount to fetchuint256 keeperId_) externalviewreturns (uint256) {//return the amount of pending jobsreturn 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.
functiongetCurrentSlasherId(//key of the job for which to produce the slasherbytes32 jobKey_) publicviewreturns (uint256) {//returns (blocknumber/rdConfig.slashingEpochBlocks+jobKey)%totalActiveKeepersreturngetSlasherIdByBlock(block.number, jobKey_);}
Provides a way to obtain historical data on assigned slasher IDs (returns ID assigned for a given job key at a given block).
functiongetSlasherIdByBlock(//block number at which to get the IDuint256 blockNumber_,//job key for which to get the IDbytes32 jobKey_) publicviewreturns (uint256) {//get the total amount of active keepersuint256 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 indexreturn 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.
functioncheckCouldBeExecuted(//address of the job to checkaddress jobAddress_,//calldata to pass for a checkbytesmemory 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,bytesmemory result) = jobAddress_.call(jobCalldata_);//job executableif (ok) {revertJobCheckCanBeExecuted(); } else//job not executable {revertJobCheckCanNotBeExecuted(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.
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 feebinJob_,//job config byteuint256 cfg_) internalpureoverridereturns (uint256) {//suppress unused parameter warning binJob_; cfg_;//return maximal possible value for outlined reasonsreturn 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 successbool ok_,//job binary representationuint256 job_,//executor (keeper/slasher) IDuint256 keeperId_,//gas price, now capped only by type(uint256).maxuint256 gasPrice_,//total gas expendeduint256 gasUsed_) internalviewoverridereturns (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 stakeuint256 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) *1ether;//cap the stake for computational purposes if neededif (_jobMaxCvpStake >0&& _jobMaxCvpStake < stake) { stake = _jobMaxCvpStake; }//consider also the Agent-wide stake cap, if one is setif (_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);}