diff --git a/src/implementation/automation/GelatoAutomation.sol b/src/implementation/automation/GelatoAutomation.sol index c75b3b7..ba0297f 100644 --- a/src/implementation/automation/GelatoAutomation.sol +++ b/src/implementation/automation/GelatoAutomation.sol @@ -1,27 +1,44 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.20; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; -// import "./AutomationBase.sol"; +import {AbstractAutomation} from "../../abstract/AbstractAutomation.sol"; -// /// @title GelatoAutomation -// /// @notice Gelato Network compatible automation implementation -// /// @dev Implements Gelato automation interface for yield distribution -// contract GelatoAutomation is AutomationBase { -// constructor(address _distributionManager) AutomationBase(_distributionManager) {} +/// @title GelatoAutomation +/// @notice Gelato Network compatible automation implementation for yield distribution +/// @dev Implements Gelato's resolver/executor interface using AutomationBase. +/// Gelato executors call checker() to decide whether to execute, and execute() +/// to run the distribution. No authentication is required on execute() because +/// executeDistribution() is safe to call from any address (the DistributionManager +/// enforces its own access controls). If executeDistribution() reverts, Gelato +/// will retry — this is safe because the DistributionManager's cycle tracking +/// prevents double-distribution within a single cycle. +contract GelatoAutomation is AbstractAutomation { + /// @notice Constructs the GelatoAutomation contract + /// @param _distributionManager Address of the DistributionManager to automate + constructor(address _distributionManager) AbstractAutomation(_distributionManager) {} -// /// @notice Gelato-compatible resolver function -// /// @dev Called by Gelato executors to check if work needs to be performed -// /// @return canExec Whether execution can proceed -// /// @return execPayload The calldata to execute -// function checker() external view returns (bool canExec, bytes memory execPayload) { -// canExec = isDistributionReady(); -// execPayload = canExec ? getAutomationData() : new bytes(0); -// } + /// @notice Gelato-compatible resolver function + /// @dev Called by Gelato executors ( Gelato bots ) off-chain to check if + /// performUpkeep should be called. Returns true when a distribution is ready. + /// @return canExec Whether execution can proceed + /// @return execPayload The bytes payload to pass to execute() if canExec is true + function checker() external view returns (bool canExec, bytes memory execPayload) { + canExec = isDistributionReady(); + execPayload = canExec ? getAutomationData() : new bytes(0); + } -// /// @notice Gelato-compatible execution function -// /// @dev Called by Gelato executors when checker returns true -// /// @param execData The data for execution (not used but can be for validation) -// function execute(bytes calldata execData) external { -// executeDistribution(); -// } -// } + /// @notice Gelato-compatible execution function + /// @dev Called by Gelato executors when checker() returned true. The execPayload + /// from checker() is passed as performData by Gelato and is intentionally + /// unused here — the DistributionManager has no per-call parameters. + /// Security: No msg.sender check is needed because executeDistribution() is + /// safe to call from any address; the DistributionManager enforces its own + /// internal invariants and prevents duplicate cycles. + function execute( + bytes calldata /* execData */ + ) + external + { + executeDistribution(); + } +} diff --git a/test/automation/AutomationBase.t.sol b/test/automation/AutomationBase.t.sol index c98f927..b957565 100644 --- a/test/automation/AutomationBase.t.sol +++ b/test/automation/AutomationBase.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {ChainlinkAutomation} from "../../src/implementation/automation/ChainlinkAutomation.sol"; import {AbstractAutomation} from "../../src/abstract/AbstractAutomation.sol"; -// import "../../src/implementation/automation/GelatoAutomation.sol"; +import {GelatoAutomation} from "../../src/implementation/automation/GelatoAutomation.sol"; import {MockDistributionManager} from "../mocks/MockDistributionManager.sol"; import {IDistributionModule} from "../../src/interfaces/IDistributionModule.sol"; @@ -59,12 +59,12 @@ contract MockDistributionModule is IDistributionModule { contract AutomationBaseTest is Test { ChainlinkAutomation public chainlinkAutomation; - // GelatoAutomation public gelatoAutomation; + GelatoAutomation public gelatoAutomation; MockDistributionManager public distributionManager; MockDistributionModule public distributionModule; address public chainlinkKeeper = address(0x1); - // address public gelatoExecutor = address(0x2); + address public gelatoExecutor = address(0x2); event AutomationExecuted(address indexed executor, uint256 blockNumber); event DistributionExecuted(uint256 blockNumber, uint256 yield, uint256 votes); @@ -78,7 +78,7 @@ contract AutomationBaseTest is Test { // Deploy automation implementations chainlinkAutomation = new ChainlinkAutomation(address(distributionManager)); - // gelatoAutomation = new GelatoAutomation(address(distributionManager)); + gelatoAutomation = new GelatoAutomation(address(distributionManager)); // Setup initial state distributionManager.setCurrentVotes(100); @@ -118,39 +118,40 @@ contract AutomationBaseTest is Test { assertEq(distributionManager.currentCycleNumber(), 2); } - // function testGelatoChecker() public { - // // Initially should not be executable (too soon) - // (bool canExec, bytes memory execPayload) = gelatoAutomation.checker(); - // assertFalse(canExec); + function testGelatoChecker() public { + // Initially should not be executable (too soon) + (bool canExec, bytes memory execPayload) = gelatoAutomation.checker(); + assertFalse(canExec); + assertEq(execPayload.length, 0); - // // Advance blocks - // vm.roll(block.number + 101); + // Advance blocks + vm.roll(block.number + 101); - // // Now should be executable - // (canExec, execPayload) = gelatoAutomation.checker(); - // assertTrue(canExec); - // assertGt(execPayload.length, 0); - // } + // Now should be executable + (canExec, execPayload) = gelatoAutomation.checker(); + assertTrue(canExec); + assertGt(execPayload.length, 0); + } - // function testGelatoExecute() public { - // // Advance blocks to make distribution ready - // vm.roll(block.number + 101); + function testGelatoExecute() public { + // Advance blocks to make distribution ready + vm.roll(block.number + 101); - // // Check if executable - // (bool canExec,) = gelatoAutomation.checker(); - // assertTrue(canExec); + // Check if executable + (bool canExec,) = gelatoAutomation.checker(); + assertTrue(canExec); - // // Execute - // vm.expectEmit(true, false, false, true); - // emit AutomationExecuted(gelatoExecutor, block.number); + // Execute + vm.expectEmit(true, false, false, true); + emit AutomationExecuted(gelatoExecutor, block.number); - // vm.prank(gelatoExecutor); - // gelatoAutomation.execute(""); + vm.prank(gelatoExecutor); + gelatoAutomation.execute(""); - // // Verify distribution was called - // assertEq(distributionModule.distributeCallCount(), 1); - // assertEq(distributionManager.currentCycleNumber(), 2); - // } + // Verify distribution was called + assertEq(distributionModule.distributeCallCount(), 1); + assertEq(distributionManager.currentCycleNumber(), 2); + } function testResolveDistributionConditions() public { // Test: Not enough blocks passed @@ -226,25 +227,71 @@ contract AutomationBaseTest is Test { assertEq(endBlock, block.number + 100); } - // function testBothAutomationTypesWork() public { - // // Test Chainlink automation - // vm.roll(block.number + 101); - // distributionManager.setCurrentVotes(100); - // distributionManager.setAvailableYield(2000); - - // vm.prank(chainlinkKeeper); - // chainlinkAutomation.performUpkeep(""); - // assertEq(distributionModule.distributeCallCount(), 1); - - // // // Test Gelato automation - // // vm.roll(block.number + 101); - // // distributionManager.setCurrentVotes(100); - // // distributionManager.setAvailableYield(2000); - - // // vm.prank(gelatoExecutor); - // // gelatoAutomation.execute(""); - // // assertEq(distributionModule.distributeCallCount(), 2); - // } + /// @notice GelatoAutomation.execute() has no auth — anyone can call it safely + /// @dev The DistributionManager enforces its own invariants; no duplicate cycles possible + function testGelatoExecutePermissionless() public { + // Advance blocks to make distribution ready + vm.roll(block.number + 101); + + // A random address (not gelatoExecutor) can also call execute() — no auth check + address randomCaller = address(0xdead); + vm.prank(randomCaller); + gelatoAutomation.execute(""); + + // Distribution still went through + assertEq(distributionModule.distributeCallCount(), 1); + assertEq(distributionManager.currentCycleNumber(), 2); + } + + /// @notice execute() reverts with NotResolved when distribution is not ready + function testGelatoExecuteRevertsWhenNotReady() public { + vm.expectRevert(AbstractAutomation.NotResolved.selector); + gelatoAutomation.execute(""); + } + + /// @notice checker() returns canExec=false when isDistributionReady() is false + function testGelatoCheckerFalseWhenNotReady() public { + // No blocks advanced — cycle not complete yet + (bool canExec, bytes memory execPayload) = gelatoAutomation.checker(); + assertFalse(canExec); + assertEq(execPayload.length, 0); + + // Advance blocks but remove votes + vm.roll(block.number + 101); + distributionManager.setCurrentVotes(0); + (canExec, execPayload) = gelatoAutomation.checker(); + assertFalse(canExec); + assertEq(execPayload.length, 0); + + // Restore votes but drop yield below minimum + distributionManager.setCurrentVotes(100); + distributionManager.setAvailableYield(500); + (canExec, execPayload) = gelatoAutomation.checker(); + assertFalse(canExec); + assertEq(execPayload.length, 0); + + // Restore yield but disable system + distributionManager.setAvailableYield(2000); + distributionManager.setEnabled(false); + (canExec, execPayload) = gelatoAutomation.checker(); + assertFalse(canExec); + assertEq(execPayload.length, 0); + } + + /// @notice checker() returns canExec=true and non-empty execPayload when all conditions met + function testGelatoCheckerTrueWhenReady() public { + // Advance blocks past cycle length + vm.roll(block.number + 101); + + // All conditions already met from setUp: votes=100, yield=2000, enabled=true + (bool canExec, bytes memory execPayload) = gelatoAutomation.checker(); + assertTrue(canExec); + assertGt(execPayload.length, 0); + + // execPayload should encode the executeDistribution selector + bytes4 selector = bytes4(execPayload); + assertEq(selector, gelatoAutomation.executeDistribution.selector); + } function testMinYieldRequired() public { vm.roll(block.number + 101);