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: 63 additions & 0 deletions src/abstract/AbstractPaidAutomation.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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);
}

Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AbstractAutomation.executeDistribution() checks DISTRIBUTION_MANAGER.isDistributionReady() directly, so callers can bypass the paid automation yield/fee gate implemented in this override. AbstractPaidAutomation should override executeDistribution() to gate on isDistributionReady() (or explicitly re-check PAYMENT_PROVIDER.isYieldSufficient(_getAvailableYield())) before calling claimAndDistribute(), otherwise unprofitable distributions can still be forced by any caller.

Suggested change
/// @notice Executes distribution only when the paid automation readiness checks pass
/// @dev Uses the overridden readiness gate so fee/yield validation cannot be bypassed
function executeDistribution() public virtual override {
require(isDistributionReady(), "Distribution not ready");
claimAndDistribute();
}

Copilot uses AI. Check for mistakes.
/// @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
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);
}
Comment on lines +48 to +57
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_deductFee() is currently unused by the base flow (there is no override of executeDistribution() that calls it), so no fee is actually deducted/emitted during real executions. If fee deduction is intended to be part of the framework, integrate _deductFee() into the execution path (e.g., override executeDistribution() to compute the available yield, deduct the fee, and only distribute the remaining amount) or expose a clear hook that concrete paid automations must call.

Copilot uses AI. Check for mistakes.

/// @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);
}
34 changes: 34 additions & 0 deletions src/implementation/automation/FixedFeePayment.sol
Original file line number Diff line number Diff line change
@@ -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 && totalYield - FEE_AMOUNT >= MINIMUM_YIELD;
}
}
51 changes: 51 additions & 0 deletions src/implementation/automation/PercentagePayment.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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
/// @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_DENOMINATOR) * BASIS_POINTS
+ (totalYield % BASIS_POINTS_DENOMINATOR) * BASIS_POINTS / BASIS_POINTS_DENOMINATOR;
}
Comment on lines +26 to +32
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

calculateFee() (and the duplicated calculation in isYieldSufficient()) does totalYield * BASIS_POINTS which can overflow and revert for very large totalYield values, even though the final result would fit after division. Using a mulDiv-style calculation (or dividing first when safe) avoids this edge-case revert and makes fee calculation robust.

Copilot uses AI. Check for mistakes.

/// @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_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;
}
}
37 changes: 37 additions & 0 deletions src/interfaces/IAutomationPayment.sol
Original file line number Diff line number Diff line change
@@ -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);
}
Loading