Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
66cc655
fee module
pocikerim Dec 13, 2025
a6618c0
Refactor dynamic fee system into BlueprintBase with shared _payDynami…
pocikerim Jan 26, 2026
fd9b225
Refactor dynamic fee system to use centralized DYNAMIC_FEE_GAS_BUFFER…
pocikerim Jan 26, 2026
432f11e
Merge fd9b225927e61ff192f66e59f9ceb75ad32780cd into e40de231c2abc4c8b…
pocikerim Jan 26, 2026
02e7871
auto-format: prettier formatting for Solidity files
actions-user Jan 26, 2026
3bae67b
Fix CEI pattern violations and add overflow-safe dynamic fee addition…
pocikerim Jan 26, 2026
2805217
Refactor dynamic fee and tip handling into shared _processFeesAndTip …
pocikerim Jan 26, 2026
28a412b
Merge branch 'frijo/release/PI-15' into feat/dynamic-fee-module
pocikerim Jan 26, 2026
482f7ee
refactors
pocikerim Jan 26, 2026
daba802
use BeanstalkPrice
pocikerim Jan 27, 2026
6896c73
Merge daba802623f8b45a194543ab1d7cfa2cc21d2998 into 991a8aec6a2bc4a2f…
pocikerim Jan 27, 2026
5f023ce
auto-format: prettier formatting for Solidity files
actions-user Jan 27, 2026
ec4a383
Refactor pinto terminology to bean
pocikerim Jan 27, 2026
b0d00b6
Merge branch 'feat/dynamic-fee-module' of https://github.com/pinto-or…
pocikerim Jan 27, 2026
49022d9
Merge b0d00b6b6f281594e859b96cd3ebeb96e6262261 into 991a8aec6a2bc4a2f…
pocikerim Jan 27, 2026
d6332ee
auto-format: prettier formatting for Solidity files
actions-user Jan 27, 2026
5daf6ff
test commments rafactor
pocikerim Jan 27, 2026
1a18f6b
Merge branch 'feat/dynamic-fee-module' of https://github.com/pinto-or…
pocikerim Jan 27, 2026
55888d0
test comments refactor
pocikerim Jan 27, 2026
519c12e
blueprintbase tests
pocikerim Jan 27, 2026
a5c24c0
oracle timeout change
pocikerim Jan 27, 2026
c3c4105
remove safe prefixes
pocikerim Jan 27, 2026
acae728
removed unnecessary tests
pocikerim Jan 27, 2026
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
148 changes: 147 additions & 1 deletion contracts/ecosystem/BlueprintBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,77 @@ pragma solidity ^0.8.20;
import {IBeanstalk} from "contracts/interfaces/IBeanstalk.sol";
import {TractorHelpers} from "contracts/ecosystem/tractor/utils/TractorHelpers.sol";
import {PerFunctionPausable} from "contracts/ecosystem/tractor/utils/PerFunctionPausable.sol";
import {GasCostCalculator} from "contracts/ecosystem/tractor/utils/GasCostCalculator.sol";
import {SiloHelpers} from "contracts/ecosystem/tractor/utils/SiloHelpers.sol";
import {LibSiloHelpers} from "contracts/libraries/Silo/LibSiloHelpers.sol";
import {LibTransfer} from "contracts/libraries/Token/LibTransfer.sol";

/**
* @title BlueprintBase
* @notice Abstract base contract for Tractor blueprints providing shared state and validation functions
*/
abstract contract BlueprintBase is PerFunctionPausable {
/**
* @notice Gas buffer for dynamic fee calculation to account for remaining operations
* @dev This buffer covers the gas cost of fee withdrawal and subsequent tip operations
*/
uint256 public constant DYNAMIC_FEE_GAS_BUFFER = 15000;
/**
* @notice Struct to hold operator parameters
* @param whitelistedOperators Array of whitelisted operator addresses
* @param tipAddress Address to send tip to
* @param operatorTipAmount Amount of tip to pay to operator
* @param useDynamicFee Whether to use dynamic gas-based fee calculation
* @param feeMarginBps Additional margin for dynamic fee in basis points (0 = no margin, 1000 = 10%)
*/
struct OperatorParams {
address[] whitelistedOperators;
address tipAddress;
int256 operatorTipAmount;
bool useDynamicFee;
uint256 feeMarginBps;
}

/**
* @notice Struct to hold dynamic fee parameters
* @param account The account to withdraw fee from
* @param sourceTokenIndices Indices of source tokens to withdraw from
* @param gasUsed Total gas used for fee calculation
* @param feeMarginBps Additional margin in basis points
* @param maxGrownStalkPerBdv Maximum grown stalk per BDV for withdrawal filtering
* @param slippageRatio Slippage ratio for LP token withdrawals
*/
struct DynamicFeeParams {
address account;
uint8[] sourceTokenIndices;
uint256 gasUsed;
uint256 feeMarginBps;
uint256 maxGrownStalkPerBdv;
uint256 slippageRatio;
}

/**
* @notice Struct to hold parameters for tip processing with dynamic fees
* @param account The user account to process tips for
* @param tipAddress Address to send the tip to
* @param sourceTokenIndices Indices of source tokens for fee withdrawal
* @param operatorTipAmount Base tip amount for the operator
* @param useDynamicFee Whether to add dynamic gas-based fee
* @param feeMarginBps Margin in basis points for dynamic fee
* @param maxGrownStalkPerBdv Maximum grown stalk per BDV for fee withdrawal
* @param slippageRatio Slippage ratio for LP token withdrawals
* @param startGas Gas at function start for fee calculation
*/
struct TipParams {
address account;
address tipAddress;
uint8[] sourceTokenIndices;
int256 operatorTipAmount;
bool useDynamicFee;
uint256 feeMarginBps;
uint256 maxGrownStalkPerBdv;
uint256 slippageRatio;
uint256 startGas;
}

/**
Expand All @@ -33,15 +88,21 @@ abstract contract BlueprintBase is PerFunctionPausable {
IBeanstalk public immutable beanstalk;
address public immutable beanToken;
TractorHelpers public immutable tractorHelpers;
GasCostCalculator public immutable gasCostCalculator;
SiloHelpers public immutable siloHelpers;

constructor(
address _beanstalk,
address _owner,
address _tractorHelpers
address _tractorHelpers,
address _gasCostCalculator,
address _siloHelpers
) PerFunctionPausable(_owner) {
beanstalk = IBeanstalk(_beanstalk);
beanToken = beanstalk.getBeanToken();
tractorHelpers = TractorHelpers(_tractorHelpers);
gasCostCalculator = GasCostCalculator(_gasCostCalculator);
siloHelpers = SiloHelpers(_siloHelpers);
}

/**
Expand Down Expand Up @@ -95,4 +156,89 @@ abstract contract BlueprintBase is PerFunctionPausable {
function _resolveTipAddress(address providedTipAddress) internal view returns (address) {
return providedTipAddress == address(0) ? beanstalk.operator() : providedTipAddress;
}

/**
* @notice Calculates and withdraws dynamic fee from user's deposits
* @param feeParams Struct containing all parameters for dynamic fee calculation
* @return fee The calculated fee amount in Bean
*/
function _payDynamicFee(DynamicFeeParams memory feeParams) internal returns (uint256 fee) {
fee = gasCostCalculator.calculateFeeInBean(feeParams.gasUsed, feeParams.feeMarginBps);

// Validate fee doesn't overflow when cast to int256
require(fee <= uint256(type(int256).max), "BlueprintBase: fee overflow");

LibSiloHelpers.FilterParams memory filterParams = LibSiloHelpers.getDefaultFilterParams(
feeParams.maxGrownStalkPerBdv
);
LibSiloHelpers.WithdrawalPlan memory emptyPlan;

siloHelpers.withdrawBeansFromSources(
feeParams.account,
feeParams.sourceTokenIndices,
fee,
filterParams,
feeParams.slippageRatio,
LibTransfer.To.INTERNAL,
emptyPlan
);
}

/**
* @notice Safely adds dynamic fee to existing tip amount with overflow protection
* @param currentTip The current tip amount (can be negative for operator-pays-user)
* @param dynamicFee The dynamic fee to add (always positive)
* @return newTip The new total tip amount after adding dynamic fee
* @dev Reverts if addition would overflow int256
*/
function _addDynamicFee(
int256 currentTip,
uint256 dynamicFee
) internal pure returns (int256 newTip) {
// Fee is already validated to fit in int256 by _payDynamicFee
int256 feeAsInt = int256(dynamicFee);

if (currentTip > 0 && feeAsInt > type(int256).max - currentTip) {
revert("BlueprintBase: tip + fee overflow");
}

newTip = currentTip + feeAsInt;
}

/**
* @notice Handles dynamic fee calculation and tip payment
* @param tipParams Parameters for tip processing
* @dev This is a shared implementation for blueprints with simple tip flows
* (single operatorTipAmount + optional dynamic fee).
* Blueprints with complex tip logic (e.g., multiple accumulated tips,
* special bean handling) should implement their own tip handling.
*/
function _processFeesAndTip(TipParams memory tipParams) internal {
int256 totalTipAmount = tipParams.operatorTipAmount;

if (tipParams.useDynamicFee) {
uint256 gasUsedBeforeFee = tipParams.startGas - gasleft();
uint256 estimatedTotalGas = gasUsedBeforeFee + DYNAMIC_FEE_GAS_BUFFER;
uint256 dynamicFee = _payDynamicFee(
DynamicFeeParams({
account: tipParams.account,
sourceTokenIndices: tipParams.sourceTokenIndices,
gasUsed: estimatedTotalGas,
feeMarginBps: tipParams.feeMarginBps,
maxGrownStalkPerBdv: tipParams.maxGrownStalkPerBdv,
slippageRatio: tipParams.slippageRatio
})
);
totalTipAmount = _addDynamicFee(totalTipAmount, dynamicFee);
}

tractorHelpers.tip(
beanToken,
tipParams.account,
tipParams.tipAddress,
totalTipAmount,
LibTransfer.From.INTERNAL,
LibTransfer.To.INTERNAL
);
}
}
28 changes: 21 additions & 7 deletions contracts/ecosystem/MowPlantHarvestBlueprint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ pragma solidity ^0.8.20;
import {LibTransfer} from "contracts/libraries/Token/LibTransfer.sol";
import {IBeanstalk} from "contracts/interfaces/IBeanstalk.sol";
import {LibSiloHelpers} from "contracts/libraries/Silo/LibSiloHelpers.sol";
import {SiloHelpers} from "contracts/ecosystem/tractor/utils/SiloHelpers.sol";
import {BlueprintBase} from "./BlueprintBase.sol";

/**
Expand Down Expand Up @@ -119,17 +118,13 @@ contract MowPlantHarvestBlueprint is BlueprintBase {
UserFieldHarvestResults[] userFieldHarvestResults;
}

// Silo helpers for withdrawal functionality
SiloHelpers public immutable siloHelpers;

constructor(
address _beanstalk,
address _owner,
address _tractorHelpers,
address _gasCostCalculator,
address _siloHelpers
) BlueprintBase(_beanstalk, _owner, _tractorHelpers) {
siloHelpers = SiloHelpers(_siloHelpers);
}
) BlueprintBase(_beanstalk, _owner, _tractorHelpers, _gasCostCalculator, _siloHelpers) {}

/**
* @notice Main entry point for the mow, plant and harvest blueprint
Expand All @@ -138,6 +133,8 @@ contract MowPlantHarvestBlueprint is BlueprintBase {
function mowPlantHarvestBlueprint(
MowPlantHarvestBlueprintStruct calldata params
) external payable whenFunctionNotPaused {
uint256 startGas = gasleft();

// Initialize local variables
MowPlantHarvestLocalVars memory vars;

Expand Down Expand Up @@ -198,6 +195,23 @@ contract MowPlantHarvestBlueprint is BlueprintBase {
vars.totalBeanTip += params.opParams.harvestTipAmount;
}

// Add dynamic fee if enabled
if (params.opParams.baseOpParams.useDynamicFee) {
uint256 gasUsedBeforeFee = startGas - gasleft();
uint256 estimatedTotalGas = gasUsedBeforeFee + DYNAMIC_FEE_GAS_BUFFER;
uint256 dynamicFee = _payDynamicFee(
DynamicFeeParams({
account: vars.account,
sourceTokenIndices: params.mowPlantHarvestParams.sourceTokenIndices,
gasUsed: estimatedTotalGas,
feeMarginBps: params.opParams.baseOpParams.feeMarginBps,
maxGrownStalkPerBdv: params.mowPlantHarvestParams.maxGrownStalkPerBdv,
slippageRatio: params.mowPlantHarvestParams.slippageRatio
})
);
vars.totalBeanTip = _addDynamicFee(vars.totalBeanTip, dynamicFee);
}

// Handle tip payment
handleBeansAndTip(
vars.account,
Expand Down
33 changes: 17 additions & 16 deletions contracts/ecosystem/tractor/blueprints/ConvertUpBlueprint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {LibSiloHelpers} from "contracts/libraries/Silo/LibSiloHelpers.sol";
import {LibConvertData} from "contracts/libraries/Convert/LibConvertData.sol";
import {ReservesType} from "../../price/WellPrice.sol";
import {Call, IWell, IERC20} from "contracts/interfaces/basin/IWell.sol";
import {SiloHelpers} from "../utils/SiloHelpers.sol";

/**
* @title ConvertUpBlueprint
Expand Down Expand Up @@ -106,7 +105,6 @@ contract ConvertUpBlueprint is BlueprintBase {
}

BeanstalkPrice public immutable beanstalkPrice;
SiloHelpers public immutable siloHelpers;

// Default slippage ratio for conversions (1%)
uint256 internal constant DEFAULT_SLIPPAGE_RATIO = 0.01e18;
Expand All @@ -128,10 +126,10 @@ contract ConvertUpBlueprint is BlueprintBase {
address _beanstalk,
address _owner,
address _tractorHelpers,
address _gasCostCalculator,
address _siloHelpers,
address _beanstalkPrice
) BlueprintBase(_beanstalk, _owner, _tractorHelpers) {
siloHelpers = SiloHelpers(_siloHelpers);
) BlueprintBase(_beanstalk, _owner, _tractorHelpers, _gasCostCalculator, _siloHelpers) {
beanstalkPrice = BeanstalkPrice(_beanstalkPrice);
}

Expand All @@ -142,6 +140,7 @@ contract ConvertUpBlueprint is BlueprintBase {
function convertUpBlueprint(
ConvertUpBlueprintStruct calldata params
) external payable whenFunctionNotPaused {
uint256 startGas = gasleft();
// Initialize local variables
ConvertUpLocalVars memory vars;

Expand Down Expand Up @@ -252,22 +251,24 @@ contract ConvertUpBlueprint is BlueprintBase {
uint256 beansRemaining = vars.beansLeftToConvert - vars.amountBeansConverted;
if (beansRemaining == 0) beansRemaining = type(uint256).max;

// Update the BDV left to convert
updateBeansLeftToConvert(vars.orderHash, beansRemaining);

// Tip the operator
tractorHelpers.tip(
beanToken,
vars.account,
tipAddress,
params.opParams.operatorTipAmount,
LibTransfer.From.INTERNAL,
LibTransfer.To.INTERNAL
);

// Update the last executed timestamp for this blueprint
updateLastExecutedTimestamp(vars.orderHash, block.timestamp);

_processFeesAndTip(
TipParams({
account: vars.account,
tipAddress: tipAddress,
sourceTokenIndices: params.convertUpParams.sourceTokenIndices,
operatorTipAmount: params.opParams.operatorTipAmount,
useDynamicFee: params.opParams.useDynamicFee,
feeMarginBps: params.opParams.feeMarginBps,
maxGrownStalkPerBdv: params.convertUpParams.maxGrownStalkPerBdv,
slippageRatio: slippageRatio,
startGas: startGas
})
);

// Emit completion event
if (beansRemaining == type(uint256).max) {
emit ConvertUpOrderComplete(
Expand Down
6 changes: 4 additions & 2 deletions contracts/ecosystem/tractor/blueprints/SowBlueprint.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {SowBlueprintBase, LibSiloHelpers} from "./SowBlueprintBase.sol";
import {SowBlueprintBase} from "./SowBlueprintBase.sol";
import {LibSiloHelpers} from "contracts/libraries/Silo/LibSiloHelpers.sol";

/**
* @title SowBlueprint
Expand All @@ -13,8 +14,9 @@ contract SowBlueprint is SowBlueprintBase {
address _beanstalk,
address _owner,
address _tractorHelpers,
address _gasCostCalculator,
address _siloHelpers
) SowBlueprintBase(_beanstalk, _owner, _tractorHelpers, _siloHelpers) {}
) SowBlueprintBase(_beanstalk, _owner, _tractorHelpers, _gasCostCalculator, _siloHelpers) {}

/**
* @notice Sows beans using specified source tokens in order of preference
Expand Down
Loading