diff --git a/solidity/contracts/periphery/Keep3rSponsor.sol b/solidity/contracts/periphery/Keep3rSponsor.sol index 256838c..a213a3d 100644 --- a/solidity/contracts/periphery/Keep3rSponsor.sol +++ b/solidity/contracts/periphery/Keep3rSponsor.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.19; -import {IKeep3rSponsor, IKeep3rV2} from '../../interfaces/periphery/IKeep3rSponsor.sol'; -import {IAutomationVault, IOpenRelay} from '../../interfaces/relays/IOpenRelay.sol'; import {EnumerableSet} from 'openzeppelin/utils/structs/EnumerableSet.sol'; +import {IKeep3rSponsor, IKeep3rV2, IKeep3rHelper} from '../../interfaces/periphery/IKeep3rSponsor.sol'; +import {IAutomationVault, IOpenRelay} from '../../interfaces/relays/IOpenRelay.sol'; + /** * @title Keep3rSponsor * @notice This contract managed by Keep3r Network will sponsor some execution in determined jobs @@ -16,6 +17,15 @@ contract Keep3rSponsor is IKeep3rSponsor { /// @inheritdoc IKeep3rSponsor IKeep3rV2 public immutable KEEP3R_V2; + /// @inheritdoc IKeep3rSponsor + IKeep3rHelper public immutable KEEP3R_HELPER; + + /// @inheritdoc IKeep3rSponsor + uint32 public immutable BASE = 10_000; + + /// @inheritdoc IKeep3rSponsor + uint256 public bonus = 15_000; + /// @inheritdoc IKeep3rSponsor IOpenRelay public openRelay; @@ -37,12 +47,21 @@ contract Keep3rSponsor is IKeep3rSponsor { * @param _owner The address of the owner * @param _feeRecipient The address of the fee recipient * @param _openRelay The address of the open relay + * @param _keep3rV2 The address of the keep3rV2 + * @param _keep3rHelper The address of the keep3rHelper */ - constructor(address _owner, address _feeRecipient, IOpenRelay _openRelay, IKeep3rV2 _keep3rV2) { - openRelay = _openRelay; + constructor( + address _owner, + address _feeRecipient, + IOpenRelay _openRelay, + IKeep3rV2 _keep3rV2, + IKeep3rHelper _keep3rHelper + ) { owner = _owner; feeRecipient = _feeRecipient; + openRelay = _openRelay; KEEP3R_V2 = _keep3rV2; + KEEP3R_HELPER = _keep3rHelper; } /// @inheritdoc IKeep3rSponsor @@ -75,6 +94,12 @@ contract Keep3rSponsor is IKeep3rSponsor { emit OpenRelaySetted(_openRelay); } + /// @inheritdoc IKeep3rSponsor + function setBonus(uint256 _bonus) external onlyOwner { + bonus = _bonus; + emit BonusSetted(_bonus); + } + /// @inheritdoc IKeep3rSponsor function addSponsoredJobs(address[] calldata _jobs) public onlyOwner { for (uint256 _i; _i < _jobs.length;) { @@ -100,6 +125,13 @@ contract Keep3rSponsor is IKeep3rSponsor { } function exec(IAutomationVault _automationVault, IAutomationVault.ExecData[] calldata _execData) external { + if (_execData.length == 0) revert Keep3rSponsor_NoJobs(); + + // The first call to `isKeeper` ensures the caller is a valid keeper + bool _isKeeper = KEEP3R_V2.isKeeper(msg.sender); + if (!_isKeeper) revert Keep3rSponsor_NotKeeper(); + uint256 _initialGas = gasleft(); + for (uint256 _i; _i < _execData.length;) { if (!_sponsoredJobs.contains(_execData[_i].job)) revert Keep3rSponsor_JobNotSponsored(); @@ -108,13 +140,12 @@ contract Keep3rSponsor is IKeep3rSponsor { } } - // The first call to `isKeeper` ensures the caller is a valid keeper - bool _isKeeper = KEEP3R_V2.isKeeper(msg.sender); - if (!_isKeeper) revert Keep3rSponsor_NotKeeper(); - openRelay.exec(_automationVault, _execData, feeRecipient); - KEEP3R_V2.worked(msg.sender); + uint256 _gasAfterWork = gasleft(); + uint256 _reward = IKeep3rHelper(KEEP3R_HELPER).getRewardAmountFor(msg.sender, _initialGas - _gasAfterWork); + _reward = (_reward * bonus) / BASE; + KEEP3R_V2.bondedPayment(msg.sender, _reward); } /** diff --git a/solidity/interfaces/periphery/IKeep3rSponsor.sol b/solidity/interfaces/periphery/IKeep3rSponsor.sol index 8725bcf..2a34e3c 100644 --- a/solidity/interfaces/periphery/IKeep3rSponsor.sol +++ b/solidity/interfaces/periphery/IKeep3rSponsor.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.19; import {IAutomationVault, IOpenRelay} from '../../interfaces/relays/IOpenRelay.sol'; import {IKeep3rV2} from '../../interfaces/external/IKeep3rV2.sol'; +import {IKeep3rHelper} from '../../interfaces/external/IKeep3rHelper.sol'; interface IKeep3rSponsor { /*/////////////////////////////////////////////////////////////// @@ -39,6 +40,12 @@ interface IKeep3rSponsor { */ event OpenRelaySetted(IOpenRelay indexed _openRelay); + /** + * @notice Emitted when the bonus is setted + * @param _bonus The sponsored bonus + */ + event BonusSetted(uint256 indexed _bonus); + /** * @notice Emitted when a sponsored job is approved * @param _job The address of the sponsored job @@ -75,6 +82,11 @@ interface IKeep3rSponsor { */ error Keep3rSponsor_NotKeeper(); + /** + * @notice Thrown when the exec data is empty + */ + error Keep3rSponsor_NoJobs(); + /*/////////////////////////////////////////////////////////////// VIEW FUNCTIONS //////////////////////////////////////////////////////////////*/ @@ -85,6 +97,25 @@ interface IKeep3rSponsor { */ function KEEP3R_V2() external view returns (IKeep3rV2 _keep3rV2); + /** + * @notice Returns the keep3r helper contract + * @return _keep3rHelper The address of the keep3r helper contract + */ + function KEEP3R_HELPER() external view returns (IKeep3rHelper _keep3rHelper); + + /** + * @notice Returns the base + * @return _base The base + */ + function BASE() external view returns (uint32 _base); + + /** + * @notice Returns the bonus + * @dev The bonus is in base 10_000 + * @return _bonus The bonus + */ + function bonus() external view returns (uint256 _bonus); + /** * @notice Returns the open relay * @return _openRelay The address of the open relay @@ -143,6 +174,12 @@ interface IKeep3rSponsor { */ function setOpenRelay(IOpenRelay _openRelay) external; + /** + * @notice Sets the bonus + * @param _bonus The bonus + */ + function setBonus(uint256 _bonus) external; + /** * @notice Adds a job to the sponsored list * @param _jobs List of jobs to add diff --git a/solidity/test/unit/Keep3rSponsor.t.sol b/solidity/test/unit/Keep3rSponsor.t.sol index da26176..3aa1e6e 100644 --- a/solidity/test/unit/Keep3rSponsor.t.sol +++ b/solidity/test/unit/Keep3rSponsor.t.sol @@ -8,9 +8,10 @@ import { IKeep3rSponsor, IOpenRelay, IAutomationVault, + IKeep3rV2, + IKeep3rHelper, EnumerableSet } from '../../contracts/periphery/Keep3rSponsor.sol'; -import {IKeep3rV2} from '../../interfaces/external/IKeep3rV2.sol'; contract Keep3rSponsorForTest is Keep3rSponsor { using EnumerableSet for EnumerableSet.AddressSet; @@ -19,8 +20,9 @@ contract Keep3rSponsorForTest is Keep3rSponsor { address _owner, address _feeRecipient, IOpenRelay _openRelay, - IKeep3rV2 _keep3rV2 - ) Keep3rSponsor(_owner, _feeRecipient, _openRelay, _keep3rV2) {} + IKeep3rV2 _keep3rV2, + IKeep3rHelper _keep3rHelper + ) Keep3rSponsor(_owner, _feeRecipient, _openRelay, _keep3rV2, _keep3rHelper) {} function addSponsorJobsForTest(address[] memory _jobs) external { for (uint256 _i; _i < _jobs.length;) { @@ -49,6 +51,7 @@ contract Keep3rSponsorUnitTest is Test { event OpenRelaySetted(IOpenRelay indexed _openRelay); event ApproveSponsoredJob(address indexed _job); event DeleteSponsoredJob(address indexed _job); + event BonusSetted(uint256 indexed _bonus); // Keep3rSponsor contract Keep3rSponsorForTest public keep3rSponsor; @@ -56,6 +59,7 @@ contract Keep3rSponsorUnitTest is Test { // External contracts IOpenRelay public openRelay; IKeep3rV2 public keep3rV2; + IKeep3rHelper public keep3rHelper; /// EOAs address public owner; @@ -69,8 +73,9 @@ contract Keep3rSponsorUnitTest is Test { openRelay = IOpenRelay(makeAddr('OpenRelay')); keep3rV2 = IKeep3rV2(makeAddr('Keep3rV2')); + keep3rHelper = IKeep3rHelper(makeAddr('Keep3rHelper')); - keep3rSponsor = new Keep3rSponsorForTest(owner, feeRecipient, openRelay, keep3rV2); + keep3rSponsor = new Keep3rSponsorForTest(owner, feeRecipient, openRelay, keep3rV2, keep3rHelper); } /** @@ -88,6 +93,7 @@ contract UnitKeep3rSponsorConstructor is Keep3rSponsorUnitTest { assertEq(keep3rSponsor.feeRecipient(), feeRecipient); assertEq(address(keep3rSponsor.openRelay()), address(openRelay)); assertEq(address(keep3rSponsor.KEEP3R_V2()), address(keep3rV2)); + assertEq(address(keep3rSponsor.KEEP3R_HELPER()), address(keep3rHelper)); } } @@ -233,6 +239,27 @@ contract UnitKeep3rSponsorSetOpenRelay is Keep3rSponsorUnitTest { } } +contract UnitKeep3rSponsorSetBonus is Keep3rSponsorUnitTest { + function setUp() public override { + Keep3rSponsorUnitTest.setUp(); + + vm.startPrank(owner); + } + + function testSetBonus(uint256 _bonus) public { + keep3rSponsor.setBonus(_bonus); + + assertEq(keep3rSponsor.bonus(), _bonus); + } + + function testEmitBonusSetted(uint256 _bonus) public { + vm.expectEmit(); + emit BonusSetted(_bonus); + + keep3rSponsor.setBonus(_bonus); + } +} + contract UnitKeep3rSponsorAddSponsoredJobs is Keep3rSponsorUnitTest { using EnumerableSet for EnumerableSet.AddressSet; @@ -355,18 +382,28 @@ contract UnitKeep3rSponsorExec is Keep3rSponsorUnitTest { abi.encodeWithSelector(IOpenRelay.exec.selector, _automationVault, _execData, feeRecipient), abi.encode(true) ); - vm.mockCall(address(keep3rV2), abi.encodeWithSelector(IKeep3rV2.worked.selector, address(this)), abi.encode(true)); + _; } + function testRevertIfNoJobs( + IAutomationVault _automationVault, + IAutomationVault.ExecData[] memory _execData + ) public happyPath(_automationVault, _execData) { + vm.expectRevert(abi.encodeWithSelector(IKeep3rSponsor.Keep3rSponsor_NoJobs.selector)); + + keep3rSponsor.exec(_automationVault, new IAutomationVault.ExecData[](0)); + } + function testRevertIfJobIsNotSponsored( IAutomationVault _automationVault, IAutomationVault.ExecData[] memory _execData ) public { vm.assume(_execData.length > 0 && _execData.length < 5); - vm.assume(_automationVault != IAutomationVault(address(0))); - vm.expectRevert(abi.encodeWithSelector(IKeep3rSponsor.Keep3rSponsor_JobNotSponsored.selector)); + vm.mockCall(address(keep3rV2), abi.encodeWithSelector(IKeep3rV2.isKeeper.selector, address(this)), abi.encode(true)); + + _execData[0].job = makeAddr('JobNotSponsored'); keep3rSponsor.exec(_automationVault, _execData); } @@ -386,12 +423,49 @@ contract UnitKeep3rSponsorExec is Keep3rSponsorUnitTest { function testExecOpenRelay( IAutomationVault _automationVault, - IAutomationVault.ExecData[] memory _execData + IAutomationVault.ExecData[] memory _execData, + uint128 _reward ) public happyPath(_automationVault, _execData) { + vm.assume(_reward > 0); vm.expectCall( address(openRelay), abi.encodeWithSelector(IOpenRelay.exec.selector, _automationVault, _execData, feeRecipient) ); - vm.expectCall(address(keep3rV2), abi.encodeWithSelector(IKeep3rV2.worked.selector, address(this))); + + vm.mockCall( + address(keep3rHelper), abi.encodeWithSelector(IKeep3rHelper.getRewardAmountFor.selector), abi.encode(_reward) + ); + + vm.mockCall( + address(keep3rV2), + abi.encodeWithSelector(IKeep3rV2.bondedPayment.selector, address(this), _reward), + abi.encode(true) + ); + + keep3rSponsor.exec(_automationVault, _execData); + } + + function testExecRewardAmount( + IAutomationVault _automationVault, + IAutomationVault.ExecData[] memory _execData, + uint128 _reward + ) public happyPath(_automationVault, _execData) { + vm.assume(_reward > 0); + + vm.mockCall( + address(keep3rHelper), abi.encodeWithSelector(IKeep3rHelper.getRewardAmountFor.selector), abi.encode(_reward) + ); + + uint256 rewardWithBonus = (_reward * keep3rSponsor.bonus()) / keep3rSponsor.BASE(); + + vm.mockCall( + address(keep3rV2), + abi.encodeWithSelector(IKeep3rV2.bondedPayment.selector, address(this), rewardWithBonus), + abi.encode(true) + ); + + vm.expectCall( + address(keep3rV2), abi.encodeWithSelector(IKeep3rV2.bondedPayment.selector, address(this), rewardWithBonus) + ); keep3rSponsor.exec(_automationVault, _execData); }