Job (Old)

The Job is a smart contract to be automated that is registered in The Agent.

Overview

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.

\cdot \cdot \cdot

Specification

Structure

The Job inside the Agent is represented as a data structure struct Job containing the following fields:

struct Job {

    //Job config
    uint8 config;
    
    //Function selector to call
    bytes4 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 use
    uint16 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 jobs
    uint24 intervalSeconds;
    
    //Last execution timestamp for interval jobs
    uint32 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:

Binary layout
0x                       |    name        | size int | bytes |  bits     
00000000                 | lastExecAt     |  uint32  |   4   |  0-31     
000000                   | interval       |  uint24  |   3   |  32-55    
00                       | calldataSource |  uint8   |   1   |  56-63    
00000000                 | fixedReward    |  uint32  |   4   |  64-95    
0000                     | rewardPct      |  uint16  |   2   |  96-111   
0000000000000000000000   | nativeCredits  |  uint88  |   11  |  112-199  
0000                     | maxBaseFeeGwei |  uint16  |   2   |  200-215  
00000000                 | selector       |  uint32  |   4   |  216-247  
00                       | config         |  uint8   |   1   |  248-255  
Parameters description
//Timestamp of last execution of a job
uint32 lastExecAt;

//Interval size for job execution in seconds
uint24 interval;

//Indicator of calldata source (job type: selector, predefined byte array, resolver)
uint8 calldataSource;

//Fixed reward divided by the corresp. multiplier agent parameter
uint32 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 rewards
uint16 maxBaseFeeGwei;

//Native tokens available to the job for disbursement to keepers
uint88 nativeCredits;

//Resolver selector, if any
uint32 selector;

//Job config
uint8 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.

uint8 config
0x |               name               | binary
01 | CFG_ACTIVE                       | 00000001
02 | CFG_USE_JOB_OWNER_CREDITS        | 00000010
04 | CFG_ASSERT_RESOLVER_SELECTOR     | 00000100
08 | CFG_CHECK_KEEPER_MIN_CVP_DEPOSIT | 00001000
  • CFG_ACTIVE

    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.

struct Resolver {

    //Address of the Resolver (may coincide with the Job address)
    address resolverAddress;
    
    //Calldata with which to call the Resolver
    bytes resolverCalldata;
    
}

The execution calldata layout is presented below:

Calldata layout
0x                       |        name       | size int  | bytes |   bits    |
00000000                 | selector          |  uint32   |   4   |  0-31     |
0000...0000 x40          | JobContractAddress|  uint160  |   20  |  32-191   |
000000                   | jobId             |  uint24   |   3   |  192-215  |
00                       | config            |  uint8    |   1   |  216-225  |
000000                   | keeperId          |  uint24   |   3   |  226-249  |
0000... >=1              | executionCalldata |  bytes    |  >=1  |  250-any  |
Data parameters description
//Selector of execute_44g58pv
uint32 selector;

//Job contract address
address JobContractAddress;

//Job ordinal number in the equivalence class of same-address jobs
uint24 jobId;

//Config byte with flags governing acceptance of capped dynamic reward and accrual vs immediate withdrawal of rewards
uint8 config;

//ID of executor keeper
uint24 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 keeper
mapping(bytes32 => uint256) jobMinKeeperCvp;

//Defines a job key/job owner address isomorphism
mapping(bytes32 => address) jobOwners;

//Resolver
mapping(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 key
mapping(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.

function registerJob (

    //parameters of the Job
    registerJobParams calldata params_,
    
    //address of the resolver
    Resolver calldata resolver_,
    
    //predefined calldata for the Job
    bytes calldata 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
    ) public returns (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.

function updateJob (
    //jobKey of the job to update
    bytes32 jobKey_
    
    //new maxBaseFeeGwei for the job
    uint16 maxBaseFeeGwei_
    
    //new rewardPct
    uint16 rewardPct_
    
    //new fixedReward 
    uint32 fixedReward_
    
    //new jobMinCvp
    uint256 jobMinCvp_
    
    //new interval (in seconds) for interval jobs
    uint24 intervalSeconds_

    ) external
{
...
}

setJobResolver

Sets the job resolver parameters

function setJobResolver (
    //jobKey of the job to set resolver for
    bytes32 jobKey_
    
    //new address for the job resolver
    Resolver calldata resolver_
    ) external
    
{
...
}

setJobPredefinedCalldata

Sets the job predefined calldata (for jobs of corresp type)

function setJobPredefinedCalldata (
    //jobKey of the job to set predefined calldata for
    bytes32 jobKey_
    
    //predefined calldata for the job
    bytes calldata preDefinedCalldata_
    ) external
    
{
...
}

setJobConfig

Sets the job config flags to the specified values.

function setJobConfig (
    bytes32 jobKey_
    bool isActive_
    bool useJobOwnerCredits_
    bool assertResolverSelector
    ) virtual
    
{
    //only job owner can change job config
    _assertOnyJobOwner();
}
RanDAO realisation peculiarities

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 question
    uint256 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 required
    if(!wasActiveBefore && isActive_)  {
      _assignNextKeeperIfRequired(jobKey_);
    }

    // job was and remains active, but the credits source has changed: assign or release the keeper if requried
    if (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 keeper
    if (wasActiveBefore && !isActive_) {
      uint256 expectedKeeperId = jobNextKeeperId[jobKey_];
      _releaseKeeper(jobKey_, expectedKeeperId);
    }

Logic for assignment and release can be found in Keeper assignment and release in RanDAO realisation.

initiateJobTransfer

Initiates transfer of job ownership to a specified address; must be finalized by the recipient calling the following function.

function initiateJobTransfer (

    //jobKey of the job to change owner for
    bytes32 jobKey_,
    
    //address to delegate ownership of the job to
    address to_
    ) virtual
{
    //Only job owner can transfer ownership
    _assertOnlyJobOwner();
    ...
}

acceptJobTransfer

Finalises previously initialised job ownership transfer.

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

function depositJobCredits(

    //jobKey of the job to deposit credits to.
    //Deposited in native tokens
    bytes32 jobKey_
    ) virtual payable
    
{
    //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.

function withdrawJobCredits (

    //jobKey of the job to withdraw credits from
    bytes32 jobKey_,
    
    //address to withdraw credits to
    address payable to_,
    
    //amount to withdraw
    uint256 amount_
    ) external virtual
    
{
    //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

function depositJobOwnerCredits (
    
    //job owner address to deposit credits for
    address for_
    ) external payable
    
{
    //Can't deposit nothing
    _assertNonZeroValue();
    ...
}

withdrawJobOwnerCredits

Withdraws native tokens from the job owner’s balance to a specified address.

To withdraw all credits, specify amount_ as type(uint256).max.

function withdrawJobOwnerCredits (

    //address to withdraw credits to
    address payable to_
    
    //amount to withdraw
    uint256 amount_
    ) external

{
    //Can't withdraw nothing
    _assertNonZeroAmount();
    
    //There are enough credits to withdraw
    _sufficient_credits_to_withdraw();
    ...
}

assignKeeper

Manually assigns a keeper to each job of a certain list (unless one is already extant).

function assignKeeper(
//keys of jobs for which keepers are needed
bytes32[] calldata jobKeys_
) external {
  //iterate over jobs in the passed list
  for (uint256 i = 0; i < jobKeys_.length; i++) {
    //fetch the job key
    bytes32 jobKey = jobKeys_[i];
    //fetch the currently assigned keeper
    uint256 assignedKeeperId = jobNextKeeperId[jobKey];
    //assert keeper is not already assigned
    if (assignedKeeperId != 0) {
      revert JobHasKeeperAssigned(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.

function releaseJob(
//key of the job to release
bytes32 jobKey_
) external {
  //get the assigned keeper
  uint256 assignedKeeperId = jobNextKeeperId[jobKey_];
  
  // Job owner can unassign a keeper without any restrictions
  if (msg.sender == jobOwners[jobKey_]) {
    _releaseKeeper(jobKey_, assignedKeeperId);
    return;
}
_releaseKeeper(jobKey_, assignedKeeperId);
}

Last updated