To automate their smart contract execution, a user registers it in The Agent, and the Job is created.
The Job consists of the smart contract and the binary information, stored inside The Agent.
There may be three types of the Jobs:
Selector
Selector-type job is the basic task that requires execution of a certain function in the smart contract based on some conditions, for example, periodically. To register this Job, the provider specifies the selector of the function to be called during execution.
Predefined calldata
A slightly more complex Job. Alongside with the selector, the provider supplies via the function setJobPredefinedCalldata the byte array of predefined calldata that must be passed to the function during execution. The above function is only invoked manually when a job is switched to have this type; for jobs that are registered as ones with predefined calldata, the calldata is simply passed in the corresponding argument. For example, a vault reward harvest with option of automatic restake will be such a task: the harvest function is called with a boolean (True or False) argument to either restake or collect the reward.
Resolver
The most complex type of Job. Resolver is the function in a smart contract that defines the conditions of the target function execution (may be the same address, though likely not also selector, as the Job). The Resolver contains the logic to define if the Job should be executed and returns the calldata to pass to the job function for execution. The Resolver is configured by the Job provider themselves.
⋅⋅⋅
Specification
Structure
The Job inside the Agent is represented as a data structure struct Job containing the following fields:
structJob {//Job configuint8 config;//Function selector to callbytes4 selector;//Native tokens credited to the job for disbursing payment. uint88 credits;//A cap on the block’s base gas fee for the purposes of computing rewards. uint16 maxBaseFeeGwei; //Premium on gas expenditure plus overhead to be paid to the keeper as reward. Cannot be zero together with fixedReward.
//In current implementation, has no useuint16 rewardPct; //Fixed (i.e. gas-independent) reward to be paid to the job executor. Mind that at compensation computation this value is multiplied by the FIXED_PAYMENT_MULTIPLIER constant. Cannot be zero together with rewardPct.
//In current implementation, has the meaning of denoting the upper bound of stake for the purposes of computing rewards and slash amounts
uint32 fixedReward;//Type of the job. One of SELECTOR (0), PRE_DEFINED (1), RESOLVER (2)uint8 calldataSource;//Interval size in seconds for interval jobsuint24 intervalSeconds;//Last execution timestamp for interval jobsuint32 lastExecutionAt;}
Binary layout
To achieve efficiency and save on gas during execution, all manipulations with the Job structure are performed in assembly inserts. The binary representation of struct Job is as follows:
//Timestamp of last execution of a jobuint32 lastExecAt;//Interval size for job execution in secondsuint24 interval;//Indicator of calldata source (job type: selector, predefined byte array, resolver)uint8 calldataSource;//Fixed reward divided by the corresp. multiplier agent parameteruint32 fixedReward;//Reward premium in percents (multiplied by the total gas (usage and overhead) cost at current block). Defines the dynamic part of the reward
uint16 rewardPct;//A cap on block base fee per gas to use for computing dynamic keeper rewardsuint16 maxBaseFeeGwei;//Native tokens available to the job for disbursement to keepersuint88 nativeCredits;//Resolver selector, if anyuint32 selector;//Job configuint8 config;
Since in RanDAO realisation the fixed reward actually corresponds to a portion of the executor keeper/slasher's stake and not a pre-defined job parameter, for RanDAO jobs the fixedReward field retains its position and size, but now corresponds to the value _jobMaxCvpStake, which is the upper bound on the size of stake for the purposes of computing rewards. The purpose of this value is the prevention of disproportionately excessive payouts should the keeper have too great a stake.
Job config
The Job configuration is a single byte inside the Job structure and defines the properties of the Job. The bits are allocated consecutively in the uint8 config.
Flag of a job being active (i.e., admissible for execution given satisfaction of specified logic).
CFG_USE_JOB_OWNER_CREDITS
Whether to use job owner credits in native tokens for payment. This makes a hard switch from using job credits to using job owner credits and not just enables it as an option.
CFG_ASSERT_RESOLVER_SELECTOR
Indicates whether to enforce the selector of a resolver job function matching between the calldata passed by the Keeper and the job binary representation. This may be desirable because under the hood, resolver jobs work like PRE_DEFINED jobs, except the calldata is provided by the Keeper. It would therefore be possible for a malicious Keeper to supply a different selector, which is not checkable unless this flag is set to True. Has no bearing on interval jobs, since for them, the calldata is fixed.
CFG_CHECK_KEEPER_MIN_CVP_DEPOSIT
Whether to enforce conformance of the stake of keepers and slashers to the minimum required stake of the job.
Resolver & Calldata
Resolver is a method in a contract (may be a separate resolver contract or the same contract as the Job) that is called by the Keeper before execution (with some calldata (resolverCalldata), may be empty) and returns a flag (whether or not the target method of the Job should be executed) and the calldata (executionCalldata) necessary for the execution.
structResolver {//Address of the Resolver (may coincide with the Job address)address resolverAddress;//Calldata with which to call the Resolverbytes resolverCalldata;}
//Selector of execute_44g58pvuint32 selector;//Job contract addressaddress JobContractAddress;//Job ordinal number in the equivalence class of same-address jobsuint24 jobId;//Config byte with flags governing acceptance of capped dynamic reward and accrual vs immediate withdrawal of rewardsuint8 config;//ID of executor keeperuint24 keeperId;//Execution calldata of arbitrary length to pass into the underlying function which is being automated. For selector jobs, contains the selector of the function to execute. For PRE_DEFINED jobs, absent.
bytes executionCalldata ;
The last field (executionCalldata) has to do with the calldata with which to call the underlying function consists of a function selector (in the contract at jobContractAddress) for SELECTOR-type jobs, predefined calldata for PRE_DEFINED-type jobs, and whatever calldata is returned by the resolver for RESOLVER-type jobs. Mind that the RESOLVER-returned calldata must obey the ABI specification, including the demand for the first four bytes to be the selector of the called function.
Mappings
A list of mappings in the Agent contract which are related to the Job structure (bytes32 indicates the key is the jobKey, a value obtained by taking keccak256 hash of a concatenation of a Job's address and ID, in that order):
Job mappings list
//Defines an isomorphism between keys and Job objects, implicitly imparts ID. mapping(bytes32=> Job) jobs;//Predefined calldata (for jobs that admit it)mapping(bytes32=>bytes) preDefinedCalldatas;//Minimal CVP stake demanded of an admissible keepermapping(bytes32=>uint256) jobMinKeeperCvp;//Defines a job key/job owner address isomorphismmapping(bytes32=>address) jobOwners;//Resolvermapping(bytes32=> Resolver) resolvers;//Pending new owner of the job (permitted to invoke acceptJobTransfer to finalise the process)mapping(bytes32=>address) jobPendingTransfers;//Last registered jobId (+1 to the previous jobId, starting from 0, for each new Job registered for the same Job contract address)
mapping(address=>uint256) jobLastIds;
RanDAO-specific mappings
//the ID of the next keeper, i.e. the keeper assigned to perform the next execution of the job with this keymapping(bytes32=>uint256) public jobNextKeeperId;//the ID of the reserved slasher, i.e. the keeper who initiated slashing of the job with this key and is now granted exclusive rights to perform slashing of executors thereof (until the end of the admissibility period or the slashing event, whichever comes first). Nonzero only for resolver jobs, since interval jobs do not require initiation of slashing.
mapping(bytes32=>uint256) public jobReservedSlasherId;//timestamp after which slashing is possible; nonzero only for resolver jobs, since this value is computed at initiation of slashing, and for interval jobs it is replaced with the sum lastExecutionAt+rdConfig.period1.
mapping(bytes32=>uint256) public jobSlashingPossibleAfter;//timestamp at which the Job was created. Sole use is to assign lastExecutionAt to this value for interval jobs that yet lack such data (i.e. have not been executed by a Keeper yet)
mapping(bytes32=>uint256) public jobCreatedAt;
Functions
registerJob
Registers a new Job.
functionregisterJob (//parameters of the Job registerJobParams calldata params_,//address of the resolverResolvercalldata resolver_,//predefined calldata for the Jobbytescalldata preDefinedCalldata_//Eitel's comment: all asserts missing //Returns jobKey (defined as a keccak-256 hash of a concatenation of the job address and index, providing thereby a unique identifier for each job) and jobId of the newly registered Job
) publicreturns (bytes32 jobKey,uint256 jobId){...}
In RanDAO realisation, the timsetamp at which the job was created is stored in the mapping jobCreatedAt. The reason for this is elucidated in .
updateJob
Updates the job parameters to the provided values.
functionupdateJob (//jobKey of the job to updatebytes32 jobKey_//new maxBaseFeeGwei for the jobuint16 maxBaseFeeGwei_//new rewardPctuint16 rewardPct_//new fixedReward uint32 fixedReward_//new jobMinCvpuint256 jobMinCvp_//new interval (in seconds) for interval jobsuint24 intervalSeconds_ ) external{...}
setJobResolver
Sets the job resolver parameters
functionsetJobResolver (//jobKey of the job to set resolver forbytes32 jobKey_//new address for the job resolverResolvercalldata resolver_ ) external{...}
setJobPredefinedCalldata
Sets the job predefined calldata (for jobs of corresp type)
functionsetJobPredefinedCalldata (//jobKey of the job to set predefined calldata forbytes32 jobKey_//predefined calldata for the jobbytescalldata preDefinedCalldata_ ) external{...}
setJobConfig
Sets the job config flags to the specified values.
Since in RanDAO realisation executor keepers are assigned beforehand, we need to handle possible assignment and release at every job parameter update, since otherwise we may have a keeper with an inactive job assigned or an active job that does not get a keeper assigned for some time.
The inputs of the function stay much the same, but the internal logic changes. We supply the annotated code:
//get the previous binary representation of the job in questionuint256 rawJobBefore =getJobRaw(jobKey_);//update the job config flag byte super.setJobConfig(jobKey_, isActive_, useJobOwnerCredits_, assertResolverSelector_); //whether the job was active before. Needed to detect activity changes, since only the new values are supplied at each call
bool wasActiveBefore = ConfigFlags.check(rawJobBefore, CFG_ACTIVE);// Previously inactive job activated: assign keeper if requiredif(!wasActiveBefore && isActive_) {_assignNextKeeperIfRequired(jobKey_); }// job was and remains active, but the credits source has changed: assign or release the keeper if requriedif (wasActiveBefore && isActive_ && (ConfigFlags.check(rawJobBefore, CFG_USE_JOB_OWNER_CREDITS) != useJobOwnerCredits_)) {if (!_assignNextKeeperIfRequired(jobKey_)) {uint256 expectedKeeperId = jobNextKeeperId[jobKey_];_releaseKeeperIfRequired(jobKey_, expectedKeeperId); } }// Previously active job deactivated: unassign keeperif (wasActiveBefore &&!isActive_) {uint256 expectedKeeperId = jobNextKeeperId[jobKey_];_releaseKeeper(jobKey_, expectedKeeperId); }
Initiates transfer of job ownership to a specified address; must be finalized by the recipient calling the following function.
functioninitiateJobTransfer (//jobKey of the job to change owner forbytes32 jobKey_,//address to delegate ownership of the job toaddress to_ ) virtual{//Only job owner can transfer ownership_assertOnlyJobOwner(); ...}
function acceptJobTransfer (
//jobKey of the transferred job
bytes32 jobKey_
) public
{
//Only new job owner can finalize transfer
_msg_sender_is_pending_owner_assertion();
..
}
depositJobCredits
Replenishes the amount of native chain tokens the job has for disbursement to keepers as a reward.
This function is payable; the depositor calls the function and specifies the amount of native token to deposit (i.e., Ether) within the value field of the message
functiondepositJobCredits(//jobKey of the job to deposit credits to.//Deposited in native tokensbytes32 jobKey_ ) virtualpayable{//Assert that non-zero amount of native token is sent_assertNonZeroValue(); //Asserts that the job has an address: at no point is the owner address set to be address(0). This check here is really quite superfluous, as a similar check is performed whenever a job is registered, but one cannot get too careful.
_target_job_has_owner_assertion();//Assert that the fee-adjusted credit total after deposit doesn't exceed type(uint88).max (overflow check)_fee_adjusted_credits_after_deposit_do_not_overflow_uint88_assertion(); ...}
withdrawJobCredits
Withdraws native tokens from a job’s credit total to a specified address.
To withdraw all credits, specify amount_ as type(uint256).max.
functionwithdrawJobCredits (//jobKey of the job to withdraw credits frombytes32 jobKey_,//address to withdraw credits toaddress payable to_,//amount to withdrawuint256 amount_ ) externalvirtual{//Only job owner can withdraw credits_assertOnlyJobOwner();//Can't withdraw nothing_assertNonZeroAmount();//There are enough credits to withdraw_sufficient_credits_to_withdraw(); ...}
depositJobOwnerCredits
Deposits native chain tokens to the job owner’s balance, which may be used by this owner to pay for execution of any of his jobs, provided the appropriate flag in the job config is set to True.
This function is payable; the depositor calls the function and specifies the amount of native token to deposit (i.e., Ether) within the value field of the message
Withdraws native tokens from the job owner’s balance to a specified address.
To withdraw all credits, specify amount_ as type(uint256).max.
functionwithdrawJobOwnerCredits (//address to withdraw credits toaddress payable to_//amount to withdrawuint256 amount_ ) external{//Can't withdraw nothing_assertNonZeroAmount();//There are enough credits to withdraw_sufficient_credits_to_withdraw(); ...}
Job lock-related RanDAO-specific functions
assignKeeper
Manually assigns a keeper to each job of a certain list (unless one is already extant).
functionassignKeeper(//keys of jobs for which keepers are neededbytes32[] calldata jobKeys_) external {//iterate over jobs in the passed listfor (uint256 i =0; i < jobKeys_.length; i++) {//fetch the job keybytes32 jobKey = jobKeys_[i];//fetch the currently assigned keeperuint256 assignedKeeperId = jobNextKeeperId[jobKey];//assert keeper is not already assignedif (assignedKeeperId !=0) {revertJobHasKeeperAssigned(assignedKeeperId); }//only a job owner can call this function_assertOnlyJobOwner(jobKey);//perform keeper assignment_assignNextKeeper(jobKey); }}
releaseJob
Manually releases a keeper from executing a job.
functionreleaseJob(//key of the job to releasebytes32 jobKey_) external {//get the assigned keeperuint256 assignedKeeperId = jobNextKeeperId[jobKey_];// Job owner can unassign a keeper without any restrictionsif (msg.sender == jobOwners[jobKey_]) {_releaseKeeper(jobKey_, assignedKeeperId);return;}_releaseKeeper(jobKey_, assignedKeeperId);}