Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 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
53700aa
Replace single DYNAMIC_FEE_GAS_BUFFER with measured Bean/LP gas overh…
pocikerim Feb 5, 2026
c8a44d8
Update gas overhead constants with fork measurements and reorganize b…
pocikerim Feb 7, 2026
e8ba9d8
Remove stale overflow validation comment in _addDynamicFee
pocikerim Feb 7, 2026
fcb54ed
Move sow execution before _processFeesAndTip so sow gas is captured i…
pocikerim Feb 7, 2026
7491198
Measure oracle gas at runtime in calculateFeeInBeanWithMeasuredOracle…
pocikerim Feb 10, 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
98 changes: 0 additions & 98 deletions contracts/ecosystem/BlueprintBase.sol

This file was deleted.

272 changes: 272 additions & 0 deletions contracts/ecosystem/tractor/blueprints/BlueprintBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
// SPDX-License-Identifier: MIT
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 overhead for withdrawal + tip when source is Bean deposits.
* @dev Covers withdrawBeansFromSources (Bean path) + tip.
* Oracle gas is measured at runtime by GasCostCalculator.calculateFeeInBeanWithMeasuredOracle.
* Raw measurement ~650K gas, measured via Base mainnet fork test (see GasMeasurementFork.t.sol).
*/
uint256 public constant WITHDRAWAL_TIP_GAS_BEAN = 1_000_000;

/**
* @notice Gas overhead for withdrawal + tip when source is LP deposits.
* @dev Covers withdrawBeansFromSources (LP path) + tip.
* Oracle gas is measured at runtime by GasCostCalculator.calculateFeeInBeanWithMeasuredOracle.
* Raw measurement ~1.50M gas, measured via Base mainnet fork test (seeGasMeasurementFork.t.sol).
*/
uint256 public constant WITHDRAWAL_TIP_GAS_LP = 2_200_000;
/**
* @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 startGas The gasleft() value captured at the beginning of the blueprint function
* @param remainingGasOverhead Estimated gas for withdrawal + tip (from constants)
* @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 startGas;
uint256 remainingGasOverhead;
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;
}

/**
* Mapping to track the last executed season for each order hash
* If a Blueprint needs to track more state about orders, an additional
* mapping(orderHash => state) can be added to the contract inheriting from BlueprintBase.
*/
mapping(bytes32 orderHash => uint32 lastExecutedSeason) public orderLastExecutedSeason;

// Contracts
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 _gasCostCalculator,
address _siloHelpers
) PerFunctionPausable(_owner) {
beanstalk = IBeanstalk(_beanstalk);
beanToken = beanstalk.getBeanToken();
tractorHelpers = TractorHelpers(_tractorHelpers);
gasCostCalculator = GasCostCalculator(_gasCostCalculator);
siloHelpers = SiloHelpers(_siloHelpers);
}

/**
* @notice Updates the last executed season for a given tractor order hash
* @param orderHash The hash of the order
* @param season The season number
*/
function _updateLastExecutedSeason(bytes32 orderHash, uint32 season) internal {
orderLastExecutedSeason[orderHash] = season;
}

/**
* @notice Validates shared blueprint execution conditions
* @param orderHash The hash of the blueprint
* @param currentSeason The current season
*/
function _validateBlueprint(bytes32 orderHash, uint32 currentSeason) internal view {
require(orderHash != bytes32(0), "No active blueprint, function must run from Tractor");
require(
orderLastExecutedSeason[orderHash] < currentSeason,
"Blueprint already executed this season"
);
// add any additional shared validation for blueprints here
}

/**
* @notice Validates operator parameters
* @param opParams The operator parameters to validate
*/
function _validateOperatorParams(OperatorParams calldata opParams) internal view {
require(
tractorHelpers.isOperatorWhitelisted(opParams.whitelistedOperators),
"Operator not whitelisted"
);
// add any additional shared validation for operators here
}

/**
* @notice Validates source token indices
* @param sourceTokenIndices Array of source token indices
*/
function _validateSourceTokens(uint8[] calldata sourceTokenIndices) internal pure {
require(sourceTokenIndices.length > 0, "Must provide at least one source token");
}

/**
* @notice Resolves tip address, defaulting to operator if not provided
* @param providedTipAddress The provided tip address
* @return The resolved tip address
*/
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.calculateFeeInBeanWithMeasuredOracle(
feeParams.startGas,
feeParams.remainingGasOverhead,
feeParams.feeMarginBps
);

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) {
int256 feeAsInt = int256(dynamicFee);

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

newTip = currentTip + feeAsInt;
}

/**
* @notice Returns the remaining gas overhead constant based on whether source tokens are Bean or LP.
* @param sourceTokenIndices Array of source token indices from the blueprint params.
* @return Gas overhead for withdrawal + tip (oracle gas is measured at runtime).
* @dev Bean token is always index 0 in the whitelist (see SiloHelpers.getTokenIndex).
* For strategies (LOWEST_PRICE, LOWEST_SEED) or any LP index, we use the LP
* constant as the worst case since the resolved token is unknown at compile time.
*/
function _getRemainingGasOverhead(
uint8[] memory sourceTokenIndices
) internal pure returns (uint256) {
if (sourceTokenIndices.length == 1 && sourceTokenIndices[0] == 0) {
return WITHDRAWAL_TIP_GAS_BEAN;
}
return WITHDRAWAL_TIP_GAS_LP;
}

/**
* @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 dynamicFee = _payDynamicFee(
DynamicFeeParams({
account: tipParams.account,
sourceTokenIndices: tipParams.sourceTokenIndices,
startGas: tipParams.startGas,
remainingGasOverhead: _getRemainingGasOverhead(tipParams.sourceTokenIndices),
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
);
}
}
Loading