Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 40 additions & 23 deletions src/implementation/automation/GelatoAutomation.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
145 changes: 96 additions & 49 deletions test/automation/AutomationBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down