Skip to content

Commit

Permalink
feat: add gas capped gov chain and voting chain robots
Browse files Browse the repository at this point in the history
  • Loading branch information
brotherlymite committed Apr 10, 2024
1 parent de3198c commit 057ac20
Show file tree
Hide file tree
Showing 13 changed files with 562 additions and 148 deletions.
2 changes: 1 addition & 1 deletion src/contracts/GovernanceChainRobotKeeper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ contract GovernanceChainRobotKeeper is Ownable, IGovernanceChainRobotKeeper {
* @inheritdoc AutomationCompatibleInterface
* @dev run off-chain, checks if proposals should be moved to executed, cancelled state or if voting could be activated
*/
function checkUpkeep(bytes calldata) external view override returns (bool, bytes memory) {
function checkUpkeep(bytes memory) public view virtual override returns (bool, bytes memory) {
ActionWithId[] memory actionsWithIds = new ActionWithId[](MAX_ACTIONS);

uint256 index = IGovernanceCore(GOVERNANCE).getProposalsCount();
Expand Down
7 changes: 2 additions & 5 deletions src/contracts/VotingChainRobotKeeper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,7 @@ contract VotingChainRobotKeeper is Ownable, IVotingChainRobotKeeper {
* @param votingMachine address of the voting machine contract.
* @param rootsConsumer address of the roots consumer contract to registers the roots.
*/
constructor(
address votingMachine,
address rootsConsumer
) {
constructor(address votingMachine, address rootsConsumer) {
VOTING_MACHINE = votingMachine;
ROOTS_CONSUMER = rootsConsumer;
VOTING_STRATEGY = address(IVotingMachineWithProofs(VOTING_MACHINE).VOTING_STRATEGY());
Expand All @@ -69,7 +66,7 @@ contract VotingChainRobotKeeper is Ownable, IVotingChainRobotKeeper {
* @dev run off-chain, checks if payload should be executed, createVote closeAndSendVote needs
* to be called or if roots needs to be submitted.
*/
function checkUpkeep(bytes calldata) external view override returns (bool, bytes memory) {
function checkUpkeep(bytes memory) public view virtual override returns (bool, bytes memory) {
ActionWithId[] memory actionsWithIds = new ActionWithId[](MAX_ACTIONS);

bool canVotingActionBePerformed;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,24 @@ pragma solidity ^0.8.0;

import {ExecutionChainRobotKeeper} from '../ExecutionChainRobotKeeper.sol';
import {AutomationCompatibleInterface} from 'chainlink/src/v0.8/interfaces/automation/AutomationCompatibleInterface.sol';
import {IGasPriceCappedRobot} from '../../interfaces/IGasPriceCappedRobot.sol';
import {AggregatorInterface} from 'aave-address-book/AaveV3.sol';
import {GasCappedRobotBase} from './GasCappedRobotBase.sol';

/**
* @title GasCappedExecutionChainRobotKeeper
* @author BGD Labs
* @notice Contract to perform automation on payloads controller.
* The difference from ExecutionChainRobot Keeper is that automation is only
* performed when network gas price in within the maximum configured range.
* The difference from ExecutionChainRobotKeeper is that automation is only
* performed when the network gas price in within the maximum configured range.
*/
contract GasCappedExecutionChainRobotKeeper is ExecutionChainRobotKeeper, IGasPriceCappedRobot {
/// @inheritdoc IGasPriceCappedRobot
address public immutable GAS_PRICE_ORACLE;

uint256 internal _maxGasPrice;

contract GasCappedExecutionChainRobotKeeper is GasCappedRobotBase, ExecutionChainRobotKeeper {
/**
* @param payloadsController address of the payloads controller contract.
* @param gasPriceOracle address of the gas price oracle contract.
*/
constructor(address payloadsController, address gasPriceOracle) ExecutionChainRobotKeeper(payloadsController) {
GAS_PRICE_ORACLE = gasPriceOracle;
}
constructor(
address payloadsController,
address gasPriceOracle
) ExecutionChainRobotKeeper(payloadsController) GasCappedRobotBase(gasPriceOracle) {}

/**
* @inheritdoc AutomationCompatibleInterface
Expand All @@ -37,22 +32,4 @@ contract GasCappedExecutionChainRobotKeeper is ExecutionChainRobotKeeper, IGasPr

return super.checkUpkeep('');
}

/// @inheritdoc IGasPriceCappedRobot
function setMaxGasPrice(uint256 maxGasPrice) external onlyOwner {
_maxGasPrice = maxGasPrice;
}

/// @inheritdoc IGasPriceCappedRobot
function getMaxGasPrice() external view returns (uint256) {
return _maxGasPrice;
}

/// @inheritdoc IGasPriceCappedRobot
function isGasPriceInRange() public view virtual returns (bool) {
if (uint256(AggregatorInterface(GAS_PRICE_ORACLE).latestAnswer()) > _maxGasPrice) {
return false;
}
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import {GovernanceChainRobotKeeper} from '../GovernanceChainRobotKeeper.sol';
import {AutomationCompatibleInterface} from 'chainlink/src/v0.8/interfaces/automation/AutomationCompatibleInterface.sol';
import {GasCappedRobotBase} from './GasCappedRobotBase.sol';

/**
* @title GasCappedGovernanceChainRobotKeeper
* @author BGD Labs
* @notice Contract to perform automation on governance contract for goveranance v3.
* The difference from GovernanceChainRobotKeeper is that automation is only
* performed when the network gas price in within the maximum configured range.
*/
contract GasCappedGovernanceChainRobotKeeper is GasCappedRobotBase, GovernanceChainRobotKeeper {
/**
* @param governance address of the governance contract.
* @param gasPriceOracle address of the gas price oracle contract.
*/
constructor(
address governance,
address gasPriceOracle
) GovernanceChainRobotKeeper(governance) GasCappedRobotBase(gasPriceOracle) {}

/**
* @inheritdoc AutomationCompatibleInterface
* @dev run off-chain, checks if payload should be executed
* also checks that the gas price of the network in within range to perform actions
*/
function checkUpkeep(bytes memory) public view override returns (bool, bytes memory) {
if (!isGasPriceInRange()) return (false, '');

return super.checkUpkeep('');
}
}
44 changes: 44 additions & 0 deletions src/contracts/gasprice-capped-robots/GasCappedRobotBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import {IGasPriceCappedRobot} from '../../interfaces/IGasPriceCappedRobot.sol';
import {AggregatorInterface} from 'aave-address-book/AaveV3.sol';
import {Ownable} from 'solidity-utils/contracts/oz-common/Ownable.sol';

/**
* @title GasCappedRobotBase
* @author BGD Labs
* @notice Abstract contract to be inherited by robots to limit actions by configured gasPrice.
*/
abstract contract GasCappedRobotBase is Ownable, IGasPriceCappedRobot {
/// @inheritdoc IGasPriceCappedRobot
address public immutable GAS_PRICE_ORACLE;

uint256 internal _maxGasPrice;

/**
* @param gasPriceOracle address of the gas price oracle contract.
*/
constructor(address gasPriceOracle) {
GAS_PRICE_ORACLE = gasPriceOracle;
}

/// @inheritdoc IGasPriceCappedRobot
function setMaxGasPrice(uint256 maxGasPrice) external onlyOwner {
_maxGasPrice = maxGasPrice;
emit MaxGasPriceSet(maxGasPrice);
}

/// @inheritdoc IGasPriceCappedRobot
function getMaxGasPrice() external view returns (uint256) {
return _maxGasPrice;
}

/// @inheritdoc IGasPriceCappedRobot
function isGasPriceInRange() public view virtual returns (bool) {
if (uint256(AggregatorInterface(GAS_PRICE_ORACLE).latestAnswer()) > _maxGasPrice) {
return false;
}
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import {VotingChainRobotKeeper} from '../VotingChainRobotKeeper.sol';
import {AutomationCompatibleInterface} from 'chainlink/src/v0.8/interfaces/automation/AutomationCompatibleInterface.sol';
import {GasCappedRobotBase} from './GasCappedRobotBase.sol';

/**
* @title GasCappedVotingChainRobotKeeper
* @author BGD Labs
* @notice Contract to perform automation on voting machine and data warehouse contract for goveranance v3.
* The difference from VotingChainRobotKeeper is that automation is only
* performed when the network gas price in within the maximum configured range.
*/
contract GasCappedVotingChainRobotKeeper is GasCappedRobotBase, VotingChainRobotKeeper {
/**
* @param votingMachine address of the voting machine contract.
* @param rootsConsumer address of the roots consumer contract to registers the roots.
* @param gasPriceOracle address of the gas price oracle contract.
*/
constructor(
address votingMachine,
address rootsConsumer,
address gasPriceOracle
) VotingChainRobotKeeper(votingMachine, rootsConsumer) GasCappedRobotBase(gasPriceOracle) {}

/**
* @inheritdoc AutomationCompatibleInterface
* @dev run off-chain, checks if payload should be executed
* also checks that the gas price of the network in within range to perform actions
*/
function checkUpkeep(bytes memory) public view override returns (bool, bytes memory) {
if (!isGasPriceInRange()) return (false, '');

return super.checkUpkeep('');
}
}
8 changes: 7 additions & 1 deletion src/interfaces/IGasPriceCappedRobot.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ import {AggregatorInterface} from 'aave-address-book/AaveV3.sol';
/**
* @title IGasPriceCappedRobot
* @author BGD Labs
* @notice Defines the interface for the contract to automate actions for the payloads controller on execution chain.
* @notice Defines the interface for the gas price capped robot.
**/
interface IGasPriceCappedRobot {
/**
* @notice Emitted when maxGasPrice has been set by the owner.
* @param maxGasPrice new maximum gas price of the network set by the owner.
*/
event MaxGasPriceSet(uint256 indexed maxGasPrice);

/**
* @notice method to check if the current gas prices is lesser than the configured maximum gas prices.
* @return bool if the current network gasPrice is in range or not.
Expand Down
5 changes: 4 additions & 1 deletion tests/ExecutionChainRobotKeeper.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,14 @@ contract ExecutionChainRobotKeeperTest is Test {
}
}

function _checkAndPerformUpKeep(ExecutionChainRobotKeeper executionChainRobotKeeper) internal {
function _checkAndPerformUpKeep(
ExecutionChainRobotKeeper executionChainRobotKeeper
) internal returns (bool) {
(bool shouldRunKeeper, bytes memory performData) = executionChainRobotKeeper.checkUpkeep('');
if (shouldRunKeeper) {
executionChainRobotKeeper.performUpkeep(performData);
}
return shouldRunKeeper;
}

function _createPayloadAndQueue() internal virtual returns (uint40) {
Expand Down
99 changes: 92 additions & 7 deletions tests/GasCappedExecutionChainRobotKeeper.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,102 @@ import {MockAggregator} from 'chainlink/src/v0.8/mocks/MockAggregator.sol';
import './ExecutionChainRobotKeeper.t.sol';

contract GasCappedExecutionChainRobotKeeperTest is ExecutionChainRobotKeeperTest {
function setUp() override public {
address public constant GUARDIAN = address(1);
MockAggregator public chainLinkFastGasFeed;

event MaxGasPriceSet(uint256 indexed maxGasPrice);

function setUp() public override {
vm.createSelectFork('mainnet', 19609260); // Apr-8-2024

proxyFactory = TransparentProxyFactory(MiscEthereum.TRANSPARENT_PROXY_FACTORY);
shortExecutor = Executor(payable(GovernanceV3Ethereum.EXECUTOR_LVL_1));

executor.executorConfig.executor = address(shortExecutor);

payloadsController = PayloadsControllerMock(payable(address(GovernanceV3Ethereum.PAYLOADS_CONTROLLER)));
payloadsController = PayloadsControllerMock(
payable(address(GovernanceV3Ethereum.PAYLOADS_CONTROLLER))
);

chainLinkFastGasFeed = MockAggregator(0x169E633A2D1E6c10dD91238Ba11c4A708dfEF37C);

MockAggregator chainLinkFastGasFeed = MockAggregator(0x169E633A2D1E6c10dD91238Ba11c4A708dfEF37C);
robotKeeper = new GasCappedExecutionChainRobotKeeper(address(payloadsController), address(chainLinkFastGasFeed));
vm.startPrank(GUARDIAN);
robotKeeper = new GasCappedExecutionChainRobotKeeper(
address(payloadsController),
address(chainLinkFastGasFeed)
);

GasCappedExecutionChainRobotKeeper(address(robotKeeper)).setMaxGasPrice(uint256(chainLinkFastGasFeed.latestAnswer()));
GasCappedExecutionChainRobotKeeper(address(robotKeeper)).setMaxGasPrice(
uint256(chainLinkFastGasFeed.latestAnswer())
);
vm.stopPrank();

// to make sure all the current queued payloads expire
vm.warp(block.timestamp + 100 days);
}

function test_setMaxGasPrice(uint256 newMaxGasPrice) public {
vm.expectEmit();
emit MaxGasPriceSet(newMaxGasPrice);

vm.startPrank(GUARDIAN);
GasCappedExecutionChainRobotKeeper(address(robotKeeper)).setMaxGasPrice(newMaxGasPrice);
vm.stopPrank();

assertEq(
GasCappedExecutionChainRobotKeeper(address(robotKeeper)).getMaxGasPrice(),
newMaxGasPrice
);

vm.expectRevert('Ownable: caller is not the owner');
vm.startPrank(address(5));
GasCappedExecutionChainRobotKeeper(address(robotKeeper)).setMaxGasPrice(newMaxGasPrice);
vm.stopPrank();
}

function test_isGasPriceInRange() public {
assertEq(GasCappedExecutionChainRobotKeeper(address(robotKeeper)).isGasPriceInRange(), true);

vm.startPrank(GUARDIAN);
GasCappedExecutionChainRobotKeeper(address(robotKeeper)).setMaxGasPrice(
uint256(chainLinkFastGasFeed.latestAnswer()) - 1
);
vm.stopPrank();

assertEq(GasCappedExecutionChainRobotKeeper(address(robotKeeper)).isGasPriceInRange(), false);
}

function test_robotExecutionOnlyWhenGasPriceInRange() public {
vm.startPrank(GUARDIAN);
GasCappedExecutionChainRobotKeeper(address(robotKeeper)).setMaxGasPrice(
uint256(chainLinkFastGasFeed.latestAnswer()) - 1
);
vm.stopPrank();

uint40 payloadId = _createPayloadAndQueue();

IPayloadsControllerCore.Payload memory payload = payloadsController.getPayloadById(payloadId);

uint256 extraTime = 10;
uint256 skipTimeToTimelock = payload.queuedAt +
payloadsController
.getExecutorSettingsByAccessControl(PayloadsControllerUtils.AccessControl.Level_1)
.delay +
extraTime;
vm.warp(skipTimeToTimelock);

assertEq(uint256(payload.state), uint256(IPayloadsControllerCore.PayloadState.Queued));

bool didRobotRun = _checkAndPerformUpKeep(robotKeeper);

assertEq(didRobotRun, false);

assertEq(
uint256(payloadsController.getPayloadById(payloadId).state),
uint256(IPayloadsControllerCore.PayloadState.Queued)
);
}

function _createPayloadAndQueue() internal override returns (uint40) {
PayloadTest payload = new PayloadTest();

Expand All @@ -42,8 +119,16 @@ contract GasCappedExecutionChainRobotKeeperTest is ExecutionChainRobotKeeperTest
actions[0].withDelegateCall = true;
actions[0].accessLevel = PayloadsUtils.AccessControl.Level_1;

uint40 payloadId = GovV3StorageHelpers.injectPayload(vm, IPayloadsController(address(payloadsController)), actions);
GovV3StorageHelpers.readyPayloadId(vm, IPayloadsController(address(payloadsController)), payloadId);
uint40 payloadId = GovV3StorageHelpers.injectPayload(
vm,
IPayloadsController(address(payloadsController)),
actions
);
GovV3StorageHelpers.readyPayloadId(
vm,
IPayloadsController(address(payloadsController)),
payloadId
);

return payloadId;
}
Expand Down
Loading

0 comments on commit 057ac20

Please sign in to comment.