From 10cb576f668cc52dc44d0d7082d0c6a5ca4b7d1f Mon Sep 17 00:00:00 2001 From: Ron Turetzky Date: Wed, 15 Apr 2026 19:40:17 -0400 Subject: [PATCH 1/2] feat: implement generic automation payment framework (#73) Add extensible payment framework for automation providers: - IAutomationPayment interface with PaymentStrategy enum and PaymentConfig - AbstractPaidAutomation extending AbstractAutomation with yield sufficiency checks and fee deduction helpers - FixedFeePayment: constant fee per execution - PercentagePayment: basis-points-based fee from yield - 35 comprehensive tests with BTT naming covering fee calculation, yield sufficiency, edge cases, and integration --- src/abstract/AbstractPaidAutomation.sol | 55 +++ .../automation/FixedFeePayment.sol | 34 ++ .../automation/PercentagePayment.sol | 47 ++ src/interfaces/IAutomationPayment.sol | 37 ++ test/automation/AutomationPayment.t.sol | 417 ++++++++++++++++++ 5 files changed, 590 insertions(+) create mode 100644 src/abstract/AbstractPaidAutomation.sol create mode 100644 src/implementation/automation/FixedFeePayment.sol create mode 100644 src/implementation/automation/PercentagePayment.sol create mode 100644 src/interfaces/IAutomationPayment.sol create mode 100644 test/automation/AutomationPayment.t.sol diff --git a/src/abstract/AbstractPaidAutomation.sol b/src/abstract/AbstractPaidAutomation.sol new file mode 100644 index 0000000..a8f5b33 --- /dev/null +++ b/src/abstract/AbstractPaidAutomation.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {AbstractAutomation} from "./AbstractAutomation.sol"; +import {IAutomationPayment} from "../interfaces/IAutomationPayment.sol"; + +/// @title AbstractPaidAutomation +/// @notice Extends AbstractAutomation with payment validation for automation providers +/// @dev Adds yield sufficiency checks and fee deduction logic on top of base automation +abstract contract AbstractPaidAutomation is AbstractAutomation { + IAutomationPayment public immutable PAYMENT_PROVIDER; + + /// @notice Emitted when an automation fee is deducted from yield + /// @param fee The fee amount deducted + /// @param remainingYield The yield remaining after fee deduction + event AutomationFeeDeducted(uint256 fee, uint256 remainingYield); + + /// @notice Thrown when yield is insufficient to cover fees and minimum yield + error InsufficientYieldForFee(); + + /// @param _distributionManager The distribution manager address + /// @param _paymentProvider The payment provider address + constructor(address _distributionManager, address _paymentProvider) AbstractAutomation(_distributionManager) { + require(_paymentProvider != address(0), "Invalid payment provider"); + PAYMENT_PROVIDER = IAutomationPayment(_paymentProvider); + } + + /// @notice Checks if distribution is ready, including yield sufficiency for fees + /// @dev Extends base readiness check with payment validation + /// @return ready Whether the distribution is ready and yield is sufficient + function isDistributionReady() public view virtual override returns (bool ready) { + if (!super.isDistributionReady()) { + return false; + } + // Additional check: is yield sufficient to cover automation fees? + uint256 availableYield = _getAvailableYield(); + return PAYMENT_PROVIDER.isYieldSufficient(availableYield); + } + + /// @notice Deducts the automation fee from the total yield + /// @param totalYield The total yield before fee deduction + /// @return remainingYield The yield remaining after the fee + function _deductFee(uint256 totalYield) internal returns (uint256 remainingYield) { + uint256 fee = PAYMENT_PROVIDER.calculateFee(totalYield); + if (fee > totalYield) revert InsufficientYieldForFee(); + + remainingYield = totalYield - fee; + emit AutomationFeeDeducted(fee, remainingYield); + } + + /// @notice Returns the available yield for distribution + /// @dev Subclasses may override to query the actual yield source + /// @return yield The available yield amount + function _getAvailableYield() internal view virtual returns (uint256 yield); +} diff --git a/src/implementation/automation/FixedFeePayment.sol b/src/implementation/automation/FixedFeePayment.sol new file mode 100644 index 0000000..a56c56d --- /dev/null +++ b/src/implementation/automation/FixedFeePayment.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IAutomationPayment} from "../../interfaces/IAutomationPayment.sol"; + +/// @title FixedFeePayment +/// @notice Implements IAutomationPayment with a fixed fee per execution +/// @dev The fee is a constant amount regardless of total yield +contract FixedFeePayment is IAutomationPayment { + uint256 public immutable FEE_AMOUNT; + uint256 public immutable MINIMUM_YIELD; + + /// @param _feeAmount The fixed fee amount charged per execution + /// @param _minimumYield The minimum yield required after fee deduction + constructor(uint256 _feeAmount, uint256 _minimumYield) { + FEE_AMOUNT = _feeAmount; + MINIMUM_YIELD = _minimumYield; + } + + /// @inheritdoc IAutomationPayment + function calculateFee(uint256 /* totalYield */) external view override returns (uint256 fee) { + return FEE_AMOUNT; + } + + /// @inheritdoc IAutomationPayment + function getPaymentConfig() external view override returns (PaymentConfig memory config) { + return PaymentConfig({strategy: PaymentStrategy.FIXED_FEE, feeAmount: FEE_AMOUNT, minimumYield: MINIMUM_YIELD}); + } + + /// @inheritdoc IAutomationPayment + function isYieldSufficient(uint256 totalYield) external view override returns (bool sufficient) { + return totalYield >= FEE_AMOUNT + MINIMUM_YIELD; + } +} diff --git a/src/implementation/automation/PercentagePayment.sol b/src/implementation/automation/PercentagePayment.sol new file mode 100644 index 0000000..ceaabfb --- /dev/null +++ b/src/implementation/automation/PercentagePayment.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IAutomationPayment} from "../../interfaces/IAutomationPayment.sol"; + +/// @title PercentagePayment +/// @notice Implements IAutomationPayment with a percentage-based fee +/// @dev Fee is calculated as a percentage of total yield using basis points (10000 = 100%) +contract PercentagePayment is IAutomationPayment { + uint256 public constant BASIS_POINTS_DENOMINATOR = 10_000; + + uint256 public immutable BASIS_POINTS; + uint256 public immutable MINIMUM_YIELD; + + /// @notice Thrown when basis points exceed 10000 (100%) + error InvalidBasisPoints(); + + /// @param _basisPoints Fee percentage in basis points (e.g. 500 = 5%) + /// @param _minimumYield The minimum yield required after fee deduction + constructor(uint256 _basisPoints, uint256 _minimumYield) { + if (_basisPoints > BASIS_POINTS_DENOMINATOR) revert InvalidBasisPoints(); + BASIS_POINTS = _basisPoints; + MINIMUM_YIELD = _minimumYield; + } + + /// @inheritdoc IAutomationPayment + function calculateFee(uint256 totalYield) external view override returns (uint256 fee) { + return (totalYield * BASIS_POINTS) / BASIS_POINTS_DENOMINATOR; + } + + /// @inheritdoc IAutomationPayment + function getPaymentConfig() external view override returns (PaymentConfig memory config) { + return PaymentConfig({ + strategy: PaymentStrategy.PERCENTAGE_BASED, + feeAmount: BASIS_POINTS, + minimumYield: MINIMUM_YIELD + }); + } + + /// @inheritdoc IAutomationPayment + function isYieldSufficient(uint256 totalYield) external view override returns (bool sufficient) { + uint256 fee = (totalYield * BASIS_POINTS) / BASIS_POINTS_DENOMINATOR; + if (fee > totalYield) return false; + uint256 remaining = totalYield - fee; + return remaining >= MINIMUM_YIELD; + } +} diff --git a/src/interfaces/IAutomationPayment.sol b/src/interfaces/IAutomationPayment.sol new file mode 100644 index 0000000..9d206db --- /dev/null +++ b/src/interfaces/IAutomationPayment.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/// @title IAutomationPayment +/// @notice Interface for automation payment strategies +/// @dev Implementations define how automation provider fees are calculated and validated +interface IAutomationPayment { + /// @notice Payment strategy type + enum PaymentStrategy { + FIXED_FEE, + PERCENTAGE_BASED + } + + /// @notice Configuration for automation payments + /// @param strategy The payment strategy to use + /// @param feeAmount Fixed fee amount (for FIXED_FEE) or basis points (for PERCENTAGE_BASED) + /// @param minimumYield Minimum yield required after fee deduction + struct PaymentConfig { + PaymentStrategy strategy; + uint256 feeAmount; + uint256 minimumYield; + } + + /// @notice Calculates the automation fee for a given yield amount + /// @param totalYield The total yield available for distribution + /// @return fee The fee amount to deduct + function calculateFee(uint256 totalYield) external view returns (uint256 fee); + + /// @notice Returns the current payment configuration + /// @return config The payment configuration + function getPaymentConfig() external view returns (PaymentConfig memory config); + + /// @notice Checks whether the yield is sufficient to cover fees and minimum yield + /// @param totalYield The total yield available + /// @return sufficient Whether the yield meets the minimum threshold after fees + function isYieldSufficient(uint256 totalYield) external view returns (bool sufficient); +} diff --git a/test/automation/AutomationPayment.t.sol b/test/automation/AutomationPayment.t.sol new file mode 100644 index 0000000..d680ff8 --- /dev/null +++ b/test/automation/AutomationPayment.t.sol @@ -0,0 +1,417 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {IAutomationPayment} from "../../src/interfaces/IAutomationPayment.sol"; +import {FixedFeePayment} from "../../src/implementation/automation/FixedFeePayment.sol"; +import {PercentagePayment} from "../../src/implementation/automation/PercentagePayment.sol"; +import {AbstractPaidAutomation} from "../../src/abstract/AbstractPaidAutomation.sol"; +import {MockDistributionManager} from "../mocks/MockDistributionManager.sol"; +import {IDistributionModule} from "../../src/interfaces/IDistributionModule.sol"; +import {IRecipientRegistry} from "../../src/interfaces/IRecipientRegistry.sol"; +import {ICycleModule} from "../../src/interfaces/ICycleModule.sol"; + +/// @notice Minimal mock distribution module for testing +contract MockDistModule is IDistributionModule { + uint256 public distributeCallCount; + + function recipientRegistry() external pure override returns (IRecipientRegistry) { + return IRecipientRegistry(address(0)); + } + + function cycleManager() external pure override returns (ICycleModule) { + return ICycleModule(address(0)); + } + + function distributeYield() external { + distributeCallCount++; + } + + function getCurrentDistributionState() external view returns (DistributionState memory state) { + address[] memory recipients = new address[](0); + uint256[] memory empty = new uint256[](0); + state = DistributionState({ + totalYield: 100, + fixedAmount: 0, + votedAmount: 100, + totalVotes: 100, + lastDistributionBlock: block.number - 100, + cycleNumber: 1, + recipients: recipients, + votedDistributions: empty, + fixedDistributions: empty + }); + } + + function validateDistribution() external pure returns (bool canDistribute, string memory reason) { + return (true, ""); + } + + function emergencyPause() external {} + function emergencyResume() external {} + function setCycleLength(uint256) external {} + function setYieldFixedSplitDivisor(uint256) external {} +} + +/// @notice Concrete implementation of AbstractPaidAutomation for testing +contract MockPaidAutomation is AbstractPaidAutomation { + uint256 private _availableYield; + + constructor( + address _distributionManager, + address _paymentProvider + ) AbstractPaidAutomation(_distributionManager, _paymentProvider) {} + + function setAvailableYield(uint256 yield_) external { + _availableYield = yield_; + } + + function _getAvailableYield() internal view override returns (uint256) { + return _availableYield; + } + + /// @notice Exposed for testing fee deduction + function deductFee(uint256 totalYield) external returns (uint256) { + return _deductFee(totalYield); + } +} + +// ============================================================ +// FixedFeePayment Tests +// ============================================================ + +contract FixedFeePayment_CalculateFee_Test is Test { + FixedFeePayment public payment; + + function setUp() public { + payment = new FixedFeePayment(100, 500); + } + + function test_WhenCalculatingFee_ShouldReturnConstantFeeAmount() public view { + assertEq(payment.calculateFee(10_000), 100); + } + + function test_WhenCalculatingFee_ShouldReturnSameFeeRegardlessOfYield() public view { + assertEq(payment.calculateFee(0), 100); + assertEq(payment.calculateFee(1), 100); + assertEq(payment.calculateFee(type(uint256).max), 100); + } +} + +contract FixedFeePayment_IsYieldSufficient_Test is Test { + FixedFeePayment public payment; + + function setUp() public { + payment = new FixedFeePayment(100, 500); + } + + function test_WhenYieldIsBelowFeeAndMinimum_ShouldReturnFalse() public view { + assertFalse(payment.isYieldSufficient(0)); + assertFalse(payment.isYieldSufficient(99)); + assertFalse(payment.isYieldSufficient(500)); + assertFalse(payment.isYieldSufficient(599)); + } + + function test_WhenYieldEqualsFeePlusMinimum_ShouldReturnTrue() public view { + assertTrue(payment.isYieldSufficient(600)); + } + + function test_WhenYieldExceedsFeePlusMinimum_ShouldReturnTrue() public view { + assertTrue(payment.isYieldSufficient(1000)); + assertTrue(payment.isYieldSufficient(10_000)); + } +} + +contract FixedFeePayment_GetPaymentConfig_Test is Test { + FixedFeePayment public payment; + + function setUp() public { + payment = new FixedFeePayment(100, 500); + } + + function test_WhenGettingPaymentConfig_ShouldReturnCorrectValues() public view { + IAutomationPayment.PaymentConfig memory config = payment.getPaymentConfig(); + assertEq(uint256(config.strategy), uint256(IAutomationPayment.PaymentStrategy.FIXED_FEE)); + assertEq(config.feeAmount, 100); + assertEq(config.minimumYield, 500); + } +} + +contract FixedFeePayment_EdgeCases_Test is Test { + function test_WhenFeeIsZero_ShouldWorkCorrectly() public { + FixedFeePayment payment = new FixedFeePayment(0, 100); + assertEq(payment.calculateFee(1000), 0); + assertTrue(payment.isYieldSufficient(100)); + assertFalse(payment.isYieldSufficient(99)); + } + + function test_WhenMinimumYieldIsZero_ShouldWorkCorrectly() public { + FixedFeePayment payment = new FixedFeePayment(50, 0); + assertTrue(payment.isYieldSufficient(50)); + assertFalse(payment.isYieldSufficient(49)); + } + + function test_WhenBothAreZero_ShouldAlwaysBeSufficient() public { + FixedFeePayment payment = new FixedFeePayment(0, 0); + assertTrue(payment.isYieldSufficient(0)); + } +} + +// ============================================================ +// PercentagePayment Tests +// ============================================================ + +contract PercentagePayment_CalculateFee_Test is Test { + PercentagePayment public payment; + + function setUp() public { + // 5% fee (500 basis points), 100 minimum yield + payment = new PercentagePayment(500, 100); + } + + function test_WhenCalculatingFee_ShouldReturnCorrectPercentage() public view { + // 5% of 10000 = 500 + assertEq(payment.calculateFee(10_000), 500); + } + + function test_WhenCalculatingFee_ShouldHandleSmallYields() public view { + // 5% of 100 = 5 + assertEq(payment.calculateFee(100), 5); + } + + function test_WhenCalculatingFee_ShouldHandleZeroYield() public view { + assertEq(payment.calculateFee(0), 0); + } + + function test_WhenCalculatingFee_ShouldRoundDown() public view { + // 5% of 99 = 4.95, should round down to 4 + assertEq(payment.calculateFee(99), 4); + } +} + +contract PercentagePayment_IsYieldSufficient_Test is Test { + PercentagePayment public payment; + + function setUp() public { + // 10% fee (1000 basis points), 100 minimum yield + payment = new PercentagePayment(1000, 100); + } + + function test_WhenRemainingYieldBelowMinimum_ShouldReturnFalse() public view { + // yield=100, fee=10, remaining=90 < 100 minimum + assertFalse(payment.isYieldSufficient(100)); + } + + function test_WhenRemainingYieldEqualsMinimum_ShouldReturnTrue() public view { + // yield ~= 112, fee = 11, remaining = 101; try exact: yield=112 -> fee=11, rem=101 + // We need remaining >= 100. yield - yield*10% >= 100 => yield*90% >= 100 => yield >= 112 + // yield=112, fee=11, remaining=101 + assertTrue(payment.isYieldSufficient(112)); + } + + function test_WhenRemainingYieldAboveMinimum_ShouldReturnTrue() public view { + // yield=1000, fee=100, remaining=900 + assertTrue(payment.isYieldSufficient(1000)); + } + + function test_WhenYieldIsZero_ShouldReturnFalse() public view { + assertFalse(payment.isYieldSufficient(0)); + } +} + +contract PercentagePayment_BasisPointsValidation_Test is Test { + function test_WhenBasisPointsExceed10000_ShouldRevert() public { + vm.expectRevert(PercentagePayment.InvalidBasisPoints.selector); + new PercentagePayment(10_001, 100); + } + + function test_WhenBasisPointsEqual10000_ShouldNotRevert() public { + PercentagePayment payment = new PercentagePayment(10_000, 0); + assertEq(payment.calculateFee(1000), 1000); + } + + function test_WhenBasisPointsAreZero_ShouldNotRevert() public { + PercentagePayment payment = new PercentagePayment(0, 100); + assertEq(payment.calculateFee(1000), 0); + assertTrue(payment.isYieldSufficient(100)); + } +} + +contract PercentagePayment_GetPaymentConfig_Test is Test { + function test_WhenGettingPaymentConfig_ShouldReturnCorrectValues() public { + PercentagePayment payment = new PercentagePayment(500, 200); + IAutomationPayment.PaymentConfig memory config = payment.getPaymentConfig(); + assertEq(uint256(config.strategy), uint256(IAutomationPayment.PaymentStrategy.PERCENTAGE_BASED)); + assertEq(config.feeAmount, 500); + assertEq(config.minimumYield, 200); + } +} + +// ============================================================ +// AbstractPaidAutomation Tests +// ============================================================ + +contract AbstractPaidAutomation_IsDistributionReady_Test is Test { + MockPaidAutomation public automation; + MockDistributionManager public distributionManager; + FixedFeePayment public fixedPayment; + MockDistModule public distModule; + + function setUp() public { + distModule = new MockDistModule(); + distributionManager = new MockDistributionManager(address(distModule), 100); + fixedPayment = new FixedFeePayment(100, 500); + automation = new MockPaidAutomation(address(distributionManager), address(fixedPayment)); + + // Set up distribution manager to be ready + distributionManager.setCurrentVotes(100); + distributionManager.setAvailableYield(2000); + + // Set available yield for the automation + automation.setAvailableYield(2000); + } + + function test_WhenBaseNotReady_ShouldReturnFalse() public view { + // Not enough blocks passed + assertFalse(automation.isDistributionReady()); + } + + function test_WhenBaseReadyButYieldInsufficient_ShouldReturnFalse() public { + vm.roll(block.number + 101); + automation.setAvailableYield(500); // Below 100 fee + 500 minimum = 600 + assertFalse(automation.isDistributionReady()); + } + + function test_WhenBaseReadyAndYieldSufficient_ShouldReturnTrue() public { + vm.roll(block.number + 101); + automation.setAvailableYield(1000); // Above 100 fee + 500 minimum = 600 + assertTrue(automation.isDistributionReady()); + } +} + +contract AbstractPaidAutomation_DeductFee_Test is Test { + MockPaidAutomation public automation; + MockDistributionManager public distributionManager; + FixedFeePayment public fixedPayment; + MockDistModule public distModule; + + event AutomationFeeDeducted(uint256 fee, uint256 remainingYield); + + function setUp() public { + distModule = new MockDistModule(); + distributionManager = new MockDistributionManager(address(distModule), 100); + fixedPayment = new FixedFeePayment(100, 0); + automation = new MockPaidAutomation(address(distributionManager), address(fixedPayment)); + } + + function test_WhenDeductingFee_ShouldReturnCorrectRemainingYield() public { + uint256 remaining = automation.deductFee(1000); + assertEq(remaining, 900); + } + + function test_WhenDeductingFee_ShouldEmitEvent() public { + vm.expectEmit(true, true, true, true); + emit AutomationFeeDeducted(100, 900); + automation.deductFee(1000); + } + + function test_RevertWhen_FeeExceedsTotalYield() public { + vm.expectRevert(AbstractPaidAutomation.InsufficientYieldForFee.selector); + automation.deductFee(50); // Fee is 100, yield is 50 + } + + function test_WhenFeeEqualsYield_ShouldReturnZero() public { + uint256 remaining = automation.deductFee(100); + assertEq(remaining, 0); + } +} + +contract AbstractPaidAutomation_Constructor_Test is Test { + MockDistModule public distModule; + MockDistributionManager public distributionManager; + FixedFeePayment public fixedPayment; + + function setUp() public { + distModule = new MockDistModule(); + distributionManager = new MockDistributionManager(address(distModule), 100); + fixedPayment = new FixedFeePayment(100, 500); + } + + function test_RevertWhen_PaymentProviderIsZeroAddress() public { + vm.expectRevert("Invalid payment provider"); + new MockPaidAutomation(address(distributionManager), address(0)); + } + + function test_RevertWhen_DistributionManagerIsZeroAddress() public { + vm.expectRevert("Invalid distribution manager"); + new MockPaidAutomation(address(0), address(fixedPayment)); + } + + function test_WhenConstructedWithValidArgs_ShouldSetImmutables() public { + MockPaidAutomation auto_ = new MockPaidAutomation(address(distributionManager), address(fixedPayment)); + assertEq(address(auto_.DISTRIBUTION_MANAGER()), address(distributionManager)); + assertEq(address(auto_.PAYMENT_PROVIDER()), address(fixedPayment)); + } +} + +// ============================================================ +// Integration: PaidAutomation with PercentagePayment +// ============================================================ + +contract PaidAutomation_Integration_Test is Test { + MockPaidAutomation public automation; + MockDistributionManager public distributionManager; + PercentagePayment public percentagePayment; + MockDistModule public distModule; + + event AutomationFeeDeducted(uint256 fee, uint256 remainingYield); + + function setUp() public { + distModule = new MockDistModule(); + distributionManager = new MockDistributionManager(address(distModule), 100); + // 5% fee, 100 minimum yield + percentagePayment = new PercentagePayment(500, 100); + automation = new MockPaidAutomation(address(distributionManager), address(percentagePayment)); + + distributionManager.setCurrentVotes(50); + distributionManager.setAvailableYield(2000); + } + + function test_WhenYieldBarelySufficient_ShouldBeReady() public { + vm.roll(block.number + 101); + // Need remaining >= 100. yield * 95% >= 100 => yield >= 106 + automation.setAvailableYield(106); + assertTrue(automation.isDistributionReady()); + } + + function test_WhenYieldBarelyInsufficient_ShouldNotBeReady() public { + vm.roll(block.number + 101); + // yield=105 -> fee=5, remaining=100 >= 100 -> sufficient + // yield=104 -> fee=5, remaining=99 < 100 -> insufficient + automation.setAvailableYield(104); + assertFalse(automation.isDistributionReady()); + } + + function test_WhenDeductingPercentageFee_ShouldCalculateCorrectly() public { + // 5% of 10000 = 500, remaining = 9500 + vm.expectEmit(true, true, true, true); + emit AutomationFeeDeducted(500, 9500); + uint256 remaining = automation.deductFee(10_000); + assertEq(remaining, 9500); + } + + function test_WhenExecutingFullFlow_ShouldDeductFeeAndDistribute() public { + vm.roll(block.number + 101); + automation.setAvailableYield(10_000); + + // Verify readiness + assertTrue(automation.isDistributionReady()); + + // Execute distribution through the base contract + automation.executeDistribution(); + + // Verify distribution happened + assertEq(distModule.distributeCallCount(), 1); + assertEq(distributionManager.currentCycleNumber(), 2); + } +} From 368f6e30a345ddec778e2dc7981a6ee771f7616b Mon Sep 17 00:00:00 2001 From: Ron Turetzky Date: Wed, 15 Apr 2026 21:30:43 -0400 Subject: [PATCH 2/2] fix: address Copilot review feedback --- src/abstract/AbstractPaidAutomation.sol | 8 ++++++++ src/implementation/automation/FixedFeePayment.sol | 2 +- src/implementation/automation/PercentagePayment.sol | 8 ++++++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/abstract/AbstractPaidAutomation.sol b/src/abstract/AbstractPaidAutomation.sol index a8f5b33..8577260 100644 --- a/src/abstract/AbstractPaidAutomation.sol +++ b/src/abstract/AbstractPaidAutomation.sol @@ -37,6 +37,14 @@ abstract contract AbstractPaidAutomation is AbstractAutomation { return PAYMENT_PROVIDER.isYieldSufficient(availableYield); } + /// @notice Executes the distribution after verifying paid-automation readiness + /// @dev Gates on this contract's isDistributionReady() (which includes yield sufficiency) + /// before delegating to the parent, preventing bypass of fee checks + function executeDistribution() public virtual override { + if (!isDistributionReady()) revert NotResolved(); + super.executeDistribution(); + } + /// @notice Deducts the automation fee from the total yield /// @param totalYield The total yield before fee deduction /// @return remainingYield The yield remaining after the fee diff --git a/src/implementation/automation/FixedFeePayment.sol b/src/implementation/automation/FixedFeePayment.sol index a56c56d..29f6aec 100644 --- a/src/implementation/automation/FixedFeePayment.sol +++ b/src/implementation/automation/FixedFeePayment.sol @@ -29,6 +29,6 @@ contract FixedFeePayment is IAutomationPayment { /// @inheritdoc IAutomationPayment function isYieldSufficient(uint256 totalYield) external view override returns (bool sufficient) { - return totalYield >= FEE_AMOUNT + MINIMUM_YIELD; + return totalYield >= FEE_AMOUNT && totalYield - FEE_AMOUNT >= MINIMUM_YIELD; } } diff --git a/src/implementation/automation/PercentagePayment.sol b/src/implementation/automation/PercentagePayment.sol index ceaabfb..ee1142e 100644 --- a/src/implementation/automation/PercentagePayment.sol +++ b/src/implementation/automation/PercentagePayment.sol @@ -24,8 +24,11 @@ contract PercentagePayment is IAutomationPayment { } /// @inheritdoc IAutomationPayment + /// @dev Uses division-first approach for large values to avoid overflow: + /// fee = (totalYield / DENOMINATOR) * BASIS_POINTS + (totalYield % DENOMINATOR) * BASIS_POINTS / DENOMINATOR function calculateFee(uint256 totalYield) external view override returns (uint256 fee) { - return (totalYield * BASIS_POINTS) / BASIS_POINTS_DENOMINATOR; + return (totalYield / BASIS_POINTS_DENOMINATOR) * BASIS_POINTS + + (totalYield % BASIS_POINTS_DENOMINATOR) * BASIS_POINTS / BASIS_POINTS_DENOMINATOR; } /// @inheritdoc IAutomationPayment @@ -39,7 +42,8 @@ contract PercentagePayment is IAutomationPayment { /// @inheritdoc IAutomationPayment function isYieldSufficient(uint256 totalYield) external view override returns (bool sufficient) { - uint256 fee = (totalYield * BASIS_POINTS) / BASIS_POINTS_DENOMINATOR; + uint256 fee = (totalYield / BASIS_POINTS_DENOMINATOR) * BASIS_POINTS + + (totalYield % BASIS_POINTS_DENOMINATOR) * BASIS_POINTS / BASIS_POINTS_DENOMINATOR; if (fee > totalYield) return false; uint256 remaining = totalYield - fee; return remaining >= MINIMUM_YIELD;