From ce556b49d37a85e3e3bd20f238e5c1bb86caa635 Mon Sep 17 00:00:00 2001 From: defi-dev <69192116+defi-dev@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:23:13 +0700 Subject: [PATCH] make PPAgentV2 implementation compatible with shanghai (#27) --- contracts/AgentRewards.sol | 2 +- contracts/PPAgentV2.sol | 1466 +---------------- contracts/PPAgentV2Based.sol | 1479 ++++++++++++++++++ contracts/PPAgentV2Lens.sol | 4 +- contracts/PPAgentV2Randao.sol | 873 +---------- contracts/PPAgentV2RandaoBased.sol | 880 +++++++++++ contracts/PPAgentV2RandaoWithGasTracking.sol | 6 +- contracts/PPAgentV2VRF.sol | 34 +- contracts/PPAgentV2VRFBased.sol | 35 + contracts/VRFAgentManager.sol | 20 +- test/AgentOwnerTest.t.sol | 5 +- test/CompensationTest.t.sol | 2 +- test/ExecuteResolverTest.t.sol | 8 +- test/ExecuteSelectorTest.t.sol | 14 +- test/JobManagementTest.t.sol | 42 +- test/JobOwnerTest.t.sol | 6 +- test/KeeperTest.t.sol | 14 +- test/RegisterJobTest.t.sol | 24 +- test/SlashingTest.t.sol | 11 +- test/StakingTest.t.sol | 16 +- test/TestHelper.sol | 2 +- test/TestHelperRandao.sol | 2 +- test/abstract/AbstractTestHelper.sol | 2 +- test/jobs/JobTopupTestJob.sol | 2 +- test/jobs/JobWithdrawTestJob.sol | 4 +- test/mocks/MockExposedAgent.sol | 6 +- test/mocks/MockVRFCoordinator.sol | 2 +- test/randao/ActorsTest.t.sol | 12 +- test/randao/AgentOwnerTest.t.sol | 4 +- test/randao/AssignKeeperTest.t.sol | 6 +- test/randao/ExecuteResolverTest.t.sol | 22 +- test/randao/ExecuteSelectorTest.t.sol | 12 +- test/randao/ExecuteShanghaiTest.t.sol | 152 ++ test/randao/JobOwnerTest.t.sol | 2 +- test/randao/KeeperTest.t.sol | 4 +- test/randao/OwnerSlashingTest.t.sol | 3 +- test/randao/RandaoGasTrackerTest.t.sol | 3 +- test/randao/VRFTest.t.sol | 5 +- 38 files changed, 2718 insertions(+), 2468 deletions(-) create mode 100644 contracts/PPAgentV2Based.sol create mode 100644 contracts/PPAgentV2RandaoBased.sol create mode 100644 contracts/PPAgentV2VRFBased.sol create mode 100644 test/randao/ExecuteShanghaiTest.t.sol diff --git a/contracts/AgentRewards.sol b/contracts/AgentRewards.sol index 8e3dc0d..b8cc1a2 100644 --- a/contracts/AgentRewards.sol +++ b/contracts/AgentRewards.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/security/Pausable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IPPAgentV2Viewer } from "../contracts/PPAgentV2.sol"; +import { IPPAgentV2Viewer } from "../contracts/PPAgentV2Based.sol"; contract AgentRewards is Ownable, Pausable { event SetSinglePayoutStakePpm(uint256 singlePayoutStakePpm); diff --git a/contracts/PPAgentV2.sol b/contracts/PPAgentV2.sol index 62e967c..8bdbf8f 100644 --- a/contracts/PPAgentV2.sol +++ b/contracts/PPAgentV2.sol @@ -1,192 +1,19 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity ^0.8.19; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "./utils/InitializableOptimized.sol"; -import "./utils/OwnableOptimized.sol"; -import "./PPAgentV2Flags.sol"; -import "./PPAgentV2Interfaces.sol"; - -library ConfigFlags { - function check(uint256 cfg, uint256 flag) internal pure returns (bool) { - return (cfg & flag) != 0; - } -} +import {PPAgentV2Based} from "./PPAgentV2Based.sol"; /** - * @title PowerAgentLite + * @title PPAgentV2 * @author PowerPool */ -contract PPAgentV2 is IPPAgentV2Executor, IPPAgentV2Viewer, IPPAgentV2JobOwner, PPAgentV2Flags, InitializableOptimized, OwnableOptimized { - error NonEOASender(); - error InsufficientKeeperStake(); - error InsufficientJobScopedKeeperStake(); - error KeeperWorkerNotAuthorized(); - error InsufficientJobCredits(uint256 actual, uint256 wanted); - error InsufficientJobOwnerCredits(uint256 actual, uint256 wanted); - error InactiveJob(bytes32 jobKey); - error JobIdOverflow(); - error OnlyJobOwner(); - error JobWithoutOwner(); - error MissingJobAddress(); - error MissingMaxBaseFeeGwei(); - error NoFixedNorPremiumPctReward(); - error CreditsDepositOverflow(); - error StakeAmountOverflow(); - error CreditsWithdrawalUnderflow(); - error MissingDeposit(); - error IntervalNotReached(uint256 lastExecutedAt, uint256 interval, uint256 _now); - error BaseFeeGtGasPrice(uint256 baseFee, uint256 jobMaxBaseFeeGwei); - error InvalidCalldataSource(); - error MissingInputCalldata(); - error SelectorCheckFailed(); - error JobCallRevertedWithoutDetails(); - error InsufficientAmountToCoverSlashedStake(uint256 wanted, uint256 actual); - error AmountGtStake(uint256 wanted, uint256 actualStake, uint256 actualSlashedStake); - error WithdrawalTimoutNotReached(); - error NoPendingWithdrawal(); - error MissingAmount(); - error WithdrawAmountExceedsAvailable(uint256 wanted, uint256 actual); - error JobShouldHaveInterval(); - error JobDoesNotSupposedToHaveInterval(); - error InvalidJobAddress(); - error InvalidKeeperId(); - error MissingResolverAddress(); - error NotSupportedByJobCalldataSource(); - error OnlyKeeperAdmin(); - error OnlyKeeperAdminOrJobOwner(); - error OnlyKeeperAdminOrWorker(); - error InvalidMinKeeperCvp(); - error TimeoutTooBig(); - error FeeTooBig(); - error InsufficientAmount(); - error OnlyPendingOwner(); - error WorkerAlreadyAssigned(); - error ExecutionReentrancyLocked(); - error JobCheckResolverReturnedFalse(); - error UnableToDecodeResolverResponse(); - error UnexpectedCodeBlock(); - error JobCheckCanBeExecuted(bytes returndata); - error JobCheckCanNotBeExecuted(bytes errReason); - error JobCheckUnexpectedError(); - error JobCheckCalldataError(); - - string public constant VERSION = "2.5.0"; - uint256 internal constant MAX_PENDING_WITHDRAWAL_TIMEOUT_SECONDS = 30 days; - uint256 internal constant MAX_FEE_PPM = 5e4; - uint256 internal constant FIXED_PAYMENT_MULTIPLIER = 1e12; - bytes32 internal constant EXECUTION_LOCK_KEY = keccak256("executionLock"); - - enum CalldataSourceType { - SELECTOR, - PRE_DEFINED, - RESOLVER, - OFFCHAIN - } +contract PPAgentV2 is PPAgentV2Based { - address public immutable CVP; + constructor(address cvp_) PPAgentV2Based(cvp_) { - event Execute( - bytes32 indexed jobKey, - address indexed job, - uint256 indexed keeperId, - uint256 gasUsed, - uint256 baseFee, - uint256 gasPrice, - uint256 compensation, - bytes32 binJobAfter - ); - event WithdrawFees(address indexed to, uint256 amount); - event OwnerSlash(uint256 indexed keeperId, address indexed to, uint256 currentAmount, uint256 pendingAmount); - event RegisterAsKeeper(uint256 indexed keeperId, address indexed keeperAdmin, address indexed keeperWorker); - event SetWorkerAddress(uint256 indexed keeperId, address indexed prev, address indexed worker); - event Stake(uint256 indexed keeperId, uint256 amount, address staker); - event InitiateRedeem(uint256 indexed keeperId, uint256 redeemAmount, uint256 stakeAmount, uint256 slashedStakeAmount); - event FinalizeRedeem(uint256 indexed keeperId, address indexed beneficiary, uint256 amount); - event WithdrawCompensation(uint256 indexed keeperId, address indexed to, uint256 amount); - event DepositJobCredits(bytes32 indexed jobKey, address indexed depositor, uint256 amount, uint256 fee); - event WithdrawJobCredits(bytes32 indexed jobKey, address indexed owner, address indexed to, uint256 amount); - event DepositJobOwnerCredits(address indexed jobOwner, address indexed depositor, uint256 amount, uint256 fee); - event WithdrawJobOwnerCredits(address indexed jobOwner, address indexed to, uint256 amount); - event InitiateJobTransfer(bytes32 indexed jobKey, address indexed from, address indexed to); - event AcceptJobTransfer(bytes32 indexed jobKey_, address indexed to_); - event SetJobConfig(bytes32 indexed jobKey, bool isActive_, bool useJobOwnerCredits_, bool assertResolverSelector_, bool callResolverBeforeExecute_); - event SetJobResolver(bytes32 indexed jobKey, address resolverAddress, bytes resolverCalldata); - event SetJobPreDefinedCalldata(bytes32 indexed jobKey, bytes preDefinedCalldata); - event SetAgentParams(uint256 minKeeperCvp_, uint256 timeoutSeconds_, uint256 feePpm_); - event RegisterJob( - bytes32 indexed jobKey, - address indexed jobAddress, - uint256 indexed jobId, - address owner, - RegisterJobParams params - ); - event JobUpdate( - bytes32 indexed jobKey, - uint256 maxBaseFeeGwei, - uint256 rewardPct, - uint256 fixedReward, - uint256 jobMinCvp, - uint256 intervalSeconds - ); - - struct Keeper { - address worker; - uint88 cvpStake; - bool isActive; - } - - struct ExecutionResponsesData { - bytes resolverResponse; - bytes executionResponse; } - // WARNING: the minKeeperCvp slot is also used as a non-reentrancy lock - uint256 internal minKeeperCvp; - uint256 internal pendingWithdrawalTimeoutSeconds; - uint256 internal feeTotal; - uint256 internal feePpm; - uint256 internal lastKeeperId; - - // keccak256(jobAddress, id) => ethBalance - mapping(bytes32 => Job) internal jobs; - // keccak256(jobAddress, id) => customCalldata - mapping(bytes32 => bytes) internal preDefinedCalldatas; - // keccak256(jobAddress, id) => minKeeperCvpStake - mapping(bytes32 => uint256) internal jobMinKeeperCvp; - // keccak256(jobAddress, id) => owner - mapping(bytes32 => address) internal jobOwners; - // keccak256(jobAddress, id) => resolver(address,calldata) - mapping(bytes32 => Resolver) internal resolvers; - // keccak256(jobAddress, id) => pendingAddress - mapping(bytes32 => address) internal jobPendingTransfers; - - // jobAddress => lastIdRegistered(actually uint24) - mapping(address => uint256) public jobLastIds; - - // keeperId => (worker,CVP stake) - mapping(uint256 => Keeper) internal keepers; - // keeperId => admin - mapping(uint256 => address) internal keeperAdmins; - // keeperId => the slashed CVP amount - mapping(uint256 => uint256) internal slashedStakeOf; - // keeperId => native token compensation - mapping(uint256 => uint256) internal compensations; - - // keeperId => pendingWithdrawalCVP amount - mapping(uint256 => uint256) internal pendingWithdrawalAmounts; - // keeperId => pendingWithdrawalEndsAt timestamp - mapping(uint256 => uint256) internal pendingWithdrawalEndsAt; - - // owner => credits - mapping(address => uint256) public jobOwnerCredits; - - // worker => keeperIs - mapping(address => uint256) public workerKeeperIds; - - /*** PSEUDO-MODIFIERS ***/ - - function _assertExecutionNotLocked() internal view { + function _assertExecutionNotLocked() internal override view { bytes32 lockKey = EXECUTION_LOCK_KEY; assembly ("memory-safe") { let isLocked := tload(lockKey) @@ -197,1289 +24,10 @@ contract PPAgentV2 is IPPAgentV2Executor, IPPAgentV2Viewer, IPPAgentV2JobOwner, } } - function _setExecutionLock(uint value_) internal { + function _setExecutionLock(uint value_) internal override { bytes32 lockKey = EXECUTION_LOCK_KEY; assembly ("memory-safe") { tstore(lockKey, value_) } } - - function _assertOnlyJobOwner(bytes32 jobKey_) internal view { - if (msg.sender != jobOwners[jobKey_]) { - revert OnlyJobOwner(); - } - } - - function _assertOnlyKeeperAdmin(uint256 keeperId_) internal view { - if (msg.sender != keeperAdmins[keeperId_]) { - revert OnlyKeeperAdmin(); - } - } - - function _assertOnlyKeeperAdminOrWorker(uint256 keeperId_) internal view { - if (msg.sender != keeperAdmins[keeperId_] && msg.sender != keepers[keeperId_].worker) { - revert OnlyKeeperAdminOrWorker(); - } - } - - function _assertKeeperIdExists(uint256 keeperId_) internal view { - if (keeperId_ > lastKeeperId) { - revert InvalidKeeperId(); - } - } - - function _assertJobParams(uint256 maxBaseFeeGwei_, uint256 fixedReward_, uint256 rewardPct_) internal pure { - if (maxBaseFeeGwei_ == 0) { - revert MissingMaxBaseFeeGwei(); - } - - if (fixedReward_ == 0 && rewardPct_ == 0) { - revert NoFixedNorPremiumPctReward(); - } - } - - function _assertInterval(uint256 interval_, CalldataSourceType cdSource_) internal pure { - if (interval_ == 0 && - (cdSource_ == CalldataSourceType.SELECTOR || cdSource_ == CalldataSourceType.PRE_DEFINED)) { - revert JobShouldHaveInterval(); - } - if (interval_ != 0 && (cdSource_ == CalldataSourceType.RESOLVER || cdSource_ == CalldataSourceType.OFFCHAIN)) { - revert JobDoesNotSupposedToHaveInterval(); - } - } - - constructor(address cvp_) { - CVP = cvp_; - } - - function initialize( - address owner_, - uint256 minKeeperCvp_, - uint256 pendingWithdrawalTimeoutSeconds_ - ) public initializer { - _setAgentParams(minKeeperCvp_, pendingWithdrawalTimeoutSeconds_, 0); - _transferOwnership(owner_); - } - - /*** HOOKS ***/ - function _beforeExecute(bytes32 jobKey_, uint256 actualKeeperId_, uint256 binJob_) internal view virtual {} - function _beforeInitiateRedeem(uint256 keeperId_) internal view virtual {} - - function _afterExecute(uint256 actualKeeperId_, uint256 gasUsed_) internal virtual {} - function _afterExecutionSucceeded(bytes32 jobKey_, uint256 actualKeeperId_, uint256 binJob_) internal virtual {} - function _afterInitiateRedeem(uint256 keeperId_) internal view virtual {} - function _afterRegisterJob(bytes32 jobKey_) internal virtual {} - function _afterDepositJobCredits(bytes32 jobKey_) internal virtual {} - function _afterWithdrawJobCredits(bytes32 jobKey_) internal virtual {} - function _afterAcceptJobTransfer(bytes32 jobKey_) internal virtual {} - function _afterRegisterAsKeeper(uint256 keeperId_) internal virtual {} - - /*** CONSTANT GETTERS ***/ - function getStrategy() public pure virtual returns (string memory) { - return "basic"; - } - - function _getJobGasOverhead() internal pure virtual returns (uint256) { - return 40_000; - } - - /*** UPKEEP INTERFACE ***/ - - /** - * Executes a job. - * The method arguments a tightly coupled with a custom layout in order to save some gas. - * The calldata has the following layout : - * 0x 00000000 1b48315d66ba5267aac8d0ab63c49038b56b1dbc 0000f1 03 00001a 402b2eed11 - * name selector jobContractAddress jobId config keeperId calldata (optional) - * size b bytes4 bytes20 uint24 uint8 uint24 any - * size u uint32 uint160 bytes3 bytes1 bytes3 any - * bits 0-3 4-23 24-26 27-27 28-30 31+ - */ - function execute_44g58pv() external { - uint256 gasStart = gasleft(); - bytes32 jobKey; - - assembly ("memory-safe") { - // size of (address(bytes20)+id(uint24/bytes3)) - let size := 23 - - // keccack256(address+id(uint24)) to memory to generate jobKey - calldatacopy(0, 4, size) - jobKey := keccak256(0, size) - } - - address jobAddress; - uint256 actualKeeperId; - uint256 cfg; - - assembly ("memory-safe") { - // load jobAddress, cfg, and keeperId from calldata to the stack - jobAddress := shr(96, calldataload(4)) - cfg := shr(248, calldataload(27)) - actualKeeperId := shr(232, calldataload(28)) - } - - uint256 binJob = getJobRaw(jobKey); - - _beforeExecute(jobKey, actualKeeperId, binJob); - - // 0. Keeper has sufficient stake - { - uint256 minKeeperCvp_ = minKeeperCvp; - if (keepers[actualKeeperId].worker != msg.sender) { - revert KeeperWorkerNotAuthorized(); - } - if (keepers[actualKeeperId].cvpStake < minKeeperCvp_) { - revert InsufficientKeeperStake(); - } - - // Execution LOCK - _setExecutionLock(1); - } - - // 1. Assert the job is active - { - if (!ConfigFlags.check(binJob, CFG_ACTIVE)) { - revert InactiveJob(jobKey); - } - } - - // 2. Assert job-scoped keeper's minimum CVP deposit - if (ConfigFlags.check(binJob, CFG_CHECK_KEEPER_MIN_CVP_DEPOSIT) && keepers[actualKeeperId].cvpStake < jobMinKeeperCvp[jobKey]) { - revert InsufficientJobScopedKeeperStake(); - } - - // 3. For interval job ensure the interval has passed - { - uint256 intervalSeconds = (binJob << 32) >> 232; - - if (intervalSeconds > 0) { - uint256 lastExecutionAt = binJob >> 224; - if (lastExecutionAt > 0) { - uint256 nextExecutionAt; - unchecked { - nextExecutionAt = lastExecutionAt + intervalSeconds; - } - if (nextExecutionAt > block.timestamp) { - revert IntervalNotReached(lastExecutionAt, intervalSeconds, block.timestamp); - } - } - } - } - - // 4. Ensure gas price fits base fee - uint256 maxBaseFee = _checkBaseFee(binJob, cfg); - - // 5. Ensure msg.sender is EOA - if (msg.sender != tx.origin) { - revert NonEOASender(); - } - - bool ok; - - // Source: Selector - CalldataSourceType calldataSource = CalldataSourceType((binJob << 56) >> 248); - if (calldataSource == CalldataSourceType.SELECTOR) { - bytes4 selector; - assembly ("memory-safe") { - selector := shl(224, shr(8, binJob)) - } - (ok,) = jobAddress.call{ gas: gasleft() - 50_000 }(bytes.concat(abi.encode(selector), jobKey)); - // Source: Bytes - } else if (calldataSource == CalldataSourceType.PRE_DEFINED) { - (ok,) = jobAddress.call{ gas: gasleft() - 50_000 }(bytes.concat(preDefinedCalldatas[jobKey], jobKey)); - // Source: Resolver - } else if (calldataSource == CalldataSourceType.RESOLVER || calldataSource == CalldataSourceType.OFFCHAIN) { - bytes32 cdataHash; - if (ConfigFlags.check(binJob, CFG_CALL_RESOLVER_BEFORE_EXECUTE)) { - cdataHash = _checkJobResolverCall(jobKey); - } - assembly ("memory-safe") { - let cdInCdSize := calldatasize() - // calldata offset is 31 - let beforeCdSize := 31 - let ptr := mload(0x40) - if lt(cdInCdSize, beforeCdSize) { - // revert MissingInputCalldata() - mstore(ptr, 0x47a0bafb00000000000000000000000000000000000000000000000000000000) - revert(ptr, 4) - } - let cdSize := sub(cdInCdSize, beforeCdSize) - mstore(0x40, add(add(ptr, cdSize), 0x20)) - calldatacopy(ptr, beforeCdSize, cdSize) - mstore(add(ptr, cdSize), jobKey) - // CFG_ASSERT_RESOLVER_SELECTOR = 0x04 from PPAgentLiteFlags - if and(binJob, 0x04) { - if iszero(eq( - // actual - shl(224, shr(224, calldataload(31))), - // expected - shl(224, shr(8, binJob)) - )) { - // revert SelectorCheckFailed() - mstore(ptr, 0x74ab678100000000000000000000000000000000000000000000000000000000) - revert(ptr, 4) - } - } - if and(binJob, 0x10) { - if eq(calldataSource, 2) { - let hash := keccak256(ptr, cdSize) - if iszero(eq(hash, cdataHash)) { - // revert JobCheckCalldataError() - mstore(ptr, 0x428ac18600000000000000000000000000000000000000000000000000000000) - revert(ptr, 4) - } - } - } - // The remaining gas could not be less than 50_000 - ok := call(sub(gas(), 50000), jobAddress, 0, ptr, add(cdSize, 0x20), 0x0, 0x0) - } - } else { - // Should never be reached - revert InvalidCalldataSource(); - } - - // Load returned response only if the job call had failed - bytes memory executionResponse; - if (!ok) { - assembly ("memory-safe") { - let size := returndatasize() - if gt(size, 0) { - executionResponse := mload(0x40) - mstore(executionResponse, size) - let p := add(executionResponse, 0x20) - returndatacopy(p, 0, size) - mstore(0x40, add(executionResponse, add(32, size))) - } - } - } - - // Payout block - uint256 compensation; - uint256 gasUsed; - { - binJob = getJobRaw(jobKey); - unchecked { - gasUsed = gasStart - gasleft(); - } - - { - uint256 min = block.basefee; - if (maxBaseFee < min) { - min = maxBaseFee; - } - - compensation = calculateCompensation(ok, binJob, actualKeeperId, min, gasUsed); - } - { - bool jobChanged; - - if (ConfigFlags.check(binJob, CFG_USE_JOB_OWNER_CREDITS)) { - // use job owner credits - _useJobOwnerCredits(ok, jobKey, compensation); - } else { - // use job credits - uint256 creditsBefore = (binJob << 128) >> 168; - if (creditsBefore < compensation) { - if (ok) { - revert InsufficientJobCredits(creditsBefore, compensation); - } else { - compensation = creditsBefore; - } - } - - uint256 creditsAfter; - unchecked { - creditsAfter = creditsBefore - compensation; - } - // update job credits - binJob = binJob & BM_CLEAR_CREDITS | (creditsAfter << 40); - jobChanged = true; - } - - if (ConfigFlags.check(cfg, FLAG_ACCRUE_REWARD)) { - compensations[actualKeeperId] += compensation; - } else { - payable(msg.sender).transfer(compensation); - } - - // Update lastExecutionAt for interval jobs - { - uint256 intervalSeconds = (binJob << 32) >> 232; - if (intervalSeconds > 0) { - uint256 lastExecutionAt = uint32(block.timestamp); - binJob = binJob & BM_CLEAR_LAST_UPDATE_AT | (lastExecutionAt << 224); - jobChanged = true; - } - } - - if (jobChanged) { - _updateRawJob(jobKey, binJob); - } - } - } - - // Execution UNLOCK - _setExecutionLock(0); - - if (ok) { - // Transaction succeeded - emit Execute( - jobKey, - jobAddress, - actualKeeperId, - gasUsed, - block.basefee, - tx.gasprice, - compensation, - bytes32(binJob) - ); - - _afterExecutionSucceeded(jobKey, actualKeeperId, binJob); - } else { - // Tx reverted - _afterExecutionReverted(jobKey, calldataSource, actualKeeperId, executionResponse, compensation); - } - - _afterExecute(actualKeeperId, gasUsed); - } - - function _checkJobResolverCall(bytes32 jobKey_) internal returns (bytes32 hash) { - (bool ok, bytes memory result) = address(this).call( - abi.encodeWithSelector(PPAgentV2.checkCouldBeExecuted.selector, resolvers[jobKey_].resolverAddress, resolvers[jobKey_].resolverCalldata) - ); - if (ok) { - revert UnexpectedCodeBlock(); - } - - bytes4 selector = bytes4(result); - if (selector == PPAgentV2.JobCheckCanNotBeExecuted.selector) { - assembly ("memory-safe") { - revert(add(32, result), mload(result)) - } - } else if (selector != PPAgentV2.JobCheckCanBeExecuted.selector) { - revert JobCheckUnexpectedError(); - } // else resolver was executed - - uint256 len; - assembly ("memory-safe") { - len := mload(result) - } - // We need at least canExecute flag. 32 * 4 + 4. - if (len < 132) { - revert UnableToDecodeResolverResponse(); - } - - uint256 canExecute; - assembly ("memory-safe") { - canExecute := mload(add(result, 100)) - // 5 * 32 + 4 - let dataLen := mload(add(result, 164)) - // starts from 6 * 32 + 4 - hash := keccak256(add(result, 196), dataLen) - } - - if (canExecute != 1) { - revert JobCheckResolverReturnedFalse(); - } - return hash; - } - - function _getBaseFee(uint256 binJob_) internal pure returns (uint256 maxBaseFee) { - unchecked { - maxBaseFee = ((binJob_ << 112) >> 240) * 1 gwei; - } - } - - function _checkBaseFee(uint256 binJob_, uint256 cfg_) internal view virtual returns (uint256) { - uint256 maxBaseFee = _getBaseFee(binJob_); - if (block.basefee > maxBaseFee && !ConfigFlags.check(cfg_, FLAG_ACCEPT_MAX_BASE_FEE_LIMIT)) { - revert BaseFeeGtGasPrice(block.basefee, maxBaseFee); - } - return maxBaseFee; - } - - function _afterExecutionReverted( - bytes32 jobKey_, - CalldataSourceType cdSource_, - uint256 actualKeeperId_, - bytes memory executionResponse_, - uint256 - ) internal virtual { - jobKey_; - actualKeeperId_; - cdSource_; - - if (executionResponse_.length == 0) { - revert JobCallRevertedWithoutDetails(); - } else { - assembly ("memory-safe") { - revert(add(32, executionResponse_), mload(executionResponse_)) - } - } - } - - function calculateCompensation( - bool ok_, - uint256 job_, - uint256 keeperId_, - uint256 baseFee_, - uint256 gasUsed_ - ) public view virtual returns (uint256) { - ok_; // silence unused param warning - keeperId_; // silence unused param warning - uint256 fixedReward = (job_ << 64) >> 224; - uint256 rewardPct = (job_ << 96) >> 240; - return calculateCompensationPure(rewardPct, fixedReward, baseFee_, gasUsed_); - } - - function _useJobOwnerCredits(bool ok_, bytes32 jobKey_, uint256 compensation_) internal { - uint256 jobOwnerCreditsBefore = jobOwnerCredits[jobOwners[jobKey_]]; - if (jobOwnerCreditsBefore < compensation_) { - if (ok_) { - revert InsufficientJobOwnerCredits(jobOwnerCreditsBefore, compensation_); - } else { - compensation_ = jobOwnerCreditsBefore; - } - } - - unchecked { - jobOwnerCredits[jobOwners[jobKey_]] = jobOwnerCreditsBefore - compensation_; - } - } - - /*** JOB OWNER INTERFACE ***/ - - /** - * Registers a new job. - * - * Job id is unique counter for a given job address. Up to 2**24-1 jobs per address. - * Job key is a keccak256(address, jobId). - * The following options are immutable: - * - `params_.jobaddress` - * - `params_.calldataSource` - * If you need to modify one of the immutable options above later consider creating a new job. - * - * @param params_ Job-specific params - * @param resolver_ Resolver details(address, calldata), required only for CALLDATA_SOURCE_RESOLVER - * job type. Use empty values for the other job types. - * @param preDefinedCalldata_ Calldata to call a job with, required only for CALLDATA_SOURCE_PRE_DEFINED - * job type. Keep empty for the other job types. - */ - function registerJob( - RegisterJobParams calldata params_, - Resolver calldata resolver_, - bytes calldata preDefinedCalldata_ - ) external payable virtual returns (bytes32 jobKey, uint256 jobId){ - jobId = jobLastIds[params_.jobAddress] + 1; - - if (jobId > type(uint24).max) { - revert JobIdOverflow(); - } - - if (msg.value > type(uint88).max) { - revert CreditsDepositOverflow(); - } - - if (params_.jobAddress == address(0)) { - revert MissingJobAddress(); - } - - if (params_.calldataSource > 3) { - revert InvalidCalldataSource(); - } - - if (params_.jobAddress == address(CVP) || params_.jobAddress == address(this)) { - revert InvalidJobAddress(); - } - - _assertInterval(params_.intervalSeconds, CalldataSourceType(params_.calldataSource)); - _assertJobParams(params_.maxBaseFeeGwei, params_.fixedReward, params_.rewardPct); - jobKey = getJobKey(params_.jobAddress, jobId); - - emit RegisterJob( - jobKey, - params_.jobAddress, - jobId, - msg.sender, - params_ - ); - - if (CalldataSourceType(params_.calldataSource) == CalldataSourceType.PRE_DEFINED) { - _setJobPreDefinedCalldata(jobKey, preDefinedCalldata_); - } else if ( - CalldataSourceType(params_.calldataSource) == CalldataSourceType.RESOLVER || - CalldataSourceType(params_.calldataSource) == CalldataSourceType.OFFCHAIN - ) { - _setJobResolver(jobKey, resolver_); - } - - { - bytes4 selector = 0x00000000; - if (CalldataSourceType(params_.calldataSource) != CalldataSourceType.PRE_DEFINED) { - selector = params_.jobSelector; - } - - uint256 config = CFG_ACTIVE; - if (params_.useJobOwnerCredits) { - config = config | CFG_USE_JOB_OWNER_CREDITS; - } - if (params_.assertResolverSelector) { - config = config | CFG_ASSERT_RESOLVER_SELECTOR; - } - if (params_.jobMinCvp > 0) { - config = config | CFG_CHECK_KEEPER_MIN_CVP_DEPOSIT; - } - - jobs[jobKey] = Job({ - config: uint8(config), - selector: selector, - credits: 0, - maxBaseFeeGwei: params_.maxBaseFeeGwei, - fixedReward: params_.fixedReward, - rewardPct: params_.rewardPct, - calldataSource: params_.calldataSource, - - // For interval jobs - intervalSeconds: params_.intervalSeconds, - lastExecutionAt: 0 - }); - jobMinKeeperCvp[jobKey] = params_.jobMinCvp; - } - - jobLastIds[params_.jobAddress] = jobId; - jobOwners[jobKey] = msg.sender; - - if (msg.value > 0) { - if (params_.useJobOwnerCredits) { - _processJobOwnerCreditsDeposit(msg.sender); - } else { - _processJobCreditsDeposit(jobKey); - } - } - - _afterRegisterJob(jobKey); - } - - /** - * Updates a job details. - * - * The following options are immutable: - * - `jobAddress` - * - `job.selector` - * - `job.calldataSource` - * If you need to modify one of the immutable options above later consider creating a new job. - * - * @param jobKey_ The job key - * @param maxBaseFeeGwei_ The maximum basefee in gwei to use for a job compensation - * @param rewardPct_ The reward premium in pct, where 1 == 1% - * @param fixedReward_ The fixed reward divided by FIXED_PAYMENT_MULTIPLIER - * @param jobMinCvp_ The keeper minimal CVP stake to be eligible to execute this job - * @param intervalSeconds_ The interval for a job execution - */ - function updateJob( - bytes32 jobKey_, - uint16 maxBaseFeeGwei_, - uint16 rewardPct_, - uint32 fixedReward_, - uint256 jobMinCvp_, - uint24 intervalSeconds_ - ) external { - _assertOnlyJobOwner(jobKey_); - _assertExecutionNotLocked(); - _assertJobParams(maxBaseFeeGwei_, fixedReward_, rewardPct_); - _assertInterval(intervalSeconds_, CalldataSourceType(jobs[jobKey_].calldataSource)); - - uint256 cfg = jobs[jobKey_].config; - - if (jobMinCvp_ > 0 && !ConfigFlags.check(jobs[jobKey_].config, CFG_CHECK_KEEPER_MIN_CVP_DEPOSIT)) { - cfg = cfg | CFG_CHECK_KEEPER_MIN_CVP_DEPOSIT; - } - if (jobMinCvp_ == 0 && ConfigFlags.check(jobs[jobKey_].config, CFG_CHECK_KEEPER_MIN_CVP_DEPOSIT)) { - cfg = cfg ^ CFG_CHECK_KEEPER_MIN_CVP_DEPOSIT; - } - - jobs[jobKey_].config = uint8(cfg); - jobMinKeeperCvp[jobKey_] = jobMinCvp_; - - jobs[jobKey_].maxBaseFeeGwei = maxBaseFeeGwei_; - jobs[jobKey_].rewardPct = rewardPct_; - jobs[jobKey_].fixedReward = fixedReward_; - jobs[jobKey_].intervalSeconds = intervalSeconds_; - - emit JobUpdate(jobKey_, maxBaseFeeGwei_, rewardPct_, fixedReward_, jobMinCvp_, intervalSeconds_); - } - - /** - * A job owner updates job resolver details. - * - * @param jobKey_ The jobKey - * @param resolver_ The new job resolver details - */ - function setJobResolver(bytes32 jobKey_, Resolver calldata resolver_) external { - _assertOnlyJobOwner(jobKey_); - _assertExecutionNotLocked(); - - if ( - CalldataSourceType(jobs[jobKey_].calldataSource) != CalldataSourceType.RESOLVER && - CalldataSourceType(jobs[jobKey_].calldataSource) != CalldataSourceType.OFFCHAIN - ) { - revert NotSupportedByJobCalldataSource(); - } - - _setJobResolver(jobKey_, resolver_); - } - - function _setJobResolver(bytes32 jobKey_, Resolver calldata resolver_) internal { - if (resolver_.resolverAddress == address(0)) { - revert MissingResolverAddress(); - } - resolvers[jobKey_] = resolver_; - emit SetJobResolver(jobKey_, resolver_.resolverAddress, resolver_.resolverCalldata); - } - - /** - * A job owner updates pre-defined calldata. - * - * @param jobKey_ The jobKey - * @param preDefinedCalldata_ The new job pre-defined calldata - */ - function setJobPreDefinedCalldata(bytes32 jobKey_, bytes calldata preDefinedCalldata_) external { - _assertOnlyJobOwner(jobKey_); - _assertExecutionNotLocked(); - - if (CalldataSourceType(jobs[jobKey_].calldataSource) != CalldataSourceType.PRE_DEFINED) { - revert NotSupportedByJobCalldataSource(); - } - - _setJobPreDefinedCalldata(jobKey_, preDefinedCalldata_); - } - - function _setJobPreDefinedCalldata(bytes32 jobKey_, bytes calldata preDefinedCalldata_) internal { - preDefinedCalldatas[jobKey_] = preDefinedCalldata_; - emit SetJobPreDefinedCalldata(jobKey_, preDefinedCalldata_); - } - - /** - * A job owner updates a job config flag. - * - * @param jobKey_ The jobKey - * @param isActive_ Whether the job is active or not - * @param useJobOwnerCredits_ The useJobOwnerCredits flag - * @param assertResolverSelector_ The assertResolverSelector flag - */ - function setJobConfig( - bytes32 jobKey_, - bool isActive_, - bool useJobOwnerCredits_, - bool assertResolverSelector_, - bool callResolverBeforeExecute_ - ) public virtual { - _assertOnlyJobOwner(jobKey_); - _assertExecutionNotLocked(); - uint256 newConfig = 0; - - if (isActive_) { - newConfig = newConfig | CFG_ACTIVE; - } - if (useJobOwnerCredits_) { - newConfig = newConfig | CFG_USE_JOB_OWNER_CREDITS; - } - if (assertResolverSelector_) { - newConfig = newConfig | CFG_ASSERT_RESOLVER_SELECTOR; - } - if (callResolverBeforeExecute_) { - newConfig = newConfig | CFG_CALL_RESOLVER_BEFORE_EXECUTE; - } - - uint256 job = getJobRaw(jobKey_) & BM_CLEAR_CONFIG | newConfig; - _updateRawJob(jobKey_, job); - - emit SetJobConfig(jobKey_, isActive_, useJobOwnerCredits_, assertResolverSelector_, callResolverBeforeExecute_); - } - - function _updateRawJob(bytes32 jobKey_, uint256 job_) internal { - Job storage job = jobs[jobKey_]; - assembly ("memory-safe") { - sstore(job.slot, job_) - } - } - - /** - * A job owner initiates the job transfer to a new owner. - * The actual owner doesn't update until the pending owner accepts the transfer. - * - * @param jobKey_ The jobKey - * @param to_ The new job owner - */ - function initiateJobTransfer(bytes32 jobKey_, address to_) external { - _assertOnlyJobOwner(jobKey_); - _assertExecutionNotLocked(); - jobPendingTransfers[jobKey_] = to_; - emit InitiateJobTransfer(jobKey_, msg.sender, to_); - } - - /** - * A pending job owner accepts the job transfer. - * - * @param jobKey_ The jobKey - */ - function acceptJobTransfer(bytes32 jobKey_) external { - _assertExecutionNotLocked(); - - if (msg.sender != jobPendingTransfers[jobKey_]) { - revert OnlyPendingOwner(); - } - - jobOwners[jobKey_] = msg.sender; - delete jobPendingTransfers[jobKey_]; - - _afterAcceptJobTransfer(jobKey_); - - emit AcceptJobTransfer(jobKey_, msg.sender); - } - - /** - * Top-ups the job credits in NATIVE tokens. - * - * @param jobKey_ The jobKey to deposit for - */ - function depositJobCredits(bytes32 jobKey_) external virtual payable { - if (msg.value == 0) { - revert MissingDeposit(); - } - - if (jobOwners[jobKey_] == address(0)) { - revert JobWithoutOwner(); - } - - _processJobCreditsDeposit(jobKey_); - - _afterDepositJobCredits(jobKey_); - } - - function _processJobCreditsDeposit(bytes32 jobKey_) internal { - (uint256 fee, uint256 amount) = _calculateDepositFee(); - uint256 creditsAfter = jobs[jobKey_].credits + amount; - if (creditsAfter > type(uint88).max) { - revert CreditsDepositOverflow(); - } - - unchecked { - feeTotal += fee; - } - jobs[jobKey_].credits = uint88(creditsAfter); - - emit DepositJobCredits(jobKey_, msg.sender, amount, fee); - } - - function _calculateDepositFee() internal view returns (uint256 fee, uint256 amount) { - fee = msg.value * feePpm / 1e6 /* 100% in ppm */; - amount = msg.value - fee; - } - - /** - * A job owner withdraws the job credits in NATIVE tokens. - * - * @param jobKey_ The jobKey - * @param to_ The address to send NATIVE tokens to - * @param amount_ The amount to withdraw. Use type(uint256).max for the total available credits withdrawal. - */ - function withdrawJobCredits( - bytes32 jobKey_, - address payable to_, - uint256 amount_ - ) external { - uint88 creditsBefore = jobs[jobKey_].credits; - if (amount_ == type(uint256).max) { - amount_ = creditsBefore; - } - - _assertOnlyJobOwner(jobKey_); - _assertExecutionNotLocked(); - if (amount_ == 0) { - revert MissingAmount(); - } - - if (creditsBefore < amount_) { - revert CreditsWithdrawalUnderflow(); - } - - unchecked { - jobs[jobKey_].credits = creditsBefore - uint88(amount_); - } - - to_.transfer(amount_); - - emit WithdrawJobCredits(jobKey_, msg.sender, to_, amount_); - - _afterWithdrawJobCredits(jobKey_); - } - - /** - * Top-ups the job owner credits in NATIVE tokens. - * - * @param for_ The job owner address to deposit for - */ - function depositJobOwnerCredits(address for_) external payable { - if (msg.value == 0) { - revert MissingDeposit(); - } - - _processJobOwnerCreditsDeposit(for_); - } - - function _processJobOwnerCreditsDeposit(address for_) internal { - (uint256 fee, uint256 amount) = _calculateDepositFee(); - - unchecked { - feeTotal += fee; - jobOwnerCredits[for_] += amount; - } - - emit DepositJobOwnerCredits(for_, msg.sender, amount, fee); - } - - /** - * A job owner withdraws the job owner credits in NATIVE tokens. - * - * @param to_ The address to send NATIVE tokens to - * @param amount_ The amount to withdraw. Use type(uint256).max for the total available credits withdrawal. - */ - function withdrawJobOwnerCredits(address payable to_, uint256 amount_) external { - uint256 creditsBefore = jobOwnerCredits[msg.sender]; - if (amount_ == type(uint256).max) { - amount_ = creditsBefore; - } - - _assertExecutionNotLocked(); - if (amount_ == 0) { - revert MissingAmount(); - } - - if (creditsBefore < amount_) { - revert CreditsWithdrawalUnderflow(); - } - - unchecked { - jobOwnerCredits[msg.sender] = creditsBefore - amount_; - } - - to_.transfer(amount_); - - emit WithdrawJobOwnerCredits(msg.sender, to_, amount_); - } - - /*** KEEPER INTERFACE ***/ - - /** - * Actor registers as a keeper. - * One keeper address could have multiple keeper IDs. Requires at least `minKeepCvp` as an initial CVP deposit. - * - * @dev Overflow-safe only for CVP which total supply is less than type(uint96).max - * @dev Maximum 2^24-1 keepers supported. There is no explicit check for overflow, but the keepers with ID >= 2^24 - * won't be able to perform upkeep operations. - * - * @param worker_ The worker address - * @param initialDepositAmount_ The initial CVP deposit. Should be no less than `minKeepCvp` - * @return keeperId The registered keeper ID - */ - function registerAsKeeper(address worker_, uint256 initialDepositAmount_) public virtual returns (uint256 keeperId) { - if (workerKeeperIds[worker_] != 0) { - revert WorkerAlreadyAssigned(); - } - if (initialDepositAmount_ < minKeeperCvp) { - revert InsufficientAmount(); - } - - keeperId = ++lastKeeperId; - keeperAdmins[keeperId] = msg.sender; - keepers[keeperId] = Keeper(worker_, 0, false); - workerKeeperIds[worker_] = keeperId; - emit RegisterAsKeeper(keeperId, msg.sender, worker_); - - _afterRegisterAsKeeper(keeperId); - - _stake(keeperId, initialDepositAmount_); - } - - /** - * A keeper updates a keeper worker address - * - * @param keeperId_ The keeper ID - * @param worker_ The new worker address - */ - function setWorkerAddress(uint256 keeperId_, address worker_) external { - _assertOnlyKeeperAdmin(keeperId_); - if (workerKeeperIds[worker_] != 0) { - revert WorkerAlreadyAssigned(); - } - - address prev = keepers[keeperId_].worker; - delete workerKeeperIds[prev]; - workerKeeperIds[worker_] = keeperId_; - keepers[keeperId_].worker = worker_; - - emit SetWorkerAddress(keeperId_, prev, worker_); - } - - /** - * A keeper withdraws NATIVE token rewards. - * - * @param keeperId_ The keeper ID - * @param to_ The address to withdraw to - * @param amount_ The amount to withdraw. Use type(uint256).max for the total available compensation withdrawal. - */ - function withdrawCompensation(uint256 keeperId_, address payable to_, uint256 amount_) external { - uint256 available = compensations[keeperId_]; - if (amount_ == type(uint256).max) { - amount_ = available; - } - - if (amount_ == 0) { - revert MissingAmount(); - } - _assertOnlyKeeperAdminOrWorker(keeperId_); - - if (amount_ > available) { - revert WithdrawAmountExceedsAvailable(amount_, available); - } - - unchecked { - compensations[keeperId_] = available - amount_; - } - - to_.transfer(amount_); - - emit WithdrawCompensation(keeperId_, to_, amount_); - } - - /** - * Deposits CVP for the given keeper ID. The beneficiary receives a derivative erc20 token in exchange of CVP. - * Accounts the staking amount on the beneficiary's stakeOf balance. - * - * @param keeperId_ The keeper ID - * @param amount_ The amount to stake - */ - function stake(uint256 keeperId_, uint256 amount_) external { - if (amount_ == 0) { - revert MissingAmount(); - } - _assertKeeperIdExists(keeperId_); - _stake(keeperId_, amount_); - } - - function _stake(uint256 keeperId_, uint256 amount_) internal { - uint256 amountAfter = keepers[keeperId_].cvpStake + amount_; - if (amountAfter > type(uint88).max) { - revert StakeAmountOverflow(); - } - IERC20(CVP).transferFrom(msg.sender, address(this), amount_); - keepers[keeperId_].cvpStake += uint88(amount_); - - emit Stake(keeperId_, amount_, msg.sender); - } - - /** - * A keeper initiates CVP withdrawal. - * The given CVP amount needs to go through the cooldown stage. After the cooldown is complete this amount could be - * withdrawn using `finalizeRedeem()` method. - * The msg.sender burns the paCVP token in exchange of the corresponding CVP amount. - * Accumulates the existing pending for withdrawal amounts and re-initiates cooldown period. - * If there is any slashed amount for the msg.sender, it should be compensated within the first initiateRedeem transaction - * by burning the equivalent amount of paCVP tokens. The remaining CVP tokens won't be redeemed unless the slashed - * amount is compensated. - * - * @param keeperId_ The keeper ID - * @param amount_ The amount to cooldown - * @return pendingWithdrawalAfter The total pending for withdrawal amount - */ - function initiateRedeem(uint256 keeperId_, uint256 amount_) external returns (uint256 pendingWithdrawalAfter) { - _assertOnlyKeeperAdmin(keeperId_); - if (amount_ == 0) { - revert MissingAmount(); - } - _beforeInitiateRedeem(keeperId_); - - uint256 stakeOfBefore = keepers[keeperId_].cvpStake; - uint256 slashedStakeOfBefore = slashedStakeOf[keeperId_]; - uint256 totalStakeBefore = stakeOfBefore + slashedStakeOfBefore; - - // Should burn at least the total slashed stake - if (amount_ < slashedStakeOfBefore) { - revert InsufficientAmountToCoverSlashedStake(amount_, slashedStakeOfBefore); - } - - if (amount_ > totalStakeBefore) { - revert AmountGtStake(amount_, stakeOfBefore, slashedStakeOfBefore); - } - - slashedStakeOf[keeperId_] = 0; - uint256 stakeOfToReduceAmount; - unchecked { - stakeOfToReduceAmount = amount_ - slashedStakeOfBefore; - keepers[keeperId_].cvpStake = uint88(stakeOfBefore - stakeOfToReduceAmount); - pendingWithdrawalAmounts[keeperId_] += stakeOfToReduceAmount; - } - - pendingWithdrawalAfter = block.timestamp + pendingWithdrawalTimeoutSeconds; - pendingWithdrawalEndsAt[keeperId_] = pendingWithdrawalAfter; - - _afterInitiateRedeem(keeperId_); - - emit InitiateRedeem(keeperId_, amount_, stakeOfToReduceAmount, slashedStakeOfBefore); - } - - /** - * A keeper finalizes CVP withdrawal and receives the staked CVP tokens. - * - * @param keeperId_ The keeper ID - * @param to_ The address to transfer CVP to - * @return redeemedCvp The redeemed CVP amount - */ - function finalizeRedeem(uint256 keeperId_, address to_) external returns (uint256 redeemedCvp) { - _assertOnlyKeeperAdmin(keeperId_); - - if (pendingWithdrawalEndsAt[keeperId_] > block.timestamp) { - revert WithdrawalTimoutNotReached(); - } - - redeemedCvp = pendingWithdrawalAmounts[keeperId_]; - if (redeemedCvp == 0) { - revert NoPendingWithdrawal(); - } - - pendingWithdrawalAmounts[keeperId_] = 0; - IERC20(CVP).transfer(to_, redeemedCvp); - - emit FinalizeRedeem(keeperId_, to_, redeemedCvp); - } - - /*** CONTRACT OWNER INTERFACE ***/ - /** - * Slashes any keeper_ for an amount within keeper's deposit. - * Penalises a keeper for malicious behaviour like sandwitching upkeep transactions. - * - * @param keeperId_ The keeper ID to slash - * @param to_ The address to send the slashed CVP to - * @param currentAmount_ The amount to slash from the current keeper.cvpStake balance - * @param pendingAmount_ The amount to slash from the pendingWithdrawals balance - */ - function ownerSlash(uint256 keeperId_, address to_, uint256 currentAmount_, uint256 pendingAmount_) public { - _checkOwner(); - uint256 totalAmount = currentAmount_ + pendingAmount_; - if (totalAmount == 0) { - revert MissingAmount(); - } - - if (currentAmount_ > 0) { - keepers[keeperId_].cvpStake -= uint88(currentAmount_); - slashedStakeOf[keeperId_] += currentAmount_; - } - - if (pendingAmount_ > 0) { - pendingWithdrawalAmounts[keeperId_] -= pendingAmount_; - } - - IERC20(CVP).transfer(to_, totalAmount); - - emit OwnerSlash(keeperId_, to_, currentAmount_, pendingAmount_); - } - - /** - * Owner withdraws all the accrued rewards in native tokens to the provided address. - * - * @param to_ The address to send rewards to - */ - function withdrawFees(address payable to_) external { - _checkOwner(); - - uint256 amount = feeTotal; - feeTotal = 0; - - to_.transfer(amount); - - emit WithdrawFees(to_, amount); - } - - /** - * Owner updates minKeeperCVP value - * - * @param minKeeperCvp_ The new minKeeperCVP value - */ - function setAgentParams( - uint256 minKeeperCvp_, - uint256 timeoutSeconds_, - uint256 feePpm_ - ) external { - _checkOwner(); - _assertExecutionNotLocked(); - _setAgentParams(minKeeperCvp_, timeoutSeconds_, feePpm_); - } - - function _setAgentParams( - uint256 minKeeperCvp_, - uint256 timeoutSeconds_, - uint256 feePpm_ - ) internal { - if (minKeeperCvp_ == 0) { - revert InvalidMinKeeperCvp(); - } - if (timeoutSeconds_ > MAX_PENDING_WITHDRAWAL_TIMEOUT_SECONDS) { - revert TimeoutTooBig(); - } - if (feePpm_ > MAX_FEE_PPM) { - revert FeeTooBig(); - } - - minKeeperCvp = minKeeperCvp_; - pendingWithdrawalTimeoutSeconds = timeoutSeconds_; - feePpm = feePpm_; - - emit SetAgentParams(minKeeperCvp_, timeoutSeconds_, feePpm_); - } - - /*** GETTERS ***/ - - /** - * Pure method that calculates keeper compensation based on a dynamic and a fixed multipliers. - * DANGER: could overflow when used externally - * - * @param rewardPct_ The fixed percent. uint16. 0 == 0%, 100 == 100%, 500 == 500%, max 56535 == 56535% - * @param fixedReward_ The fixed reward. uint32. Always multiplied by 1e15 (FIXED_PAYMENT_MULTIPLIER). - * For ex. 2 == 2e15, 1_000 = 1e18, max 4294967295 == 4_294_967.295e18 - * @param blockBaseFee_ The block.basefee value. - * @param gasUsed_ The gas used in wei. - * - */ - function calculateCompensationPure( - uint256 rewardPct_, - uint256 fixedReward_, - uint256 blockBaseFee_, - uint256 gasUsed_ - ) public pure returns (uint256) { - unchecked { - return (gasUsed_ + _getJobGasOverhead()) * blockBaseFee_ * rewardPct_ / 100 - + fixedReward_ * FIXED_PAYMENT_MULTIPLIER; - } - } - - function getKeeperWorkerAndStake(uint256 keeperId_) - external view returns ( - address worker, - uint256 currentStake, - bool isActive - ) - { - return ( - keepers[keeperId_].worker, - keepers[keeperId_].cvpStake, - keepers[keeperId_].isActive - ); - } - - function getConfig() - external view returns ( - uint256 minKeeperCvp_, - uint256 pendingWithdrawalTimeoutSeconds_, - uint256 feeTotal_, - uint256 feePpm_, - uint256 lastKeeperId_ - ) - { - return ( - minKeeperCvp, - pendingWithdrawalTimeoutSeconds, - feeTotal, - feePpm, - lastKeeperId - ); - } - - 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_]; - - currentStake = keepers[keeperId_].cvpStake; - isActive = keepers[keeperId_].isActive; - worker = keepers[keeperId_].worker; - - admin = keeperAdmins[keeperId_]; - } - - function getJob(bytes32 jobKey_) - external view returns ( - address owner, - address pendingTransfer, - uint256 jobLevelMinKeeperCvp, - Job memory details, - bytes memory preDefinedCalldata, - Resolver memory resolver - ) - { - return ( - jobOwners[jobKey_], - jobPendingTransfers[jobKey_], - jobMinKeeperCvp[jobKey_], - jobs[jobKey_], - preDefinedCalldatas[jobKey_], - resolvers[jobKey_] - ); - } - - /** - * Returns the principal job data stored in a single EVM slot. - * @notice To get parsed job data use `getJob()` method instead. - * - * The job slot data layout: - * 0x0000000000000a000000000a002300640000000de0b6b3a7640000d09de08a01 - * 0x 00000000 00000a 00 0000000a 0023 0064 0000000de0b6b3a7640000 d09de08a 01 - * name lastExecAt interval calldataSource fixedReward rewardPct maxBaseFeeGwei nativeCredits selector config bitmask - * size b bytes4 bytes3 bytes4 bytes4 bytes2 bytes2 bytes11 bytes4 bytes1 - * size u uint32 uint24 uint8 uint32 uint16 uint16 uint88 uint32 uint8 - * bits 0-3 4-6 7-7 8-11 12-13 14-15 16-26 27-30 31-31 - */ - function getJobRaw(bytes32 jobKey_) public view returns (uint256 rawJob) { - Job storage job = jobs[jobKey_]; - assembly ("memory-safe") { - rawJob := sload(job.slot) - } - } - - function getJobKey(address jobAddress_, uint256 jobId_) public pure returns (bytes32 jobKey) { - assembly ("memory-safe") { - mstore(0, shl(96, jobAddress_)) - mstore(20, shl(232, jobId_)) - jobKey := keccak256(0, 23) - } - } - - // The function that always reverts - function checkCouldBeExecuted(address jobAddress_, bytes memory jobCalldata_) external { - // 1. LOCK - _setExecutionLock(1); - // 2. EXECUTE - (bool ok, bytes memory result) = jobAddress_.call(jobCalldata_); - // 3. UNLOCK - _setExecutionLock(0); - - if (ok) { - revert JobCheckCanBeExecuted(result); - } else { - revert JobCheckCanNotBeExecuted(result); - } - } } diff --git a/contracts/PPAgentV2Based.sol b/contracts/PPAgentV2Based.sol new file mode 100644 index 0000000..04ad2dd --- /dev/null +++ b/contracts/PPAgentV2Based.sol @@ -0,0 +1,1479 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./utils/InitializableOptimized.sol"; +import "./utils/OwnableOptimized.sol"; +import "./PPAgentV2Flags.sol"; +import "./PPAgentV2Interfaces.sol"; + +library ConfigFlags { + function check(uint256 cfg, uint256 flag) internal pure returns (bool) { + return (cfg & flag) != 0; + } +} + +/** + * @title PPAgentV2Based + * @author PowerPool + */ +contract PPAgentV2Based is IPPAgentV2Executor, IPPAgentV2Viewer, IPPAgentV2JobOwner, PPAgentV2Flags, InitializableOptimized, OwnableOptimized { + error NonEOASender(); + error InsufficientKeeperStake(); + error InsufficientJobScopedKeeperStake(); + error KeeperWorkerNotAuthorized(); + error InsufficientJobCredits(uint256 actual, uint256 wanted); + error InsufficientJobOwnerCredits(uint256 actual, uint256 wanted); + error InactiveJob(bytes32 jobKey); + error JobIdOverflow(); + error OnlyJobOwner(); + error JobWithoutOwner(); + error MissingJobAddress(); + error MissingMaxBaseFeeGwei(); + error NoFixedNorPremiumPctReward(); + error CreditsDepositOverflow(); + error StakeAmountOverflow(); + error CreditsWithdrawalUnderflow(); + error MissingDeposit(); + error IntervalNotReached(uint256 lastExecutedAt, uint256 interval, uint256 _now); + error BaseFeeGtGasPrice(uint256 baseFee, uint256 jobMaxBaseFeeGwei); + error InvalidCalldataSource(); + error MissingInputCalldata(); + error SelectorCheckFailed(); + error JobCallRevertedWithoutDetails(); + error InsufficientAmountToCoverSlashedStake(uint256 wanted, uint256 actual); + error AmountGtStake(uint256 wanted, uint256 actualStake, uint256 actualSlashedStake); + error WithdrawalTimoutNotReached(); + error NoPendingWithdrawal(); + error MissingAmount(); + error WithdrawAmountExceedsAvailable(uint256 wanted, uint256 actual); + error JobShouldHaveInterval(); + error JobDoesNotSupposedToHaveInterval(); + error InvalidJobAddress(); + error InvalidKeeperId(); + error MissingResolverAddress(); + error NotSupportedByJobCalldataSource(); + error OnlyKeeperAdmin(); + error OnlyKeeperAdminOrJobOwner(); + error OnlyKeeperAdminOrWorker(); + error InvalidMinKeeperCvp(); + error TimeoutTooBig(); + error FeeTooBig(); + error InsufficientAmount(); + error OnlyPendingOwner(); + error WorkerAlreadyAssigned(); + error ExecutionReentrancyLocked(); + error JobCheckResolverReturnedFalse(); + error UnableToDecodeResolverResponse(); + error UnexpectedCodeBlock(); + error JobCheckCanBeExecuted(bytes returndata); + error JobCheckCanNotBeExecuted(bytes errReason); + error JobCheckUnexpectedError(); + error JobCheckCalldataError(); + + string public constant VERSION = "2.5.0"; + uint256 internal constant MAX_PENDING_WITHDRAWAL_TIMEOUT_SECONDS = 30 days; + uint256 internal constant MAX_FEE_PPM = 5e4; + uint256 internal constant FIXED_PAYMENT_MULTIPLIER = 1e12; + bytes32 internal constant EXECUTION_LOCK_KEY = keccak256("executionLock"); + + enum CalldataSourceType { + SELECTOR, + PRE_DEFINED, + RESOLVER, + OFFCHAIN + } + + address public immutable CVP; + + event Execute( + bytes32 indexed jobKey, + address indexed job, + uint256 indexed keeperId, + uint256 gasUsed, + uint256 baseFee, + uint256 gasPrice, + uint256 compensation, + bytes32 binJobAfter + ); + event WithdrawFees(address indexed to, uint256 amount); + event OwnerSlash(uint256 indexed keeperId, address indexed to, uint256 currentAmount, uint256 pendingAmount); + event RegisterAsKeeper(uint256 indexed keeperId, address indexed keeperAdmin, address indexed keeperWorker); + event SetWorkerAddress(uint256 indexed keeperId, address indexed prev, address indexed worker); + event Stake(uint256 indexed keeperId, uint256 amount, address staker); + event InitiateRedeem(uint256 indexed keeperId, uint256 redeemAmount, uint256 stakeAmount, uint256 slashedStakeAmount); + event FinalizeRedeem(uint256 indexed keeperId, address indexed beneficiary, uint256 amount); + event WithdrawCompensation(uint256 indexed keeperId, address indexed to, uint256 amount); + event DepositJobCredits(bytes32 indexed jobKey, address indexed depositor, uint256 amount, uint256 fee); + event WithdrawJobCredits(bytes32 indexed jobKey, address indexed owner, address indexed to, uint256 amount); + event DepositJobOwnerCredits(address indexed jobOwner, address indexed depositor, uint256 amount, uint256 fee); + event WithdrawJobOwnerCredits(address indexed jobOwner, address indexed to, uint256 amount); + event InitiateJobTransfer(bytes32 indexed jobKey, address indexed from, address indexed to); + event AcceptJobTransfer(bytes32 indexed jobKey_, address indexed to_); + event SetJobConfig(bytes32 indexed jobKey, bool isActive_, bool useJobOwnerCredits_, bool assertResolverSelector_, bool callResolverBeforeExecute_); + event SetJobResolver(bytes32 indexed jobKey, address resolverAddress, bytes resolverCalldata); + event SetJobPreDefinedCalldata(bytes32 indexed jobKey, bytes preDefinedCalldata); + event SetAgentParams(uint256 minKeeperCvp_, uint256 timeoutSeconds_, uint256 feePpm_); + event RegisterJob( + bytes32 indexed jobKey, + address indexed jobAddress, + uint256 indexed jobId, + address owner, + RegisterJobParams params + ); + event JobUpdate( + bytes32 indexed jobKey, + uint256 maxBaseFeeGwei, + uint256 rewardPct, + uint256 fixedReward, + uint256 jobMinCvp, + uint256 intervalSeconds + ); + + struct Keeper { + address worker; + uint88 cvpStake; + bool isActive; + } + + struct ExecutionResponsesData { + bytes resolverResponse; + bytes executionResponse; + } + + // WARNING: the minKeeperCvp slot is also used as a non-reentrancy lock + uint256 internal minKeeperCvp; + uint256 internal pendingWithdrawalTimeoutSeconds; + uint256 internal feeTotal; + uint256 internal feePpm; + uint256 internal lastKeeperId; + + // keccak256(jobAddress, id) => ethBalance + mapping(bytes32 => Job) internal jobs; + // keccak256(jobAddress, id) => customCalldata + mapping(bytes32 => bytes) internal preDefinedCalldatas; + // keccak256(jobAddress, id) => minKeeperCvpStake + mapping(bytes32 => uint256) internal jobMinKeeperCvp; + // keccak256(jobAddress, id) => owner + mapping(bytes32 => address) internal jobOwners; + // keccak256(jobAddress, id) => resolver(address,calldata) + mapping(bytes32 => Resolver) internal resolvers; + // keccak256(jobAddress, id) => pendingAddress + mapping(bytes32 => address) internal jobPendingTransfers; + + // jobAddress => lastIdRegistered(actually uint24) + mapping(address => uint256) public jobLastIds; + + // keeperId => (worker,CVP stake) + mapping(uint256 => Keeper) internal keepers; + // keeperId => admin + mapping(uint256 => address) internal keeperAdmins; + // keeperId => the slashed CVP amount + mapping(uint256 => uint256) internal slashedStakeOf; + // keeperId => native token compensation + mapping(uint256 => uint256) internal compensations; + + // keeperId => pendingWithdrawalCVP amount + mapping(uint256 => uint256) internal pendingWithdrawalAmounts; + // keeperId => pendingWithdrawalEndsAt timestamp + mapping(uint256 => uint256) internal pendingWithdrawalEndsAt; + + // owner => credits + mapping(address => uint256) public jobOwnerCredits; + + // worker => keeperIs + mapping(address => uint256) public workerKeeperIds; + + bool internal reentrancyLock; + + /*** PSEUDO-MODIFIERS ***/ + + function _assertExecutionNotLocked() internal virtual view { + if (reentrancyLock) { + revert ExecutionReentrancyLocked(); + } + } + + function _setExecutionLock(uint value_) internal virtual { + reentrancyLock = value_ == 1; + } + + function _assertOnlyJobOwner(bytes32 jobKey_) internal view { + if (msg.sender != jobOwners[jobKey_]) { + revert OnlyJobOwner(); + } + } + + function _assertOnlyKeeperAdmin(uint256 keeperId_) internal view { + if (msg.sender != keeperAdmins[keeperId_]) { + revert OnlyKeeperAdmin(); + } + } + + function _assertOnlyKeeperAdminOrWorker(uint256 keeperId_) internal view { + if (msg.sender != keeperAdmins[keeperId_] && msg.sender != keepers[keeperId_].worker) { + revert OnlyKeeperAdminOrWorker(); + } + } + + function _assertKeeperIdExists(uint256 keeperId_) internal view { + if (keeperId_ > lastKeeperId) { + revert InvalidKeeperId(); + } + } + + function _assertJobParams(uint256 maxBaseFeeGwei_, uint256 fixedReward_, uint256 rewardPct_) internal pure { + if (maxBaseFeeGwei_ == 0) { + revert MissingMaxBaseFeeGwei(); + } + + if (fixedReward_ == 0 && rewardPct_ == 0) { + revert NoFixedNorPremiumPctReward(); + } + } + + function _assertInterval(uint256 interval_, CalldataSourceType cdSource_) internal pure { + if (interval_ == 0 && + (cdSource_ == CalldataSourceType.SELECTOR || cdSource_ == CalldataSourceType.PRE_DEFINED)) { + revert JobShouldHaveInterval(); + } + if (interval_ != 0 && (cdSource_ == CalldataSourceType.RESOLVER || cdSource_ == CalldataSourceType.OFFCHAIN)) { + revert JobDoesNotSupposedToHaveInterval(); + } + } + + constructor(address cvp_) { + CVP = cvp_; + } + + function initialize( + address owner_, + uint256 minKeeperCvp_, + uint256 pendingWithdrawalTimeoutSeconds_ + ) public initializer { + _setAgentParams(minKeeperCvp_, pendingWithdrawalTimeoutSeconds_, 0); + _transferOwnership(owner_); + } + + /*** HOOKS ***/ + function _beforeExecute(bytes32 jobKey_, uint256 actualKeeperId_, uint256 binJob_) internal view virtual {} + function _beforeInitiateRedeem(uint256 keeperId_) internal view virtual {} + + function _afterExecute(uint256 actualKeeperId_, uint256 gasUsed_) internal virtual {} + function _afterExecutionSucceeded(bytes32 jobKey_, uint256 actualKeeperId_, uint256 binJob_) internal virtual {} + function _afterInitiateRedeem(uint256 keeperId_) internal view virtual {} + function _afterRegisterJob(bytes32 jobKey_) internal virtual {} + function _afterDepositJobCredits(bytes32 jobKey_) internal virtual {} + function _afterWithdrawJobCredits(bytes32 jobKey_) internal virtual {} + function _afterAcceptJobTransfer(bytes32 jobKey_) internal virtual {} + function _afterRegisterAsKeeper(uint256 keeperId_) internal virtual {} + + /*** CONSTANT GETTERS ***/ + function getStrategy() public pure virtual returns (string memory) { + return "basic"; + } + + function _getJobGasOverhead() internal pure virtual returns (uint256) { + return 40_000; + } + + /*** UPKEEP INTERFACE ***/ + + /** + * Executes a job. + * The method arguments a tightly coupled with a custom layout in order to save some gas. + * The calldata has the following layout : + * 0x 00000000 1b48315d66ba5267aac8d0ab63c49038b56b1dbc 0000f1 03 00001a 402b2eed11 + * name selector jobContractAddress jobId config keeperId calldata (optional) + * size b bytes4 bytes20 uint24 uint8 uint24 any + * size u uint32 uint160 bytes3 bytes1 bytes3 any + * bits 0-3 4-23 24-26 27-27 28-30 31+ + */ + function execute_44g58pv() external { + uint256 gasStart = gasleft(); + bytes32 jobKey; + + assembly ("memory-safe") { + // size of (address(bytes20)+id(uint24/bytes3)) + let size := 23 + + // keccack256(address+id(uint24)) to memory to generate jobKey + calldatacopy(0, 4, size) + jobKey := keccak256(0, size) + } + + address jobAddress; + uint256 actualKeeperId; + uint256 cfg; + + assembly ("memory-safe") { + // load jobAddress, cfg, and keeperId from calldata to the stack + jobAddress := shr(96, calldataload(4)) + cfg := shr(248, calldataload(27)) + actualKeeperId := shr(232, calldataload(28)) + } + + uint256 binJob = getJobRaw(jobKey); + + _beforeExecute(jobKey, actualKeeperId, binJob); + + // 0. Keeper has sufficient stake + { + uint256 minKeeperCvp_ = minKeeperCvp; + if (keepers[actualKeeperId].worker != msg.sender) { + revert KeeperWorkerNotAuthorized(); + } + if (keepers[actualKeeperId].cvpStake < minKeeperCvp_) { + revert InsufficientKeeperStake(); + } + + // Execution LOCK + _setExecutionLock(1); + } + + // 1. Assert the job is active + { + if (!ConfigFlags.check(binJob, CFG_ACTIVE)) { + revert InactiveJob(jobKey); + } + } + + // 2. Assert job-scoped keeper's minimum CVP deposit + if (ConfigFlags.check(binJob, CFG_CHECK_KEEPER_MIN_CVP_DEPOSIT) && keepers[actualKeeperId].cvpStake < jobMinKeeperCvp[jobKey]) { + revert InsufficientJobScopedKeeperStake(); + } + + // 3. For interval job ensure the interval has passed + { + uint256 intervalSeconds = (binJob << 32) >> 232; + + if (intervalSeconds > 0) { + uint256 lastExecutionAt = binJob >> 224; + if (lastExecutionAt > 0) { + uint256 nextExecutionAt; + unchecked { + nextExecutionAt = lastExecutionAt + intervalSeconds; + } + if (nextExecutionAt > block.timestamp) { + revert IntervalNotReached(lastExecutionAt, intervalSeconds, block.timestamp); + } + } + } + } + + // 4. Ensure gas price fits base fee + uint256 maxBaseFee = _checkBaseFee(binJob, cfg); + + // 5. Ensure msg.sender is EOA + if (msg.sender != tx.origin) { + revert NonEOASender(); + } + + bool ok; + + // Source: Selector + CalldataSourceType calldataSource = CalldataSourceType((binJob << 56) >> 248); + if (calldataSource == CalldataSourceType.SELECTOR) { + bytes4 selector; + assembly ("memory-safe") { + selector := shl(224, shr(8, binJob)) + } + (ok,) = jobAddress.call{ gas: gasleft() - 50_000 }(bytes.concat(abi.encode(selector), jobKey)); + // Source: Bytes + } else if (calldataSource == CalldataSourceType.PRE_DEFINED) { + (ok,) = jobAddress.call{ gas: gasleft() - 50_000 }(bytes.concat(preDefinedCalldatas[jobKey], jobKey)); + // Source: Resolver + } else if (calldataSource == CalldataSourceType.RESOLVER || calldataSource == CalldataSourceType.OFFCHAIN) { + bytes32 cdataHash; + if (ConfigFlags.check(binJob, CFG_CALL_RESOLVER_BEFORE_EXECUTE)) { + cdataHash = _checkJobResolverCall(jobKey); + } + assembly ("memory-safe") { + let cdInCdSize := calldatasize() + // calldata offset is 31 + let beforeCdSize := 31 + let ptr := mload(0x40) + if lt(cdInCdSize, beforeCdSize) { + // revert MissingInputCalldata() + mstore(ptr, 0x47a0bafb00000000000000000000000000000000000000000000000000000000) + revert(ptr, 4) + } + let cdSize := sub(cdInCdSize, beforeCdSize) + mstore(0x40, add(add(ptr, cdSize), 0x20)) + calldatacopy(ptr, beforeCdSize, cdSize) + mstore(add(ptr, cdSize), jobKey) + // CFG_ASSERT_RESOLVER_SELECTOR = 0x04 from PPAgentLiteFlags + if and(binJob, 0x04) { + if iszero(eq( + // actual + shl(224, shr(224, calldataload(31))), + // expected + shl(224, shr(8, binJob)) + )) { + // revert SelectorCheckFailed() + mstore(ptr, 0x74ab678100000000000000000000000000000000000000000000000000000000) + revert(ptr, 4) + } + } + if and(binJob, 0x10) { + if eq(calldataSource, 2) { + let hash := keccak256(ptr, cdSize) + if iszero(eq(hash, cdataHash)) { + // revert JobCheckCalldataError() + mstore(ptr, 0x428ac18600000000000000000000000000000000000000000000000000000000) + revert(ptr, 4) + } + } + } + // The remaining gas could not be less than 50_000 + ok := call(sub(gas(), 50000), jobAddress, 0, ptr, add(cdSize, 0x20), 0x0, 0x0) + } + } else { + // Should never be reached + revert InvalidCalldataSource(); + } + + // Load returned response only if the job call had failed + bytes memory executionResponse; + if (!ok) { + assembly ("memory-safe") { + let size := returndatasize() + if gt(size, 0) { + executionResponse := mload(0x40) + mstore(executionResponse, size) + let p := add(executionResponse, 0x20) + returndatacopy(p, 0, size) + mstore(0x40, add(executionResponse, add(32, size))) + } + } + } + + // Payout block + uint256 compensation; + uint256 gasUsed; + { + binJob = getJobRaw(jobKey); + unchecked { + gasUsed = gasStart - gasleft(); + } + + { + uint256 min = block.basefee; + if (maxBaseFee < min) { + min = maxBaseFee; + } + + compensation = calculateCompensation(ok, binJob, actualKeeperId, min, gasUsed); + } + { + bool jobChanged; + + if (ConfigFlags.check(binJob, CFG_USE_JOB_OWNER_CREDITS)) { + // use job owner credits + _useJobOwnerCredits(ok, jobKey, compensation); + } else { + // use job credits + uint256 creditsBefore = (binJob << 128) >> 168; + if (creditsBefore < compensation) { + if (ok) { + revert InsufficientJobCredits(creditsBefore, compensation); + } else { + compensation = creditsBefore; + } + } + + uint256 creditsAfter; + unchecked { + creditsAfter = creditsBefore - compensation; + } + // update job credits + binJob = binJob & BM_CLEAR_CREDITS | (creditsAfter << 40); + jobChanged = true; + } + + if (ConfigFlags.check(cfg, FLAG_ACCRUE_REWARD)) { + compensations[actualKeeperId] += compensation; + } else { + payable(msg.sender).transfer(compensation); + } + + // Update lastExecutionAt for interval jobs + { + uint256 intervalSeconds = (binJob << 32) >> 232; + if (intervalSeconds > 0) { + uint256 lastExecutionAt = uint32(block.timestamp); + binJob = binJob & BM_CLEAR_LAST_UPDATE_AT | (lastExecutionAt << 224); + jobChanged = true; + } + } + + if (jobChanged) { + _updateRawJob(jobKey, binJob); + } + } + } + + // Execution UNLOCK + _setExecutionLock(0); + + if (ok) { + // Transaction succeeded + emit Execute( + jobKey, + jobAddress, + actualKeeperId, + gasUsed, + block.basefee, + tx.gasprice, + compensation, + bytes32(binJob) + ); + + _afterExecutionSucceeded(jobKey, actualKeeperId, binJob); + } else { + // Tx reverted + _afterExecutionReverted(jobKey, calldataSource, actualKeeperId, executionResponse, compensation); + } + + _afterExecute(actualKeeperId, gasUsed); + } + + function _checkJobResolverCall(bytes32 jobKey_) internal returns (bytes32 hash) { + (bool ok, bytes memory result) = address(this).call( + abi.encodeWithSelector(PPAgentV2Based.checkCouldBeExecuted.selector, resolvers[jobKey_].resolverAddress, resolvers[jobKey_].resolverCalldata) + ); + if (ok) { + revert UnexpectedCodeBlock(); + } + + bytes4 selector = bytes4(result); + if (selector == PPAgentV2Based.JobCheckCanNotBeExecuted.selector) { + assembly ("memory-safe") { + revert(add(32, result), mload(result)) + } + } else if (selector != PPAgentV2Based.JobCheckCanBeExecuted.selector) { + revert JobCheckUnexpectedError(); + } // else resolver was executed + + uint256 len; + assembly ("memory-safe") { + len := mload(result) + } + // We need at least canExecute flag. 32 * 4 + 4. + if (len < 132) { + revert UnableToDecodeResolverResponse(); + } + + uint256 canExecute; + assembly ("memory-safe") { + canExecute := mload(add(result, 100)) + // 5 * 32 + 4 + let dataLen := mload(add(result, 164)) + // starts from 6 * 32 + 4 + hash := keccak256(add(result, 196), dataLen) + } + + if (canExecute != 1) { + revert JobCheckResolverReturnedFalse(); + } + return hash; + } + + function _getBaseFee(uint256 binJob_) internal pure returns (uint256 maxBaseFee) { + unchecked { + maxBaseFee = ((binJob_ << 112) >> 240) * 1 gwei; + } + } + + function _checkBaseFee(uint256 binJob_, uint256 cfg_) internal view virtual returns (uint256) { + uint256 maxBaseFee = _getBaseFee(binJob_); + if (block.basefee > maxBaseFee && !ConfigFlags.check(cfg_, FLAG_ACCEPT_MAX_BASE_FEE_LIMIT)) { + revert BaseFeeGtGasPrice(block.basefee, maxBaseFee); + } + return maxBaseFee; + } + + function _afterExecutionReverted( + bytes32 jobKey_, + CalldataSourceType cdSource_, + uint256 actualKeeperId_, + bytes memory executionResponse_, + uint256 + ) internal virtual { + jobKey_; + actualKeeperId_; + cdSource_; + + if (executionResponse_.length == 0) { + revert JobCallRevertedWithoutDetails(); + } else { + assembly ("memory-safe") { + revert(add(32, executionResponse_), mload(executionResponse_)) + } + } + } + + function calculateCompensation( + bool ok_, + uint256 job_, + uint256 keeperId_, + uint256 baseFee_, + uint256 gasUsed_ + ) public view virtual returns (uint256) { + ok_; // silence unused param warning + keeperId_; // silence unused param warning + uint256 fixedReward = (job_ << 64) >> 224; + uint256 rewardPct = (job_ << 96) >> 240; + return calculateCompensationPure(rewardPct, fixedReward, baseFee_, gasUsed_); + } + + function _useJobOwnerCredits(bool ok_, bytes32 jobKey_, uint256 compensation_) internal { + uint256 jobOwnerCreditsBefore = jobOwnerCredits[jobOwners[jobKey_]]; + if (jobOwnerCreditsBefore < compensation_) { + if (ok_) { + revert InsufficientJobOwnerCredits(jobOwnerCreditsBefore, compensation_); + } else { + compensation_ = jobOwnerCreditsBefore; + } + } + + unchecked { + jobOwnerCredits[jobOwners[jobKey_]] = jobOwnerCreditsBefore - compensation_; + } + } + + /*** JOB OWNER INTERFACE ***/ + + /** + * Registers a new job. + * + * Job id is unique counter for a given job address. Up to 2**24-1 jobs per address. + * Job key is a keccak256(address, jobId). + * The following options are immutable: + * - `params_.jobaddress` + * - `params_.calldataSource` + * If you need to modify one of the immutable options above later consider creating a new job. + * + * @param params_ Job-specific params + * @param resolver_ Resolver details(address, calldata), required only for CALLDATA_SOURCE_RESOLVER + * job type. Use empty values for the other job types. + * @param preDefinedCalldata_ Calldata to call a job with, required only for CALLDATA_SOURCE_PRE_DEFINED + * job type. Keep empty for the other job types. + */ + function registerJob( + RegisterJobParams calldata params_, + Resolver calldata resolver_, + bytes calldata preDefinedCalldata_ + ) external payable virtual returns (bytes32 jobKey, uint256 jobId){ + jobId = jobLastIds[params_.jobAddress] + 1; + + if (jobId > type(uint24).max) { + revert JobIdOverflow(); + } + + if (msg.value > type(uint88).max) { + revert CreditsDepositOverflow(); + } + + if (params_.jobAddress == address(0)) { + revert MissingJobAddress(); + } + + if (params_.calldataSource > 3) { + revert InvalidCalldataSource(); + } + + if (params_.jobAddress == address(CVP) || params_.jobAddress == address(this)) { + revert InvalidJobAddress(); + } + + _assertInterval(params_.intervalSeconds, CalldataSourceType(params_.calldataSource)); + _assertJobParams(params_.maxBaseFeeGwei, params_.fixedReward, params_.rewardPct); + jobKey = getJobKey(params_.jobAddress, jobId); + + emit RegisterJob( + jobKey, + params_.jobAddress, + jobId, + msg.sender, + params_ + ); + + if (CalldataSourceType(params_.calldataSource) == CalldataSourceType.PRE_DEFINED) { + _setJobPreDefinedCalldata(jobKey, preDefinedCalldata_); + } else if ( + CalldataSourceType(params_.calldataSource) == CalldataSourceType.RESOLVER || + CalldataSourceType(params_.calldataSource) == CalldataSourceType.OFFCHAIN + ) { + _setJobResolver(jobKey, resolver_); + } + + { + bytes4 selector = 0x00000000; + if (CalldataSourceType(params_.calldataSource) != CalldataSourceType.PRE_DEFINED) { + selector = params_.jobSelector; + } + + uint256 config = CFG_ACTIVE; + if (params_.useJobOwnerCredits) { + config = config | CFG_USE_JOB_OWNER_CREDITS; + } + if (params_.assertResolverSelector) { + config = config | CFG_ASSERT_RESOLVER_SELECTOR; + } + if (params_.jobMinCvp > 0) { + config = config | CFG_CHECK_KEEPER_MIN_CVP_DEPOSIT; + } + + jobs[jobKey] = Job({ + config: uint8(config), + selector: selector, + credits: 0, + maxBaseFeeGwei: params_.maxBaseFeeGwei, + fixedReward: params_.fixedReward, + rewardPct: params_.rewardPct, + calldataSource: params_.calldataSource, + + // For interval jobs + intervalSeconds: params_.intervalSeconds, + lastExecutionAt: 0 + }); + jobMinKeeperCvp[jobKey] = params_.jobMinCvp; + } + + jobLastIds[params_.jobAddress] = jobId; + jobOwners[jobKey] = msg.sender; + + if (msg.value > 0) { + if (params_.useJobOwnerCredits) { + _processJobOwnerCreditsDeposit(msg.sender); + } else { + _processJobCreditsDeposit(jobKey); + } + } + + _afterRegisterJob(jobKey); + } + + /** + * Updates a job details. + * + * The following options are immutable: + * - `jobAddress` + * - `job.selector` + * - `job.calldataSource` + * If you need to modify one of the immutable options above later consider creating a new job. + * + * @param jobKey_ The job key + * @param maxBaseFeeGwei_ The maximum basefee in gwei to use for a job compensation + * @param rewardPct_ The reward premium in pct, where 1 == 1% + * @param fixedReward_ The fixed reward divided by FIXED_PAYMENT_MULTIPLIER + * @param jobMinCvp_ The keeper minimal CVP stake to be eligible to execute this job + * @param intervalSeconds_ The interval for a job execution + */ + function updateJob( + bytes32 jobKey_, + uint16 maxBaseFeeGwei_, + uint16 rewardPct_, + uint32 fixedReward_, + uint256 jobMinCvp_, + uint24 intervalSeconds_ + ) external { + _assertOnlyJobOwner(jobKey_); + _assertExecutionNotLocked(); + _assertJobParams(maxBaseFeeGwei_, fixedReward_, rewardPct_); + _assertInterval(intervalSeconds_, CalldataSourceType(jobs[jobKey_].calldataSource)); + + uint256 cfg = jobs[jobKey_].config; + + if (jobMinCvp_ > 0 && !ConfigFlags.check(jobs[jobKey_].config, CFG_CHECK_KEEPER_MIN_CVP_DEPOSIT)) { + cfg = cfg | CFG_CHECK_KEEPER_MIN_CVP_DEPOSIT; + } + if (jobMinCvp_ == 0 && ConfigFlags.check(jobs[jobKey_].config, CFG_CHECK_KEEPER_MIN_CVP_DEPOSIT)) { + cfg = cfg ^ CFG_CHECK_KEEPER_MIN_CVP_DEPOSIT; + } + + jobs[jobKey_].config = uint8(cfg); + jobMinKeeperCvp[jobKey_] = jobMinCvp_; + + jobs[jobKey_].maxBaseFeeGwei = maxBaseFeeGwei_; + jobs[jobKey_].rewardPct = rewardPct_; + jobs[jobKey_].fixedReward = fixedReward_; + jobs[jobKey_].intervalSeconds = intervalSeconds_; + + emit JobUpdate(jobKey_, maxBaseFeeGwei_, rewardPct_, fixedReward_, jobMinCvp_, intervalSeconds_); + } + + /** + * A job owner updates job resolver details. + * + * @param jobKey_ The jobKey + * @param resolver_ The new job resolver details + */ + function setJobResolver(bytes32 jobKey_, Resolver calldata resolver_) external { + _assertOnlyJobOwner(jobKey_); + _assertExecutionNotLocked(); + + if ( + CalldataSourceType(jobs[jobKey_].calldataSource) != CalldataSourceType.RESOLVER && + CalldataSourceType(jobs[jobKey_].calldataSource) != CalldataSourceType.OFFCHAIN + ) { + revert NotSupportedByJobCalldataSource(); + } + + _setJobResolver(jobKey_, resolver_); + } + + function _setJobResolver(bytes32 jobKey_, Resolver calldata resolver_) internal { + if (resolver_.resolverAddress == address(0)) { + revert MissingResolverAddress(); + } + resolvers[jobKey_] = resolver_; + emit SetJobResolver(jobKey_, resolver_.resolverAddress, resolver_.resolverCalldata); + } + + /** + * A job owner updates pre-defined calldata. + * + * @param jobKey_ The jobKey + * @param preDefinedCalldata_ The new job pre-defined calldata + */ + function setJobPreDefinedCalldata(bytes32 jobKey_, bytes calldata preDefinedCalldata_) external { + _assertOnlyJobOwner(jobKey_); + _assertExecutionNotLocked(); + + if (CalldataSourceType(jobs[jobKey_].calldataSource) != CalldataSourceType.PRE_DEFINED) { + revert NotSupportedByJobCalldataSource(); + } + + _setJobPreDefinedCalldata(jobKey_, preDefinedCalldata_); + } + + function _setJobPreDefinedCalldata(bytes32 jobKey_, bytes calldata preDefinedCalldata_) internal { + preDefinedCalldatas[jobKey_] = preDefinedCalldata_; + emit SetJobPreDefinedCalldata(jobKey_, preDefinedCalldata_); + } + + /** + * A job owner updates a job config flag. + * + * @param jobKey_ The jobKey + * @param isActive_ Whether the job is active or not + * @param useJobOwnerCredits_ The useJobOwnerCredits flag + * @param assertResolverSelector_ The assertResolverSelector flag + */ + function setJobConfig( + bytes32 jobKey_, + bool isActive_, + bool useJobOwnerCredits_, + bool assertResolverSelector_, + bool callResolverBeforeExecute_ + ) public virtual { + _assertOnlyJobOwner(jobKey_); + _assertExecutionNotLocked(); + uint256 newConfig = 0; + + if (isActive_) { + newConfig = newConfig | CFG_ACTIVE; + } + if (useJobOwnerCredits_) { + newConfig = newConfig | CFG_USE_JOB_OWNER_CREDITS; + } + if (assertResolverSelector_) { + newConfig = newConfig | CFG_ASSERT_RESOLVER_SELECTOR; + } + if (callResolverBeforeExecute_) { + newConfig = newConfig | CFG_CALL_RESOLVER_BEFORE_EXECUTE; + } + + uint256 job = getJobRaw(jobKey_) & BM_CLEAR_CONFIG | newConfig; + _updateRawJob(jobKey_, job); + + emit SetJobConfig(jobKey_, isActive_, useJobOwnerCredits_, assertResolverSelector_, callResolverBeforeExecute_); + } + + function _updateRawJob(bytes32 jobKey_, uint256 job_) internal { + Job storage job = jobs[jobKey_]; + assembly ("memory-safe") { + sstore(job.slot, job_) + } + } + + /** + * A job owner initiates the job transfer to a new owner. + * The actual owner doesn't update until the pending owner accepts the transfer. + * + * @param jobKey_ The jobKey + * @param to_ The new job owner + */ + function initiateJobTransfer(bytes32 jobKey_, address to_) external { + _assertOnlyJobOwner(jobKey_); + _assertExecutionNotLocked(); + jobPendingTransfers[jobKey_] = to_; + emit InitiateJobTransfer(jobKey_, msg.sender, to_); + } + + /** + * A pending job owner accepts the job transfer. + * + * @param jobKey_ The jobKey + */ + function acceptJobTransfer(bytes32 jobKey_) external { + _assertExecutionNotLocked(); + + if (msg.sender != jobPendingTransfers[jobKey_]) { + revert OnlyPendingOwner(); + } + + jobOwners[jobKey_] = msg.sender; + delete jobPendingTransfers[jobKey_]; + + _afterAcceptJobTransfer(jobKey_); + + emit AcceptJobTransfer(jobKey_, msg.sender); + } + + /** + * Top-ups the job credits in NATIVE tokens. + * + * @param jobKey_ The jobKey to deposit for + */ + function depositJobCredits(bytes32 jobKey_) external virtual payable { + if (msg.value == 0) { + revert MissingDeposit(); + } + + if (jobOwners[jobKey_] == address(0)) { + revert JobWithoutOwner(); + } + + _processJobCreditsDeposit(jobKey_); + + _afterDepositJobCredits(jobKey_); + } + + function _processJobCreditsDeposit(bytes32 jobKey_) internal { + (uint256 fee, uint256 amount) = _calculateDepositFee(); + uint256 creditsAfter = jobs[jobKey_].credits + amount; + if (creditsAfter > type(uint88).max) { + revert CreditsDepositOverflow(); + } + + unchecked { + feeTotal += fee; + } + jobs[jobKey_].credits = uint88(creditsAfter); + + emit DepositJobCredits(jobKey_, msg.sender, amount, fee); + } + + function _calculateDepositFee() internal view returns (uint256 fee, uint256 amount) { + fee = msg.value * feePpm / 1e6 /* 100% in ppm */; + amount = msg.value - fee; + } + + /** + * A job owner withdraws the job credits in NATIVE tokens. + * + * @param jobKey_ The jobKey + * @param to_ The address to send NATIVE tokens to + * @param amount_ The amount to withdraw. Use type(uint256).max for the total available credits withdrawal. + */ + function withdrawJobCredits( + bytes32 jobKey_, + address payable to_, + uint256 amount_ + ) external { + uint88 creditsBefore = jobs[jobKey_].credits; + if (amount_ == type(uint256).max) { + amount_ = creditsBefore; + } + + _assertOnlyJobOwner(jobKey_); + _assertExecutionNotLocked(); + if (amount_ == 0) { + revert MissingAmount(); + } + + if (creditsBefore < amount_) { + revert CreditsWithdrawalUnderflow(); + } + + unchecked { + jobs[jobKey_].credits = creditsBefore - uint88(amount_); + } + + to_.transfer(amount_); + + emit WithdrawJobCredits(jobKey_, msg.sender, to_, amount_); + + _afterWithdrawJobCredits(jobKey_); + } + + /** + * Top-ups the job owner credits in NATIVE tokens. + * + * @param for_ The job owner address to deposit for + */ + function depositJobOwnerCredits(address for_) external payable { + if (msg.value == 0) { + revert MissingDeposit(); + } + + _processJobOwnerCreditsDeposit(for_); + } + + function _processJobOwnerCreditsDeposit(address for_) internal { + (uint256 fee, uint256 amount) = _calculateDepositFee(); + + unchecked { + feeTotal += fee; + jobOwnerCredits[for_] += amount; + } + + emit DepositJobOwnerCredits(for_, msg.sender, amount, fee); + } + + /** + * A job owner withdraws the job owner credits in NATIVE tokens. + * + * @param to_ The address to send NATIVE tokens to + * @param amount_ The amount to withdraw. Use type(uint256).max for the total available credits withdrawal. + */ + function withdrawJobOwnerCredits(address payable to_, uint256 amount_) external { + uint256 creditsBefore = jobOwnerCredits[msg.sender]; + if (amount_ == type(uint256).max) { + amount_ = creditsBefore; + } + + _assertExecutionNotLocked(); + if (amount_ == 0) { + revert MissingAmount(); + } + + if (creditsBefore < amount_) { + revert CreditsWithdrawalUnderflow(); + } + + unchecked { + jobOwnerCredits[msg.sender] = creditsBefore - amount_; + } + + to_.transfer(amount_); + + emit WithdrawJobOwnerCredits(msg.sender, to_, amount_); + } + + /*** KEEPER INTERFACE ***/ + + /** + * Actor registers as a keeper. + * One keeper address could have multiple keeper IDs. Requires at least `minKeepCvp` as an initial CVP deposit. + * + * @dev Overflow-safe only for CVP which total supply is less than type(uint96).max + * @dev Maximum 2^24-1 keepers supported. There is no explicit check for overflow, but the keepers with ID >= 2^24 + * won't be able to perform upkeep operations. + * + * @param worker_ The worker address + * @param initialDepositAmount_ The initial CVP deposit. Should be no less than `minKeepCvp` + * @return keeperId The registered keeper ID + */ + function registerAsKeeper(address worker_, uint256 initialDepositAmount_) public virtual returns (uint256 keeperId) { + if (workerKeeperIds[worker_] != 0) { + revert WorkerAlreadyAssigned(); + } + if (initialDepositAmount_ < minKeeperCvp) { + revert InsufficientAmount(); + } + + keeperId = ++lastKeeperId; + keeperAdmins[keeperId] = msg.sender; + keepers[keeperId] = Keeper(worker_, 0, false); + workerKeeperIds[worker_] = keeperId; + emit RegisterAsKeeper(keeperId, msg.sender, worker_); + + _afterRegisterAsKeeper(keeperId); + + _stake(keeperId, initialDepositAmount_); + } + + /** + * A keeper updates a keeper worker address + * + * @param keeperId_ The keeper ID + * @param worker_ The new worker address + */ + function setWorkerAddress(uint256 keeperId_, address worker_) external { + _assertOnlyKeeperAdmin(keeperId_); + if (workerKeeperIds[worker_] != 0) { + revert WorkerAlreadyAssigned(); + } + + address prev = keepers[keeperId_].worker; + delete workerKeeperIds[prev]; + workerKeeperIds[worker_] = keeperId_; + keepers[keeperId_].worker = worker_; + + emit SetWorkerAddress(keeperId_, prev, worker_); + } + + /** + * A keeper withdraws NATIVE token rewards. + * + * @param keeperId_ The keeper ID + * @param to_ The address to withdraw to + * @param amount_ The amount to withdraw. Use type(uint256).max for the total available compensation withdrawal. + */ + function withdrawCompensation(uint256 keeperId_, address payable to_, uint256 amount_) external { + uint256 available = compensations[keeperId_]; + if (amount_ == type(uint256).max) { + amount_ = available; + } + + if (amount_ == 0) { + revert MissingAmount(); + } + _assertOnlyKeeperAdminOrWorker(keeperId_); + + if (amount_ > available) { + revert WithdrawAmountExceedsAvailable(amount_, available); + } + + unchecked { + compensations[keeperId_] = available - amount_; + } + + to_.transfer(amount_); + + emit WithdrawCompensation(keeperId_, to_, amount_); + } + + /** + * Deposits CVP for the given keeper ID. The beneficiary receives a derivative erc20 token in exchange of CVP. + * Accounts the staking amount on the beneficiary's stakeOf balance. + * + * @param keeperId_ The keeper ID + * @param amount_ The amount to stake + */ + function stake(uint256 keeperId_, uint256 amount_) external { + if (amount_ == 0) { + revert MissingAmount(); + } + _assertKeeperIdExists(keeperId_); + _stake(keeperId_, amount_); + } + + function _stake(uint256 keeperId_, uint256 amount_) internal { + uint256 amountAfter = keepers[keeperId_].cvpStake + amount_; + if (amountAfter > type(uint88).max) { + revert StakeAmountOverflow(); + } + IERC20(CVP).transferFrom(msg.sender, address(this), amount_); + keepers[keeperId_].cvpStake += uint88(amount_); + + emit Stake(keeperId_, amount_, msg.sender); + } + + /** + * A keeper initiates CVP withdrawal. + * The given CVP amount needs to go through the cooldown stage. After the cooldown is complete this amount could be + * withdrawn using `finalizeRedeem()` method. + * The msg.sender burns the paCVP token in exchange of the corresponding CVP amount. + * Accumulates the existing pending for withdrawal amounts and re-initiates cooldown period. + * If there is any slashed amount for the msg.sender, it should be compensated within the first initiateRedeem transaction + * by burning the equivalent amount of paCVP tokens. The remaining CVP tokens won't be redeemed unless the slashed + * amount is compensated. + * + * @param keeperId_ The keeper ID + * @param amount_ The amount to cooldown + * @return pendingWithdrawalAfter The total pending for withdrawal amount + */ + function initiateRedeem(uint256 keeperId_, uint256 amount_) external returns (uint256 pendingWithdrawalAfter) { + _assertOnlyKeeperAdmin(keeperId_); + if (amount_ == 0) { + revert MissingAmount(); + } + _beforeInitiateRedeem(keeperId_); + + uint256 stakeOfBefore = keepers[keeperId_].cvpStake; + uint256 slashedStakeOfBefore = slashedStakeOf[keeperId_]; + uint256 totalStakeBefore = stakeOfBefore + slashedStakeOfBefore; + + // Should burn at least the total slashed stake + if (amount_ < slashedStakeOfBefore) { + revert InsufficientAmountToCoverSlashedStake(amount_, slashedStakeOfBefore); + } + + if (amount_ > totalStakeBefore) { + revert AmountGtStake(amount_, stakeOfBefore, slashedStakeOfBefore); + } + + slashedStakeOf[keeperId_] = 0; + uint256 stakeOfToReduceAmount; + unchecked { + stakeOfToReduceAmount = amount_ - slashedStakeOfBefore; + keepers[keeperId_].cvpStake = uint88(stakeOfBefore - stakeOfToReduceAmount); + pendingWithdrawalAmounts[keeperId_] += stakeOfToReduceAmount; + } + + pendingWithdrawalAfter = block.timestamp + pendingWithdrawalTimeoutSeconds; + pendingWithdrawalEndsAt[keeperId_] = pendingWithdrawalAfter; + + _afterInitiateRedeem(keeperId_); + + emit InitiateRedeem(keeperId_, amount_, stakeOfToReduceAmount, slashedStakeOfBefore); + } + + /** + * A keeper finalizes CVP withdrawal and receives the staked CVP tokens. + * + * @param keeperId_ The keeper ID + * @param to_ The address to transfer CVP to + * @return redeemedCvp The redeemed CVP amount + */ + function finalizeRedeem(uint256 keeperId_, address to_) external returns (uint256 redeemedCvp) { + _assertOnlyKeeperAdmin(keeperId_); + + if (pendingWithdrawalEndsAt[keeperId_] > block.timestamp) { + revert WithdrawalTimoutNotReached(); + } + + redeemedCvp = pendingWithdrawalAmounts[keeperId_]; + if (redeemedCvp == 0) { + revert NoPendingWithdrawal(); + } + + pendingWithdrawalAmounts[keeperId_] = 0; + IERC20(CVP).transfer(to_, redeemedCvp); + + emit FinalizeRedeem(keeperId_, to_, redeemedCvp); + } + + /*** CONTRACT OWNER INTERFACE ***/ + /** + * Slashes any keeper_ for an amount within keeper's deposit. + * Penalises a keeper for malicious behaviour like sandwitching upkeep transactions. + * + * @param keeperId_ The keeper ID to slash + * @param to_ The address to send the slashed CVP to + * @param currentAmount_ The amount to slash from the current keeper.cvpStake balance + * @param pendingAmount_ The amount to slash from the pendingWithdrawals balance + */ + function ownerSlash(uint256 keeperId_, address to_, uint256 currentAmount_, uint256 pendingAmount_) public { + _checkOwner(); + uint256 totalAmount = currentAmount_ + pendingAmount_; + if (totalAmount == 0) { + revert MissingAmount(); + } + + if (currentAmount_ > 0) { + keepers[keeperId_].cvpStake -= uint88(currentAmount_); + slashedStakeOf[keeperId_] += currentAmount_; + } + + if (pendingAmount_ > 0) { + pendingWithdrawalAmounts[keeperId_] -= pendingAmount_; + } + + IERC20(CVP).transfer(to_, totalAmount); + + emit OwnerSlash(keeperId_, to_, currentAmount_, pendingAmount_); + } + + /** + * Owner withdraws all the accrued rewards in native tokens to the provided address. + * + * @param to_ The address to send rewards to + */ + function withdrawFees(address payable to_) external { + _checkOwner(); + + uint256 amount = feeTotal; + feeTotal = 0; + + to_.transfer(amount); + + emit WithdrawFees(to_, amount); + } + + /** + * Owner updates minKeeperCVP value + * + * @param minKeeperCvp_ The new minKeeperCVP value + */ + function setAgentParams( + uint256 minKeeperCvp_, + uint256 timeoutSeconds_, + uint256 feePpm_ + ) external { + _checkOwner(); + _assertExecutionNotLocked(); + _setAgentParams(minKeeperCvp_, timeoutSeconds_, feePpm_); + } + + function _setAgentParams( + uint256 minKeeperCvp_, + uint256 timeoutSeconds_, + uint256 feePpm_ + ) internal { + if (minKeeperCvp_ == 0) { + revert InvalidMinKeeperCvp(); + } + if (timeoutSeconds_ > MAX_PENDING_WITHDRAWAL_TIMEOUT_SECONDS) { + revert TimeoutTooBig(); + } + if (feePpm_ > MAX_FEE_PPM) { + revert FeeTooBig(); + } + + minKeeperCvp = minKeeperCvp_; + pendingWithdrawalTimeoutSeconds = timeoutSeconds_; + feePpm = feePpm_; + + emit SetAgentParams(minKeeperCvp_, timeoutSeconds_, feePpm_); + } + + /*** GETTERS ***/ + + /** + * Pure method that calculates keeper compensation based on a dynamic and a fixed multipliers. + * DANGER: could overflow when used externally + * + * @param rewardPct_ The fixed percent. uint16. 0 == 0%, 100 == 100%, 500 == 500%, max 56535 == 56535% + * @param fixedReward_ The fixed reward. uint32. Always multiplied by 1e15 (FIXED_PAYMENT_MULTIPLIER). + * For ex. 2 == 2e15, 1_000 = 1e18, max 4294967295 == 4_294_967.295e18 + * @param blockBaseFee_ The block.basefee value. + * @param gasUsed_ The gas used in wei. + * + */ + function calculateCompensationPure( + uint256 rewardPct_, + uint256 fixedReward_, + uint256 blockBaseFee_, + uint256 gasUsed_ + ) public pure returns (uint256) { + unchecked { + return (gasUsed_ + _getJobGasOverhead()) * blockBaseFee_ * rewardPct_ / 100 + + fixedReward_ * FIXED_PAYMENT_MULTIPLIER; + } + } + + function getKeeperWorkerAndStake(uint256 keeperId_) + external view returns ( + address worker, + uint256 currentStake, + bool isActive + ) + { + return ( + keepers[keeperId_].worker, + keepers[keeperId_].cvpStake, + keepers[keeperId_].isActive + ); + } + + function getConfig() + external view returns ( + uint256 minKeeperCvp_, + uint256 pendingWithdrawalTimeoutSeconds_, + uint256 feeTotal_, + uint256 feePpm_, + uint256 lastKeeperId_ + ) + { + return ( + minKeeperCvp, + pendingWithdrawalTimeoutSeconds, + feeTotal, + feePpm, + lastKeeperId + ); + } + + 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_]; + + currentStake = keepers[keeperId_].cvpStake; + isActive = keepers[keeperId_].isActive; + worker = keepers[keeperId_].worker; + + admin = keeperAdmins[keeperId_]; + } + + function getJob(bytes32 jobKey_) + external view returns ( + address owner, + address pendingTransfer, + uint256 jobLevelMinKeeperCvp, + Job memory details, + bytes memory preDefinedCalldata, + Resolver memory resolver + ) + { + return ( + jobOwners[jobKey_], + jobPendingTransfers[jobKey_], + jobMinKeeperCvp[jobKey_], + jobs[jobKey_], + preDefinedCalldatas[jobKey_], + resolvers[jobKey_] + ); + } + + /** + * Returns the principal job data stored in a single EVM slot. + * @notice To get parsed job data use `getJob()` method instead. + * + * The job slot data layout: + * 0x0000000000000a000000000a002300640000000de0b6b3a7640000d09de08a01 + * 0x 00000000 00000a 00 0000000a 0023 0064 0000000de0b6b3a7640000 d09de08a 01 + * name lastExecAt interval calldataSource fixedReward rewardPct maxBaseFeeGwei nativeCredits selector config bitmask + * size b bytes4 bytes3 bytes4 bytes4 bytes2 bytes2 bytes11 bytes4 bytes1 + * size u uint32 uint24 uint8 uint32 uint16 uint16 uint88 uint32 uint8 + * bits 0-3 4-6 7-7 8-11 12-13 14-15 16-26 27-30 31-31 + */ + function getJobRaw(bytes32 jobKey_) public view returns (uint256 rawJob) { + Job storage job = jobs[jobKey_]; + assembly ("memory-safe") { + rawJob := sload(job.slot) + } + } + + function getJobKey(address jobAddress_, uint256 jobId_) public pure returns (bytes32 jobKey) { + assembly ("memory-safe") { + mstore(0, shl(96, jobAddress_)) + mstore(20, shl(232, jobId_)) + jobKey := keccak256(0, 23) + } + } + + // The function that always reverts + function checkCouldBeExecuted(address jobAddress_, bytes memory jobCalldata_) external { + // 1. LOCK + _setExecutionLock(1); + // 2. EXECUTE + (bool ok, bytes memory result) = jobAddress_.call(jobCalldata_); + // 3. UNLOCK + _setExecutionLock(0); + + if (ok) { + revert JobCheckCanBeExecuted(result); + } else { + revert JobCheckCanNotBeExecuted(result); + } + } +} diff --git a/contracts/PPAgentV2Lens.sol b/contracts/PPAgentV2Lens.sol index ef36c6c..121ca24 100644 --- a/contracts/PPAgentV2Lens.sol +++ b/contracts/PPAgentV2Lens.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.19; import "./PPAgentV2.sol"; -contract PPAgentV2Lens is PPAgentV2 { - constructor(address cvp_) PPAgentV2(cvp_) { +contract PPAgentV2Lens is PPAgentV2Based { + constructor(address cvp_) PPAgentV2Based(cvp_) { } function isJobActive(bytes32 jobKey_) external view returns (bool) { diff --git a/contracts/PPAgentV2Randao.sol b/contracts/PPAgentV2Randao.sol index 4d803d2..3afa87f 100644 --- a/contracts/PPAgentV2Randao.sol +++ b/contracts/PPAgentV2Randao.sol @@ -1,880 +1,33 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { PPAgentV2, ConfigFlags } from "./PPAgentV2.sol"; -import "./utils/CustomizedEnumerableSet.sol"; -import "./PPAgentV2Flags.sol"; -import "./PPAgentV2Interfaces.sol"; +import {PPAgentV2RandaoBased} from "./PPAgentV2RandaoBased.sol"; /** * @title PPAgentV2Randao * @author PowerPool */ -contract PPAgentV2Randao is IPPAgentV2RandaoViewer, PPAgentV2 { - using EnumerableSet for EnumerableSet.Bytes32Set; - using EnumerableSet for EnumerableSet.UintSet; +contract PPAgentV2Randao is PPAgentV2RandaoBased { - error JobHasKeeperAssigned(uint256 keeperId); - error JobHasNoKeeperAssigned(); - error SlashingEpochBlocksTooLow(); - error InvalidPeriod1(); - error InvalidPeriod2(); - error InvalidSlashingFeeFixedCVP(); - error SlashingBpsGt5000Bps(); - error InvalidStakeDivisor(); - error JobCompensationMultiplierBpsLT10000(); - error InactiveKeeper(); - error KeeperIsAssignedToJobs(uint256 amountOfJobs); - error OnlyCurrentSlasher(uint256 expectedSlasherId); - error OnlyReservedSlasher(uint256 reservedSlasherId); - error TooEarlyForSlashing(uint256 now_, uint256 possibleAfter); - error SlashingNotInitiated(); - error SlashingNotInitiatedExecutionReverted(); - error AssignedKeeperCantSlash(); - error KeeperIsAlreadyActive(); - error KeeperIsAlreadyInactive(); - error CantAssignKeeper(); - error NonIntervalJob(); - error TooEarlyToReinitiateSlashing(); - error TooEarlyToRelease(bytes32 jobKey, uint256 period2End); - error TooEarlyForActivationFinalization(uint256 now, uint256 availableAt); - error KeeperShouldBeDisabledForStakeLTMinKeeperCvp(); - error CantRelease(); - error OnlyNextKeeper( - uint256 assignedKeeperId, - uint256 lastExecutedAt, - uint256 interval, - uint256 slashingInterval, - uint256 _now - ); - error InsufficientKeeperStakeToSlash( - bytes32 jobKey, - uint256 assignedKeeperId, - uint256 keeperCurrentStake, - uint256 amountToSlash - ); - error ActivationNotInitiated(); + constructor(address cvp_) PPAgentV2RandaoBased(cvp_) { - event DisableKeeper(uint256 indexed keeperId); - event InitiateKeeperActivation(uint256 indexed keeperId, uint256 canBeFinalizedAt); - event FinalizeKeeperActivation(uint256 indexed keeperId); - event InitiateKeeperSlashing( - bytes32 indexed jobKey, - uint256 indexed slasherKeeperId, - bool useResolver, - uint256 jobSlashingPossibleAfter - ); - event ExecutionReverted( - bytes32 indexed jobKey, - uint256 indexed assignedKeeperId, - uint256 indexed actualKeeperId, - bytes executionReturndata, - uint256 compensation - ); - event SlashKeeper( - bytes32 indexed jobKey, - uint256 indexed assignedKeeperId, - uint256 indexed actualKeeperId, - uint256 fixedSlashAmount, - uint256 dynamicSlashAmount, - uint256 slashAmountMissing - ); - event SetRdConfig(RandaoConfig rdConfig); - event JobKeeperChanged(bytes32 indexed jobKey, uint256 indexed keeperFrom, uint256 indexed keeperTo); - - IPPAgentV2RandaoViewer.RandaoConfig internal rdConfig; - - // keccak256(jobAddress, id) => nextKeeperId - mapping(bytes32 => uint256) public jobNextKeeperId; - // keccak256(jobAddress, id) => nextSlasherId - mapping(bytes32 => uint256) public jobReservedSlasherId; - // keccak256(jobAddress, id) => timestamp, for non-interval jobs - mapping(bytes32 => uint256) public jobSlashingPossibleAfter; - // keccak256(jobAddress, id) => timestamp - mapping(bytes32 => uint256) public jobCreatedAt; - // keeperId => (pending jobs) - mapping(uint256 => EnumerableSet.Bytes32Set) internal keeperLocksByJob; - // keeperId => timestamp - mapping(uint256 => uint256) public keeperActivationCanBeFinalizedAt; - - EnumerableSet.UintSet internal activeKeepers; - - function getStrategy() public pure override returns (string memory) { - return "randao"; - } - - function _getJobGasOverhead() internal pure override returns (uint256) { - return 136_000; - } - - constructor(address cvp_) PPAgentV2(cvp_) { - } - - function initializeRandao( - address owner_, - uint256 minKeeperCvp_, - uint256 pendingWithdrawalTimeoutSeconds_, - RandaoConfig memory rdConfig_) external { - PPAgentV2.initialize(owner_, minKeeperCvp_, pendingWithdrawalTimeoutSeconds_); - _setRdConfig(rdConfig_); - } - - /*** AGENT OWNER METHODS ***/ - function setRdConfig(RandaoConfig calldata rdConfig_) external onlyOwner { - _setRdConfig(rdConfig_); - } - - function _setRdConfig(RandaoConfig memory rdConfig_) internal { - if (rdConfig_.slashingEpochBlocks < 3) { - revert SlashingEpochBlocksTooLow(); - } - if (rdConfig_.period1 < 15 seconds) { - revert InvalidPeriod1(); - } - if (rdConfig_.period2 < 15 seconds) { - revert InvalidPeriod2(); - } - if (rdConfig_.slashingFeeFixedCVP > (minKeeperCvp / 2)) { - revert InvalidSlashingFeeFixedCVP(); - } - if (rdConfig_.slashingFeeBps > 5000) { - revert SlashingBpsGt5000Bps(); - } - if (rdConfig_.stakeDivisor == 0) { - revert InvalidStakeDivisor(); - } - if (rdConfig_.jobCompensationMultiplierBps < 10_000) { - revert JobCompensationMultiplierBpsLT10000(); - } - emit SetRdConfig(rdConfig_); - - rdConfig = rdConfig_; - } - - function ownerSlashDisable( - uint256 keeperId_, - address to_, - uint256 currentAmount_, - uint256 pendingAmount_, - bool disable_ - ) external { - ownerSlash(keeperId_, to_, currentAmount_, pendingAmount_); - if (disable_) { - _disableKeeper(keeperId_); - } - } - - /*** JOB OWNER METHODS ***/ - /** - * Assigns a keeper for all the jobs in jobKeys_ list. - * The msg.sender should be the owner of all the jobs in the jobKeys_ list. - * Will revert if there is at least one job with an already assigned keeper. - * - * @param jobKeys_ The list of job keys to activate - */ - function assignKeeper(bytes32[] calldata jobKeys_) external { - _assignKeeper(jobKeys_); - } - - /** - * Top-ups the job owner credits in NATIVE tokens AND activates the jobs passed in jobKeys_ array. - * - * If the jobKeys_ list is empty the function behaves the same way as `depositJobOwnerCredits(address for_)`. - * If there is at least one jobKeys_ element the msg.sender should be the owner of all the jobs in the jobKeys_ list. - * Will revert if there is at least one job with an assigned keeper. - * - * @param for_ The job owner address to deposit for - * @param jobKeys_ The list of job keys to activate - */ - function depositJobOwnerCreditsAndAssignKeepers(address for_, bytes32[] calldata jobKeys_) external payable { - if (msg.value == 0) { - revert MissingDeposit(); - } - - _processJobOwnerCreditsDeposit(for_); - - _assignKeeper(jobKeys_); - } - - function _assignKeeper(bytes32[] calldata jobKeys_) internal { - for (uint256 i = 0; i < jobKeys_.length; i++) { - bytes32 jobKey = jobKeys_[i]; - uint256 assignedKeeperId = jobNextKeeperId[jobKey]; - if (jobNextKeeperId[jobKey] != 0) { - revert JobHasKeeperAssigned(assignedKeeperId); - } - _assertOnlyJobOwner(jobKey); - - if (!_assignNextKeeperIfRequiredAndUpdateLastExecutedAt(jobKey, 0)) { - revert CantAssignKeeper(); - } - } } - /*** KEEPER METHODS ***/ - function releaseJob(bytes32 jobKey_) external { - uint256 assignedKeeperId = jobNextKeeperId[jobKey_]; - - // Job owner can unassign a keeper without any restriction - if (msg.sender == jobOwners[jobKey_] || msg.sender == owner()) { - _assertExecutionNotLocked(); - _releaseKeeper(jobKey_, assignedKeeperId); - return; - } - // Otherwise this is a keeper's call - - _assertOnlyKeeperAdminOrWorker(assignedKeeperId); - - uint256 binJob = getJobRaw(jobKey_); - uint256 intervalSeconds = (binJob << 32) >> 232; - - // 1. Release if insufficient credits - if (_releaseKeeperIfRequired(jobKey_, assignedKeeperId)) { - return; - } - - // 2. Check interval timeouts otherwise - // 2.1 If interval job - if (intervalSeconds != 0) { - uint256 lastExecutionAt = binJob >> 224; - if (lastExecutionAt == 0) { - lastExecutionAt = jobCreatedAt[jobKey_]; - } - uint256 period2EndsAt = lastExecutionAt + rdConfig.period1 + rdConfig.period2; - if (period2EndsAt > block.timestamp) { - revert TooEarlyToRelease(jobKey_, period2EndsAt); - } // else can release - // 2.2 If resolver job - } else { - // if slashing process initiated - uint256 _jobSlashingPossibleAfter = jobSlashingPossibleAfter[jobKey_]; - if (_jobSlashingPossibleAfter != 0) { - uint256 period2EndsAt = _jobSlashingPossibleAfter + rdConfig.period2; - if (period2EndsAt > block.timestamp) { - revert TooEarlyToRelease(jobKey_, period2EndsAt); - } - // if no slashing initiated - } else { - revert CantRelease(); - } - } - - _releaseKeeper(jobKey_, assignedKeeperId); - } - - function disableKeeper(uint256 keeperId_) external { - _assertOnlyKeeperAdmin(keeperId_); - _disableKeeper(keeperId_); - } - - function _disableKeeper(uint256 keeperId_) internal { - if (!keepers[keeperId_].isActive) { - revert KeeperIsAlreadyInactive(); - } - - activeKeepers.remove(keeperId_); - keepers[keeperId_].isActive = false; - - emit DisableKeeper(keeperId_); - } - - function initiateKeeperActivation(uint256 keeperId_) external { - _assertOnlyKeeperAdmin(keeperId_); - - if (keepers[keeperId_].isActive) { - revert KeeperIsAlreadyActive(); - } - if (keepers[keeperId_].cvpStake < minKeeperCvp) { - revert InsufficientKeeperStake(); - } - - _initiateKeeperActivation(keeperId_, false); - } - - function _initiateKeeperActivation(uint256 keeperId_, bool _firstActivation) internal { - uint256 canBeFinalizedAt = block.timestamp; - if (!_firstActivation) { - canBeFinalizedAt += rdConfig.keeperActivationTimeoutHours * 1 hours; - } - - keeperActivationCanBeFinalizedAt[keeperId_] = canBeFinalizedAt; - - emit InitiateKeeperActivation(keeperId_, canBeFinalizedAt); - } - - function finalizeKeeperActivation(uint256 keeperId_) external { - _assertOnlyKeeperAdmin(keeperId_); - - uint256 availableAt = keeperActivationCanBeFinalizedAt[keeperId_]; - if (availableAt > block.timestamp) { - revert TooEarlyForActivationFinalization(block.timestamp, availableAt); - } - if (availableAt == 0) { - revert ActivationNotInitiated(); - } - if (keepers[keeperId_].cvpStake < minKeeperCvp) { - revert InsufficientKeeperStake(); - } - - activeKeepers.add(keeperId_); - keepers[keeperId_].isActive = true; - keeperActivationCanBeFinalizedAt[keeperId_] = 0; - - emit FinalizeKeeperActivation(keeperId_); - } - - function _afterExecutionReverted( - bytes32 jobKey_, - CalldataSourceType calldataSource_, - uint256 actualKeeperId_, - bytes memory executionResponse_, - uint256 compensation_ - ) internal virtual override { - if (calldataSource_ == CalldataSourceType.RESOLVER && - jobReservedSlasherId[jobKey_] == 0 && jobSlashingPossibleAfter[jobKey_] == 0) { - revert SlashingNotInitiatedExecutionReverted(); - } - - uint256 assignedKeeperId = jobNextKeeperId[jobKey_]; - - emit ExecutionReverted(jobKey_, assignedKeeperId, actualKeeperId_, executionResponse_, compensation_); - - _releaseKeeper(jobKey_, assignedKeeperId); - } - - function initiateKeeperSlashing( - address jobAddress_, - uint256 jobId_, - uint256 slasherKeeperId_, - bool useResolver_, - bytes memory jobCalldata_ - ) external { - bytes32 jobKey = getJobKey(jobAddress_, jobId_); - uint256 binJob = getJobRaw(jobKey); - - // 0. Keeper has sufficient stake - { - if (keepers[slasherKeeperId_].worker != msg.sender) { - revert KeeperWorkerNotAuthorized(); - } - if (keepers[slasherKeeperId_].cvpStake < minKeeperCvp) { - revert InsufficientKeeperStake(); - } - if (!keepers[slasherKeeperId_].isActive) { - revert InactiveKeeper(); - } - } - - // 1. Assert the job is active - { - if (!ConfigFlags.check(binJob, CFG_ACTIVE)) { - revert InactiveJob(jobKey); - } - } - - // 2. Assert job-scoped keeper's minimum CVP deposit - if (ConfigFlags.check(binJob, CFG_CHECK_KEEPER_MIN_CVP_DEPOSIT) && - keepers[slasherKeeperId_].cvpStake < jobMinKeeperCvp[jobKey]) { - revert InsufficientJobScopedKeeperStake(); - } - - // 3. Not an interval job - { - uint256 intervalSeconds = (binJob << 32) >> 232; - if (intervalSeconds != 0) { - revert NonIntervalJob(); - } - } - - // 4. keeper can't slash - if (jobNextKeeperId[jobKey] == slasherKeeperId_) { - revert AssignedKeeperCantSlash(); - } - - // 5. current slasher - { - uint256 currentSlasherId = getCurrentSlasherId(jobKey); - if (slasherKeeperId_ != currentSlasherId) { - revert OnlyCurrentSlasher(currentSlasherId); - } - } - - // 6. Slashing not initiated yet - uint256 _jobSlashingPossibleAfter = jobSlashingPossibleAfter[jobKey]; - // if is already initiated - if (_jobSlashingPossibleAfter != 0 && - // but not overdue yet - (_jobSlashingPossibleAfter + rdConfig.period2) > block.timestamp - ) { - revert TooEarlyToReinitiateSlashing(); - } - - // 7. check if could be executed - if (useResolver_) { - _checkJobResolverCall(jobKey); - } else { - _assertJobCalldataMatchesSelector(binJob, jobCalldata_); - (bool ok, bytes memory result) = address(this).call( - abi.encodeWithSelector(PPAgentV2.checkCouldBeExecuted.selector, jobAddress_, jobCalldata_) - ); - if (ok) { - revert UnexpectedCodeBlock(); - } - bytes4 selector = bytes4(result); - if (selector == PPAgentV2.JobCheckCanNotBeExecuted.selector) { - assembly ("memory-safe") { - revert(add(32, result), mload(result)) - } - } else if (selector != PPAgentV2.JobCheckCanBeExecuted.selector) { - revert JobCheckUnexpectedError(); - } // else can be executed - } - - jobReservedSlasherId[jobKey] = slasherKeeperId_; - _jobSlashingPossibleAfter = block.timestamp + rdConfig.period1; - jobSlashingPossibleAfter[jobKey] = _jobSlashingPossibleAfter; - - emit InitiateKeeperSlashing(jobKey, slasherKeeperId_, useResolver_, _jobSlashingPossibleAfter); - } - - /*** OVERRIDES ***/ - function registerAsKeeper(address worker_, uint256 initialDepositAmount_) public override returns (uint256 keeperId) { - keeperId = super.registerAsKeeper(worker_, initialDepositAmount_); - // The placeholder bytes32(0) element remains constant in the set, ensuring - // the set's size EVM slot is never 0, resulting in gas savings. - keeperLocksByJob[keeperId].add(bytes32(uint256(0))); - } - - function setJobConfig( - bytes32 jobKey_, - bool isActive_, - bool useJobOwnerCredits_, - bool assertResolverSelector_, - bool callResolverBeforeExecute_ - ) public override { - uint256 rawJobBefore = getJobRaw(jobKey_); - super.setJobConfig(jobKey_, isActive_, useJobOwnerCredits_, assertResolverSelector_, callResolverBeforeExecute_); - bool wasActiveBefore = ConfigFlags.check(rawJobBefore, CFG_ACTIVE); - uint256 assignedKeeperId = jobNextKeeperId[jobKey_]; - - // inactive => active: assign if required - if(!wasActiveBefore && isActive_) { - _assignNextKeeperIfRequiredAndUpdateLastExecutedAt(jobKey_, assignedKeeperId); - } - - // job was and remain active, but the credits source has changed: assign or release if required - if (wasActiveBefore && isActive_ && - (ConfigFlags.check(rawJobBefore, CFG_USE_JOB_OWNER_CREDITS) != useJobOwnerCredits_)) { - - if (!_assignNextKeeperIfRequiredAndUpdateLastExecutedAt(jobKey_, assignedKeeperId)) { - _releaseKeeperIfRequired(jobKey_, assignedKeeperId); - } - } - - // active => inactive: unassign - if (wasActiveBefore && !isActive_) { - _releaseKeeper(jobKey_, assignedKeeperId); - } - } - - /*** HOOKS ***/ - function _beforeExecute(bytes32 jobKey_, uint256 actualKeeperId_, uint256 binJob_) internal view override { - uint256 nextKeeperId = jobNextKeeperId[jobKey_]; - if (nextKeeperId == 0) { - revert JobHasNoKeeperAssigned(); - } - - uint256 intervalSeconds = (binJob_ << 32) >> 232; - uint256 lastExecutionAt = binJob_ >> 224; - - // if interval task is called by a slasher - if (intervalSeconds > 0 && nextKeeperId != actualKeeperId_) { - uint256 nextExecutionTimeoutAt; - uint256 _lastExecutionAt = lastExecutionAt; - if (_lastExecutionAt == 0) { - _lastExecutionAt = jobCreatedAt[jobKey_]; - } - unchecked { - nextExecutionTimeoutAt = _lastExecutionAt + intervalSeconds + rdConfig.period1; - } - // if it is to early to slash this job - if (block.timestamp < nextExecutionTimeoutAt) { - revert OnlyNextKeeper(nextKeeperId, lastExecutionAt, intervalSeconds, rdConfig.period1, block.timestamp); - } - - 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_) { - uint256 _jobSlashingPossibleAfter = jobSlashingPossibleAfter[jobKey_]; - if (_jobSlashingPossibleAfter == 0) { - revert SlashingNotInitiated(); - } - if (_jobSlashingPossibleAfter > block.timestamp) { - revert TooEarlyForSlashing(block.timestamp, jobSlashingPossibleAfter[jobKey_]); - } - - uint256 _jobReservedSlasherId = jobReservedSlasherId[jobKey_]; - if (_jobReservedSlasherId != actualKeeperId_) { - revert OnlyReservedSlasher(_jobReservedSlasherId); - } - } - } - - function _afterDepositJobCredits(bytes32 jobKey_) internal override { - _assignNextKeeperIfRequiredAndUpdateLastExecutedAt(jobKey_, jobNextKeeperId[jobKey_]); - } - - function _afterWithdrawJobCredits(bytes32 jobKey_) internal override { - _releaseKeeperIfRequired(jobKey_, jobNextKeeperId[jobKey_]); - } - - function _afterRegisterAsKeeper(uint256 keeperId_) internal override { - _initiateKeeperActivation(keeperId_, true); - } - - function _afterExecutionSucceeded(bytes32 jobKey_, uint256 actualKeeperId_, uint256 binJob_) internal virtual override { - uint256 assignedKeeperId = jobNextKeeperId[jobKey_]; - - uint256 intervalSeconds = (binJob_ << 32) >> 232; - - if (intervalSeconds == 0) { - jobReservedSlasherId[jobKey_] = 0; - jobSlashingPossibleAfter[jobKey_] = 0; - } - - // if slashing - if (assignedKeeperId != actualKeeperId_) { - uint256 keeperStake = _getKeeperLimitedStake({ - keeperCurrentStake_: keepers[assignedKeeperId].cvpStake, - agentMaxCvpStakeCvp_: uint256(rdConfig.agentMaxCvpStake), - job_: binJob_ - }); - uint256 dynamicSlashAmount = keeperStake * uint256(rdConfig.slashingFeeBps) / 10_000; - uint256 fixedSlashAmount = uint256(rdConfig.slashingFeeFixedCVP) * 1 ether; - // NOTICE: totalSlashAmount can't be >= uint88 - uint88 totalSlashAmount = uint88(fixedSlashAmount + dynamicSlashAmount); - uint256 slashAmountMissing = 0; - if (totalSlashAmount > keepers[assignedKeeperId].cvpStake) { - unchecked { - slashAmountMissing = totalSlashAmount - keepers[assignedKeeperId].cvpStake; - } - totalSlashAmount = keepers[assignedKeeperId].cvpStake; - } - keepers[assignedKeeperId].cvpStake -= totalSlashAmount; - keepers[actualKeeperId_].cvpStake += totalSlashAmount; - - if (keepers[assignedKeeperId].isActive && keepers[assignedKeeperId].cvpStake < minKeeperCvp) { - _disableKeeper(assignedKeeperId); - } - - emit SlashKeeper( - jobKey_, assignedKeeperId, actualKeeperId_, fixedSlashAmount, dynamicSlashAmount, slashAmountMissing - ); - } - - if (_shouldAssignKeeperBin(jobKey_, getJobRaw(jobKey_))) { - _unassignKeeper(jobKey_, assignedKeeperId); - _chooseNextKeeper(jobKey_, assignedKeeperId); - } else { - _releaseKeeper(jobKey_, assignedKeeperId); - } - } - - function _beforeInitiateRedeem(uint256 keeperId_) internal view override { - // ensure can release keeper - uint256 len = getJobsAssignedToKeeperLength(keeperId_); - if (len > 0) { - revert KeeperIsAssignedToJobs(len); - } - } - - function _afterInitiateRedeem(uint256 keeperId_) internal view override { - if (keepers[keeperId_].isActive && keepers[keeperId_].cvpStake < minKeeperCvp) { - revert KeeperShouldBeDisabledForStakeLTMinKeeperCvp(); - } - } - - function _afterRegisterJob(bytes32 jobKey_) internal override { - jobCreatedAt[jobKey_] = block.timestamp; - _assignNextKeeperIfRequired(jobKey_, 0); - } - - function _afterAcceptJobTransfer(bytes32 jobKey_) internal override { - uint256 binJob = getJobRaw(jobKey_); - uint256 assignedKeeperId = jobNextKeeperId[jobKey_]; - - if (ConfigFlags.check(binJob, CFG_ACTIVE) && ConfigFlags.check(binJob, CFG_USE_JOB_OWNER_CREDITS)) { - if (!_assignNextKeeperIfRequiredAndUpdateLastExecutedAt(jobKey_, assignedKeeperId)) { - _releaseKeeperIfRequired(jobKey_, assignedKeeperId); - } - } - } - - /*** HELPERS ***/ - function _releaseKeeper(bytes32 jobKey_, uint256 keeperId_) internal { - _unassignKeeper(jobKey_, keeperId_); - - emit JobKeeperChanged(jobKey_, keeperId_, 0); - } - - // Assumes another keeper will be assigned later within the same transaction - function _unassignKeeper(bytes32 jobKey_, uint256 keeperId_) internal { - keeperLocksByJob[keeperId_].remove(jobKey_); - - jobNextKeeperId[jobKey_] = 0; - jobSlashingPossibleAfter[jobKey_] = 0; - jobReservedSlasherId[jobKey_] = 0; - } - - function _assignNextKeeper(bytes32 jobKey_, uint256 previousKeeperId_, uint256 nextKeeperId_) internal { - keeperLocksByJob[nextKeeperId_].add(jobKey_); - - jobNextKeeperId[jobKey_] = nextKeeperId_; - - emit JobKeeperChanged(jobKey_, previousKeeperId_, nextKeeperId_); - } - - function _getPseudoRandom() internal virtual returns (uint256) { - return block.prevrandao; - } - - function _releaseKeeperIfRequired(bytes32 jobKey_, uint256 keeperId_) internal returns (bool released) { - uint256 binJob = getJobRaw(jobKey_); - if (jobNextKeeperId[jobKey_] != 0 && !_shouldAssignKeeperBin(jobKey_, binJob)) { - _releaseKeeper(jobKey_, keeperId_); - return true; - } - return false; - } - - function _assignNextKeeperIfRequiredAndUpdateLastExecutedAt( - bytes32 jobKey_, - uint256 currentKeeperId_ - ) internal returns (bool assigned) { - assigned = _assignNextKeeperIfRequired(jobKey_, currentKeeperId_); - if (assigned) { - uint256 binJob = getJobRaw(jobKey_); - uint256 intervalSeconds = (binJob << 32) >> 232; - if (intervalSeconds > 0) { - uint256 lastExecutionAt = uint32(block.timestamp); - binJob = binJob & BM_CLEAR_LAST_UPDATE_AT | (lastExecutionAt << 224); - _updateRawJob(jobKey_, binJob); - } - } - } - - function _assignNextKeeperIfRequired(bytes32 jobKey_, uint256 currentKeeperId_) internal returns (bool assigned) { - if (currentKeeperId_ == 0 && _shouldAssignKeeperBin(jobKey_, getJobRaw(jobKey_))) { - _chooseNextKeeper(jobKey_, currentKeeperId_); - return true; - } - return false; - } - - function _shouldAssignKeeperBin(bytes32 jobKey_, uint256 binJob_) internal view returns (bool) { - uint256 credits; - - if (!ConfigFlags.check(binJob_, CFG_ACTIVE)) { - return false; - } - - if (ConfigFlags.check(binJob_, CFG_USE_JOB_OWNER_CREDITS)) { - credits = jobOwnerCredits[jobOwners[jobKey_]]; - } else { - credits = (binJob_ << 128) >> 168; - } - - if (credits >= (uint256(rdConfig.jobMinCreditsFinney) * 0.001 ether)) { - return true; - } - - return false; - } - - function _chooseNextKeeper(bytes32 jobKey_, uint256 previousKeeperId_) internal { - uint256 totalActiveKeepers = activeKeepers.length(); - if (totalActiveKeepers == 0) { - emit JobKeeperChanged(jobKey_, previousKeeperId_, 0); - return; - } - uint256 index; - { - uint256 pseudoRandom = _getPseudoRandom(); - unchecked { - index = ((pseudoRandom + uint256(jobKey_)) % totalActiveKeepers); - } - } - uint256 requiredStake; - { - uint256 _jobMinKeeperCvp = jobMinKeeperCvp[jobKey_]; - requiredStake = _jobMinKeeperCvp > minKeeperCvp ? _jobMinKeeperCvp : minKeeperCvp; - } - uint256 closestUnderRequiredStakeValue = 0; - uint256 closestUnderRequiredStakeKeeperId = 0; - bool indexResetTo0 = false; - uint256 initialIndex = index; - - while (!indexResetTo0 || (indexResetTo0 && index < initialIndex)) { - if (index >= totalActiveKeepers) { - index = 0; - indexResetTo0 = true; - } - uint256 _nextExecutionKeeperId = activeKeepers.at(index); - - Keeper memory keeper = keepers[_nextExecutionKeeperId]; - - if (keeper.isActive) { - if (keeper.cvpStake >= requiredStake) { - _assignNextKeeper(jobKey_, previousKeeperId_, _nextExecutionKeeperId); - return; - } else { - if (keeper.cvpStake > closestUnderRequiredStakeValue) { - closestUnderRequiredStakeKeeperId = _nextExecutionKeeperId; - closestUnderRequiredStakeValue = keeper.cvpStake; - } - } - } - index += 1; - } - - if (closestUnderRequiredStakeValue > minKeeperCvp) { - _assignNextKeeper(jobKey_, previousKeeperId_, closestUnderRequiredStakeKeeperId); - return; - } - - // release job - emit JobKeeperChanged(jobKey_, previousKeeperId_, 0); - } - - function _checkBaseFee(uint256 binJob_, uint256 cfg_) internal pure override returns (uint256) { - binJob_; - cfg_; - - return type(uint256).max; - } - - function _assertJobCalldataMatchesSelector(uint256 binJob_, bytes memory jobCalldata_) internal pure { + function _assertExecutionNotLocked() internal override view { + bytes32 lockKey = EXECUTION_LOCK_KEY; assembly ("memory-safe") { - // CFG_ASSERT_RESOLVER_SELECTOR = 0x04 from PPAgentLiteFlags - if and(binJob_, 0x04) { - if iszero(eq( - // actual - shl(224, shr(224, mload(add(jobCalldata_, 32)))), - // expected - shl(224, shr(8, binJob_)) - )) { - // revert SelectorCheckFailed() - mstore(0, 0x74ab678100000000000000000000000000000000000000000000000000000000) - revert(0, 4) - } + let isLocked := tload(lockKey) + if isLocked { + mstore(0x1c, 0x0815283600000000000000000000000000000000000000000000000000000000) + revert(0x1c, 4) } } } - function calculateCompensation( - bool ok_, - uint256 job_, - uint256 keeperId_, - uint256 baseFee_, - uint256 gasUsed_ - ) public view override returns (uint256) { - if (!ok_) { - return (gasUsed_ + _getJobGasOverhead()) * baseFee_; - } - - uint256 stake = _getKeeperLimitedStake({ - keeperCurrentStake_: keepers[keeperId_].cvpStake, - agentMaxCvpStakeCvp_: uint256(rdConfig.agentMaxCvpStake), - job_: job_ - }); - - return rdConfig.jobFixedRewardFinney * FIXED_PAYMENT_MULTIPLIER + - (baseFee_ * (gasUsed_ + _getJobGasOverhead()) * rdConfig.jobCompensationMultiplierBps / 10_000) + - (stake / rdConfig.stakeDivisor); - } - - /* - * Returns a limited stake to be used for calculating the slashing and compensation amounts. - * - * @dev There are two limitations applied to the initial keeper stake: - * 1. It can't be > job-level max CVP limit defined by a job owner. - * 2. It can't be > agent-level(global) max CVP limit defined by the contract owner. - * @param keeperCurrentStake_ in CVP wei - * @param agentMaxCvpStakeCvp_ in CVP ether - * @param job_ binJob where jobMaxCvpStake in CVP ether is encoded into fixedReward field - * @return limitedStake in CVP wei - */ - function _getKeeperLimitedStake( - uint256 keeperCurrentStake_, - uint256 agentMaxCvpStakeCvp_, - uint256 job_ - ) internal pure returns (uint256 limitedStake) { - limitedStake = keeperCurrentStake_; - - // fixedReward field for randao jobs contains _jobMaxCvpStake - uint256 _jobMaxCvpStake = ((job_ << 64) >> 224) * 1 ether; - if (_jobMaxCvpStake > 0 && _jobMaxCvpStake < limitedStake) { - limitedStake = _jobMaxCvpStake; - } - uint256 _agentMaxCvpStake = agentMaxCvpStakeCvp_ * 1 ether; - if (_agentMaxCvpStake > 0 && _agentMaxCvpStake < limitedStake) { - limitedStake = _agentMaxCvpStake; - } - - return limitedStake; - } - - /*** GETTERS ***/ - - /* - * Returns a list of the jobsKeys assigned to a keeperId_. - * - * @dev The jobKeys array should exclude the constant placeholder bytes32(0) from its first element. - */ - function getJobsAssignedToKeeper(uint256 keeperId_) external view returns (bytes32[] memory actualJobKeys) { - bytes32[] memory allJobKeys = keeperLocksByJob[keeperId_].values(); - uint256 len = getJobsAssignedToKeeperLength(keeperId_); - if (len == 0) { - return new bytes32[](0); - } - actualJobKeys = new bytes32[](len); - for (uint256 i = 0; i < len; i++) { - actualJobKeys[i] = allJobKeys[i + 1]; - } - } - - function getJobsAssignedToKeeperLength(uint256 keeperId_) public view returns (uint256) { - uint256 len = keeperLocksByJob[keeperId_].length(); - if (len > 0) { - return len - 1; + function _setExecutionLock(uint value_) internal override { + bytes32 lockKey = EXECUTION_LOCK_KEY; + assembly ("memory-safe") { + tstore(lockKey, value_) } - return 0; - } - - function getCurrentSlasherId(bytes32 jobKey_) public view returns (uint256) { - return getSlasherIdByBlock(block.number, jobKey_); - } - - function getActiveKeepersLength() external view returns (uint256) { - return activeKeepers.length(); - } - - function getActiveKeepers() external view returns (uint256[] memory) { - return activeKeepers.values(); - } - - function getRdConfig() external view returns (RandaoConfig memory) { - return rdConfig; - } - - function getSlasherIdByBlock(uint256 blockNumber_, bytes32 jobKey_) public view returns (uint256) { - uint256 totalActiveKeepers = activeKeepers.length(); - uint256 index = ((blockNumber_ / rdConfig.slashingEpochBlocks + uint256(jobKey_)) % totalActiveKeepers); - return activeKeepers.at(index); } } diff --git a/contracts/PPAgentV2RandaoBased.sol b/contracts/PPAgentV2RandaoBased.sol new file mode 100644 index 0000000..88a4c8a --- /dev/null +++ b/contracts/PPAgentV2RandaoBased.sol @@ -0,0 +1,880 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {PPAgentV2Based, ConfigFlags } from "./PPAgentV2Based.sol"; +import "./utils/CustomizedEnumerableSet.sol"; +import "./PPAgentV2Flags.sol"; +import "./PPAgentV2Interfaces.sol"; + +/** + * @title PPAgentV2RandaoBased + * @author PowerPool + */ +contract PPAgentV2RandaoBased is IPPAgentV2RandaoViewer, PPAgentV2Based { + using EnumerableSet for EnumerableSet.Bytes32Set; + using EnumerableSet for EnumerableSet.UintSet; + + error JobHasKeeperAssigned(uint256 keeperId); + error JobHasNoKeeperAssigned(); + error SlashingEpochBlocksTooLow(); + error InvalidPeriod1(); + error InvalidPeriod2(); + error InvalidSlashingFeeFixedCVP(); + error SlashingBpsGt5000Bps(); + error InvalidStakeDivisor(); + error JobCompensationMultiplierBpsLT10000(); + error InactiveKeeper(); + error KeeperIsAssignedToJobs(uint256 amountOfJobs); + error OnlyCurrentSlasher(uint256 expectedSlasherId); + error OnlyReservedSlasher(uint256 reservedSlasherId); + error TooEarlyForSlashing(uint256 now_, uint256 possibleAfter); + error SlashingNotInitiated(); + error SlashingNotInitiatedExecutionReverted(); + error AssignedKeeperCantSlash(); + error KeeperIsAlreadyActive(); + error KeeperIsAlreadyInactive(); + error CantAssignKeeper(); + error NonIntervalJob(); + error TooEarlyToReinitiateSlashing(); + error TooEarlyToRelease(bytes32 jobKey, uint256 period2End); + error TooEarlyForActivationFinalization(uint256 now, uint256 availableAt); + error KeeperShouldBeDisabledForStakeLTMinKeeperCvp(); + error CantRelease(); + error OnlyNextKeeper( + uint256 assignedKeeperId, + uint256 lastExecutedAt, + uint256 interval, + uint256 slashingInterval, + uint256 _now + ); + error InsufficientKeeperStakeToSlash( + bytes32 jobKey, + uint256 assignedKeeperId, + uint256 keeperCurrentStake, + uint256 amountToSlash + ); + error ActivationNotInitiated(); + + event DisableKeeper(uint256 indexed keeperId); + event InitiateKeeperActivation(uint256 indexed keeperId, uint256 canBeFinalizedAt); + event FinalizeKeeperActivation(uint256 indexed keeperId); + event InitiateKeeperSlashing( + bytes32 indexed jobKey, + uint256 indexed slasherKeeperId, + bool useResolver, + uint256 jobSlashingPossibleAfter + ); + event ExecutionReverted( + bytes32 indexed jobKey, + uint256 indexed assignedKeeperId, + uint256 indexed actualKeeperId, + bytes executionReturndata, + uint256 compensation + ); + event SlashKeeper( + bytes32 indexed jobKey, + uint256 indexed assignedKeeperId, + uint256 indexed actualKeeperId, + uint256 fixedSlashAmount, + uint256 dynamicSlashAmount, + uint256 slashAmountMissing + ); + event SetRdConfig(RandaoConfig rdConfig); + event JobKeeperChanged(bytes32 indexed jobKey, uint256 indexed keeperFrom, uint256 indexed keeperTo); + + IPPAgentV2RandaoViewer.RandaoConfig internal rdConfig; + + // keccak256(jobAddress, id) => nextKeeperId + mapping(bytes32 => uint256) public jobNextKeeperId; + // keccak256(jobAddress, id) => nextSlasherId + mapping(bytes32 => uint256) public jobReservedSlasherId; + // keccak256(jobAddress, id) => timestamp, for non-interval jobs + mapping(bytes32 => uint256) public jobSlashingPossibleAfter; + // keccak256(jobAddress, id) => timestamp + mapping(bytes32 => uint256) public jobCreatedAt; + // keeperId => (pending jobs) + mapping(uint256 => EnumerableSet.Bytes32Set) internal keeperLocksByJob; + // keeperId => timestamp + mapping(uint256 => uint256) public keeperActivationCanBeFinalizedAt; + + EnumerableSet.UintSet internal activeKeepers; + + function getStrategy() public pure override returns (string memory) { + return "randao"; + } + + function _getJobGasOverhead() internal pure override returns (uint256) { + return 136_000; + } + + constructor(address cvp_) PPAgentV2Based(cvp_) { + } + + function initializeRandao( + address owner_, + uint256 minKeeperCvp_, + uint256 pendingWithdrawalTimeoutSeconds_, + RandaoConfig memory rdConfig_) external { + PPAgentV2Based.initialize(owner_, minKeeperCvp_, pendingWithdrawalTimeoutSeconds_); + _setRdConfig(rdConfig_); + } + + /*** AGENT OWNER METHODS ***/ + function setRdConfig(RandaoConfig calldata rdConfig_) external onlyOwner { + _setRdConfig(rdConfig_); + } + + function _setRdConfig(RandaoConfig memory rdConfig_) internal { + if (rdConfig_.slashingEpochBlocks < 3) { + revert SlashingEpochBlocksTooLow(); + } + if (rdConfig_.period1 < 15 seconds) { + revert InvalidPeriod1(); + } + if (rdConfig_.period2 < 15 seconds) { + revert InvalidPeriod2(); + } + if (rdConfig_.slashingFeeFixedCVP > (minKeeperCvp / 2)) { + revert InvalidSlashingFeeFixedCVP(); + } + if (rdConfig_.slashingFeeBps > 5000) { + revert SlashingBpsGt5000Bps(); + } + if (rdConfig_.stakeDivisor == 0) { + revert InvalidStakeDivisor(); + } + if (rdConfig_.jobCompensationMultiplierBps < 10_000) { + revert JobCompensationMultiplierBpsLT10000(); + } + emit SetRdConfig(rdConfig_); + + rdConfig = rdConfig_; + } + + function ownerSlashDisable( + uint256 keeperId_, + address to_, + uint256 currentAmount_, + uint256 pendingAmount_, + bool disable_ + ) external { + ownerSlash(keeperId_, to_, currentAmount_, pendingAmount_); + if (disable_) { + _disableKeeper(keeperId_); + } + } + + /*** JOB OWNER METHODS ***/ + /** + * Assigns a keeper for all the jobs in jobKeys_ list. + * The msg.sender should be the owner of all the jobs in the jobKeys_ list. + * Will revert if there is at least one job with an already assigned keeper. + * + * @param jobKeys_ The list of job keys to activate + */ + function assignKeeper(bytes32[] calldata jobKeys_) external { + _assignKeeper(jobKeys_); + } + + /** + * Top-ups the job owner credits in NATIVE tokens AND activates the jobs passed in jobKeys_ array. + * + * If the jobKeys_ list is empty the function behaves the same way as `depositJobOwnerCredits(address for_)`. + * If there is at least one jobKeys_ element the msg.sender should be the owner of all the jobs in the jobKeys_ list. + * Will revert if there is at least one job with an assigned keeper. + * + * @param for_ The job owner address to deposit for + * @param jobKeys_ The list of job keys to activate + */ + function depositJobOwnerCreditsAndAssignKeepers(address for_, bytes32[] calldata jobKeys_) external payable { + if (msg.value == 0) { + revert MissingDeposit(); + } + + _processJobOwnerCreditsDeposit(for_); + + _assignKeeper(jobKeys_); + } + + function _assignKeeper(bytes32[] calldata jobKeys_) internal { + for (uint256 i = 0; i < jobKeys_.length; i++) { + bytes32 jobKey = jobKeys_[i]; + uint256 assignedKeeperId = jobNextKeeperId[jobKey]; + if (jobNextKeeperId[jobKey] != 0) { + revert JobHasKeeperAssigned(assignedKeeperId); + } + _assertOnlyJobOwner(jobKey); + + if (!_assignNextKeeperIfRequiredAndUpdateLastExecutedAt(jobKey, 0)) { + revert CantAssignKeeper(); + } + } + } + + /*** KEEPER METHODS ***/ + function releaseJob(bytes32 jobKey_) external { + uint256 assignedKeeperId = jobNextKeeperId[jobKey_]; + + // Job owner can unassign a keeper without any restriction + if (msg.sender == jobOwners[jobKey_] || msg.sender == owner()) { + _assertExecutionNotLocked(); + _releaseKeeper(jobKey_, assignedKeeperId); + return; + } + // Otherwise this is a keeper's call + + _assertOnlyKeeperAdminOrWorker(assignedKeeperId); + + uint256 binJob = getJobRaw(jobKey_); + uint256 intervalSeconds = (binJob << 32) >> 232; + + // 1. Release if insufficient credits + if (_releaseKeeperIfRequired(jobKey_, assignedKeeperId)) { + return; + } + + // 2. Check interval timeouts otherwise + // 2.1 If interval job + if (intervalSeconds != 0) { + uint256 lastExecutionAt = binJob >> 224; + if (lastExecutionAt == 0) { + lastExecutionAt = jobCreatedAt[jobKey_]; + } + uint256 period2EndsAt = lastExecutionAt + rdConfig.period1 + rdConfig.period2; + if (period2EndsAt > block.timestamp) { + revert TooEarlyToRelease(jobKey_, period2EndsAt); + } // else can release + // 2.2 If resolver job + } else { + // if slashing process initiated + uint256 _jobSlashingPossibleAfter = jobSlashingPossibleAfter[jobKey_]; + if (_jobSlashingPossibleAfter != 0) { + uint256 period2EndsAt = _jobSlashingPossibleAfter + rdConfig.period2; + if (period2EndsAt > block.timestamp) { + revert TooEarlyToRelease(jobKey_, period2EndsAt); + } + // if no slashing initiated + } else { + revert CantRelease(); + } + } + + _releaseKeeper(jobKey_, assignedKeeperId); + } + + function disableKeeper(uint256 keeperId_) external { + _assertOnlyKeeperAdmin(keeperId_); + _disableKeeper(keeperId_); + } + + function _disableKeeper(uint256 keeperId_) internal { + if (!keepers[keeperId_].isActive) { + revert KeeperIsAlreadyInactive(); + } + + activeKeepers.remove(keeperId_); + keepers[keeperId_].isActive = false; + + emit DisableKeeper(keeperId_); + } + + function initiateKeeperActivation(uint256 keeperId_) external { + _assertOnlyKeeperAdmin(keeperId_); + + if (keepers[keeperId_].isActive) { + revert KeeperIsAlreadyActive(); + } + if (keepers[keeperId_].cvpStake < minKeeperCvp) { + revert InsufficientKeeperStake(); + } + + _initiateKeeperActivation(keeperId_, false); + } + + function _initiateKeeperActivation(uint256 keeperId_, bool _firstActivation) internal { + uint256 canBeFinalizedAt = block.timestamp; + if (!_firstActivation) { + canBeFinalizedAt += rdConfig.keeperActivationTimeoutHours * 1 hours; + } + + keeperActivationCanBeFinalizedAt[keeperId_] = canBeFinalizedAt; + + emit InitiateKeeperActivation(keeperId_, canBeFinalizedAt); + } + + function finalizeKeeperActivation(uint256 keeperId_) external { + _assertOnlyKeeperAdmin(keeperId_); + + uint256 availableAt = keeperActivationCanBeFinalizedAt[keeperId_]; + if (availableAt > block.timestamp) { + revert TooEarlyForActivationFinalization(block.timestamp, availableAt); + } + if (availableAt == 0) { + revert ActivationNotInitiated(); + } + if (keepers[keeperId_].cvpStake < minKeeperCvp) { + revert InsufficientKeeperStake(); + } + + activeKeepers.add(keeperId_); + keepers[keeperId_].isActive = true; + keeperActivationCanBeFinalizedAt[keeperId_] = 0; + + emit FinalizeKeeperActivation(keeperId_); + } + + function _afterExecutionReverted( + bytes32 jobKey_, + CalldataSourceType calldataSource_, + uint256 actualKeeperId_, + bytes memory executionResponse_, + uint256 compensation_ + ) internal virtual override { + if (calldataSource_ == CalldataSourceType.RESOLVER && + jobReservedSlasherId[jobKey_] == 0 && jobSlashingPossibleAfter[jobKey_] == 0) { + revert SlashingNotInitiatedExecutionReverted(); + } + + uint256 assignedKeeperId = jobNextKeeperId[jobKey_]; + + emit ExecutionReverted(jobKey_, assignedKeeperId, actualKeeperId_, executionResponse_, compensation_); + + _releaseKeeper(jobKey_, assignedKeeperId); + } + + function initiateKeeperSlashing( + address jobAddress_, + uint256 jobId_, + uint256 slasherKeeperId_, + bool useResolver_, + bytes memory jobCalldata_ + ) external { + bytes32 jobKey = getJobKey(jobAddress_, jobId_); + uint256 binJob = getJobRaw(jobKey); + + // 0. Keeper has sufficient stake + { + if (keepers[slasherKeeperId_].worker != msg.sender) { + revert KeeperWorkerNotAuthorized(); + } + if (keepers[slasherKeeperId_].cvpStake < minKeeperCvp) { + revert InsufficientKeeperStake(); + } + if (!keepers[slasherKeeperId_].isActive) { + revert InactiveKeeper(); + } + } + + // 1. Assert the job is active + { + if (!ConfigFlags.check(binJob, CFG_ACTIVE)) { + revert InactiveJob(jobKey); + } + } + + // 2. Assert job-scoped keeper's minimum CVP deposit + if (ConfigFlags.check(binJob, CFG_CHECK_KEEPER_MIN_CVP_DEPOSIT) && + keepers[slasherKeeperId_].cvpStake < jobMinKeeperCvp[jobKey]) { + revert InsufficientJobScopedKeeperStake(); + } + + // 3. Not an interval job + { + uint256 intervalSeconds = (binJob << 32) >> 232; + if (intervalSeconds != 0) { + revert NonIntervalJob(); + } + } + + // 4. keeper can't slash + if (jobNextKeeperId[jobKey] == slasherKeeperId_) { + revert AssignedKeeperCantSlash(); + } + + // 5. current slasher + { + uint256 currentSlasherId = getCurrentSlasherId(jobKey); + if (slasherKeeperId_ != currentSlasherId) { + revert OnlyCurrentSlasher(currentSlasherId); + } + } + + // 6. Slashing not initiated yet + uint256 _jobSlashingPossibleAfter = jobSlashingPossibleAfter[jobKey]; + // if is already initiated + if (_jobSlashingPossibleAfter != 0 && + // but not overdue yet + (_jobSlashingPossibleAfter + rdConfig.period2) > block.timestamp + ) { + revert TooEarlyToReinitiateSlashing(); + } + + // 7. check if could be executed + if (useResolver_) { + _checkJobResolverCall(jobKey); + } else { + _assertJobCalldataMatchesSelector(binJob, jobCalldata_); + (bool ok, bytes memory result) = address(this).call( + abi.encodeWithSelector(PPAgentV2Based.checkCouldBeExecuted.selector, jobAddress_, jobCalldata_) + ); + if (ok) { + revert UnexpectedCodeBlock(); + } + bytes4 selector = bytes4(result); + if (selector == PPAgentV2Based.JobCheckCanNotBeExecuted.selector) { + assembly ("memory-safe") { + revert(add(32, result), mload(result)) + } + } else if (selector != PPAgentV2Based.JobCheckCanBeExecuted.selector) { + revert JobCheckUnexpectedError(); + } // else can be executed + } + + jobReservedSlasherId[jobKey] = slasherKeeperId_; + _jobSlashingPossibleAfter = block.timestamp + rdConfig.period1; + jobSlashingPossibleAfter[jobKey] = _jobSlashingPossibleAfter; + + emit InitiateKeeperSlashing(jobKey, slasherKeeperId_, useResolver_, _jobSlashingPossibleAfter); + } + + /*** OVERRIDES ***/ + function registerAsKeeper(address worker_, uint256 initialDepositAmount_) public override returns (uint256 keeperId) { + keeperId = super.registerAsKeeper(worker_, initialDepositAmount_); + // The placeholder bytes32(0) element remains constant in the set, ensuring + // the set's size EVM slot is never 0, resulting in gas savings. + keeperLocksByJob[keeperId].add(bytes32(uint256(0))); + } + + function setJobConfig( + bytes32 jobKey_, + bool isActive_, + bool useJobOwnerCredits_, + bool assertResolverSelector_, + bool callResolverBeforeExecute_ + ) public override { + uint256 rawJobBefore = getJobRaw(jobKey_); + super.setJobConfig(jobKey_, isActive_, useJobOwnerCredits_, assertResolverSelector_, callResolverBeforeExecute_); + bool wasActiveBefore = ConfigFlags.check(rawJobBefore, CFG_ACTIVE); + uint256 assignedKeeperId = jobNextKeeperId[jobKey_]; + + // inactive => active: assign if required + if(!wasActiveBefore && isActive_) { + _assignNextKeeperIfRequiredAndUpdateLastExecutedAt(jobKey_, assignedKeeperId); + } + + // job was and remain active, but the credits source has changed: assign or release if required + if (wasActiveBefore && isActive_ && + (ConfigFlags.check(rawJobBefore, CFG_USE_JOB_OWNER_CREDITS) != useJobOwnerCredits_)) { + + if (!_assignNextKeeperIfRequiredAndUpdateLastExecutedAt(jobKey_, assignedKeeperId)) { + _releaseKeeperIfRequired(jobKey_, assignedKeeperId); + } + } + + // active => inactive: unassign + if (wasActiveBefore && !isActive_) { + _releaseKeeper(jobKey_, assignedKeeperId); + } + } + + /*** HOOKS ***/ + function _beforeExecute(bytes32 jobKey_, uint256 actualKeeperId_, uint256 binJob_) internal view override { + uint256 nextKeeperId = jobNextKeeperId[jobKey_]; + if (nextKeeperId == 0) { + revert JobHasNoKeeperAssigned(); + } + + uint256 intervalSeconds = (binJob_ << 32) >> 232; + uint256 lastExecutionAt = binJob_ >> 224; + + // if interval task is called by a slasher + if (intervalSeconds > 0 && nextKeeperId != actualKeeperId_) { + uint256 nextExecutionTimeoutAt; + uint256 _lastExecutionAt = lastExecutionAt; + if (_lastExecutionAt == 0) { + _lastExecutionAt = jobCreatedAt[jobKey_]; + } + unchecked { + nextExecutionTimeoutAt = _lastExecutionAt + intervalSeconds + rdConfig.period1; + } + // if it is to early to slash this job + if (block.timestamp < nextExecutionTimeoutAt) { + revert OnlyNextKeeper(nextKeeperId, lastExecutionAt, intervalSeconds, rdConfig.period1, block.timestamp); + } + + 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_) { + uint256 _jobSlashingPossibleAfter = jobSlashingPossibleAfter[jobKey_]; + if (_jobSlashingPossibleAfter == 0) { + revert SlashingNotInitiated(); + } + if (_jobSlashingPossibleAfter > block.timestamp) { + revert TooEarlyForSlashing(block.timestamp, jobSlashingPossibleAfter[jobKey_]); + } + + uint256 _jobReservedSlasherId = jobReservedSlasherId[jobKey_]; + if (_jobReservedSlasherId != actualKeeperId_) { + revert OnlyReservedSlasher(_jobReservedSlasherId); + } + } + } + + function _afterDepositJobCredits(bytes32 jobKey_) internal override { + _assignNextKeeperIfRequiredAndUpdateLastExecutedAt(jobKey_, jobNextKeeperId[jobKey_]); + } + + function _afterWithdrawJobCredits(bytes32 jobKey_) internal override { + _releaseKeeperIfRequired(jobKey_, jobNextKeeperId[jobKey_]); + } + + function _afterRegisterAsKeeper(uint256 keeperId_) internal override { + _initiateKeeperActivation(keeperId_, true); + } + + function _afterExecutionSucceeded(bytes32 jobKey_, uint256 actualKeeperId_, uint256 binJob_) internal virtual override { + uint256 assignedKeeperId = jobNextKeeperId[jobKey_]; + + uint256 intervalSeconds = (binJob_ << 32) >> 232; + + if (intervalSeconds == 0) { + jobReservedSlasherId[jobKey_] = 0; + jobSlashingPossibleAfter[jobKey_] = 0; + } + + // if slashing + if (assignedKeeperId != actualKeeperId_) { + uint256 keeperStake = _getKeeperLimitedStake({ + keeperCurrentStake_: keepers[assignedKeeperId].cvpStake, + agentMaxCvpStakeCvp_: uint256(rdConfig.agentMaxCvpStake), + job_: binJob_ + }); + uint256 dynamicSlashAmount = keeperStake * uint256(rdConfig.slashingFeeBps) / 10_000; + uint256 fixedSlashAmount = uint256(rdConfig.slashingFeeFixedCVP) * 1 ether; + // NOTICE: totalSlashAmount can't be >= uint88 + uint88 totalSlashAmount = uint88(fixedSlashAmount + dynamicSlashAmount); + uint256 slashAmountMissing = 0; + if (totalSlashAmount > keepers[assignedKeeperId].cvpStake) { + unchecked { + slashAmountMissing = totalSlashAmount - keepers[assignedKeeperId].cvpStake; + } + totalSlashAmount = keepers[assignedKeeperId].cvpStake; + } + keepers[assignedKeeperId].cvpStake -= totalSlashAmount; + keepers[actualKeeperId_].cvpStake += totalSlashAmount; + + if (keepers[assignedKeeperId].isActive && keepers[assignedKeeperId].cvpStake < minKeeperCvp) { + _disableKeeper(assignedKeeperId); + } + + emit SlashKeeper( + jobKey_, assignedKeeperId, actualKeeperId_, fixedSlashAmount, dynamicSlashAmount, slashAmountMissing + ); + } + + if (_shouldAssignKeeperBin(jobKey_, getJobRaw(jobKey_))) { + _unassignKeeper(jobKey_, assignedKeeperId); + _chooseNextKeeper(jobKey_, assignedKeeperId); + } else { + _releaseKeeper(jobKey_, assignedKeeperId); + } + } + + function _beforeInitiateRedeem(uint256 keeperId_) internal view override { + // ensure can release keeper + uint256 len = getJobsAssignedToKeeperLength(keeperId_); + if (len > 0) { + revert KeeperIsAssignedToJobs(len); + } + } + + function _afterInitiateRedeem(uint256 keeperId_) internal view override { + if (keepers[keeperId_].isActive && keepers[keeperId_].cvpStake < minKeeperCvp) { + revert KeeperShouldBeDisabledForStakeLTMinKeeperCvp(); + } + } + + function _afterRegisterJob(bytes32 jobKey_) internal override { + jobCreatedAt[jobKey_] = block.timestamp; + _assignNextKeeperIfRequired(jobKey_, 0); + } + + function _afterAcceptJobTransfer(bytes32 jobKey_) internal override { + uint256 binJob = getJobRaw(jobKey_); + uint256 assignedKeeperId = jobNextKeeperId[jobKey_]; + + if (ConfigFlags.check(binJob, CFG_ACTIVE) && ConfigFlags.check(binJob, CFG_USE_JOB_OWNER_CREDITS)) { + if (!_assignNextKeeperIfRequiredAndUpdateLastExecutedAt(jobKey_, assignedKeeperId)) { + _releaseKeeperIfRequired(jobKey_, assignedKeeperId); + } + } + } + + /*** HELPERS ***/ + function _releaseKeeper(bytes32 jobKey_, uint256 keeperId_) internal { + _unassignKeeper(jobKey_, keeperId_); + + emit JobKeeperChanged(jobKey_, keeperId_, 0); + } + + // Assumes another keeper will be assigned later within the same transaction + function _unassignKeeper(bytes32 jobKey_, uint256 keeperId_) internal { + keeperLocksByJob[keeperId_].remove(jobKey_); + + jobNextKeeperId[jobKey_] = 0; + jobSlashingPossibleAfter[jobKey_] = 0; + jobReservedSlasherId[jobKey_] = 0; + } + + function _assignNextKeeper(bytes32 jobKey_, uint256 previousKeeperId_, uint256 nextKeeperId_) internal { + keeperLocksByJob[nextKeeperId_].add(jobKey_); + + jobNextKeeperId[jobKey_] = nextKeeperId_; + + emit JobKeeperChanged(jobKey_, previousKeeperId_, nextKeeperId_); + } + + function _getPseudoRandom() internal virtual returns (uint256) { + return block.prevrandao; + } + + function _releaseKeeperIfRequired(bytes32 jobKey_, uint256 keeperId_) internal returns (bool released) { + uint256 binJob = getJobRaw(jobKey_); + if (jobNextKeeperId[jobKey_] != 0 && !_shouldAssignKeeperBin(jobKey_, binJob)) { + _releaseKeeper(jobKey_, keeperId_); + return true; + } + return false; + } + + function _assignNextKeeperIfRequiredAndUpdateLastExecutedAt( + bytes32 jobKey_, + uint256 currentKeeperId_ + ) internal returns (bool assigned) { + assigned = _assignNextKeeperIfRequired(jobKey_, currentKeeperId_); + if (assigned) { + uint256 binJob = getJobRaw(jobKey_); + uint256 intervalSeconds = (binJob << 32) >> 232; + if (intervalSeconds > 0) { + uint256 lastExecutionAt = uint32(block.timestamp); + binJob = binJob & BM_CLEAR_LAST_UPDATE_AT | (lastExecutionAt << 224); + _updateRawJob(jobKey_, binJob); + } + } + } + + function _assignNextKeeperIfRequired(bytes32 jobKey_, uint256 currentKeeperId_) internal returns (bool assigned) { + if (currentKeeperId_ == 0 && _shouldAssignKeeperBin(jobKey_, getJobRaw(jobKey_))) { + _chooseNextKeeper(jobKey_, currentKeeperId_); + return true; + } + return false; + } + + function _shouldAssignKeeperBin(bytes32 jobKey_, uint256 binJob_) internal view returns (bool) { + uint256 credits; + + if (!ConfigFlags.check(binJob_, CFG_ACTIVE)) { + return false; + } + + if (ConfigFlags.check(binJob_, CFG_USE_JOB_OWNER_CREDITS)) { + credits = jobOwnerCredits[jobOwners[jobKey_]]; + } else { + credits = (binJob_ << 128) >> 168; + } + + if (credits >= (uint256(rdConfig.jobMinCreditsFinney) * 0.001 ether)) { + return true; + } + + return false; + } + + function _chooseNextKeeper(bytes32 jobKey_, uint256 previousKeeperId_) internal { + uint256 totalActiveKeepers = activeKeepers.length(); + if (totalActiveKeepers == 0) { + emit JobKeeperChanged(jobKey_, previousKeeperId_, 0); + return; + } + uint256 index; + { + uint256 pseudoRandom = _getPseudoRandom(); + unchecked { + index = ((pseudoRandom + uint256(jobKey_)) % totalActiveKeepers); + } + } + uint256 requiredStake; + { + uint256 _jobMinKeeperCvp = jobMinKeeperCvp[jobKey_]; + requiredStake = _jobMinKeeperCvp > minKeeperCvp ? _jobMinKeeperCvp : minKeeperCvp; + } + uint256 closestUnderRequiredStakeValue = 0; + uint256 closestUnderRequiredStakeKeeperId = 0; + bool indexResetTo0 = false; + uint256 initialIndex = index; + + while (!indexResetTo0 || (indexResetTo0 && index < initialIndex)) { + if (index >= totalActiveKeepers) { + index = 0; + indexResetTo0 = true; + } + uint256 _nextExecutionKeeperId = activeKeepers.at(index); + + Keeper memory keeper = keepers[_nextExecutionKeeperId]; + + if (keeper.isActive) { + if (keeper.cvpStake >= requiredStake) { + _assignNextKeeper(jobKey_, previousKeeperId_, _nextExecutionKeeperId); + return; + } else { + if (keeper.cvpStake > closestUnderRequiredStakeValue) { + closestUnderRequiredStakeKeeperId = _nextExecutionKeeperId; + closestUnderRequiredStakeValue = keeper.cvpStake; + } + } + } + index += 1; + } + + if (closestUnderRequiredStakeValue > minKeeperCvp) { + _assignNextKeeper(jobKey_, previousKeeperId_, closestUnderRequiredStakeKeeperId); + return; + } + + // release job + emit JobKeeperChanged(jobKey_, previousKeeperId_, 0); + } + + function _checkBaseFee(uint256 binJob_, uint256 cfg_) internal pure override returns (uint256) { + binJob_; + cfg_; + + return type(uint256).max; + } + + function _assertJobCalldataMatchesSelector(uint256 binJob_, bytes memory jobCalldata_) internal pure { + assembly ("memory-safe") { + // CFG_ASSERT_RESOLVER_SELECTOR = 0x04 from PPAgentLiteFlags + if and(binJob_, 0x04) { + if iszero(eq( + // actual + shl(224, shr(224, mload(add(jobCalldata_, 32)))), + // expected + shl(224, shr(8, binJob_)) + )) { + // revert SelectorCheckFailed() + mstore(0, 0x74ab678100000000000000000000000000000000000000000000000000000000) + revert(0, 4) + } + } + } + } + + function calculateCompensation( + bool ok_, + uint256 job_, + uint256 keeperId_, + uint256 baseFee_, + uint256 gasUsed_ + ) public view override returns (uint256) { + if (!ok_) { + return (gasUsed_ + _getJobGasOverhead()) * baseFee_; + } + + uint256 stake = _getKeeperLimitedStake({ + keeperCurrentStake_: keepers[keeperId_].cvpStake, + agentMaxCvpStakeCvp_: uint256(rdConfig.agentMaxCvpStake), + job_: job_ + }); + + return rdConfig.jobFixedRewardFinney * FIXED_PAYMENT_MULTIPLIER + + (baseFee_ * (gasUsed_ + _getJobGasOverhead()) * rdConfig.jobCompensationMultiplierBps / 10_000) + + (stake / rdConfig.stakeDivisor); + } + + /* + * Returns a limited stake to be used for calculating the slashing and compensation amounts. + * + * @dev There are two limitations applied to the initial keeper stake: + * 1. It can't be > job-level max CVP limit defined by a job owner. + * 2. It can't be > agent-level(global) max CVP limit defined by the contract owner. + * @param keeperCurrentStake_ in CVP wei + * @param agentMaxCvpStakeCvp_ in CVP ether + * @param job_ binJob where jobMaxCvpStake in CVP ether is encoded into fixedReward field + * @return limitedStake in CVP wei + */ + function _getKeeperLimitedStake( + uint256 keeperCurrentStake_, + uint256 agentMaxCvpStakeCvp_, + uint256 job_ + ) internal pure returns (uint256 limitedStake) { + limitedStake = keeperCurrentStake_; + + // fixedReward field for randao jobs contains _jobMaxCvpStake + uint256 _jobMaxCvpStake = ((job_ << 64) >> 224) * 1 ether; + if (_jobMaxCvpStake > 0 && _jobMaxCvpStake < limitedStake) { + limitedStake = _jobMaxCvpStake; + } + uint256 _agentMaxCvpStake = agentMaxCvpStakeCvp_ * 1 ether; + if (_agentMaxCvpStake > 0 && _agentMaxCvpStake < limitedStake) { + limitedStake = _agentMaxCvpStake; + } + + return limitedStake; + } + + /*** GETTERS ***/ + + /* + * Returns a list of the jobsKeys assigned to a keeperId_. + * + * @dev The jobKeys array should exclude the constant placeholder bytes32(0) from its first element. + */ + function getJobsAssignedToKeeper(uint256 keeperId_) external view returns (bytes32[] memory actualJobKeys) { + bytes32[] memory allJobKeys = keeperLocksByJob[keeperId_].values(); + uint256 len = getJobsAssignedToKeeperLength(keeperId_); + if (len == 0) { + return new bytes32[](0); + } + actualJobKeys = new bytes32[](len); + for (uint256 i = 0; i < len; i++) { + actualJobKeys[i] = allJobKeys[i + 1]; + } + } + + function getJobsAssignedToKeeperLength(uint256 keeperId_) public view returns (uint256) { + uint256 len = keeperLocksByJob[keeperId_].length(); + if (len > 0) { + return len - 1; + } + return 0; + } + + function getCurrentSlasherId(bytes32 jobKey_) public view returns (uint256) { + return getSlasherIdByBlock(block.number, jobKey_); + } + + function getActiveKeepersLength() external view returns (uint256) { + return activeKeepers.length(); + } + + function getActiveKeepers() external view returns (uint256[] memory) { + return activeKeepers.values(); + } + + function getRdConfig() external view returns (RandaoConfig memory) { + return rdConfig; + } + + function getSlasherIdByBlock(uint256 blockNumber_, bytes32 jobKey_) public view returns (uint256) { + uint256 totalActiveKeepers = activeKeepers.length(); + uint256 index = ((blockNumber_ / rdConfig.slashingEpochBlocks + uint256(jobKey_)) % totalActiveKeepers); + return activeKeepers.at(index); + } +} diff --git a/contracts/PPAgentV2RandaoWithGasTracking.sol b/contracts/PPAgentV2RandaoWithGasTracking.sol index 1e4deb3..7912dec 100644 --- a/contracts/PPAgentV2RandaoWithGasTracking.sol +++ b/contracts/PPAgentV2RandaoWithGasTracking.sol @@ -1,17 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import { PPAgentV2Randao } from "./PPAgentV2Randao.sol"; +import {PPAgentV2RandaoBased} from "./PPAgentV2Randao.sol"; import { IPPGasUsedTracker } from "./PPAgentV2Interfaces.sol"; /** * @title PPAgentV2RandaoWithGasTracking * @author PowerPool */ -contract PPAgentV2RandaoWithGasTracking is PPAgentV2Randao { +contract PPAgentV2RandaoWithGasTracking is PPAgentV2RandaoBased { address public immutable gasUsedTracker; - constructor(address gasUsedTracker_, address cvp_) PPAgentV2Randao(cvp_) { + constructor(address gasUsedTracker_, address cvp_) PPAgentV2RandaoBased(cvp_) { gasUsedTracker = gasUsedTracker_; } diff --git a/contracts/PPAgentV2VRF.sol b/contracts/PPAgentV2VRF.sol index 21af01c..59dfbda 100644 --- a/contracts/PPAgentV2VRF.sol +++ b/contracts/PPAgentV2VRF.sol @@ -1,35 +1,33 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import { PPAgentV2Randao } from "./PPAgentV2Randao.sol"; - -import "./interfaces/VRFAgentConsumerInterface.sol"; +import {PPAgentV2VRFBased} from "./PPAgentV2VRFBased.sol"; /** * @title PPAgentV2VRF * @author PowerPool */ -contract PPAgentV2VRF is PPAgentV2Randao { - - address public VRFConsumer; - - event SetVRFConsumer(address consumer); +contract PPAgentV2VRF is PPAgentV2VRFBased { - error PseudoRandomError(); - - constructor(address cvp_) PPAgentV2Randao(cvp_) { + constructor(address cvp_) PPAgentV2VRFBased(cvp_) { } - function setVRFConsumer(address VRFConsumer_) external onlyOwner { - VRFConsumer = VRFConsumer_; - emit SetVRFConsumer(VRFConsumer_); + function _assertExecutionNotLocked() internal override view { + bytes32 lockKey = EXECUTION_LOCK_KEY; + assembly ("memory-safe") { + let isLocked := tload(lockKey) + if isLocked { + mstore(0x1c, 0x0815283600000000000000000000000000000000000000000000000000000000) + revert(0x1c, 4) + } + } } - function _getPseudoRandom() internal override returns (uint256) { - if (address(VRFConsumer) == address(0)) { - return super._getPseudoRandom(); + function _setExecutionLock(uint value_) internal override { + bytes32 lockKey = EXECUTION_LOCK_KEY; + assembly ("memory-safe") { + tstore(lockKey, value_) } - return VRFAgentConsumerInterface(VRFConsumer).getPseudoRandom(); } } diff --git a/contracts/PPAgentV2VRFBased.sol b/contracts/PPAgentV2VRFBased.sol new file mode 100644 index 0000000..807d404 --- /dev/null +++ b/contracts/PPAgentV2VRFBased.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {PPAgentV2RandaoBased} from "./PPAgentV2Randao.sol"; + +import "./interfaces/VRFAgentConsumerInterface.sol"; + +/** + * @title PPAgentV2VRFBased + * @author PowerPool + */ +contract PPAgentV2VRFBased is PPAgentV2RandaoBased { + + address public VRFConsumer; + + event SetVRFConsumer(address consumer); + + error PseudoRandomError(); + + constructor(address cvp_) PPAgentV2RandaoBased(cvp_) { + + } + + function setVRFConsumer(address VRFConsumer_) external onlyOwner { + VRFConsumer = VRFConsumer_; + emit SetVRFConsumer(VRFConsumer_); + } + + function _getPseudoRandom() internal override returns (uint256) { + if (address(VRFConsumer) == address(0)) { + return super._getPseudoRandom(); + } + return VRFAgentConsumerInterface(VRFConsumer).getPseudoRandom(); + } +} diff --git a/contracts/VRFAgentManager.sol b/contracts/VRFAgentManager.sol index ec34bc9..32002cb 100644 --- a/contracts/VRFAgentManager.sol +++ b/contracts/VRFAgentManager.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { PPAgentV2VRF } from "./PPAgentV2VRF.sol"; +import {PPAgentV2VRFBased} from "./PPAgentV2VRFBased.sol"; import { IPPAgentV2JobOwner, IPPAgentV2Viewer } from "./PPAgentV2Interfaces.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "./interfaces/VRFAgentCoordinatorInterface.sol"; @@ -15,7 +15,7 @@ import "./interfaces/VRFAgentConsumerInterface.sol"; */ contract VRFAgentManager is Ownable { - PPAgentV2VRF public agent; + PPAgentV2VRFBased public agent; VRFAgentCoordinatorInterface public coordinator; VRFAgentConsumerInterface public consumer; uint256 public subId; @@ -32,7 +32,7 @@ contract VRFAgentManager is Ownable { error JobDepositNotRequired(); error DepositBalanceIsNotEnough(); - constructor(PPAgentV2VRF agent_, VRFAgentCoordinatorInterface coordinator_) { + constructor(PPAgentV2VRFBased agent_, VRFAgentCoordinatorInterface coordinator_) { agent = agent_; coordinator = coordinator_; } @@ -169,7 +169,7 @@ contract VRFAgentManager is Ownable { } } - function setJobResolver(bytes32 jobKey_, PPAgentV2VRF.Resolver calldata resolver_) external onlyOwner { + function setJobResolver(bytes32 jobKey_, PPAgentV2VRFBased.Resolver calldata resolver_) external onlyOwner { agent.setJobResolver(jobKey_, resolver_); } @@ -210,7 +210,7 @@ contract VRFAgentManager is Ownable { agent.withdrawJobCredits(jobKey_, to_, amount_); } - function setRdConfig(PPAgentV2VRF.RandaoConfig calldata rdConfig_) external onlyOwner { + function setRdConfig(PPAgentV2VRFBased.RandaoConfig calldata rdConfig_) external onlyOwner { agent.setRdConfig(rdConfig_); } @@ -315,7 +315,7 @@ contract VRFAgentManager is Ownable { uint256 autoDepositJobBalance = getAutoDepositJobBalance(); setJobConfig(autoDepositJobKey, false, false, false, false); agent.withdrawJobCredits(autoDepositJobKey, payable(address(this)), autoDepositJobBalance); - (, , uint256 jobMinKeeperCvp, PPAgentV2VRF.Job memory details, , ) = agent.getJob(autoDepositJobKey); + (, , uint256 jobMinKeeperCvp, PPAgentV2VRFBased.Job memory details, , ) = agent.getJob(autoDepositJobKey); _registerAutoDepositJob(details.maxBaseFeeGwei, details.rewardPct, details.fixedReward, jobMinKeeperCvp, true, autoDepositJobBalance); @@ -335,7 +335,7 @@ contract VRFAgentManager is Ownable { /*** GETTER ***/ function getVrfFullfillJobBalance() public view returns(uint256) { - (, , , PPAgentV2VRF.Job memory details, , ) = agent.getJob(vrfJobKey); + (, , , PPAgentV2VRFBased.Job memory details, , ) = agent.getJob(vrfJobKey); return details.credits; } @@ -345,7 +345,7 @@ contract VRFAgentManager is Ownable { } function getAutoDepositJobBalance() public view returns(uint256) { - (, , , PPAgentV2VRF.Job memory details, , ) = agent.getJob(autoDepositJobKey); + (, , , PPAgentV2VRFBased.Job memory details, , ) = agent.getJob(autoDepositJobKey); return details.credits; } @@ -424,14 +424,14 @@ contract VRFAgentManager is Ownable { ); } - function getVrfResolverStruct() public view returns(PPAgentV2VRF.Resolver memory) { + function getVrfResolverStruct() public view returns(PPAgentV2VRFBased.Resolver memory) { return IPPAgentV2Viewer.Resolver({ resolverAddress: address(consumer), resolverCalldata: abi.encodeWithSelector(VRFAgentConsumerInterface.fulfillRandomnessResolver.selector) }); } - function getAutoDepositResolverStruct() public view returns(PPAgentV2VRF.Resolver memory) { + function getAutoDepositResolverStruct() public view returns(PPAgentV2VRFBased.Resolver memory) { return IPPAgentV2Viewer.Resolver({ resolverAddress: address(this), resolverCalldata: abi.encodeWithSelector(VRFAgentManager.vrfAutoDepositJobsResolver.selector) diff --git a/test/AgentOwnerTest.t.sol b/test/AgentOwnerTest.t.sol index 9f1ba88..0f8df11 100644 --- a/test/AgentOwnerTest.t.sol +++ b/test/AgentOwnerTest.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; +import "../contracts/PPAgentV2Based.sol"; import "../contracts/PPAgentV2.sol"; import "./mocks/MockCVP.sol"; import "./TestHelper.sol"; @@ -54,13 +55,13 @@ contract AgentOwnerTest is TestHelper { } function testErrSetPendingWithdrawalTimeoutTooBig() public { - vm.expectRevert(PPAgentV2.TimeoutTooBig.selector); + vm.expectRevert(PPAgentV2Based.TimeoutTooBig.selector); vm.prank(owner); agent.setAgentParams(2, 30 days + 1, 2); } function testErrSetFeeTooBig() public { - vm.expectRevert(PPAgentV2.FeeTooBig.selector); + vm.expectRevert(PPAgentV2Based.FeeTooBig.selector); vm.prank(owner); agent.setAgentParams(2, 30 days, 5e4+1); } diff --git a/test/CompensationTest.t.sol b/test/CompensationTest.t.sol index 9c2648d..8d4c4fa 100644 --- a/test/CompensationTest.t.sol +++ b/test/CompensationTest.t.sol @@ -13,7 +13,7 @@ contract CompensationTest is Test, PPAgentV2Flags { uint256 internal constant CVP_LIMIT = 100_000_000 ether; MockCVP internal cvp; - PPAgentV2 internal agent; + PPAgentV2Based internal agent; function setUp() public { cvp = new MockCVP(); diff --git a/test/ExecuteResolverTest.t.sol b/test/ExecuteResolverTest.t.sol index 14afc5a..bafa57a 100644 --- a/test/ExecuteResolverTest.t.sol +++ b/test/ExecuteResolverTest.t.sol @@ -163,7 +163,7 @@ contract ExecuteResolverTest is TestHelper { (bool ok, bytes memory cd) = job.myResolver("myPass"); assertEq(ok, true); - vm.expectRevert(PPAgentV2.SelectorCheckFailed.selector); + vm.expectRevert(PPAgentV2Based.SelectorCheckFailed.selector); vm.prank(keeperWorker, keeperWorker); _callExecuteHelper( agent, @@ -218,7 +218,7 @@ contract ExecuteResolverTest is TestHelper { (bool ok, bytes memory cd) = job.myResolver("myPass"); assertEq(ok, true); - vm.expectRevert(PPAgentV2.SelectorCheckFailed.selector); + vm.expectRevert(PPAgentV2Based.SelectorCheckFailed.selector); vm.prank(keeperWorker, keeperWorker); _callExecuteHelper( agent, @@ -273,7 +273,7 @@ contract ExecuteResolverTest is TestHelper { (bool ok, bytes memory cd) = job.myResolver("myPass"); assertEq(ok, true); - vm.expectRevert(PPAgentV2.SelectorCheckFailed.selector); + vm.expectRevert(PPAgentV2Based.SelectorCheckFailed.selector); vm.prank(keeperWorker, keeperWorker); _callExecuteHelper( agent, @@ -294,7 +294,7 @@ contract ExecuteResolverTest is TestHelper { (bool ok, bytes memory cd) = job.myResolver("myPass"); assertEq(ok, true); - vm.expectRevert(PPAgentV2.JobCallRevertedWithoutDetails.selector); + vm.expectRevert(PPAgentV2Based.JobCallRevertedWithoutDetails.selector); vm.prank(keeperWorker, keeperWorker); _callExecuteHelper( agent, diff --git a/test/ExecuteSelectorTest.t.sol b/test/ExecuteSelectorTest.t.sol index edf09e2..2bf6f28 100644 --- a/test/ExecuteSelectorTest.t.sol +++ b/test/ExecuteSelectorTest.t.sol @@ -98,7 +98,7 @@ contract ExecuteSelectorTest is TestHelper { assertEq(minKeeperCvp, 5_001 ether); assertEq(_stakeOf(kid), 5_000 ether); - vm.expectRevert(PPAgentV2.InsufficientKeeperStake.selector); + vm.expectRevert(PPAgentV2Based.InsufficientKeeperStake.selector); vm.prank(keeperWorker, keeperWorker); _callExecuteHelper( @@ -120,7 +120,7 @@ contract ExecuteSelectorTest is TestHelper { assertEq(_stakeOf(kid), 5_000 ether); assertEq(_jobMinKeeperCvp(jobKey), 5001 ether); - vm.expectRevert(PPAgentV2.InsufficientJobScopedKeeperStake.selector); + vm.expectRevert(PPAgentV2Based.InsufficientJobScopedKeeperStake.selector); vm.prank(keeperWorker, keeperWorker); _callExecuteHelper( @@ -138,7 +138,7 @@ contract ExecuteSelectorTest is TestHelper { agent.setJobConfig(jobKey, false, false, false, false); vm.expectRevert(abi.encodeWithSelector( - PPAgentV2.InactiveJob.selector, + PPAgentV2Based.InactiveJob.selector, jobKey )); @@ -169,7 +169,7 @@ contract ExecuteSelectorTest is TestHelper { vm.warp(block.timestamp + 3); vm.expectRevert(abi.encodeWithSelector( - PPAgentV2.IntervalNotReached.selector, + PPAgentV2Based.IntervalNotReached.selector, 1600000000, 10, 1600000003 @@ -207,7 +207,7 @@ contract ExecuteSelectorTest is TestHelper { accrueReward: true }); vm.expectRevert(abi.encodeWithSelector( - PPAgentV2.BaseFeeGtGasPrice.selector, + PPAgentV2Based.BaseFeeGtGasPrice.selector, 101 gwei, 100 gwei )); @@ -281,7 +281,7 @@ contract ExecuteSelectorTest is TestHelper { } function testErrNotEOA() public { - vm.expectRevert(PPAgentV2.NonEOASender.selector); + vm.expectRevert(PPAgentV2Based.NonEOASender.selector); vm.prank(keeperWorker, bob); _callExecuteHelper( agent, @@ -415,7 +415,7 @@ contract ExecuteSelectorTest is TestHelper { (bool ok, bytes memory cdata) = topupJob.myResolver(topupJobKey); assertEq(ok, true); - vm.expectRevert(PPAgentV2.ExecutionReentrancyLocked.selector); + vm.expectRevert(PPAgentV2Based.ExecutionReentrancyLocked.selector); vm.prank(keeperWorker, keeperWorker); _callExecuteHelper( agent, diff --git a/test/JobManagementTest.t.sol b/test/JobManagementTest.t.sol index 9ab85ba..a04bfda 100644 --- a/test/JobManagementTest.t.sol +++ b/test/JobManagementTest.t.sol @@ -149,7 +149,7 @@ contract JobManagementTest is TestHelper { function testErrAddJobCreditsZeroDeposit() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.MissingDeposit.selector) + abi.encodeWithSelector(PPAgentV2Based.MissingDeposit.selector) ); vm.prank(alice); @@ -159,7 +159,7 @@ contract JobManagementTest is TestHelper { function testErrAddJobCreditsNoOwner() public { bytes32 fakeJobKey = bytes32(uint256(123)); vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.JobWithoutOwner.selector) + abi.encodeWithSelector(PPAgentV2Based.JobWithoutOwner.selector) ); vm.prank(alice); @@ -168,7 +168,7 @@ contract JobManagementTest is TestHelper { function testErrAddJobCreditsCreditsOverflowOneStep() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.CreditsDepositOverflow.selector) + abi.encodeWithSelector(PPAgentV2Based.CreditsDepositOverflow.selector) ); uint256 value = uint256(type(uint88).max) + 5; @@ -190,7 +190,7 @@ contract JobManagementTest is TestHelper { vm.prank(alice); vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.CreditsDepositOverflow.selector) + abi.encodeWithSelector(PPAgentV2Based.CreditsDepositOverflow.selector) ); agent.depositJobCredits{ value: second }(jobKey); } @@ -245,7 +245,7 @@ contract JobManagementTest is TestHelper { agent.depositJobCredits{ value: 1.2 ether}(jobKey); vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.MissingAmount.selector) + abi.encodeWithSelector(PPAgentV2Based.MissingAmount.selector) ); vm.prank(alice); @@ -257,7 +257,7 @@ contract JobManagementTest is TestHelper { agent.depositJobCredits{ value: 1.2 ether}(jobKey); vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.OnlyJobOwner.selector) + abi.encodeWithSelector(PPAgentV2Based.OnlyJobOwner.selector) ); vm.prank(bob); @@ -269,7 +269,7 @@ contract JobManagementTest is TestHelper { agent.depositJobCredits{ value: 1.2 ether}(jobKey); vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.CreditsWithdrawalUnderflow.selector) + abi.encodeWithSelector(PPAgentV2Based.CreditsWithdrawalUnderflow.selector) ); vm.prank(alice); @@ -337,7 +337,7 @@ contract JobManagementTest is TestHelper { vm.prank(alice); agent.initiateJobTransfer(jobKey, bob); vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.OnlyPendingOwner.selector) + abi.encodeWithSelector(PPAgentV2Based.OnlyPendingOwner.selector) ); vm.prank(alice); agent.acceptJobTransfer(jobKey); @@ -345,7 +345,7 @@ contract JobManagementTest is TestHelper { function testErrJobTransferNotTheOwner() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.OnlyJobOwner.selector) + abi.encodeWithSelector(PPAgentV2Based.OnlyJobOwner.selector) ); vm.prank(bob); @@ -397,7 +397,7 @@ contract JobManagementTest is TestHelper { function testErrSetJobActiveNotOwner() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.OnlyJobOwner.selector) + abi.encodeWithSelector(PPAgentV2Based.OnlyJobOwner.selector) ); vm.prank(bob); @@ -481,7 +481,7 @@ contract JobManagementTest is TestHelper { function testErrUpdateJobNotOwner() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.OnlyJobOwner.selector) + abi.encodeWithSelector(PPAgentV2Based.OnlyJobOwner.selector) ); vm.prank(bob); @@ -490,7 +490,7 @@ contract JobManagementTest is TestHelper { function testErrUpdateJobMissingMaxBaseFeeGwei() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.MissingMaxBaseFeeGwei.selector) + abi.encodeWithSelector(PPAgentV2Based.MissingMaxBaseFeeGwei.selector) ); vm.prank(alice); @@ -504,7 +504,7 @@ contract JobManagementTest is TestHelper { agent.updateJob(jobKey, 200, 0, 5, 0, 60); vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.NoFixedNorPremiumPctReward.selector) + abi.encodeWithSelector(PPAgentV2Based.NoFixedNorPremiumPctReward.selector) ); vm.prank(alice); @@ -513,7 +513,7 @@ contract JobManagementTest is TestHelper { function testErrUpdateJobWithSelectorShouldHaveInterval() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.JobShouldHaveInterval.selector) + abi.encodeWithSelector(PPAgentV2Based.JobShouldHaveInterval.selector) ); vm.prank(alice); @@ -526,7 +526,7 @@ contract JobManagementTest is TestHelper { vm.prank(alice); (bytes32 myJobKey,) = agent.registerJob(params, resolver1, new bytes(0)); vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.JobShouldHaveInterval.selector) + abi.encodeWithSelector(PPAgentV2Based.JobShouldHaveInterval.selector) ); vm.prank(alice); @@ -541,7 +541,7 @@ contract JobManagementTest is TestHelper { (bytes32 myJobKey,) = agent.registerJob(params, resolver1, new bytes(0)); vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.JobDoesNotSupposedToHaveInterval.selector) + abi.encodeWithSelector(PPAgentV2Based.JobDoesNotSupposedToHaveInterval.selector) ); vm.prank(alice); agent.updateJob(myJobKey, 200, 55, 20, 0, 1); @@ -568,7 +568,7 @@ contract JobManagementTest is TestHelper { function testErrSetResolverNotOwner() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.OnlyJobOwner.selector) + abi.encodeWithSelector(PPAgentV2Based.OnlyJobOwner.selector) ); PPAgentV2.Resolver memory newResolver = IPPAgentV2Viewer.Resolver(address(2), hex"313373"); @@ -577,7 +577,7 @@ contract JobManagementTest is TestHelper { function testErrSetResolverOnlyResolverCalldataSource() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.NotSupportedByJobCalldataSource.selector) + abi.encodeWithSelector(PPAgentV2Based.NotSupportedByJobCalldataSource.selector) ); PPAgentV2.Resolver memory newResolver = IPPAgentV2Viewer.Resolver(address(2), hex"313373"); @@ -587,7 +587,7 @@ contract JobManagementTest is TestHelper { function testErrSetResolverMissingResolverAddress() public jobWithResolverCalldataSource { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.MissingResolverAddress.selector) + abi.encodeWithSelector(PPAgentV2Based.MissingResolverAddress.selector) ); PPAgentV2.Resolver memory newResolver = IPPAgentV2Viewer.Resolver(address(0), hex"313373"); @@ -610,7 +610,7 @@ contract JobManagementTest is TestHelper { function testErrSetPreDefinedCalldataNotOwner() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.OnlyJobOwner.selector) + abi.encodeWithSelector(PPAgentV2Based.OnlyJobOwner.selector) ); agent.setJobPreDefinedCalldata(jobKey, hex"313373"); @@ -618,7 +618,7 @@ contract JobManagementTest is TestHelper { function testErrSetPreDefinedCalldataForSelectorJob() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.NotSupportedByJobCalldataSource.selector) + abi.encodeWithSelector(PPAgentV2Based.NotSupportedByJobCalldataSource.selector) ); vm.prank(alice); diff --git a/test/JobOwnerTest.t.sol b/test/JobOwnerTest.t.sol index 567b26a..e8cd65c 100644 --- a/test/JobOwnerTest.t.sol +++ b/test/JobOwnerTest.t.sol @@ -90,7 +90,7 @@ contract JobOwnerTest is TestHelper { function testErrAddJobOwnerCreditsZeroDeposit() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.MissingDeposit.selector) + abi.encodeWithSelector(PPAgentV2Based.MissingDeposit.selector) ); vm.prank(alice); @@ -144,7 +144,7 @@ contract JobOwnerTest is TestHelper { function testErrRemoveJobOwnerCreditsMissingAmount() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.MissingAmount.selector) + abi.encodeWithSelector(PPAgentV2Based.MissingAmount.selector) ); vm.prank(alice); @@ -156,7 +156,7 @@ contract JobOwnerTest is TestHelper { agent.depositJobOwnerCredits{ value: 10 ether}(alice); vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.CreditsWithdrawalUnderflow.selector) + abi.encodeWithSelector(PPAgentV2Based.CreditsWithdrawalUnderflow.selector) ); vm.prank(alice); diff --git a/test/KeeperTest.t.sol b/test/KeeperTest.t.sol index c4d1e9b..8800d6d 100644 --- a/test/KeeperTest.t.sol +++ b/test/KeeperTest.t.sol @@ -75,7 +75,7 @@ contract KeeperTest is TestHelper { vm.prank(keeperAdmin); cvp.approve(address(agent), MIN_DEPOSIT_3000_CVP); - vm.expectRevert(PPAgentV2.WorkerAlreadyAssigned.selector); + vm.expectRevert(PPAgentV2Based.WorkerAlreadyAssigned.selector); kid = agent.registerAsKeeper(keeperWorker, MIN_DEPOSIT_3000_CVP - 1); } @@ -85,7 +85,7 @@ contract KeeperTest is TestHelper { vm.prank(keeperAdmin); cvp.approve(address(agent), MIN_DEPOSIT_3000_CVP); - vm.expectRevert(PPAgentV2.InsufficientAmount.selector); + vm.expectRevert(PPAgentV2Based.InsufficientAmount.selector); address keeperWorker2 = address(1); kid = agent.registerAsKeeper(keeperWorker2, MIN_DEPOSIT_3000_CVP - 1); } @@ -147,7 +147,7 @@ contract KeeperTest is TestHelper { function testErrWithdrawExtraCompensation() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.WithdrawAmountExceedsAvailable.selector, 22 ether, 20 ether) + abi.encodeWithSelector(PPAgentV2Based.WithdrawAmountExceedsAvailable.selector, 22 ether, 20 ether) ); vm.prank(keeperAdmin); @@ -156,7 +156,7 @@ contract KeeperTest is TestHelper { function testErrWithdrawAnotherExtraCompensation() public { vm.expectRevert(abi.encodeWithSelector( - PPAgentV2.WithdrawAmountExceedsAvailable.selector, + PPAgentV2Based.WithdrawAmountExceedsAvailable.selector, 21 ether, 20 ether )); @@ -167,7 +167,7 @@ contract KeeperTest is TestHelper { function testErrWithdrawZeroCompensation() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.MissingAmount.selector) + abi.encodeWithSelector(PPAgentV2Based.MissingAmount.selector) ); vm.prank(keeperAdmin); @@ -227,7 +227,7 @@ contract KeeperTest is TestHelper { address keeperWorker2 = address(2); uint256 kid2 = agent.registerAsKeeper(keeperWorker2, MIN_DEPOSIT_3000_CVP); - vm.expectRevert(PPAgentV2.WorkerAlreadyAssigned.selector); + vm.expectRevert(PPAgentV2Based.WorkerAlreadyAssigned.selector); agent.setWorkerAddress(kid2, keeperWorker); agent.setWorkerAddress(kid, address(0)); agent.setWorkerAddress(kid2, keeperWorker); @@ -241,7 +241,7 @@ contract KeeperTest is TestHelper { vm.prank(keeperAdmin); cvp.approve(address(agent), MIN_DEPOSIT_3000_CVP); - vm.expectRevert(PPAgentV2.OnlyKeeperAdmin.selector); + vm.expectRevert(PPAgentV2Based.OnlyKeeperAdmin.selector); agent.setWorkerAddress(kid, address(1)); } } diff --git a/test/RegisterJobTest.t.sol b/test/RegisterJobTest.t.sol index 463232a..c7e7a2f 100644 --- a/test/RegisterJobTest.t.sol +++ b/test/RegisterJobTest.t.sol @@ -161,7 +161,7 @@ contract RegisterJob is TestHelper { }); (bool ok, bytes memory result) = address(agent).staticcall( - abi.encodeWithSelector(PPAgentV2.getJob.selector, jobKey) + abi.encodeWithSelector(PPAgentV2Based.getJob.selector, jobKey) ); assertEq(ok, true); assertEq(result.length, 576); @@ -197,7 +197,7 @@ contract RegisterJob is TestHelper { params.jobAddress = address(cvp); vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.InvalidJobAddress.selector) + abi.encodeWithSelector(PPAgentV2Based.InvalidJobAddress.selector) ); agent.registerJob({ params_: params, @@ -211,7 +211,7 @@ contract RegisterJob is TestHelper { params.intervalSeconds = 0; vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.JobShouldHaveInterval.selector) + abi.encodeWithSelector(PPAgentV2Based.JobShouldHaveInterval.selector) ); agent.registerJob({ params_: params, @@ -225,7 +225,7 @@ contract RegisterJob is TestHelper { params.jobAddress = address(0); vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.MissingJobAddress.selector) + abi.encodeWithSelector(PPAgentV2Based.MissingJobAddress.selector) ); agent.registerJob({ params_: params, @@ -239,7 +239,7 @@ contract RegisterJob is TestHelper { params.maxBaseFeeGwei = 0; vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.MissingMaxBaseFeeGwei.selector) + abi.encodeWithSelector(PPAgentV2Based.MissingMaxBaseFeeGwei.selector) ); agent.registerJob({ params_: params, @@ -269,7 +269,7 @@ contract RegisterJob is TestHelper { params.rewardPct = 0; vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.NoFixedNorPremiumPctReward.selector) + abi.encodeWithSelector(PPAgentV2Based.NoFixedNorPremiumPctReward.selector) ); agent.registerJob({ params_: params, @@ -283,7 +283,7 @@ contract RegisterJob is TestHelper { params.calldataSource = 4; vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.InvalidCalldataSource.selector) + abi.encodeWithSelector(PPAgentV2Based.InvalidCalldataSource.selector) ); agent.registerJob({ params_: params, @@ -342,7 +342,7 @@ contract RegisterJob is TestHelper { params.intervalSeconds = 0; vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.JobShouldHaveInterval.selector) + abi.encodeWithSelector(PPAgentV2Based.JobShouldHaveInterval.selector) ); agent.registerJob({ params_: params, @@ -396,7 +396,7 @@ contract RegisterJob is TestHelper { params.intervalSeconds = 1_000; vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.JobDoesNotSupposedToHaveInterval.selector) + abi.encodeWithSelector(PPAgentV2Based.JobDoesNotSupposedToHaveInterval.selector) ); agent.registerJob({ params_: params, @@ -411,7 +411,7 @@ contract RegisterJob is TestHelper { resolver.resolverAddress = address(0); vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.MissingResolverAddress.selector) + abi.encodeWithSelector(PPAgentV2Based.MissingResolverAddress.selector) ); agent.registerJob({ params_: params, @@ -499,7 +499,7 @@ contract RegisterJob is TestHelper { IPPAgentV2JobOwner.RegisterJobParams memory params = params1; params.useJobOwnerCredits = false; - vm.expectRevert(PPAgentV2.CreditsDepositOverflow.selector); + vm.expectRevert(PPAgentV2Based.CreditsDepositOverflow.selector); vm.deal(alice, type(uint256).max); vm.prank(alice); agent.registerJob{ value: uint256(type(uint96).max) + 1 }({ @@ -513,7 +513,7 @@ contract RegisterJob is TestHelper { IPPAgentV2JobOwner.RegisterJobParams memory params = params1; params.useJobOwnerCredits = true; - vm.expectRevert(PPAgentV2.CreditsDepositOverflow.selector); + vm.expectRevert(PPAgentV2Based.CreditsDepositOverflow.selector); vm.deal(alice, type(uint256).max); vm.prank(alice); agent.registerJob{ value: uint256(type(uint96).max) + 1 }({ diff --git a/test/SlashingTest.t.sol b/test/SlashingTest.t.sol index 7b3914f..24e9e8d 100644 --- a/test/SlashingTest.t.sol +++ b/test/SlashingTest.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; +import "../contracts/PPAgentV2Based.sol"; import "../contracts/PPAgentV2.sol"; import "./mocks/MockCVP.sol"; import "./TestHelper.sol"; @@ -29,7 +30,7 @@ contract StakingTest is TestHelper { function testErrSlashZeroTotalAmount() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.MissingAmount.selector) + abi.encodeWithSelector(PPAgentV2Based.MissingAmount.selector) ); vm.prank(owner); agent.ownerSlash(kid, bob, 0, 0); @@ -146,7 +147,7 @@ contract StakingTest is TestHelper { // Wont burn vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.InsufficientAmountToCoverSlashedStake.selector, 499 ether, 500 ether) + abi.encodeWithSelector(PPAgentV2Based.InsufficientAmountToCoverSlashedStake.selector, 499 ether, 500 ether) ); vm.prank(keeperAdmin); agent.initiateRedeem(kid, 499 ether); @@ -166,7 +167,7 @@ contract StakingTest is TestHelper { vm.warp(block.timestamp + 3 days + 1); vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.NoPendingWithdrawal.selector) + abi.encodeWithSelector(PPAgentV2Based.NoPendingWithdrawal.selector) ); vm.prank(keeperAdmin); agent.finalizeRedeem(kid, keeperAdmin); @@ -202,7 +203,7 @@ contract StakingTest is TestHelper { // Wont burn vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.InsufficientAmountToCoverSlashedStake.selector, 2_999 ether, 3_000 ether) + abi.encodeWithSelector(PPAgentV2Based.InsufficientAmountToCoverSlashedStake.selector, 2_999 ether, 3_000 ether) ); vm.prank(keeperAdmin); agent.initiateRedeem(kid, 2_999 ether); @@ -222,7 +223,7 @@ contract StakingTest is TestHelper { vm.warp(block.timestamp + 3 days + 1); vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.NoPendingWithdrawal.selector) + abi.encodeWithSelector(PPAgentV2Based.NoPendingWithdrawal.selector) ); vm.prank(keeperAdmin); agent.finalizeRedeem(kid, keeperAdmin); diff --git a/test/StakingTest.t.sol b/test/StakingTest.t.sol index 2bdcdc6..2955e13 100644 --- a/test/StakingTest.t.sol +++ b/test/StakingTest.t.sol @@ -69,10 +69,10 @@ contract StakingTest is TestHelper { vm.startPrank(keeperAdmin); agent.initiateRedeem(kid1, 500 ether); - vm.expectRevert(abi.encodeWithSelector(PPAgentV2.WithdrawalTimoutNotReached.selector)); + vm.expectRevert(abi.encodeWithSelector(PPAgentV2Based.WithdrawalTimoutNotReached.selector)); agent.finalizeRedeem(kid1, keeperAdmin); vm.warp(block.timestamp + 2 days); - vm.expectRevert(abi.encodeWithSelector(PPAgentV2.WithdrawalTimoutNotReached.selector)); + vm.expectRevert(abi.encodeWithSelector(PPAgentV2Based.WithdrawalTimoutNotReached.selector)); agent.finalizeRedeem(kid1, keeperAdmin); assertEq(_stakeOf(kid1), amount + 3_000 ether - 500 ether); @@ -108,7 +108,7 @@ contract StakingTest is TestHelper { // Redeem #1 vm.startPrank(keeperAdmin); agent.initiateRedeem(kid1, 500 ether); - vm.expectRevert(abi.encodeWithSelector(PPAgentV2.WithdrawalTimoutNotReached.selector)); + vm.expectRevert(abi.encodeWithSelector(PPAgentV2Based.WithdrawalTimoutNotReached.selector)); agent.finalizeRedeem(kid1, keeperAdmin); assertEq(_stakeOf(kid1), 2_500 ether); @@ -141,7 +141,7 @@ contract StakingTest is TestHelper { assertEq(_pendingWithdrawalAmountOf(kid1), 0); assertEq(_pendingWithdrawalEndsAt(kid1), block.timestamp - 1); - vm.expectRevert(PPAgentV2.NoPendingWithdrawal.selector); + vm.expectRevert(PPAgentV2Based.NoPendingWithdrawal.selector); agent.finalizeRedeem(kid1, keeperAdmin); vm.stopPrank(); } @@ -166,7 +166,7 @@ contract StakingTest is TestHelper { function testRedeemWithNotEnoughStake() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.AmountGtStake.selector, 3_001 ether, 3_000 ether, 0) + abi.encodeWithSelector(PPAgentV2Based.AmountGtStake.selector, 3_001 ether, 3_000 ether, 0) ); vm.prank(keeperAdmin); agent.initiateRedeem(kid1, 3_001 ether); @@ -175,21 +175,21 @@ contract StakingTest is TestHelper { function testErrInvalidKeeperId() public { assertEq(_keeperCount(), 2); vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.InvalidKeeperId.selector) + abi.encodeWithSelector(PPAgentV2Based.InvalidKeeperId.selector) ); agent.stake(3, 1 ether); } function testErrStakeZero() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.MissingAmount.selector) + abi.encodeWithSelector(PPAgentV2Based.MissingAmount.selector) ); agent.stake(kid1, 0); } function testErrRedeemZero() public { vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.MissingAmount.selector) + abi.encodeWithSelector(PPAgentV2Based.MissingAmount.selector) ); vm.prank(keeperAdmin); agent.initiateRedeem(kid1, 0); diff --git a/test/TestHelper.sol b/test/TestHelper.sol index 61de736..fd917c2 100644 --- a/test/TestHelper.sol +++ b/test/TestHelper.sol @@ -5,7 +5,7 @@ import "./abstract/AbstractTestHelper.sol"; import "../contracts/PPAgentV2.sol"; contract TestHelper is AbstractTestHelper { - PPAgentV2 internal agent; + PPAgentV2Based internal agent; function _agentViewer() internal override view returns(IPPAgentV2Viewer) { return IPPAgentV2Viewer(address(agent)); diff --git a/test/TestHelperRandao.sol b/test/TestHelperRandao.sol index 4e0dd51..5f0dbaa 100644 --- a/test/TestHelperRandao.sol +++ b/test/TestHelperRandao.sol @@ -5,7 +5,7 @@ import "./abstract/AbstractTestHelper.sol"; import "../contracts/PPAgentV2Randao.sol"; contract TestHelperRandao is AbstractTestHelper { - PPAgentV2Randao internal agent; + PPAgentV2RandaoBased internal agent; function _agentViewer() internal override view returns(IPPAgentV2Viewer) { return IPPAgentV2Viewer(address(agent)); diff --git a/test/abstract/AbstractTestHelper.sol b/test/abstract/AbstractTestHelper.sol index 7061687..53d5472 100644 --- a/test/abstract/AbstractTestHelper.sol +++ b/test/abstract/AbstractTestHelper.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.13; import "../../contracts/PPAgentV2Flags.sol"; -import { ConfigFlags } from "../../contracts/PPAgentV2.sol"; +import { ConfigFlags } from "../../contracts/PPAgentV2Based.sol"; import "../../lib/forge-std/src/Test.sol"; import "./../mocks/MockCVP.sol"; import "../../contracts/PPAgentV2Interfaces.sol"; diff --git a/test/jobs/JobTopupTestJob.sol b/test/jobs/JobTopupTestJob.sol index eaa428d..e9e5034 100644 --- a/test/jobs/JobTopupTestJob.sol +++ b/test/jobs/JobTopupTestJob.sol @@ -15,7 +15,7 @@ contract JobTopupTestJob is AgentJob { function execute(bytes32 jobKey_) external onlyAgent { require(address(this).balance >= 8.42 ether, "missing 8.42 ether"); - PPAgentV2(agent).depositJobCredits{value: 8.42 ether}(jobKey_); + PPAgentV2Based(agent).depositJobCredits{value: 8.42 ether}(jobKey_); } receive() external payable { diff --git a/test/jobs/JobWithdrawTestJob.sol b/test/jobs/JobWithdrawTestJob.sol index 3893875..9b3ee5c 100644 --- a/test/jobs/JobWithdrawTestJob.sol +++ b/test/jobs/JobWithdrawTestJob.sol @@ -14,11 +14,11 @@ contract JobWithdrawTestJob is AgentJob { } function execute(bytes32 jobKey_) external onlyAgent { - PPAgentV2(agent).withdrawJobCredits(jobKey_, payable(this), type(uint256).max); + PPAgentV2Based(agent).withdrawJobCredits(jobKey_, payable(this), type(uint256).max); } function acceptJobTransfer(bytes32 jobKey_) external { - PPAgentV2(agent).acceptJobTransfer(jobKey_); + PPAgentV2Based(agent).acceptJobTransfer(jobKey_); } receive() external payable { diff --git a/test/mocks/MockExposedAgent.sol b/test/mocks/MockExposedAgent.sol index a4da16d..6295e38 100644 --- a/test/mocks/MockExposedAgent.sol +++ b/test/mocks/MockExposedAgent.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; -import "../../contracts/PPAgentV2Randao.sol"; +import "../../contracts/PPAgentV2RandaoBased.sol"; -contract MockExposedAgent is PPAgentV2Randao { - constructor(address cvp_) PPAgentV2Randao(cvp_) { +contract MockExposedAgent is PPAgentV2RandaoBased { + constructor(address cvp_) PPAgentV2RandaoBased(cvp_) { } function assignNextKeeper(bytes32 jobKey_) external { diff --git a/test/mocks/MockVRFCoordinator.sol b/test/mocks/MockVRFCoordinator.sol index 274b461..16a0faa 100644 --- a/test/mocks/MockVRFCoordinator.sol +++ b/test/mocks/MockVRFCoordinator.sol @@ -36,7 +36,7 @@ contract MockVRFCoordinator is VRFAgentCoordinator { uint256[] memory words = new uint256[](requestedNumWords); uint256 requestId = lastRequestIdByConsumer[requestedByConsumer]; for (uint256 i = 0; i < words.length; i++) { - words[i] = i + requestId + 55; + words[i] = i + requestId + 66; } VRFAgentConsumer(requestedByConsumer).rawFulfillRandomWords(requestId, words); delete s_requestCommitments[requestId]; diff --git a/test/randao/ActorsTest.t.sol b/test/randao/ActorsTest.t.sol index d06936c..fe3d834 100644 --- a/test/randao/ActorsTest.t.sol +++ b/test/randao/ActorsTest.t.sol @@ -176,7 +176,7 @@ contract RandaoActorsTest is TestHelperRandao { vm.prank(keeperAdmin, keeperAdmin); vm.expectRevert( abi.encodeWithSelector( - PPAgentV2Randao.TooEarlyForActivationFinalization.selector, + PPAgentV2RandaoBased.TooEarlyForActivationFinalization.selector, block.timestamp, block.timestamp + 8 hours ) @@ -204,7 +204,7 @@ contract RandaoActorsTest is TestHelperRandao { vm.warp(block.timestamp + 3 days + 1); agent.finalizeRedeem(kid3, keeperAdmin); - vm.expectRevert(abi.encodeWithSelector(PPAgentV2.InsufficientKeeperStake.selector)); + vm.expectRevert(abi.encodeWithSelector(PPAgentV2Based.InsufficientKeeperStake.selector)); agent.initiateKeeperActivation(kid3); assertEq(_keeperIsActive(kid3), false); @@ -219,7 +219,7 @@ contract RandaoActorsTest is TestHelperRandao { agent.finalizeRedeem(kid3, keeperAdmin); vm.warp(block.timestamp + 9 hours); - vm.expectRevert(abi.encodeWithSelector(PPAgentV2.InsufficientKeeperStake.selector)); + vm.expectRevert(abi.encodeWithSelector(PPAgentV2Based.InsufficientKeeperStake.selector)); agent.finalizeKeeperActivation(kid3); assertEq(_keeperIsActive(kid3), false); @@ -233,11 +233,11 @@ contract RandaoActorsTest is TestHelperRandao { function testRdKeeperCantSetActiveAgain() public { assertEq(_keeperIsActive(3), true); - vm.expectRevert(PPAgentV2Randao.KeeperIsAlreadyActive.selector); + vm.expectRevert(PPAgentV2RandaoBased.KeeperIsAlreadyActive.selector); vm.prank(keeperAdmin, keeperAdmin); agent.initiateKeeperActivation(kid3); vm.roll(9 hours); - vm.expectRevert(PPAgentV2Randao.ActivationNotInitiated.selector); + vm.expectRevert(PPAgentV2RandaoBased.ActivationNotInitiated.selector); vm.prank(keeperAdmin, keeperAdmin); agent.finalizeKeeperActivation(kid3); assertEq(_keeperIsActive(3), true); @@ -247,7 +247,7 @@ contract RandaoActorsTest is TestHelperRandao { vm.prank(keeperAdmin, keeperAdmin); agent.disableKeeper(kid3); - vm.expectRevert(PPAgentV2Randao.KeeperIsAlreadyInactive.selector); + vm.expectRevert(PPAgentV2RandaoBased.KeeperIsAlreadyInactive.selector); vm.prank(keeperAdmin, keeperAdmin); agent.disableKeeper(kid3); } diff --git a/test/randao/AgentOwnerTest.t.sol b/test/randao/AgentOwnerTest.t.sol index 271aaa0..1b89725 100644 --- a/test/randao/AgentOwnerTest.t.sol +++ b/test/randao/AgentOwnerTest.t.sol @@ -31,9 +31,9 @@ contract RandaoAgentOwnerTest is TestHelperRandao { keeperActivationTimeoutHours: 8, jobFixedRewardFinney: 30 }); - PPAgentV2Randao rAgent = new PPAgentV2Randao(address(cvp)); + PPAgentV2RandaoBased rAgent = new PPAgentV2Randao(address(cvp)); - vm.expectRevert(PPAgentV2Randao.JobCompensationMultiplierBpsLT10000.selector); + vm.expectRevert(PPAgentV2RandaoBased.JobCompensationMultiplierBpsLT10000.selector); rAgent.initializeRandao(owner, 3_000 ether, 3 days, rdConfig); } } diff --git a/test/randao/AssignKeeperTest.t.sol b/test/randao/AssignKeeperTest.t.sol index 92bc6c6..65ce6ce 100644 --- a/test/randao/AssignKeeperTest.t.sol +++ b/test/randao/AssignKeeperTest.t.sol @@ -503,7 +503,7 @@ contract RandaoAssignKeeperTest is TestHelperRandao { assertEq(agent.getJobsAssignedToKeeperLength(2), 1); vm.prank(bob); - vm.expectRevert(PPAgentV2.OnlyKeeperAdminOrWorker.selector); + vm.expectRevert(PPAgentV2Based.OnlyKeeperAdminOrWorker.selector); _agent.releaseJob(jobKey); } @@ -516,7 +516,7 @@ contract RandaoAssignKeeperTest is TestHelperRandao { bytes32[] memory list = new bytes32[](1); list[0] = jobKey; - vm.expectRevert(abi.encodeWithSelector(PPAgentV2Randao.JobHasKeeperAssigned.selector, 2)); + vm.expectRevert(abi.encodeWithSelector(PPAgentV2RandaoBased.JobHasKeeperAssigned.selector, 2)); _agent.assignKeeper(list); } @@ -538,7 +538,7 @@ contract RandaoAssignKeeperTest is TestHelperRandao { assertEq(_agent.shouldAssignKeeper(jobKey), false); vm.prank(alice); - vm.expectRevert(abi.encodeWithSelector(PPAgentV2Randao.CantAssignKeeper.selector)); + vm.expectRevert(abi.encodeWithSelector(PPAgentV2RandaoBased.CantAssignKeeper.selector)); _agent.assignKeeper(list); } diff --git a/test/randao/ExecuteResolverTest.t.sol b/test/randao/ExecuteResolverTest.t.sol index b5be65a..15cc966 100644 --- a/test/randao/ExecuteResolverTest.t.sol +++ b/test/randao/ExecuteResolverTest.t.sol @@ -160,7 +160,7 @@ contract RandaoExecuteResolverTest is TestHelperRandao { vm.prevrandao(bytes32(uint256(42))); (ok, cd) = job.myResolver("myPass"); - vm.expectRevert(abi.encodeWithSelector(PPAgentV2Randao.SlashingNotInitiated.selector)); + vm.expectRevert(abi.encodeWithSelector(PPAgentV2RandaoBased.SlashingNotInitiated.selector)); _executeJob(1, cd); // time: 11, block: 43. Initiate slashing @@ -174,7 +174,7 @@ contract RandaoExecuteResolverTest is TestHelperRandao { // time: 26, block: 63. Too early for slashing vm.expectRevert(abi.encodeWithSelector( - PPAgentV2Randao.TooEarlyForSlashing.selector, 1600000011 + 8 hours, 1600000026 + 8 hours + PPAgentV2RandaoBased.TooEarlyForSlashing.selector, 1600000011 + 8 hours, 1600000026 + 8 hours )); _executeJob(kid3, cd); @@ -187,7 +187,7 @@ contract RandaoExecuteResolverTest is TestHelperRandao { // kid1 attempt should fail vm.expectRevert(abi.encodeWithSelector( - PPAgentV2Randao.OnlyReservedSlasher.selector, 3 + PPAgentV2RandaoBased.OnlyReservedSlasher.selector, 3 )); _executeJob(kid1, cd); @@ -256,7 +256,7 @@ contract RandaoExecuteResolverTest is TestHelperRandao { vm.roll(42); vm.prank(alice); - vm.expectRevert(PPAgentV2.SelectorCheckFailed.selector); + vm.expectRevert(PPAgentV2Based.SelectorCheckFailed.selector); agent.initiateKeeperSlashing(address(job), jobId, kid1, false, cd); } @@ -274,8 +274,8 @@ contract RandaoExecuteResolverTest is TestHelperRandao { vm.roll(42); vm.prank(alice); vm.expectRevert(abi.encodeWithSelector( - PPAgentV2.JobCheckCanNotBeExecuted.selector, - abi.encodePacked(PPAgentV2.ExecutionReentrancyLocked.selector) + PPAgentV2Based.JobCheckCanNotBeExecuted.selector, + abi.encodePacked(PPAgentV2Based.ExecutionReentrancyLocked.selector) )); agent.initiateKeeperSlashing(address(topupJob), jobId, kid1, false, cd); } @@ -304,7 +304,7 @@ contract RandaoExecuteResolverTest is TestHelperRandao { SimpleCustomizableCalldataTestJob(address(job)).setResolverReturnFalse(true); // resolver false - vm.expectRevert(abi.encodeWithSelector(PPAgentV2.JobCheckResolverReturnedFalse.selector)); + vm.expectRevert(abi.encodeWithSelector(PPAgentV2Based.JobCheckResolverReturnedFalse.selector)); vm.prank(bob, bob); agent.initiateKeeperSlashing(address(job), jobId, kid3, true, cd); @@ -312,7 +312,7 @@ contract RandaoExecuteResolverTest is TestHelperRandao { SimpleCustomizableCalldataTestJob(address(job)).setRevertResolver(true); // resolver revert - vm.expectRevert(abi.encodeWithSelector(PPAgentV2.JobCheckCanNotBeExecuted.selector, + vm.expectRevert(abi.encodeWithSelector(PPAgentV2Based.JobCheckCanNotBeExecuted.selector, abi.encodeWithSelector(0x08c379a0, "forced resolver revert") )); vm.prank(bob, bob); @@ -336,12 +336,12 @@ contract RandaoExecuteResolverTest is TestHelperRandao { agent.setJobConfig(jobKey, true, false, true, true); SimpleCustomizableCalldataTestJob(address(job)).setResolverReturnFalse(true); - vm.expectRevert(abi.encodeWithSelector(PPAgentV2.JobCheckResolverReturnedFalse.selector)); + vm.expectRevert(abi.encodeWithSelector(PPAgentV2Based.JobCheckResolverReturnedFalse.selector)); _executeJob(2, cd); SimpleCustomizableCalldataTestJob(address(job)).setResolverReturnFalse(false); (, bytes memory incorrectCd) = SimpleCustomizableCalldataTestJob(address(job)).myIncorrectResolver(); - vm.expectRevert(abi.encodeWithSelector(PPAgentV2.JobCheckCalldataError.selector)); + vm.expectRevert(abi.encodeWithSelector(PPAgentV2Based.JobCheckCalldataError.selector)); _executeJob(2, incorrectCd); _executeJob(2, cd); @@ -370,7 +370,7 @@ contract RandaoExecuteResolverTest is TestHelperRandao { assertEq(SimpleCustomizableCalldataTestJob(address(job)).revertResolver(), false); assertEq(SimpleCustomizableCalldataTestJob(address(job)).revertExecution(), true); - vm.expectRevert(abi.encodeWithSelector(PPAgentV2Randao.SlashingNotInitiatedExecutionReverted.selector)); + vm.expectRevert(abi.encodeWithSelector(PPAgentV2RandaoBased.SlashingNotInitiatedExecutionReverted.selector)); _executeJob(2, cd); } diff --git a/test/randao/ExecuteSelectorTest.t.sol b/test/randao/ExecuteSelectorTest.t.sol index a8ff6da..826b8ab 100644 --- a/test/randao/ExecuteSelectorTest.t.sol +++ b/test/randao/ExecuteSelectorTest.t.sol @@ -151,7 +151,7 @@ contract RandaoExecuteSelectorTest is TestHelperRandao { vm.prevrandao(bytes32(uint256(40))); vm.expectRevert( abi.encodeWithSelector( - PPAgentV2Randao.OnlyNextKeeper.selector, 2, 0, 10, 15, 1600000000 + 8 hours + PPAgentV2RandaoBased.OnlyNextKeeper.selector, 2, 0, 10, 15, 1600000000 + 8 hours ) ); vm.prank(alice, alice); @@ -173,7 +173,7 @@ contract RandaoExecuteSelectorTest is TestHelperRandao { assertEq(lockedJobs[0], jobKey); // the first attempt should fail - vm.expectRevert(abi.encodeWithSelector(PPAgentV2Randao.KeeperIsAssignedToJobs.selector, 1)); + vm.expectRevert(abi.encodeWithSelector(PPAgentV2RandaoBased.KeeperIsAssignedToJobs.selector, 1)); vm.prank(keeperAdmin, keeperAdmin); agent.initiateRedeem(kid2, 5_000 ether); @@ -227,7 +227,7 @@ contract RandaoExecuteSelectorTest is TestHelperRandao { vm.prevrandao(bytes32(uint256(42))); vm.expectRevert( abi.encodeWithSelector( - PPAgentV2Randao.OnlyNextKeeper.selector, 2, 1600000000 + 8 hours, 10, 15, 1600000011 + 8 hours + PPAgentV2RandaoBased.OnlyNextKeeper.selector, 2, 1600000000 + 8 hours, 10, 15, 1600000011 + 8 hours ) ); _callExecuteHelper( @@ -248,7 +248,7 @@ contract RandaoExecuteSelectorTest is TestHelperRandao { assertEq(agent.jobNextKeeperId(jobKey), 2); // kid3 attempt should fail - vm.expectRevert(abi.encodeWithSelector(PPAgentV2Randao.OnlyCurrentSlasher.selector, 3)); + vm.expectRevert(abi.encodeWithSelector(PPAgentV2RandaoBased.OnlyCurrentSlasher.selector, 3)); vm.prank(alice, alice); _callExecuteHelper( agent, @@ -302,7 +302,7 @@ contract RandaoExecuteSelectorTest is TestHelperRandao { vm.prevrandao(bytes32(uint256(42))); vm.expectRevert( abi.encodeWithSelector( - PPAgentV2Randao.OnlyNextKeeper.selector, 2, 1600000000 + 8 hours, 10, 15, 1600000011 + 8 hours + PPAgentV2RandaoBased.OnlyNextKeeper.selector, 2, 1600000000 + 8 hours, 10, 15, 1600000011 + 8 hours ) ); _callExecuteHelper( @@ -332,7 +332,7 @@ contract RandaoExecuteSelectorTest is TestHelperRandao { vm.expectRevert( abi.encodeWithSelector( - PPAgentV2Randao.JobHasNoKeeperAssigned.selector + PPAgentV2RandaoBased.JobHasNoKeeperAssigned.selector ) ); vm.prank(bob, bob); diff --git a/test/randao/ExecuteShanghaiTest.t.sol b/test/randao/ExecuteShanghaiTest.t.sol new file mode 100644 index 0000000..c6fd474 --- /dev/null +++ b/test/randao/ExecuteShanghaiTest.t.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "../../contracts/PPAgentV2Randao.sol"; +import "../mocks/MockCVP.sol"; +import "../jobs/JobTopupTestJob.sol"; +import "../jobs/OnlySelectorTestJob.sol"; +import "../TestHelperRandao.sol"; +import "../jobs/SimpleCalldataTestJob.sol"; +import "../jobs/SimpleCustomizableCalldataTestJob.sol"; +import "../jobs/JobWithdrawTestJob.sol"; + +contract ExecuteShanghaiTest is TestHelperRandao { + ICounter internal job; + + event Execute(bytes32 indexed jobKey, address indexed job, bool indexed success, uint256 gasUsed, uint256 baseFee, uint256 gasPrice, uint256 compensation); + event JobKeeperChanged(bytes32 indexed jobKey, uint256 indexed keeperFrom, uint256 indexed keeperTo); + event ExecutionReverted( + bytes32 indexed jobKey, + uint256 indexed assignedKeeperId, + uint256 indexed actualKeeperId, + bytes executionReturndata, + uint256 compensation + ); + + OnlySelectorTestJob internal counter; + + bytes32 internal jobKey; + uint256 internal jobId; + uint256 internal defaultFlags; + uint256 internal accrueFlags; + uint256 internal kid1; + uint256 internal kid2; + uint256 internal kid3; + uint256 internal latestKeeperStub; + + function setUp() public override { + defaultFlags = _config({ + acceptMaxBaseFeeLimit: false, + accrueReward: false + }); + accrueFlags = _config({ + acceptMaxBaseFeeLimit: false, + accrueReward: true + }); + cvp = new MockCVP(); + IPPAgentV2RandaoViewer.RandaoConfig memory rdConfig = IPPAgentV2RandaoViewer.RandaoConfig({ + slashingEpochBlocks: 10, + period1: 15, + period2: 30, + slashingFeeFixedCVP: 50, + slashingFeeBps: 300, + jobMinCreditsFinney: 100, + agentMaxCvpStake: 50_000, + jobCompensationMultiplierBps: 10_000, + stakeDivisor: 50_000_000, + keeperActivationTimeoutHours: 8, + jobFixedRewardFinney: 30 + }); + agent = new PPAgentV2Randao(address(cvp)); + agent.initializeRandao(owner, 3_000 ether, 3 days, rdConfig); + counter = new OnlySelectorTestJob(address(agent)); + + { + cvp.transfer(keeperAdmin, 15_000 ether); + + vm.startPrank(keeperAdmin); + cvp.approve(address(agent), 15_000 ether); + kid1 = agent.registerAsKeeper(alice, 5_000 ether); + kid2 = agent.registerAsKeeper(keeperWorker, 5_000 ether); + kid3 = agent.registerAsKeeper(bob, 5_000 ether); + + vm.warp(block.timestamp + 8 hours); + + agent.finalizeKeeperActivation(1); + agent.finalizeKeeperActivation(2); + agent.finalizeKeeperActivation(3); + vm.stopPrank(); + + assertEq(counter.current(), 0); + } + } + + function _setupJob(address job_, bytes4 selector_, bool assertSelector_) internal { + PPAgentV2.Resolver memory resolver = IPPAgentV2Viewer.Resolver({ + resolverAddress: job_, + resolverCalldata: abi.encodeWithSelector(SimpleCustomizableCalldataTestJob.myResolver.selector, "myPass") + }); + IPPAgentV2JobOwner.RegisterJobParams memory params = IPPAgentV2JobOwner.RegisterJobParams({ + jobAddress: job_, + jobSelector: selector_, + maxBaseFeeGwei: 100, + rewardPct: 35, + fixedReward: 10, + useJobOwnerCredits: false, + assertResolverSelector: assertSelector_, + jobMinCvp: 0, + + // For interval jobs + calldataSource: CALLDATA_SOURCE_RESOLVER, + intervalSeconds: 0 + }); + vm.prank(alice); + vm.deal(alice, 1 ether); + (jobKey,jobId) = agent.registerJob{ value: 1 ether }({ + params_: params, + resolver_: resolver, + preDefinedCalldata_: new bytes(0) + }); + } + + function _executeJob(uint256 kid, bytes memory cd) internal { + + if (kid == 1) { + vm.prank(alice, alice); + } else if (kid == 2) { + vm.prank(keeperWorker, keeperWorker); + } else if (kid == 3) { + vm.prank(bob, bob); + } else { + revert("invalid id"); + } + _callExecuteHelper( + agent, + address(job), + jobId, + defaultFlags, + kid, + cd + ); + } + + function testRdResolverSelectorSlashingReentrancyLockInShanghai() public { + JobWithdrawTestJob topupJob = new JobWithdrawTestJob(address(agent)); + job = new SimpleCalldataTestJob(address(agent)); + _setupJob(address(topupJob), JobWithdrawTestJob.execute.selector, true); + + (, bytes memory cd) = topupJob.myResolver(jobKey); + + vm.prank(alice); + agent.initiateJobTransfer(jobKey, address(topupJob)); + topupJob.acceptJobTransfer(jobKey); + + vm.roll(42); + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector( + PPAgentV2Based.JobCheckCanNotBeExecuted.selector, + abi.encodePacked(PPAgentV2Based.ExecutionReentrancyLocked.selector) + )); + agent.initiateKeeperSlashing(address(topupJob), jobId, kid1, false, cd); + } +} diff --git a/test/randao/JobOwnerTest.t.sol b/test/randao/JobOwnerTest.t.sol index e39f61a..51b9ef6 100644 --- a/test/randao/JobOwnerTest.t.sol +++ b/test/randao/JobOwnerTest.t.sol @@ -127,7 +127,7 @@ contract RandaoJobOwnerTest is TestHelperRandao { vm.deal(bob, 2 ether); vm.prank(bob); vm.expectRevert( - abi.encodeWithSelector(PPAgentV2.OnlyJobOwner.selector) + abi.encodeWithSelector(PPAgentV2Based.OnlyJobOwner.selector) ); agent.depositJobOwnerCreditsAndAssignKeepers{ value: 1 ether }(alice, jobKeys); diff --git a/test/randao/KeeperTest.t.sol b/test/randao/KeeperTest.t.sol index 2569c01..8ed970d 100644 --- a/test/randao/KeeperTest.t.sol +++ b/test/randao/KeeperTest.t.sol @@ -122,9 +122,9 @@ contract RandaoKeeperTest is TestHelperRandao { assertEq(_stakeOf(kid1), 5_000 ether); vm.startPrank(keeperAdmin, keeperAdmin); - vm.expectRevert(abi.encodeWithSelector(PPAgentV2Randao.KeeperShouldBeDisabledForStakeLTMinKeeperCvp.selector)); + vm.expectRevert(abi.encodeWithSelector(PPAgentV2RandaoBased.KeeperShouldBeDisabledForStakeLTMinKeeperCvp.selector)); agent.initiateRedeem(kid1, 2_001 ether); - vm.expectRevert(abi.encodeWithSelector(PPAgentV2Randao.KeeperShouldBeDisabledForStakeLTMinKeeperCvp.selector)); + vm.expectRevert(abi.encodeWithSelector(PPAgentV2RandaoBased.KeeperShouldBeDisabledForStakeLTMinKeeperCvp.selector)); agent.initiateRedeem(kid1, 5_000 ether); agent.initiateRedeem(kid1, 2_000 ether); diff --git a/test/randao/OwnerSlashingTest.t.sol b/test/randao/OwnerSlashingTest.t.sol index 1ad9e98..328a4cb 100644 --- a/test/randao/OwnerSlashingTest.t.sol +++ b/test/randao/OwnerSlashingTest.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; +import "../../contracts/PPAgentV2Based.sol"; import "../../contracts/PPAgentV2.sol"; import "../mocks/MockCVP.sol"; import "../TestHelperRandao.sol"; @@ -49,7 +50,7 @@ contract RandaoOwnerStakingTest is TestHelperRandao { agent.disableKeeper(kid); vm.prank(owner); - vm.expectRevert(PPAgentV2Randao.KeeperIsAlreadyInactive.selector); + vm.expectRevert(PPAgentV2RandaoBased.KeeperIsAlreadyInactive.selector); agent.ownerSlashDisable(kid, bob, 1, 0, true); } diff --git a/test/randao/RandaoGasTrackerTest.t.sol b/test/randao/RandaoGasTrackerTest.t.sol index f1e61b9..85f5077 100644 --- a/test/randao/RandaoGasTrackerTest.t.sol +++ b/test/randao/RandaoGasTrackerTest.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; +import "../../contracts/PPAgentV2RandaoBased.sol"; import "../../contracts/PPAgentV2RandaoWithGasTracking.sol"; import "../TestHelperRandao.sol"; import "../mocks/MockCVP.sol"; @@ -72,7 +73,7 @@ contract RandaoGasUsedTest is TestHelperRandao { } function _setupJob(address job_, bytes4 selector_, bool assertSelector_) internal { - PPAgentV2.Resolver memory resolver = IPPAgentV2Viewer.Resolver({ + PPAgentV2Based.Resolver memory resolver = IPPAgentV2Viewer.Resolver({ resolverAddress: address(0), resolverCalldata: bytes("") }); diff --git a/test/randao/VRFTest.t.sol b/test/randao/VRFTest.t.sol index 5de8e1d..0a54f34 100644 --- a/test/randao/VRFTest.t.sol +++ b/test/randao/VRFTest.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; +import "../../contracts/PPAgentV2Based.sol"; import "../../contracts/PPAgentV2VRF.sol"; import "../TestHelperRandao.sol"; import "../mocks/MockCVP.sol"; @@ -25,7 +26,7 @@ contract VRFTest is AbstractTestHelper { uint256 internal kid3; uint256 internal latestKeeperStub; - PPAgentV2VRF internal agent; + PPAgentV2VRFBased internal agent; function _agentViewer() internal override view returns(IPPAgentV2Viewer) { return IPPAgentV2Viewer(address(agent)); @@ -95,7 +96,7 @@ contract VRFTest is AbstractTestHelper { } function _setupJob(address job_, bytes4 selector_, bool assertSelector_) internal { - PPAgentV2.Resolver memory resolver = IPPAgentV2Viewer.Resolver({ + PPAgentV2Based.Resolver memory resolver = IPPAgentV2Viewer.Resolver({ resolverAddress: address(0), resolverCalldata: bytes("") });