diff --git a/.solhintignore b/.solhintignore index a91ab4b..9b698f8 100644 --- a/.solhintignore +++ b/.solhintignore @@ -1,3 +1,2 @@ -solidity/contracts/utils/AutomateReady.sol -solidity/contracts/utils/Types.sol +solidity/interfaces/external/** *.ignore \ No newline at end of file diff --git a/solidity/contracts/AutomationVault.sol b/solidity/contracts/AutomationVault.sol index d234961..c5d3f9e 100644 --- a/solidity/contracts/AutomationVault.sol +++ b/solidity/contracts/AutomationVault.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.19; import {IAutomationVault} from '@interfaces/IAutomationVault.sol'; import {IERC20, SafeERC20} from '@openzeppelin/token/ERC20/utils/SafeERC20.sol'; import {EnumerableSet} from '@openzeppelin/utils/structs/EnumerableSet.sol'; -import {_ETH, _NULL} from '@utils/Constants.sol'; +import {_ALL} from '@utils/Constants.sol'; /** * @title AutomationVault @@ -15,20 +15,22 @@ contract AutomationVault is IAutomationVault { using EnumerableSet for EnumerableSet.AddressSet; using EnumerableSet for EnumerableSet.Bytes32Set; + /// @inheritdoc IAutomationVault + address public immutable NATIVE_TOKEN; /// @inheritdoc IAutomationVault address public owner; /// @inheritdoc IAutomationVault address public pendingOwner; - /** * @notice Callers that are approved to call a relay */ - mapping(address _relay => EnumerableSet.AddressSet _enabledCallers) internal _relayEnabledCallers; + mapping(address _relay => EnumerableSet.AddressSet _enabledCallers) internal _relayCallers; /** - * @notice Selectors that are approved to be called + * @notice Relays that are approved to execute jobs with a specific selector */ - mapping(address _job => EnumerableSet.Bytes32Set _enabledSelectors) internal _jobEnabledSelectors; + mapping(address _relay => mapping(address _job => EnumerableSet.Bytes32Set _enabledSelectors)) internal + _relayJobSelectors; /** * @notice List of approved relays @@ -43,28 +45,31 @@ contract AutomationVault is IAutomationVault { /** * @param _owner The address of the owner */ - constructor(address _owner) payable { + constructor(address _owner, address _nativeToken) { owner = _owner; + NATIVE_TOKEN = _nativeToken; } /// @inheritdoc IAutomationVault - function relayEnabledCallers(address _relay) external view returns (address[] memory _enabledCallers) { - _enabledCallers = _relayEnabledCallers[_relay].values(); + function getRelayAndJobData( + address _relay, + address _job + ) public view returns (address[] memory _callers, bytes32[] memory _selectors) { + // Get the list of callers + _callers = _relayCallers[_relay].values(); + + // Get the list of selectors + _selectors = _relayJobSelectors[_relay][_job].values(); } /// @inheritdoc IAutomationVault - function jobEnabledSelectors(address _job) external view returns (bytes32[] memory _enabledSelectors) { - _enabledSelectors = _jobEnabledSelectors[_job].values(); + function relays() external view returns (address[] memory _relayList) { + _relayList = _relays.values(); } /// @inheritdoc IAutomationVault - function relays() external view returns (address[] memory __relays) { - __relays = _relays.values(); - } - - /// @inheritdoc IAutomationVault - function jobs() external view returns (address[] memory __jobs) { - __jobs = _jobs.values(); + function jobs() external view returns (address[] memory _jobList) { + _jobList = _jobs.values(); } /// @inheritdoc IAutomationVault @@ -81,11 +86,11 @@ contract AutomationVault is IAutomationVault { } /// @inheritdoc IAutomationVault - function withdrawFunds(address _token, uint256 _amount, address _receiver) external payable onlyOwner { - // If the token is ETH, transfer the funds to the receiver, otherwise transfer the tokens - if (_token == _ETH) { + function withdrawFunds(address _token, uint256 _amount, address _receiver) external onlyOwner { + // If the token is the native token, transfer the funds to the receiver, otherwise transfer the tokens + if (_token == NATIVE_TOKEN) { (bool _success,) = _receiver.call{value: _amount}(''); - if (!_success) revert AutomationVault_ETHTransferFailed(); + if (!_success) revert AutomationVault_NativeTokenTransferFailed(); } else { IERC20(_token).safeTransfer(_receiver, _amount); } @@ -95,9 +100,12 @@ contract AutomationVault is IAutomationVault { } /// @inheritdoc IAutomationVault - function approveRelayCallers(address _relay, address[] calldata _callers) external onlyOwner { - // Get the list of enabled callers for the relay - EnumerableSet.AddressSet storage _enabledCallers = _relayEnabledCallers[_relay]; + function approveRelayData( + address _relay, + address[] calldata _callers, + IAutomationVault.JobData[] calldata _jobsData + ) external onlyOwner { + if (_relay == address(0)) revert AutomationVault_RelayZero(); // If the relay is not approved, add it to the list of relays if (_relays.add(_relay)) { @@ -106,7 +114,7 @@ contract AutomationVault is IAutomationVault { // Iterate over the callers to approve them for (uint256 _i; _i < _callers.length;) { - if (_enabledCallers.add(_callers[_i])) { + if (_relayCallers[_relay].add(_callers[_i])) { emit ApproveRelayCaller(_relay, _callers[_i]); } @@ -114,16 +122,44 @@ contract AutomationVault is IAutomationVault { ++_i; } } + + // Iterate over the jobs to approve them and their selectors + for (uint256 _i; _i < _jobsData.length;) { + IAutomationVault.JobData memory _jobData = _jobsData[_i]; + + // If the job is not approved, add it to the list of jobs + if (_jobs.add(_jobData.job)) { + emit ApproveJob(_jobData.job); + } + + // Iterate over the selectors to approve them + for (uint256 _j; _j < _jobData.functionSelectors.length;) { + if (_relayJobSelectors[_relay][_jobData.job].add(_jobData.functionSelectors[_j])) { + emit ApproveJobSelector(_jobData.job, _jobData.functionSelectors[_j]); + } + + unchecked { + ++_j; + } + } + + unchecked { + ++_i; + } + } } /// @inheritdoc IAutomationVault - function revokeRelayCallers(address _relay, address[] calldata _callers) external onlyOwner { - // Get the list of enabled callers for the relay - EnumerableSet.AddressSet storage _enabledCallers = _relayEnabledCallers[_relay]; + function revokeRelayData( + address _relay, + address[] calldata _callers, + IAutomationVault.JobData[] calldata _jobsData + ) external onlyOwner { + if (_relay == address(0)) revert AutomationVault_RelayZero(); // Iterate over the callers to revoke them for (uint256 _i; _i < _callers.length;) { - if (_enabledCallers.remove(_callers[_i])) { + if (_relayCallers[_relay].remove(_callers[_i])) { emit RevokeRelayCaller(_relay, _callers[_i]); } @@ -132,84 +168,64 @@ contract AutomationVault is IAutomationVault { } } - // If the relay has no more callers, remove it from the list of relays - if (_enabledCallers.length() == 0) { + // If the relay has no enabled callers, remove it from the list of relays + if (_relayCallers[_relay].length() == 0) { _relays.remove(_relay); emit RevokeRelay(_relay); } - } - /// @inheritdoc IAutomationVault - function approveJobSelectors(address _job, bytes4[] calldata _functionSelectors) external onlyOwner { - // Get the list of enabled selectors for the job - EnumerableSet.Bytes32Set storage _enabledSelectors = _jobEnabledSelectors[_job]; + // Iterate over the jobs to revoke them and their selectors + for (uint256 _i; _i < _jobsData.length;) { + IAutomationVault.JobData memory _jobData = _jobsData[_i]; - // If the job is not approved, add it to the list of jobs - if (_jobs.add(_job)) { - emit ApproveJob(_job); - } - - // Iterate over the selectors to approve them - for (uint256 _i; _i < _functionSelectors.length;) { - if (_enabledSelectors.add(_functionSelectors[_i])) { - emit ApproveJobSelector(_job, _functionSelectors[_i]); - } + // Iterate over the selectors to revoke them + for (uint256 _j; _j < _jobData.functionSelectors.length;) { + if (_relayJobSelectors[_relay][_jobData.job].remove(_jobData.functionSelectors[_j])) { + emit RevokeJobSelector(_jobData.job, _jobData.functionSelectors[_j]); + } - unchecked { - ++_i; + unchecked { + ++_j; + } } - } - } - /// @inheritdoc IAutomationVault - function revokeJobSelectors(address _job, bytes4[] calldata _functionSelectors) external onlyOwner { - // Get the list of enabled selectors for the job - EnumerableSet.Bytes32Set storage _enabledSelectors = _jobEnabledSelectors[_job]; - - // Iterate over the selectors to revoke them - for (uint256 _i; _i < _functionSelectors.length;) { - if (_enabledSelectors.remove(_functionSelectors[_i])) { - emit RevokeJobSelector(_job, _functionSelectors[_i]); + if (_relayJobSelectors[_relay][_jobData.job].length() == 0) { + _jobs.remove(_jobData.job); + emit RevokeJob(_jobData.job); } unchecked { ++_i; } } - - // If the job has no more selectors, remove it from the list of jobs - if (_enabledSelectors.length() == 0) { - _jobs.remove(_job); - emit RevokeJob(_job); - } } /// @inheritdoc IAutomationVault - function exec(address _relayCaller, ExecData[] calldata _execData, FeeData[] calldata _feeData) external payable { + function exec(address _relayCaller, ExecData[] calldata _execData, FeeData[] calldata _feeData) external { // Check that the specific caller is approved to call the relay - if (!_relayEnabledCallers[msg.sender].contains(_relayCaller) && !_relayEnabledCallers[msg.sender].contains(_NULL)) { + if (!_relayCallers[msg.sender].contains(_relayCaller) && !_relayCallers[msg.sender].contains(_ALL)) { revert AutomationVault_NotApprovedRelayCaller(); } // Create the exec data needed variables - ExecData memory _execDatum; + ExecData memory _dataToExecute; uint256 _dataLength = _execData.length; uint256 _i; bool _success; // Iterate over the exec data to execute the jobs for (_i; _i < _dataLength;) { - _execDatum = _execData[_i]; + _dataToExecute = _execData[_i]; // Check that the selector is approved to be called - if (!_jobEnabledSelectors[_execDatum.job].contains(bytes4(_execDatum.jobData))) { + if (!_relayJobSelectors[msg.sender][_dataToExecute.job].contains(bytes4(_dataToExecute.jobData))) { revert AutomationVault_NotApprovedJobSelector(); } - (_success,) = _execDatum.job.call(_execDatum.jobData); + (_success,) = _dataToExecute.job.call(_dataToExecute.jobData); if (!_success) revert AutomationVault_ExecFailed(); // Emit the event - emit JobExecuted(msg.sender, _relayCaller, _execDatum.job, _execDatum.jobData); + emit JobExecuted(msg.sender, _relayCaller, _dataToExecute.job, _dataToExecute.jobData); unchecked { ++_i; @@ -217,24 +233,24 @@ contract AutomationVault is IAutomationVault { } // Create the fee data needed variables - FeeData memory _feeDatum; + FeeData memory _feeInfo; _dataLength = _feeData.length; _i = 0; // Iterate over the fee data to issue the payments for (_i; _i < _dataLength;) { - _feeDatum = _feeData[_i]; + _feeInfo = _feeData[_i]; - // If the token is ETH, transfer the funds to the receiver, otherwise transfer the tokens - if (_feeDatum.feeToken == _ETH) { - (_success,) = _feeDatum.feeRecipient.call{value: _feeDatum.fee}(''); - if (!_success) revert AutomationVault_ETHTransferFailed(); + // If the token is the native token, transfer the funds to the receiver, otherwise transfer the tokens + if (_feeInfo.feeToken == NATIVE_TOKEN) { + (_success,) = _feeInfo.feeRecipient.call{value: _feeInfo.fee}(''); + if (!_success) revert AutomationVault_NativeTokenTransferFailed(); } else { - IERC20(_feeDatum.feeToken).safeTransfer(_feeDatum.feeRecipient, _feeDatum.fee); + IERC20(_feeInfo.feeToken).safeTransfer(_feeInfo.feeRecipient, _feeInfo.fee); } // Emit the event - emit IssuePayment(msg.sender, _relayCaller, _feeDatum.feeRecipient, _feeDatum.feeToken, _feeDatum.fee); + emit IssuePayment(msg.sender, _relayCaller, _feeInfo.feeRecipient, _feeInfo.feeToken, _feeInfo.fee); unchecked { ++_i; @@ -247,7 +263,7 @@ contract AutomationVault is IAutomationVault { */ modifier onlyOwner() { address _owner = owner; - if (msg.sender != _owner) revert AutomationVault_OnlyOwner(_owner); + if (msg.sender != _owner) revert AutomationVault_OnlyOwner(); _; } @@ -256,14 +272,14 @@ contract AutomationVault is IAutomationVault { */ modifier onlyPendingOwner() { address _pendingOwner = pendingOwner; - if (msg.sender != _pendingOwner) revert AutomationVault_OnlyPendingOwner(_pendingOwner); + if (msg.sender != _pendingOwner) revert AutomationVault_OnlyPendingOwner(); _; } /** - * @notice Fallback function to receive ETH + * @notice Fallback function to receive native tokens */ receive() external payable { - emit ETHReceived(msg.sender, msg.value); + emit NativeTokenReceived(msg.sender, msg.value); } } diff --git a/solidity/contracts/AutomationVaultFactory.sol b/solidity/contracts/AutomationVaultFactory.sol index 358a926..527e73e 100644 --- a/solidity/contracts/AutomationVaultFactory.sol +++ b/solidity/contracts/AutomationVaultFactory.sol @@ -49,9 +49,15 @@ contract AutomationVaultFactory is IAutomationVaultFactory { } /// @inheritdoc IAutomationVaultFactory - function deployAutomationVault(address _owner) external returns (IAutomationVault _automationVault) { + function deployAutomationVault( + address _owner, + address _nativeToken, + uint256 _salt + ) external returns (IAutomationVault _automationVault) { // Create the new automation vault with the owner - _automationVault = new AutomationVault(_owner); + _automationVault = new AutomationVault{salt: keccak256(abi.encodePacked(msg.sender, _salt))}(_owner, _nativeToken); + + if (address(_automationVault) == address(0)) revert AutomationVaultFactory_Create2Failed(); // Add the automation vault to the list of automation vaults _automationVaults.add(address(_automationVault)); diff --git a/solidity/contracts/Keep3rBondedRelay.sol b/solidity/contracts/Keep3rBondedRelay.sol index f7b19c2..4ce61b1 100644 --- a/solidity/contracts/Keep3rBondedRelay.sol +++ b/solidity/contracts/Keep3rBondedRelay.sol @@ -17,7 +17,7 @@ contract Keep3rBondedRelay is IKeep3rBondedRelay { /// @inheritdoc IKeep3rBondedRelay function setAutomationVaultRequirements( IAutomationVault _automationVault, - IKeep3rBondedRelay.Requirements memory _requirements + IKeep3rBondedRelay.Requirements calldata _requirements ) external { if (_automationVault.owner() != msg.sender) revert Keep3rBondedRelay_NotVaultOwner(); diff --git a/solidity/interfaces/IAutomationVault.sol b/solidity/interfaces/IAutomationVault.sol index 981e139..8985a6b 100644 --- a/solidity/interfaces/IAutomationVault.sol +++ b/solidity/interfaces/IAutomationVault.sol @@ -100,20 +100,30 @@ interface IAutomationVault { ); /** - * @notice Emitted when ETH is received in the automation vault + * @notice Emitted when native token is received in the automation vault * @param _sender The sender address - * @param _amount The amount of ETH + * @param _amount The amount of native token */ - event ETHReceived(address indexed _sender, uint256 _amount); + event NativeTokenReceived(address indexed _sender, uint256 _amount); /*/////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ + /** + * @notice Thrown when the the relay is the zero address + */ + error AutomationVault_RelayZero(); + + /** + * @notice Thrown when the job is the zero address + */ + error AutomationVault_JobZero(); + /** * @notice Thrown when ether transfer fails */ - error AutomationVault_ETHTransferFailed(); + error AutomationVault_NativeTokenTransferFailed(); /** * @notice Thrown when a not approved relay caller is trying to execute a job @@ -132,15 +142,13 @@ interface IAutomationVault { /** * @notice Thrown when the caller is not the owner - * @param _owner The address of the owner */ - error AutomationVault_OnlyOwner(address _owner); + error AutomationVault_OnlyOwner(); /** * @notice Thrown when the caller is not the pending owner - * @param _pendingOwner The address of the pending owner */ - error AutomationVault_OnlyPendingOwner(address _pendingOwner); + error AutomationVault_OnlyPendingOwner(); /*/////////////////////////////////////////////////////////////// STRUCTS @@ -168,6 +176,11 @@ interface IAutomationVault { uint256 fee; } + struct JobData { + address job; + bytes4[] functionSelectors; + } + /*/////////////////////////////////////////////////////////////// VIEW FUNCTIONS //////////////////////////////////////////////////////////////*/ @@ -178,6 +191,13 @@ interface IAutomationVault { */ function owner() external view returns (address _owner); + /** + * @notice Returns the address of the native token + * @return _nativeToken The address of the native token + */ + + function NATIVE_TOKEN() external view returns (address _nativeToken); + /** * @notice Returns the pending owner address * @return _pendingOwner The address of the pending owner @@ -185,30 +205,28 @@ interface IAutomationVault { function pendingOwner() external view returns (address _pendingOwner); /** - * @notice Returns the approved relay callers for a specific relay + * @notice Returns the approved relay callers and selectors for a specific relay and job * @param _relay The address of the relay - * @return _enabledCallers The array of approved relay callers - */ - function relayEnabledCallers(address _relay) external view returns (address[] memory _enabledCallers); - - /** - * @notice Returns the approved job selectors for a specific job * @param _job The address of the job - * @return _enabledSelectors The array of approved job selectors + * @return _callers The array of approved relay callers + * @return _selectors The array of approved selectors */ - function jobEnabledSelectors(address _job) external view returns (bytes32[] memory _enabledSelectors); + function getRelayAndJobData( + address _relay, + address _job + ) external returns (address[] memory _callers, bytes32[] memory _selectors); /** * @notice Returns the approved relays - * @return __relays The array of approved relays + * @return _listRelays The array of approved relays */ - function relays() external view returns (address[] memory __relays); + function relays() external view returns (address[] memory _listRelays); /** * @notice Returns the approved jobs - * @return __jobs The array of approved jobs + * @return _listJobs The array of approved jobs */ - function jobs() external view returns (address[] memory __jobs); + function jobs() external view returns (address[] memory _listJobs); /*/////////////////////////////////////////////////////////////// EXTERNAL FUNCTIONS @@ -233,35 +251,33 @@ interface IAutomationVault { * @param _amount The amount of tokens * @param _receiver The address of the receiver */ - function withdrawFunds(address _token, uint256 _amount, address _receiver) external payable; + function withdrawFunds(address _token, uint256 _amount, address _receiver) external; /** - * @notice Approves relay callers which will be able to call a relay to execute jobs + * @notice Approves relay callers which will be able to call a relay to execute jobs with the specified selectors + * @dev You can approve all the fields or only the necessary ones, passing the empty argument in the unwanted ones * @param _relay The address of the relay * @param _callers The array of callers + * @param _jobsData The array of job data */ - function approveRelayCallers(address _relay, address[] calldata _callers) external; + function approveRelayData( + address _relay, + address[] calldata _callers, + IAutomationVault.JobData[] calldata _jobsData + ) external; /** - * @notice Revokes relay callers which will not be able to call a relay to execute jobs + * @notice Revokes both the desired callers to a relay and the jobs and their respective selectors + * @dev You can revoke all the fields or only the necessary ones, passing the empty argument in the unwanted ones * @param _relay The address of the relay * @param _callers The array of callers + * @param _jobsData The array of job data */ - function revokeRelayCallers(address _relay, address[] calldata _callers) external; - - /** - * @notice Approves job selectors which could be called to execute the job - * @param _job The address of the job - * @param _functionSelectors The array of function selectors - */ - function approveJobSelectors(address _job, bytes4[] calldata _functionSelectors) external; - - /** - * @notice Revokes job selectors which could not be called to execute the job - * @param _job The address of the job - * @param _functionSelectors The array of function selectors - */ - function revokeJobSelectors(address _job, bytes4[] calldata _functionSelectors) external; + function revokeRelayData( + address _relay, + address[] calldata _callers, + IAutomationVault.JobData[] calldata _jobsData + ) external; /** * @notice Executes a job and issues a payment to the fee data receivers @@ -271,5 +287,5 @@ interface IAutomationVault { * @param _execData The array of exec data * @param _feeData The array of fee data */ - function exec(address _relayCaller, ExecData[] calldata _execData, FeeData[] calldata _feeData) external payable; + function exec(address _relayCaller, ExecData[] calldata _execData, FeeData[] calldata _feeData) external; } diff --git a/solidity/interfaces/IAutomationVaultFactory.sol b/solidity/interfaces/IAutomationVaultFactory.sol index 436016a..a6c38c0 100644 --- a/solidity/interfaces/IAutomationVaultFactory.sol +++ b/solidity/interfaces/IAutomationVaultFactory.sol @@ -16,13 +16,13 @@ interface IAutomationVaultFactory { event DeployAutomationVault(address indexed _owner, address indexed _automationVault); /*/////////////////////////////////////////////////////////////// - ERRORS + ERRORS //////////////////////////////////////////////////////////////*/ /** - * @notice Thrown when the amount is zero + * @notice Thrown when the automation vault factory fails to deploy a new automation vault */ - error AutomationVaultFactory_AmountZero(); + error AutomationVaultFactory_Create2Failed(); /*/////////////////////////////////////////////////////////////// VIEW FUNCTIONS @@ -49,7 +49,13 @@ interface IAutomationVaultFactory { /** * @notice Deploy a new automation vault * @param _owner The address of the owner + * @param _nativeToken The address of the native token + * @param _salt The salt to use for the automation vault deployment * @return _automationVault The address of the automation vault deployed */ - function deployAutomationVault(address _owner) external returns (IAutomationVault _automationVault); + function deployAutomationVault( + address _owner, + address _nativeToken, + uint256 _salt + ) external returns (IAutomationVault _automationVault); } diff --git a/solidity/script/Deploy.s.sol b/solidity/script/DeployNativeETH.s.sol similarity index 82% rename from solidity/script/Deploy.s.sol rename to solidity/script/DeployNativeETH.s.sol index f833e06..8ef1ca8 100644 --- a/solidity/script/Deploy.s.sol +++ b/solidity/script/DeployNativeETH.s.sol @@ -9,8 +9,10 @@ import {OpenRelay, IOpenRelay} from '@contracts/OpenRelay.sol'; import {GelatoRelay, IGelatoRelay} from '@contracts/GelatoRelay.sol'; import {Keep3rRelay, IKeep3rRelay} from '@contracts/Keep3rRelay.sol'; import {Keep3rBondedRelay, IKeep3rBondedRelay} from '@contracts/Keep3rBondedRelay.sol'; +import {XKeeperMetadata, IXKeeperMetadata} from '@contracts/XKeeperMetadata.sol'; +import {_ETH} from '@utils/Constants.sol'; -abstract contract Deploy is Script { +abstract contract DeployNativeETH is Script { // Deployer EOA address public deployer; uint256 internal _deployerPk; @@ -25,6 +27,9 @@ abstract contract Deploy is Script { IKeep3rRelay public keep3rRelay; IKeep3rBondedRelay public keep3rBondedRelay; + // Metadata + IXKeeperMetadata public xKeeperMetadata; + // AutomationVault params address public owner; @@ -33,18 +38,20 @@ abstract contract Deploy is Script { vm.startBroadcast(deployer); automationVaultFactory = new AutomationVaultFactory(); - automationVault = automationVaultFactory.deployAutomationVault(owner); + automationVault = automationVaultFactory.deployAutomationVault(owner, _ETH, 0); openRelay = new OpenRelay(); gelatoRelay = new GelatoRelay(); keep3rRelay = new Keep3rRelay(); keep3rBondedRelay = new Keep3rBondedRelay(); + xKeeperMetadata = new XKeeperMetadata(); + vm.stopBroadcast(); } } -contract DeployMainnet is Deploy { +contract DeployMainnet is DeployNativeETH { function setUp() public { // Deployer setup _deployerPk = vm.envUint('MAINNET_DEPLOYER_PK'); @@ -54,7 +61,7 @@ contract DeployMainnet is Deploy { } } -contract DeployGoerli is Deploy { +contract DeployGoerli is DeployNativeETH { function setUp() public { // Deployer setup _deployerPk = vm.envUint('GOERLI_DEPLOYER_PK'); diff --git a/solidity/test/integration/AutomationVault.t.sol b/solidity/test/integration/AutomationVault.t.sol index 628ef3d..328cc30 100644 --- a/solidity/test/integration/AutomationVault.t.sol +++ b/solidity/test/integration/AutomationVault.t.sol @@ -39,7 +39,7 @@ contract IntegrationAutomationVault is CommonIntegrationTest { vm.startPrank(newOwner); // Try to withdraw funds, should fail because new owner has not confirmed - vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_OnlyOwner.selector, owner)); + vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_OnlyOwner.selector)); automationVault.withdrawFunds(address(_DAI), _amount, newOwner); // Balance of DAI should be 0 @@ -55,7 +55,7 @@ contract IntegrationAutomationVault is CommonIntegrationTest { // Check that the old owner can't withdraw funds changePrank(owner); - vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_OnlyOwner.selector, newOwner)); + vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_OnlyOwner.selector)); automationVault.withdrawFunds(_ETH, 100 ether, owner); } } diff --git a/solidity/test/integration/Common.t.sol b/solidity/test/integration/Common.t.sol index 0eab43c..293f5fa 100644 --- a/solidity/test/integration/Common.t.sol +++ b/solidity/test/integration/Common.t.sol @@ -4,9 +4,9 @@ pragma solidity 0.8.19; import {Test} from 'forge-std/Test.sol'; import {BasicJob} from '@contracts/for-test/BasicJob.sol'; -import {Deploy} from '@script/Deploy.s.sol'; +import {DeployNativeETH} from '@script/DeployNativeETH.s.sol'; -contract DeployForTest is Deploy { +contract DeployForTest is DeployNativeETH { uint256 private constant _FORK_BLOCK = 18_500_000; function setUp() public virtual { diff --git a/solidity/test/integration/GelatoRelay.t.sol b/solidity/test/integration/GelatoRelay.t.sol index 02dc9b0..fc4d5e8 100644 --- a/solidity/test/integration/GelatoRelay.t.sol +++ b/solidity/test/integration/GelatoRelay.t.sol @@ -29,6 +29,7 @@ contract IntegrationGelatoRelay is CommonIntegrationTest { bytes32 public taskId; function setUp() public override { + // AutomationVault setup CommonIntegrationTest.setUp(); // Gelato setup @@ -38,21 +39,32 @@ contract IntegrationGelatoRelay is CommonIntegrationTest { taskId = _createTask(owner); - // AutomationVault setup + // Whitelisted callers array address[] memory _whitelistedCallers = new address[](1); _whitelistedCallers[0] = _getDedicatedMsgSender(owner); + + // Job selectors array bytes4[] memory _jobSelectors = new bytes4[](2); _jobSelectors[0] = basicJob.work.selector; _jobSelectors[1] = basicJob.workHard.selector; + // Job data array + IAutomationVault.JobData[] memory _jobsData = new IAutomationVault.JobData[](1); + _jobsData[0] = IAutomationVault.JobData(address(basicJob), _jobSelectors); + startHoax(owner); - automationVault.approveRelayCallers(address(gelatoRelay), _whitelistedCallers); - automationVault.approveJobSelectors(address(basicJob), _jobSelectors); + + // AutomationVault approve relay data + automationVault.approveRelayData(address(gelatoRelay), _whitelistedCallers, _jobsData); + address(automationVault).call{value: 100 ether}(''); changePrank(address(gelato)); } + /** + * @notice Helper function used for create a module data for Gelato + */ function _createModuleData() internal pure returns (LibDataTypes.ModuleData memory _moduleData) { _moduleData.modules = new LibDataTypes.Module[](1); _moduleData.args = new bytes[](1); @@ -60,6 +72,9 @@ contract IntegrationGelatoRelay is CommonIntegrationTest { _moduleData.args[0] = bytes(''); } + /** + * @notice Helper function used for create a task in Gelato + */ function _createTask(address _taskCreator) internal returns (bytes32 _taskId) { LibDataTypes.ModuleData memory _moduleData = _createModuleData(); @@ -79,9 +94,11 @@ contract IntegrationGelatoRelay is CommonIntegrationTest { IAutomationVault.FeeData[] memory _feeData = new IAutomationVault.FeeData[](1); _feeData[0] = IAutomationVault.FeeData(bot, _ETH, 1 ether); + // Create exec data for Automate bytes memory _execDataAutomate = abi.encodeWithSelector(gelatoRelay.exec.selector, automationVault, _execData, _feeData); + // Create module data for Automate LibDataTypes.ModuleData memory _moduleData = _createModuleData(); vm.expectEmit(address(basicJob)); @@ -89,6 +106,7 @@ contract IntegrationGelatoRelay is CommonIntegrationTest { vm.expectEmit(address(automate)); emit ExecSuccess(1 ether, _ETH, address(gelatoRelay), _execDataAutomate, taskId, true); + // Execute in Automate and expect Gelato to execute the job automate.exec(owner, address(gelatoRelay), _execDataAutomate, _moduleData, 1 ether, _ETH, false, true); } @@ -104,11 +122,14 @@ contract IntegrationGelatoRelay is CommonIntegrationTest { IAutomationVault.FeeData[] memory _feeData = new IAutomationVault.FeeData[](1); _feeData[0] = IAutomationVault.FeeData(bot, _ETH, 1 ether); + // Create exec data for Automate bytes memory _execDataAutomate = abi.encodeWithSelector(gelatoRelay.exec.selector, automationVault, _execData, _feeData); + // Create module data for Automate LibDataTypes.ModuleData memory _moduleData = _createModuleData(); + // Execute in Automate and expect Gelato to execute the job automate.exec(owner, address(gelatoRelay), _execDataAutomate, _moduleData, 1 ether, _ETH, false, true); assertEq(bot.balance, 1 ether); diff --git a/solidity/test/integration/Keep3rRelay.t.sol b/solidity/test/integration/Keep3rRelay.t.sol index 02881bd..1e55e41 100644 --- a/solidity/test/integration/Keep3rRelay.t.sol +++ b/solidity/test/integration/Keep3rRelay.t.sol @@ -26,6 +26,7 @@ contract IntegrationKeep3rRelay is CommonIntegrationTest { address public keep3rGovernor; function setUp() public override { + // AutomationVault setup CommonIntegrationTest.setUp(); // Keep3r setup @@ -36,23 +37,34 @@ contract IntegrationKeep3rRelay is CommonIntegrationTest { _addJobAndLiquidity(address(automationVault), 1000 ether); - // AutomationVault setup + // Keep3r callers array address[] memory _keepers = new address[](1); _keepers[0] = bot; + + // Keep3r selectors array bytes4[] memory _keep3rSelectors = new bytes4[](2); _keep3rSelectors[0] = keep3r.isKeeper.selector; _keep3rSelectors[1] = keep3r.worked.selector; + + // Job selectors array bytes4[] memory _jobSelectors = new bytes4[](2); _jobSelectors[0] = basicJob.work.selector; _jobSelectors[1] = basicJob.workHard.selector; IKeep3rBondedRelay.Requirements memory _requirements = IKeep3rBondedRelay.Requirements(address(kp3r), 1 ether, 0, 0); + // Job data array + IAutomationVault.JobData[] memory _jobsData = new IAutomationVault.JobData[](2); + _jobsData[0] = IAutomationVault.JobData(address(keep3r), _keep3rSelectors); + _jobsData[1] = IAutomationVault.JobData(address(basicJob), _jobSelectors); + vm.startPrank(owner); + + // Keep3r bonded relay requirements setup keep3rBondedRelay.setAutomationVaultRequirements(automationVault, _requirements); - automationVault.approveRelayCallers(address(keep3rRelay), _keepers); - automationVault.approveRelayCallers(address(keep3rBondedRelay), _keepers); - automationVault.approveJobSelectors(address(keep3r), _keep3rSelectors); - automationVault.approveJobSelectors(address(basicJob), _jobSelectors); + + // AutomationVault approve relay data + automationVault.approveRelayData(address(keep3rRelay), _keepers, _jobsData); + automationVault.approveRelayData(address(keep3rBondedRelay), _keepers, _jobsData); } function _addJobAndLiquidity(address _job, uint256 _amount) internal { diff --git a/solidity/test/integration/OpenRelay.t.sol b/solidity/test/integration/OpenRelay.t.sol index 34a57ef..9b33b09 100644 --- a/solidity/test/integration/OpenRelay.t.sol +++ b/solidity/test/integration/OpenRelay.t.sol @@ -7,18 +7,26 @@ import {IAutomationVault} from '@interfaces/IAutomationVault.sol'; contract IntegrationOpenRelay is CommonIntegrationTest { function setUp() public override { + // AutomationVault setup CommonIntegrationTest.setUp(); - // AutomationVault setup + // Bot callers array address[] memory _bots = new address[](1); _bots[0] = bot; + + // Job selectors array bytes4[] memory _jobSelectors = new bytes4[](2); _jobSelectors[0] = basicJob.work.selector; _jobSelectors[1] = basicJob.workHard.selector; + // Job data array + IAutomationVault.JobData[] memory _jobsData = new IAutomationVault.JobData[](1); + _jobsData[0] = IAutomationVault.JobData(address(basicJob), _jobSelectors); + startHoax(owner); - automationVault.approveRelayCallers(address(openRelay), _bots); - automationVault.approveJobSelectors(address(basicJob), _jobSelectors); + + // AutomationVault approve relay data + automationVault.approveRelayData(address(openRelay), _bots, _jobsData); address(automationVault).call{value: 100 ether}(''); changePrank(bot); @@ -43,11 +51,15 @@ contract IntegrationOpenRelay is CommonIntegrationTest { _execData[0] = IAutomationVault.ExecData(address(basicJob), abi.encodeWithSelector(basicJob.workHard.selector, _howHard)); + // Calculate the exec gas cost uint256 _gasBeforeExec = gasleft(); openRelay.exec(automationVault, _execData, bot); uint256 _gasAfterExec = gasleft(); + // Calculate tx cost uint256 _txCost = (_gasBeforeExec - _gasAfterExec) * block.basefee; + + // Calculate payment uint256 _payment = _txCost * openRelay.GAS_MULTIPLIER() / openRelay.BASE(); assertGt(bot.balance, _payment); diff --git a/solidity/test/unit/AutomationVault.t.sol b/solidity/test/unit/AutomationVault.t.sol index f3defaa..fcd1364 100644 --- a/solidity/test/unit/AutomationVault.t.sol +++ b/solidity/test/unit/AutomationVault.t.sol @@ -1,32 +1,33 @@ -// SPDX-License-Identifier: AGPL-3.0-only +/// SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.19; import {Test} from 'forge-std/Test.sol'; -import {AutomationVault, IAutomationVault, EnumerableSet} from '@contracts/AutomationVault.sol'; -import {IERC20} from '@openzeppelin/token/ERC20/IERC20.sol'; -import {_ETH, _NULL} from '@utils/Constants.sol'; +import {AutomationVault, IAutomationVault, EnumerableSet, IERC20} from '@contracts/AutomationVault.sol'; +import {_ETH, _ALL} from '@utils/Constants.sol'; contract AutomationVaultForTest is AutomationVault { using EnumerableSet for EnumerableSet.AddressSet; using EnumerableSet for EnumerableSet.Bytes32Set; - constructor(address _owner) AutomationVault(_owner) {} + constructor(address _owner, address _nativeToken) AutomationVault(_owner, _nativeToken) {} function setPendingOwnerForTest(address _pendingOwner) public { pendingOwner = _pendingOwner; } function addRelayEnabledCallersForTest(address _relay, address _relayCaller) public { - _relayEnabledCallers[_relay].add(_relayCaller); + _relayCallers[_relay].add(_relayCaller); + _relays.add(_relay); } - function addJobEnabledSelectorsForTest(address _job, bytes4 _functionSelector) public { - _jobEnabledSelectors[_job].add(_functionSelector); + function addJobEnabledSelectorsForTest(address _relay, address _job, bytes4 _functionSelector) public { + _relayJobSelectors[_relay][_job].add(_functionSelector); + _jobs.add(_job); } - function removeJobEnabledSelectorsForTest(address _job, bytes4 _functionSelector) public { - _jobEnabledSelectors[_job].remove(_functionSelector); + function removeJobEnabledSelectorsForTest(address _relay, address _job, bytes4 _functionSelector) public { + _relayJobSelectors[_relay][_job].remove(_functionSelector); } } @@ -36,7 +37,7 @@ contract NoFallbackForTest {} * @title AutomationVault Unit tests */ abstract contract AutomationVaultUnitTest is Test { - // Events + /// Events event ChangeOwner(address indexed _pendingOwner); event AcceptOwner(address indexed _owner); event DepositFunds(address indexed _token, uint256 _amount); @@ -54,21 +55,21 @@ abstract contract AutomationVaultUnitTest is Test { address indexed _relay, address indexed _relayCaller, address indexed _feeRecipient, address _feeToken, uint256 _fee ); - // AutomationVault contract + /// AutomationVault contract AutomationVaultForTest public automationVault; - // Mock contracts + /// Mock contracts address public token; address public job; address public relay; address public relayCaller; - // EOAs + /// EOAs address public owner; address public pendingOwner; address public receiver; - // Data + /// Data bytes4 public jobSelector; bytes public jobData; @@ -85,17 +86,57 @@ abstract contract AutomationVaultUnitTest is Test { relay = makeAddr('Relay'); relayCaller = makeAddr('RelayCaller'); - automationVault = new AutomationVaultForTest(owner); - } - - function _mockTokenTransfer(address _token) internal { - vm.mockCall(_token, abi.encodeWithSelector(IERC20.transfer.selector), abi.encode(true)); + automationVault = new AutomationVaultForTest(owner, _ETH); } } contract UnitAutomationVaultConstructor is AutomationVaultUnitTest { + /** + * @notice Check that the constructor sets the params correctly + */ function testParamsAreSet() public { assertEq(automationVault.owner(), owner); + assertEq(automationVault.NATIVE_TOKEN(), _ETH); + } +} + +contract UnitGetRelayData is AutomationVaultUnitTest { + /** + * @notice Check that the relay data is correct + */ + function testRelayData(address _relay, address _relayCaller, address _job, bytes4 _functionSelector) public { + automationVault.addRelayEnabledCallersForTest(_relay, _relayCaller); + automationVault.addJobEnabledSelectorsForTest(_relay, _job, _functionSelector); + + (address[] memory _callers, bytes32[] memory _selectors) = automationVault.getRelayAndJobData(_relay, _job); + + assertEq(_callers.length, 1); + assertEq(_callers[0], _relayCaller); + + assertEq(_selectors.length, 1); + assertEq(_selectors[0], _functionSelector); + } +} + +contract UnitAutomationVaultListRelays is AutomationVaultUnitTest { + /** + * @notice Check that the relays length is correct + */ + function testRelaysLength(address _relay) public { + automationVault.addRelayEnabledCallersForTest(_relay, owner); + + assertEq(automationVault.relays().length, 1); + } +} + +contract UnitAutomationVaultListJobs is AutomationVaultUnitTest { + /** + * @notice Check that the jobs length is correct + */ + function testJobLength(address _relay, address _job) public { + automationVault.addJobEnabledSelectorsForTest(_relay, _job, jobSelector); + + assertEq(automationVault.jobs().length, 1); } } @@ -106,19 +147,28 @@ contract UnitAutomationVaultChangeOwner is AutomationVaultUnitTest { vm.startPrank(owner); } + /** + * @notice Checks that the test has to revert if the caller is not the owner + */ function testRevertIfCallerIsNotOwner() public { - vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_OnlyOwner.selector, owner)); + vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_OnlyOwner.selector)); changePrank(pendingOwner); automationVault.changeOwner(pendingOwner); } + /** + * @notice Check that the pending owner is set correctly + */ function testSetPendingOwner() public { automationVault.changeOwner(pendingOwner); assertEq(automationVault.pendingOwner(), pendingOwner); } + /** + * @notice Emit ChangeOwner event when the pending owner is set + */ function testEmitChangeOwner() public { vm.expectEmit(); emit ChangeOwner(pendingOwner); @@ -136,25 +186,37 @@ contract UnitAutomationVaultAcceptOwner is AutomationVaultUnitTest { vm.startPrank(pendingOwner); } + /** + * @notice Check that the test has to revert if the caller is not the pending owner + */ function testRevertIfCallerIsNotPendingOwner() public { - vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_OnlyPendingOwner.selector, pendingOwner)); + vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_OnlyPendingOwner.selector)); changePrank(owner); automationVault.acceptOwner(); } + /** + * @notice Check that the pending owner accepts the ownership + */ function testSetJobOwner() public { automationVault.acceptOwner(); assertEq(automationVault.owner(), pendingOwner); } + /** + * @notice Check that the pending owner is set to zero + */ function testDeletePendingOwner() public { automationVault.acceptOwner(); assertEq(automationVault.pendingOwner(), address(0)); } + /** + * @notice Emit AcceptOwner event when the pending owner accepts the ownership + */ function testEmitAcceptOwner() public { vm.expectEmit(); emit AcceptOwner(pendingOwner); @@ -164,226 +226,337 @@ contract UnitAutomationVaultAcceptOwner is AutomationVaultUnitTest { } contract UnitAutomationVaultWithdrawFunds is AutomationVaultUnitTest { - function setUp() public virtual override { - super.setUp(); + uint256 internal _balance; + IERC20 internal _token; + + modifier happyPath(uint128 _amount) { + vm.assume(_amount > 0); + + /// Add balance to the contract vm.deal(address(automationVault), 2 ** 128 - 1); + + /// Mock the token transfer and balanceOf + _token = IERC20(token); + vm.mockCall(token, abi.encodeWithSelector(IERC20.transfer.selector, receiver, _amount), abi.encode(true)); + vm.mockCall(token, abi.encodeWithSelector(IERC20.balanceOf.selector, receiver), abi.encode(_amount)); + + _balance = address(automationVault).balance; vm.startPrank(owner); + _; } - function testRevertIfCallerIsNotOwner(uint128 _amount, address _fakeOwner) public { - vm.assume(_fakeOwner != owner); - vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_OnlyOwner.selector, owner)); + /** + * @notice Checks that the test has to revert if the caller is not the owner + */ + function testRevertIfCallerIsNotOwner(uint128 _amount) public { + vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_OnlyOwner.selector)); - changePrank(_fakeOwner); automationVault.withdrawFunds(_ETH, _amount, owner); } - function testRevertIfETHTransferFailed(uint160 _amount) public { - vm.assume(_amount == type(uint160).max); - - vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_ETHTransferFailed.selector)); + /** + * @notice Checks that the test has to revert if the native token transfer failed + */ + function testRevertIfNativeTokenTransferFailed() public { + vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_NativeTokenTransferFailed.selector)); - automationVault.withdrawFunds(_ETH, _amount, address(automationVault)); + vm.prank(owner); + automationVault.withdrawFunds(_ETH, type(uint160).max, address(automationVault)); } - function testWithdrawETHAmountUpdateBalances(uint128 _amount) public { - vm.assume(_amount > 0); - - uint256 _balance = address(automationVault).balance; + /** + * @notice Checks that the balances are updated correctly + */ + function testWithdrawNativeTokenAmountUpdateBalances(uint128 _amount) public happyPath(_amount) { automationVault.withdrawFunds(_ETH, _amount, receiver); assertEq(receiver.balance, _amount); assertEq(address(automationVault).balance, _balance - _amount); } - function testEmitWithdrawETHAmount(uint128 _balance, uint128 _amount) public { - vm.assume(_balance > _amount && _amount > 0); + /** + * @notice Checks that the balances are updated correctly + */ + function testWithdrawERC20AmountUpdateBalances(uint128 _amount) public happyPath(_amount) { + automationVault.withdrawFunds(token, _amount, receiver); + assertEq(_token.balanceOf(receiver), _amount); + } + + /** + * @notice Emit WithdrawFunds event when the native token is withdrawn + */ + function testEmitWithdrawNativeTokenAmount(uint128 _amount) public happyPath(_amount) { vm.expectEmit(); emit WithdrawFunds(_ETH, _amount, receiver); automationVault.withdrawFunds(_ETH, _amount, receiver); } - function testEmitWithdrawERC2OAmount(uint128 _balance, uint128 _amount) public { - vm.assume(_balance > _amount && _amount > 0); - + /** + * @notice Emit WithdrawFunds event when the token ERC20 is withdrawn + */ + function testEmitWithdrawERC2OAmount(uint128 _amount) public happyPath(_amount) { vm.expectEmit(); emit WithdrawFunds(token, _amount, receiver); - _mockTokenTransfer(token); - vm.mockCall(token, abi.encodeWithSelector(IERC20.transfer.selector, receiver, _amount), abi.encode(true)); - automationVault.withdrawFunds(token, _amount, receiver); } } -contract UnitAutomationVaultApproveRelayCallers is AutomationVaultUnitTest { - function setUp() public override { - AutomationVaultUnitTest.setUp(); +/** + * @dev Is not possible to create in the happy path type struct IAutomationVault.JobData memory[] memory to storage because is not yet supported. + */ +contract UnitAutomationVaultApproveRelayData is AutomationVaultUnitTest { + address[] internal _callers; + bytes4[] internal _functionSelectors; + + modifier happyPath(address _relay, address _job, bytes4 _functionSelector) { + /// @dev This is a workaround for the fact that the VM does not support dynamic arrays + vm.assume(_relay != address(0)); + vm.assume(_job != address(0)); + vm.assume(_functionSelector != jobSelector); + + /// Create the data + _callers = new address[](2); + + _callers[0] = (owner); + _callers[1] = (pendingOwner); + + _functionSelectors = new bytes4[](2); + + _functionSelectors[0] = jobSelector; + _functionSelectors[1] = _functionSelector; vm.startPrank(owner); + _; } - function testRevertIfCallerIsNotOwner(address _relay, address[] memory _callers) public { - vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_OnlyOwner.selector, owner)); + /** + * @notice Checks that the test has to revert if the caller is not the owner + */ + function testRevertIfCallerIsNotOwner(address _relay, IAutomationVault.JobData[] memory _jobsData) public { + vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_OnlyOwner.selector)); - changePrank(pendingOwner); - automationVault.approveRelayCallers(_relay, _callers); + vm.prank(pendingOwner); + automationVault.approveRelayData(_relay, _callers, _jobsData); } - function testEmitApproveRelay(address _relay) public { - vm.assume(_relay != address(0)); - address[] memory _callers = new address[](2); + /** + * @notice Checks that the test has to revert if the relay address is zero + */ + function testRevertIfRelayIsZero(IAutomationVault.JobData[] memory _jobsData) public { + vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_RelayZero.selector)); - _callers[0] = (owner); - _callers[1] = (pendingOwner); + vm.prank(owner); + automationVault.approveRelayData(address(0), _callers, _jobsData); + } + + /** + * @notice Emit ApproveRelay event when the relay is approved + */ + function testEmitApproveRelay( + address _relay, + address _job, + bytes4 _functionSelector + ) public happyPath(_relay, _job, _functionSelector) { + IAutomationVault.JobData[] memory _jobsData = new IAutomationVault.JobData[](1); + _jobsData[0].job = _job; + _jobsData[0].functionSelectors = _functionSelectors; vm.expectEmit(); emit ApproveRelay(_relay); - automationVault.approveRelayCallers(_relay, _callers); + automationVault.approveRelayData(_relay, _callers, _jobsData); } - function testEmitApproveCaller(address _relay) public { - vm.assume(_relay != address(0)); - address[] memory _callers = new address[](2); - - _callers[0] = (owner); - _callers[1] = (pendingOwner); + /** + * @notice Emit ApproveRelayCaller event when the relay caller is approved + */ + function testEmitApproveCaller( + address _relay, + address _job, + bytes4 _functionSelector + ) public happyPath(_relay, _job, _functionSelector) { + IAutomationVault.JobData[] memory _jobsData = new IAutomationVault.JobData[](1); + _jobsData[0].job = _job; + _jobsData[0].functionSelectors = _functionSelectors; for (uint256 _i; _i < _callers.length; _i++) { vm.expectEmit(); emit ApproveRelayCaller(_relay, _callers[_i]); } - automationVault.approveRelayCallers(_relay, _callers); - } -} - -contract UnitAutomationVaultRevokeRelayCallers is AutomationVaultUnitTest { - function setUp() public override { - AutomationVaultUnitTest.setUp(); - - automationVault.addRelayEnabledCallersForTest(relay, owner); - - vm.startPrank(owner); - } - - function testRevertIfCallerIsNotOwner(address _relay, address[] memory _callers) public { - vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_OnlyOwner.selector, owner)); - - changePrank(pendingOwner); - automationVault.revokeRelayCallers(_relay, _callers); + automationVault.approveRelayData(_relay, _callers, _jobsData); } - function testEmitRevokeRelay() public { - address[] memory _callers = new address[](1); - - _callers[0] = (owner); + /** + * @notice Emit ApproveJob event when the job is approved + */ + function testEmitApproveJob( + address _relay, + address _job, + bytes4 _functionSelector + ) public happyPath(_relay, _job, _functionSelector) { + IAutomationVault.JobData[] memory _jobsData = new IAutomationVault.JobData[](1); + _jobsData[0].job = _job; + _jobsData[0].functionSelectors = _functionSelectors; vm.expectEmit(); - emit RevokeRelay(relay); + emit ApproveJob(_job); - automationVault.revokeRelayCallers(relay, _callers); + automationVault.approveRelayData(_relay, _callers, _jobsData); } - function testEmitRevokeCaller() public { - automationVault.addRelayEnabledCallersForTest(relay, pendingOwner); - address[] memory _callers = new address[](1); - - _callers[0] = (owner); + /** + * @notice Emit ApproveJobSelector event when the job selector is approved + */ + function testEmitApproveFunctionSelector( + address _relay, + address _job, + bytes4 _functionSelector + ) public happyPath(_relay, _job, _functionSelector) { + IAutomationVault.JobData[] memory _jobsData = new IAutomationVault.JobData[](1); + _jobsData[0].job = _job; + _jobsData[0].functionSelectors = _functionSelectors; - for (uint256 _i; _i < _callers.length; _i++) { + for (uint256 _i; _i < _functionSelectors.length; _i++) { vm.expectEmit(); - emit RevokeRelayCaller(relay, _callers[_i]); + emit ApproveJobSelector(_job, _functionSelectors[_i]); } - automationVault.revokeRelayCallers(relay, _callers); + automationVault.approveRelayData(_relay, _callers, _jobsData); } } -contract UnitAutomationVaultApproveJobSelectors is AutomationVaultUnitTest { - function setUp() public override { - AutomationVaultUnitTest.setUp(); +/** + * @dev Is not possible to create in the happy path type struct IAutomationVault.JobData memory[] memory to storage because is not yet supported. + */ +contract UnitAutomationVaultRevokeRelayData is AutomationVaultUnitTest { + address[] internal _callers; + bytes4[] internal _functionSelectors; - vm.startPrank(owner); - } + modifier happyPath(address _relay, address _job, bytes4 _functionSelector) { + /// @dev This is a workaround for the fact that the VM does not support dynamic arrays + vm.assume(_relay != address(0)); + vm.assume(_job != address(0)); + vm.assume(_functionSelector != jobSelector); - function testRevertIfCallerIsNotOwner(address _job, bytes4[] memory _functionSelectors) public { - vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_OnlyOwner.selector, owner)); + /// Create the data + _callers = new address[](2); - changePrank(pendingOwner); - automationVault.approveJobSelectors(_job, _functionSelectors); - } + _callers[0] = (owner); + _callers[1] = (pendingOwner); - function testEmitApproveJob(address _job, bytes4 _functionSelector) public { - vm.assume(_job != address(0)); - bytes4[] memory _functionSelectors = new bytes4[](2); + _functionSelectors = new bytes4[](2); _functionSelectors[0] = jobSelector; _functionSelectors[1] = _functionSelector; - vm.expectEmit(); - emit ApproveJob(_job); + automationVault.addRelayEnabledCallersForTest(_relay, owner); + automationVault.addRelayEnabledCallersForTest(_relay, pendingOwner); + + automationVault.addJobEnabledSelectorsForTest(_relay, _job, jobSelector); + automationVault.addJobEnabledSelectorsForTest(_relay, _job, _functionSelector); - automationVault.approveJobSelectors(_job, _functionSelectors); + vm.startPrank(owner); + _; } - function testEmitApproveFunctionSelector(address _job, bytes4 _functionSelector) public { - vm.assume(_job != address(0)); - vm.assume(_functionSelector != jobSelector); - bytes4[] memory _functionSelectors = new bytes4[](2); + /** + * @notice Checks that the test has to revert if the caller is not the owner + */ + function testRevertIfCallerIsNotOwner(address _relay, IAutomationVault.JobData[] memory _jobsData) public { + vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_OnlyOwner.selector)); - _functionSelectors[0] = jobSelector; - _functionSelectors[1] = _functionSelector; + vm.prank(pendingOwner); + automationVault.revokeRelayData(_relay, _callers, _jobsData); + } - for (uint256 _i; _i < _functionSelectors.length; _i++) { - vm.expectEmit(); - emit ApproveJobSelector(_job, _functionSelectors[_i]); - } + /** + * @notice Checks that the test has to revert if the relay address is zero + */ + function testRevertIfRelayIsZero(IAutomationVault.JobData[] memory _jobsData) public { + vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_RelayZero.selector)); - automationVault.approveJobSelectors(_job, _functionSelectors); + vm.prank(owner); + automationVault.revokeRelayData(address(0), _callers, _jobsData); } -} -contract UnitAutomationVaultRevokeJobSelectors is AutomationVaultUnitTest { - function setUp() public override { - AutomationVaultUnitTest.setUp(); - automationVault.addJobEnabledSelectorsForTest(job, jobSelector); - vm.startPrank(owner); - } + /** + * @notice Emit RevokeRelay event when the relay is revoked + */ + function testEmitRevokeRelay( + address _relay, + address _job, + bytes4 _functionSelector + ) public happyPath(_relay, _job, _functionSelector) { + IAutomationVault.JobData[] memory _jobsData = new IAutomationVault.JobData[](1); + _jobsData[0].job = _job; + _jobsData[0].functionSelectors = _functionSelectors; - function testRevertIfCallerIsNotOwner(address _job, bytes4[] memory _functionSelectors) public { - vm.expectRevert(abi.encodeWithSelector(IAutomationVault.AutomationVault_OnlyOwner.selector, owner)); + vm.expectEmit(); + emit RevokeRelay(_relay); - changePrank(pendingOwner); - automationVault.revokeJobSelectors(_job, _functionSelectors); + automationVault.revokeRelayData(_relay, _callers, _jobsData); } - function testEmitRevokeJob() public { - bytes4[] memory _functionSelectors = new bytes4[](1); + /** + * @notice Emit RevokeRelayCaller event when the relay caller is revoked + */ + function testEmitRevokeCaller( + address _relay, + address _job, + bytes4 _functionSelector + ) public happyPath(_relay, _job, _functionSelector) { + IAutomationVault.JobData[] memory _jobsData = new IAutomationVault.JobData[](1); + _jobsData[0].job = _job; + _jobsData[0].functionSelectors = _functionSelectors; - _functionSelectors[0] = jobSelector; + for (uint256 _i; _i < _callers.length; _i++) { + vm.expectEmit(); + emit RevokeRelayCaller(_relay, _callers[_i]); + } + + automationVault.revokeRelayData(_relay, _callers, _jobsData); + } + + /** + * @notice Emit RevokeJob event when the job is revoked + */ + function testEmitRevokeJob( + address _relay, + address _job, + bytes4 _functionSelector + ) public happyPath(_relay, _job, _functionSelector) { + IAutomationVault.JobData[] memory _jobsData = new IAutomationVault.JobData[](1); + _jobsData[0].job = _job; + _jobsData[0].functionSelectors = _functionSelectors; vm.expectEmit(); - emit RevokeJob(job); + emit RevokeJob(_job); - automationVault.revokeJobSelectors(job, _functionSelectors); + automationVault.revokeRelayData(_relay, _callers, _jobsData); } - function testEmitRevokeFunctionSelector(address _job, bytes4 _functionSelector) public { - automationVault.addJobEnabledSelectorsForTest(_job, _functionSelector); - bytes4[] memory _functionSelectors = new bytes4[](1); - - _functionSelectors[0] = _functionSelector; + /** + * @notice Emit RevokeJobSelector event when the job selector is revoked + */ + function testEmitRevokeFunctionSelector( + address _relay, + address _job, + bytes4 _functionSelector + ) public happyPath(_relay, _job, _functionSelector) { + IAutomationVault.JobData[] memory _jobsData = new IAutomationVault.JobData[](1); + _jobsData[0].job = _job; + _jobsData[0].functionSelectors = _functionSelectors; for (uint256 _i; _i < _functionSelectors.length; _i++) { vm.expectEmit(); - emit RevokeJobSelector(_job, _functionSelector); + emit RevokeJobSelector(_job, _functionSelectors[_i]); } - automationVault.revokeJobSelectors(_job, _functionSelectors); + automationVault.revokeRelayData(_relay, _callers, _jobsData); } } @@ -394,7 +567,7 @@ contract UnitAutomationVaultExec is AutomationVaultUnitTest { automationVault.addRelayEnabledCallersForTest(relay, relayCaller); for (uint256 _i; _i < _execData.length; ++_i) { - automationVault.addJobEnabledSelectorsForTest(_execData[_i].job, bytes4(_execData[_i].jobData)); + automationVault.addJobEnabledSelectorsForTest(relay, _execData[_i].job, bytes4(_execData[_i].jobData)); assumeNoPrecompiles(_execData[_i].job); vm.assume(_execData[_i].job != address(vm)); vm.mockCall(_execData[_i].job, abi.encodeWithSelector(bytes4(_execData[_i].jobData)), abi.encode()); @@ -405,7 +578,7 @@ contract UnitAutomationVaultExec is AutomationVaultUnitTest { assumePayable(_feeData[_i].feeRecipient); assumeNoPrecompiles(_feeData[_i].feeToken); vm.assume(_feeData[_i].feeToken != address(vm)); - _mockTokenTransfer(_feeData[_i].feeToken); + vm.mockCall(_feeData[_i].feeToken, abi.encodeWithSelector(IERC20.transfer.selector), abi.encode(true)); } deal(address(automationVault), type(uint256).max); @@ -414,6 +587,9 @@ contract UnitAutomationVaultExec is AutomationVaultUnitTest { _; } + /** + * @notice Checks that the test has to revert if the caller is not the relay caller + */ function testRevertIfNotApprovedRelayCaller( IAutomationVault.ExecData[] memory _execData, IAutomationVault.FeeData[] memory _feeData @@ -429,13 +605,16 @@ contract UnitAutomationVaultExec is AutomationVaultUnitTest { IAutomationVault.FeeData[] memory _feeData ) public happyPath(_execData, _feeData) { vm.assume(_execData.length > 3); - automationVault.removeJobEnabledSelectorsForTest(_execData[1].job, bytes4(_execData[1].jobData)); + automationVault.removeJobEnabledSelectorsForTest(relay, _execData[1].job, bytes4(_execData[1].jobData)); vm.expectRevert(IAutomationVault.AutomationVault_NotApprovedJobSelector.selector); automationVault.exec(relayCaller, _execData, _feeData); } + /** + * @notice Checks that the test has to revert if the job call failed + */ function testRevertIfJobCallFailed( IAutomationVault.ExecData[] memory _execData, IAutomationVault.FeeData[] memory _feeData @@ -449,6 +628,9 @@ contract UnitAutomationVaultExec is AutomationVaultUnitTest { automationVault.exec(relayCaller, _execData, _feeData); } + /** + * @notice Checks that call is executed correctly without fees + */ function testCallOnlyJobFunction(IAutomationVault.ExecData[] memory _execData) public happyPath(_execData, new IAutomationVault.FeeData[](0)) @@ -463,6 +645,9 @@ contract UnitAutomationVaultExec is AutomationVaultUnitTest { automationVault.exec(relayCaller, _execData, _feeData); } + /** + * @notice Checks that call is executed correctly with fees + */ function testCallJobFunction( IAutomationVault.ExecData[] memory _execData, IAutomationVault.FeeData[] memory _feeData @@ -476,13 +661,16 @@ contract UnitAutomationVaultExec is AutomationVaultUnitTest { automationVault.exec(relayCaller, _execData, _feeData); } + /** + * @notice Checks that call is executed correctly with fees and open sender + */ function testCallJobFunctionWithOpenSender( IAutomationVault.ExecData[] memory _execData, IAutomationVault.FeeData[] memory _feeData, address _sender ) public happyPath(_execData, _feeData) { vm.assume(_execData.length > 3); - automationVault.addRelayEnabledCallersForTest(relay, _NULL); + automationVault.addRelayEnabledCallersForTest(relay, _ALL); for (uint256 _i; _i < _execData.length; ++_i) { vm.expectCall(_execData[_i].job, _execData[_i].jobData, 1); @@ -491,6 +679,9 @@ contract UnitAutomationVaultExec is AutomationVaultUnitTest { automationVault.exec(_sender, _execData, _feeData); } + /** + * @notice Emit JobExecuted event when the job is executed + */ function testEmitJobExecuted( IAutomationVault.ExecData[] memory _execData, IAutomationVault.FeeData[] memory _feeData @@ -505,7 +696,10 @@ contract UnitAutomationVaultExec is AutomationVaultUnitTest { automationVault.exec(relayCaller, _execData, _feeData); } - function testRevertIfETHTransferFailed( + /** + * @notice Checks that the test has to revert if the native token transfer failed + */ + function testRevertIfNativeTokenTransferFailed( IAutomationVault.ExecData[] memory _execData, IAutomationVault.FeeData[] memory _feeData ) public happyPath(_execData, _feeData) { @@ -513,12 +707,15 @@ contract UnitAutomationVaultExec is AutomationVaultUnitTest { _feeData[1].feeToken = _ETH; vm.etch(_feeData[1].feeRecipient, type(NoFallbackForTest).runtimeCode); - vm.expectRevert(IAutomationVault.AutomationVault_ETHTransferFailed.selector); + vm.expectRevert(IAutomationVault.AutomationVault_NativeTokenTransferFailed.selector); automationVault.exec(relayCaller, _execData, _feeData); } - function testCallETHTransfer( + /** + * @notice Checks that native token transfer is executed correctly + */ + function testCallNativeTokenTransfer( IAutomationVault.ExecData[] memory _execData, IAutomationVault.FeeData[] memory _feeData, uint128 _fee @@ -536,6 +733,9 @@ contract UnitAutomationVaultExec is AutomationVaultUnitTest { } } + /** + * @notice Checks that token transfer is executed correctly + */ function testCallTokenTransfer( IAutomationVault.ExecData[] memory _execData, IAutomationVault.FeeData[] memory _feeData @@ -554,6 +754,9 @@ contract UnitAutomationVaultExec is AutomationVaultUnitTest { automationVault.exec(relayCaller, _execData, _feeData); } + /** + * @notice Emit IssuePayment event when the payment is issued + */ function testEmitIssuePayment( IAutomationVault.ExecData[] memory _execData, IAutomationVault.FeeData[] memory _feeData diff --git a/solidity/test/unit/AutomationVaultFactory.t.sol b/solidity/test/unit/AutomationVaultFactory.t.sol index a65a913..943b8a6 100644 --- a/solidity/test/unit/AutomationVaultFactory.t.sol +++ b/solidity/test/unit/AutomationVaultFactory.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.19; import {Test} from 'forge-std/Test.sol'; import {AutomationVaultFactory, EnumerableSet} from '@contracts/AutomationVaultFactory.sol'; -import {AutomationVault} from '@contracts/AutomationVault.sol'; +import {AutomationVault, IAutomationVault} from '@contracts/AutomationVault.sol'; contract AutomationVaultFactoryForTest is AutomationVaultFactory { using EnumerableSet for EnumerableSet.AddressSet; @@ -14,6 +14,24 @@ contract AutomationVaultFactoryForTest is AutomationVaultFactory { _automationVaults.add(_automationVaultsForTest[_index]); } } + + function preComputeAddressForTest( + address _owner, + address _nativeToken, + uint256 _salt + ) public view returns (address _precomputedAddress) { + bytes memory _bytecode = type(AutomationVault).creationCode; + + bytes32 _hashed = keccak256( + abi.encodePacked( + bytes1(0xff), + address(this), + keccak256(abi.encodePacked(msg.sender, _salt)), + keccak256(abi.encodePacked(_bytecode, abi.encode(_owner, _nativeToken))) + ) + ); + _precomputedAddress = address(uint160(uint256(_hashed))); + } } /** @@ -30,8 +48,6 @@ abstract contract AutomationVaultFactoryUnitTest is Test { AutomationVault public automationVault; function setUp() public virtual { - automationVault = AutomationVault(payable(0x104fBc016F4bb334D775a19E8A6510109AC63E00)); - automationVaultFactory = new AutomationVaultFactoryForTest(); } } @@ -79,29 +95,51 @@ contract UnitAutomationVaultFactoryGetAutomationVaults is AutomationVaultFactory } contract UnitAutomationVaultFactoryDeployAutomationVault is AutomationVaultFactoryUnitTest { - function testDeployAutomationVault(address _owner) public { - automationVaultFactory.deployAutomationVault(_owner); + address internal _precomputedAddress; + + modifier happyPath(address _owner, address _nativeToken, uint256 _salt) { + _precomputedAddress = automationVaultFactory.preComputeAddressForTest(_owner, _nativeToken, _salt); + _; + } - assertEq(address(automationVault).code, type(AutomationVault).runtimeCode); + function testDeployAutomationVault( + address _owner, + address _nativeToken, + uint256 _salt + ) public happyPath(_owner, _nativeToken, _salt) { + IAutomationVault _automationVault = automationVaultFactory.deployAutomationVault(_owner, _nativeToken, _salt); // params - assertEq(automationVault.owner(), _owner); + assertEq(_automationVault.owner(), _owner); + assertEq(_automationVault.NATIVE_TOKEN(), _nativeToken); } - function testSetAutomationVaults(address _owner) public { - automationVaultFactory.deployAutomationVault(_owner); + function testSetAutomationVaults( + address _owner, + address _nativeToken, + uint256 _salt + ) public happyPath(_owner, _nativeToken, _salt) { + automationVaultFactory.deployAutomationVault(_owner, _nativeToken, _salt); - assertEq(automationVaultFactory.automationVaults(0, 1)[0], address(automationVault)); + assertEq(automationVaultFactory.automationVaults(0, 1)[0], _precomputedAddress); } - function testEmitDeployAutomationVault(address _owner) public { + function testEmitDeployAutomationVault( + address _owner, + address _nativeToken, + uint256 _salt + ) public happyPath(_owner, _nativeToken, _salt) { vm.expectEmit(); - emit DeployAutomationVault(_owner, address(automationVault)); + emit DeployAutomationVault(_owner, _precomputedAddress); - automationVaultFactory.deployAutomationVault(_owner); + automationVaultFactory.deployAutomationVault(_owner, _nativeToken, _salt); } - function testReturnAutomationVault(address _owner) public { - assertEq(address(automationVaultFactory.deployAutomationVault(_owner)), address(automationVault)); + function testReturnAutomationVault( + address _owner, + address _nativeToken, + uint256 _salt + ) public happyPath(_owner, _nativeToken, _salt) { + assertEq(address(automationVaultFactory.deployAutomationVault(_owner, _nativeToken, _salt)), _precomputedAddress); } } diff --git a/solidity/test/unit/XKeeperMetadata.t.sol b/solidity/test/unit/XKeeperMetadata.t.sol index 76ffad2..f602c1a 100644 --- a/solidity/test/unit/XKeeperMetadata.t.sol +++ b/solidity/test/unit/XKeeperMetadata.t.sol @@ -29,8 +29,12 @@ contract XKeeperMetadataUnitTest is Test { // EOAs address public owner; + // AutomationVault contract + IAutomationVault public automationVault; + function setUp() public virtual { xKeeperMetadata = new XKeeperMetadataForTest(); + automationVault = IAutomationVault(makeAddr('AutomationVault')); owner = makeAddr('Owner'); } } @@ -61,41 +65,47 @@ contract UnitXKeeperMetadataGetMetadata is XKeeperMetadataUnitTest { } contract UnitXKeeperMetadataSetAutomationVaultMetadata is XKeeperMetadataUnitTest { - IXKeeperMetadata.AutomationVaultMetadata _automationVaultMetadata; + IXKeeperMetadata.AutomationVaultMetadata internal _automationVaultMetadata; - modifier happyPath(IAutomationVault _automationVault) { - vm.mockCall(address(_automationVault), abi.encodeWithSelector(IAutomationVault.owner.selector), abi.encode(owner)); - _automationVaultMetadata = IXKeeperMetadata.AutomationVaultMetadata('name', 'description'); + modifier happyPath(string memory _name, string memory _description) { + vm.mockCall(address(automationVault), abi.encodeWithSelector(IAutomationVault.owner.selector), abi.encode(owner)); + _automationVaultMetadata = IXKeeperMetadata.AutomationVaultMetadata(_name, _description); vm.startPrank(owner); _; } function testRevertOnlyAutomationVaultOwner( - IAutomationVault _automationVault, - address _newOwner - ) public happyPath(_automationVault) { + address _newOwner, + string memory _name, + string memory _description + ) public happyPath(_name, _description) { vm.assume(_newOwner != owner); vm.expectRevert(IXKeeperMetadata.XKeeperMetadata_OnlyAutomationVaultOwner.selector); changePrank(_newOwner); - xKeeperMetadata.setAutomationVaultMetadata(_automationVault, _automationVaultMetadata); + xKeeperMetadata.setAutomationVaultMetadata(automationVault, _automationVaultMetadata); } - function testSetAutomationVaultMetadata(IAutomationVault _automationVault) public happyPath(_automationVault) { - xKeeperMetadata.setAutomationVaultMetadata(_automationVault, _automationVaultMetadata); - (string memory _name, string memory _description) = xKeeperMetadata.automationVaultMetadata(_automationVault); + function testSetAutomationVaultMetadata( + string memory _name, + string memory _description + ) public happyPath(_name, _description) { + xKeeperMetadata.setAutomationVaultMetadata(automationVault, _automationVaultMetadata); assertEq(_name, _automationVaultMetadata.name); assertEq(_description, _automationVaultMetadata.description); } - function testEmitAutomationVaultMetadataSetted(IAutomationVault _automationVault) public happyPath(_automationVault) { + function testEmitAutomationVaultMetadataSetted( + string memory _name, + string memory _description + ) public happyPath(_name, _description) { vm.expectEmit(); emit AutomationVaultMetadataSetted( - _automationVault, _automationVaultMetadata.name, _automationVaultMetadata.description + automationVault, _automationVaultMetadata.name, _automationVaultMetadata.description ); - xKeeperMetadata.setAutomationVaultMetadata(_automationVault, _automationVaultMetadata); + xKeeperMetadata.setAutomationVaultMetadata(automationVault, _automationVaultMetadata); } } diff --git a/solidity/utils/Constants.sol b/solidity/utils/Constants.sol index a66b86e..101b496 100644 --- a/solidity/utils/Constants.sol +++ b/solidity/utils/Constants.sol @@ -9,7 +9,7 @@ import {IKeep3rHelper} from '@interfaces/external/IKeep3rHelper.sol'; import {IERC20} from '@openzeppelin/token/ERC20/utils/SafeERC20.sol'; address constant _ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; -address constant _NULL = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF; +address constant _ALL = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF; address constant _KEEP3R_GOVERNOR = 0x0D5Dc686d0a2ABBfDaFDFb4D0533E886517d4E83; address constant _KP3R_WHALE = 0x2FC52C61fB0C03489649311989CE2689D93dC1a2; address constant _DAI_WHALE = 0xDE228965da0d064b8aE171A02500602e84E8330d;