From f54bcb936e9ede980cd5075609d70863cddf30b5 Mon Sep 17 00:00:00 2001 From: exTypen <242232957+pocikerim@users.noreply.github.com> Date: Sun, 15 Feb 2026 17:48:02 +0300 Subject: [PATCH 1/4] naming refactor --- ...ueprint.sol => AutomateClaimBlueprint.sol} | 46 ++++++------- ...int.t.sol => AutomateClaimBlueprint.t.sol} | 58 ++++++++--------- test/foundry/utils/TractorTestHelper.sol | 64 +++++++++---------- 3 files changed, 84 insertions(+), 84 deletions(-) rename contracts/ecosystem/{MowPlantHarvestBlueprint.sol => AutomateClaimBlueprint.sol} (93%) rename test/foundry/ecosystem/{MowPlantHarvestBlueprint.t.sol => AutomateClaimBlueprint.t.sol} (90%) diff --git a/contracts/ecosystem/MowPlantHarvestBlueprint.sol b/contracts/ecosystem/AutomateClaimBlueprint.sol similarity index 93% rename from contracts/ecosystem/MowPlantHarvestBlueprint.sol rename to contracts/ecosystem/AutomateClaimBlueprint.sol index 0bf3fcca..a4650f8d 100644 --- a/contracts/ecosystem/MowPlantHarvestBlueprint.sol +++ b/contracts/ecosystem/AutomateClaimBlueprint.sol @@ -8,11 +8,11 @@ import {SiloHelpers} from "contracts/ecosystem/tractor/utils/SiloHelpers.sol"; import {BlueprintBase} from "./BlueprintBase.sol"; /** - * @title MowPlantHarvestBlueprint + * @title AutomateClaimBlueprint * @author DefaultJuice * @notice Contract for mowing, planting and harvesting with Tractor, with a number of conditions */ -contract MowPlantHarvestBlueprint is BlueprintBase { +contract AutomateClaimBlueprint is BlueprintBase { /** * @dev Minutes after sunrise to check if the totalDeltaB is about to be positive for the following season */ @@ -27,12 +27,12 @@ contract MowPlantHarvestBlueprint is BlueprintBase { uint256(keccak256("MowPlantHarvestBlueprint.harvestData")); /** - * @notice Main struct for mow, plant and harvest blueprint - * @param mowPlantHarvestParams Parameters related to mow, plant and harvest + * @notice Main struct for automate claim blueprint + * @param automateClaimParams Parameters related to mow, plant and harvest * @param opParams Parameters related to operators */ - struct MowPlantHarvestBlueprintStruct { - MowPlantHarvestParams mowPlantHarvestParams; + struct AutomateClaimBlueprintStruct { + AutomateClaimParams automateClaimParams; OperatorParamsExtended opParams; } @@ -72,7 +72,7 @@ contract MowPlantHarvestBlueprint is BlueprintBase { * @param slippageRatio The price slippage ratio for lp token withdrawal. * Only applicable for lp token withdrawals. */ - struct MowPlantHarvestParams { + struct AutomateClaimParams { // Mow uint256 minMowAmount; uint256 minTwaDeltaB; @@ -108,7 +108,7 @@ contract MowPlantHarvestBlueprint is BlueprintBase { * @notice Local variables for the mow, plant and harvest function * @dev Used to avoid stack too deep errors */ - struct MowPlantHarvestLocalVars { + struct AutomateClaimLocalVars { address account; int256 totalBeanTip; uint256 totalHarvestedBeans; @@ -132,14 +132,14 @@ contract MowPlantHarvestBlueprint is BlueprintBase { } /** - * @notice Main entry point for the mow, plant and harvest blueprint + * @notice Main entry point for the automate claim blueprint * @param params User-controlled parameters for automating mowing, planting and harvesting */ - function mowPlantHarvestBlueprint( - MowPlantHarvestBlueprintStruct calldata params + function automateClaimBlueprint( + AutomateClaimBlueprintStruct calldata params ) external payable whenFunctionNotPaused { // Initialize local variables - MowPlantHarvestLocalVars memory vars; + AutomateClaimLocalVars memory vars; // Validate vars.account = beanstalk.tractorUser(); @@ -152,7 +152,7 @@ contract MowPlantHarvestBlueprint is BlueprintBase { ); // validate order params and revert early if invalid - _validateSourceTokens(params.mowPlantHarvestParams.sourceTokenIndices); + _validateSourceTokens(params.automateClaimParams.sourceTokenIndices); _validateOperatorParams(params.opParams.baseOpParams); // resolve tip address (defaults to operator if not set) @@ -188,7 +188,7 @@ contract MowPlantHarvestBlueprint is BlueprintBase { // Validate post-harvest: revert if harvested amount is below minimum threshold require( harvestedBeans >= vars.userFieldHarvestResults[i].minHarvestAmount, - "MowPlantHarvestBlueprint: Harvested amount below minimum threshold" + "AutomateClaimBlueprint: Harvested amount below minimum threshold" ); // Accumulate harvested beans @@ -202,13 +202,13 @@ contract MowPlantHarvestBlueprint is BlueprintBase { handleBeansAndTip( vars.account, tipAddress, - params.mowPlantHarvestParams.sourceTokenIndices, + params.automateClaimParams.sourceTokenIndices, vars.totalBeanTip, vars.totalHarvestedBeans, vars.totalPlantedBeans, vars.plantedStem, - params.mowPlantHarvestParams.maxGrownStalkPerBdv, - params.mowPlantHarvestParams.slippageRatio + params.automateClaimParams.maxGrownStalkPerBdv, + params.automateClaimParams.slippageRatio ); } @@ -224,7 +224,7 @@ contract MowPlantHarvestBlueprint is BlueprintBase { function _getAndValidateUserState( address account, uint256 previousSeasonTimestamp, - MowPlantHarvestBlueprintStruct calldata params + AutomateClaimBlueprintStruct calldata params ) internal view @@ -239,20 +239,20 @@ contract MowPlantHarvestBlueprint is BlueprintBase { uint256 totalClaimableStalk, uint256 totalPlantableBeans, UserFieldHarvestResults[] memory userFieldHarvestResults - ) = _getUserState(account, params.mowPlantHarvestParams.fieldHarvestConfigs); + ) = _getUserState(account, params.automateClaimParams.fieldHarvestConfigs); // validate params - only revert if none of the conditions are met shouldMow = _checkMowConditions( - params.mowPlantHarvestParams.minTwaDeltaB, - params.mowPlantHarvestParams.minMowAmount, + params.automateClaimParams.minTwaDeltaB, + params.automateClaimParams.minMowAmount, totalClaimableStalk, previousSeasonTimestamp ); - shouldPlant = totalPlantableBeans >= params.mowPlantHarvestParams.minPlantAmount; + shouldPlant = totalPlantableBeans >= params.automateClaimParams.minPlantAmount; require( shouldMow || shouldPlant || userFieldHarvestResults.length > 0, - "MowPlantHarvestBlueprint: None of the order conditions are met" + "AutomateClaimBlueprint: None of the order conditions are met" ); return (shouldMow, shouldPlant, userFieldHarvestResults); diff --git a/test/foundry/ecosystem/MowPlantHarvestBlueprint.t.sol b/test/foundry/ecosystem/AutomateClaimBlueprint.t.sol similarity index 90% rename from test/foundry/ecosystem/MowPlantHarvestBlueprint.t.sol rename to test/foundry/ecosystem/AutomateClaimBlueprint.t.sol index 2837b582..c447a15d 100644 --- a/test/foundry/ecosystem/MowPlantHarvestBlueprint.t.sol +++ b/test/foundry/ecosystem/AutomateClaimBlueprint.t.sol @@ -11,10 +11,10 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {TractorTestHelper} from "test/foundry/utils/TractorTestHelper.sol"; import {BeanstalkPrice} from "contracts/ecosystem/price/BeanstalkPrice.sol"; import {IBeanstalk} from "contracts/interfaces/IBeanstalk.sol"; -import {MowPlantHarvestBlueprint} from "contracts/ecosystem/MowPlantHarvestBlueprint.sol"; +import {AutomateClaimBlueprint} from "contracts/ecosystem/AutomateClaimBlueprint.sol"; import "forge-std/console.sol"; -contract MowPlantHarvestBlueprintTest is TractorTestHelper { +contract AutomateClaimBlueprintTest is TractorTestHelper { address[] farmers; BeanstalkPrice beanstalkPrice; @@ -62,30 +62,30 @@ contract MowPlantHarvestBlueprintTest is TractorTestHelper { ); vm.label(address(siloHelpers), "SiloHelpers"); - // Deploy MowPlantHarvestBlueprint with TractorHelpers and SiloHelpers addresses - mowPlantHarvestBlueprint = new MowPlantHarvestBlueprint( + // Deploy AutomateClaimBlueprint with TractorHelpers and SiloHelpers addresses + automateClaimBlueprint = new AutomateClaimBlueprint( address(bs), address(this), address(tractorHelpers), address(siloHelpers) ); - vm.label(address(mowPlantHarvestBlueprint), "MowPlantHarvestBlueprint"); + vm.label(address(automateClaimBlueprint), "AutomateClaimBlueprint"); setTractorHelpers(address(tractorHelpers)); - setMowPlantHarvestBlueprint(address(mowPlantHarvestBlueprint)); + setAutomateClaimBlueprint(address(automateClaimBlueprint)); // Advance season to grow stalk advanceSeason(); } /** - * @notice Setup the test state for the MowPlantHarvestBlueprint test + * @notice Setup the test state for the AutomateClaimBlueprint test * @param setupPlant If true, setup the conditions for planting * @param setupHarvest If true, setup the conditions for harvesting * @param abovePeg If true, setup the conditions for above peg * @return TestState The test state */ - function setupMowPlantHarvestBlueprintTest( + function setupAutomateClaimBlueprintTest( bool setupPlant, bool setupHarvest, bool twoFields, @@ -131,10 +131,10 @@ contract MowPlantHarvestBlueprintTest is TractorTestHelper { /////////////////////////// TESTS /////////////////////////// - function test_mowPlantHarvestBlueprint_Mow() public { + function test_automateClaimBlueprint_Mow() public { // Setup test state // setupPlant: false, setupHarvest: false, abovePeg: true - TestState memory state = setupMowPlantHarvestBlueprintTest(false, false, false, true); + TestState memory state = setupAutomateClaimBlueprintTest(false, false, false, true); // Advance season to grow stalk but not enough to plant advanceSeason(); @@ -149,8 +149,8 @@ contract MowPlantHarvestBlueprintTest is TractorTestHelper { // assert totalDeltaB is greater than 0 assertGt(bs.totalDeltaB(), 0, "totalDeltaB should be greater than 0"); - // Setup mowPlantHarvestBlueprint - (IMockFBeanstalk.Requisition memory req, ) = setupMowPlantHarvestBlueprint( + // Setup automateClaimBlueprint + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( state.user, // account SourceMode.PURE_PINTO, // sourceMode for tip 1 * STALK_DECIMALS, // minMowAmount (1 stalk) @@ -170,7 +170,7 @@ contract MowPlantHarvestBlueprintTest is TractorTestHelper { ); // Try to execute before the last minutes of the season, expect revert - vm.expectRevert("MowPlantHarvestBlueprint: None of the order conditions are met"); + vm.expectRevert("AutomateClaimBlueprint: None of the order conditions are met"); executeRequisitionWithDynamicData(state.operator, req, address(bs), dynamicData); // Try to execute after in last minutes of the season @@ -191,16 +191,16 @@ contract MowPlantHarvestBlueprintTest is TractorTestHelper { ); } - function test_mowPlantHarvestBlueprint_plant_revertWhenInsufficientPlantableBeans() public { + function test_automateClaimBlueprint_plant_revertWhenInsufficientPlantableBeans() public { // Setup test state for planting // setupPlant: true, setupHarvest: false, twoFields: true, abovePeg: true - TestState memory state = setupMowPlantHarvestBlueprintTest(true, false, false, true); + TestState memory state = setupAutomateClaimBlueprintTest(true, false, false, true); // assert that the user has earned beans assertGt(bs.balanceOfEarnedBeans(state.user), 0, "user should have earned beans to plant"); // Setup blueprint with minPlantAmount greater than total plantable beans - (IMockFBeanstalk.Requisition memory req, ) = setupMowPlantHarvestBlueprint( + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( state.user, // account SourceMode.PURE_PINTO, // sourceMode for tip 1 * STALK_DECIMALS, // minMowAmount (1 stalk) @@ -220,14 +220,14 @@ contract MowPlantHarvestBlueprintTest is TractorTestHelper { ); // Execute requisition, expect revert - vm.expectRevert("MowPlantHarvestBlueprint: None of the order conditions are met"); + vm.expectRevert("AutomateClaimBlueprint: None of the order conditions are met"); executeRequisitionWithDynamicData(state.operator, req, address(bs), dynamicData); } - function test_mowPlantHarvestBlueprint_plant_success() public { + function test_automateClaimBlueprint_plant_success() public { // Setup test state for planting // setupPlant: true, setupHarvest: false, twoFields: true, abovePeg: true - TestState memory state = setupMowPlantHarvestBlueprintTest(true, false, true, true); + TestState memory state = setupAutomateClaimBlueprintTest(true, false, true, true); // get user state before plant uint256 userTotalStalkBeforePlant = bs.balanceOfStalk(state.user); @@ -239,7 +239,7 @@ contract MowPlantHarvestBlueprintTest is TractorTestHelper { assertGt(bs.balanceOfEarnedBeans(state.user), 0, "user should have earned beans to plant"); // Setup blueprint with valid minPlantAmount - (IMockFBeanstalk.Requisition memory req, ) = setupMowPlantHarvestBlueprint( + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( state.user, // account SourceMode.PURE_PINTO, // sourceMode for tip 1 * STALK_DECIMALS, // minMowAmount (1 stalk) @@ -266,10 +266,10 @@ contract MowPlantHarvestBlueprintTest is TractorTestHelper { assertGt(userTotalBdvAfterPlant, userTotalBdvBeforePlant, "userTotalBdv increase"); } - function test_mowPlantHarvestBlueprint_harvest_partialHarvest() public { + function test_automateClaimBlueprint_harvest_partialHarvest() public { // Setup test state for harvesting // setupPlant: false, setupHarvest: true, twoFields: true, abovePeg: true - TestState memory state = setupMowPlantHarvestBlueprintTest(false, true, true, true); + TestState memory state = setupAutomateClaimBlueprintTest(false, true, true, true); // advance season to print beans advanceSeason(); @@ -287,7 +287,7 @@ contract MowPlantHarvestBlueprintTest is TractorTestHelper { assertEq(userTotalBdvBeforeHarvest, 100000e6, "user should have the initial bdv"); // Setup blueprint for partial harvest - (IMockFBeanstalk.Requisition memory req, ) = setupMowPlantHarvestBlueprint( + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( state.user, // account SourceMode.PURE_PINTO, // sourceMode for tip 1 * STALK_DECIMALS, // minMowAmount (1 stalk) @@ -314,10 +314,10 @@ contract MowPlantHarvestBlueprintTest is TractorTestHelper { assertNoHarvestablePods(state.user, DEFAULT_FIELD_ID); } - function test_mowPlantHarvestBlueprint_harvest_fullHarvest() public { + function test_automateClaimBlueprint_harvest_fullHarvest() public { // Setup test state for harvesting // setupPlant: false, setupHarvest: true, twoFields: false, abovePeg: true - TestState memory state = setupMowPlantHarvestBlueprintTest(false, true, false, true); + TestState memory state = setupAutomateClaimBlueprintTest(false, true, false, true); // add even more liquidity to well to print more beans and clear the podline addLiquidityToWell(BEAN_ETH_WELL, 10000e6, 20 ether); @@ -336,7 +336,7 @@ contract MowPlantHarvestBlueprintTest is TractorTestHelper { ); // Setup blueprint for full harvest - (IMockFBeanstalk.Requisition memory req, ) = setupMowPlantHarvestBlueprint( + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( state.user, // account SourceMode.PURE_PINTO, // sourceMode for tip 1 * STALK_DECIMALS, // minMowAmount (1 stalk) @@ -371,10 +371,10 @@ contract MowPlantHarvestBlueprintTest is TractorTestHelper { assertNoHarvestablePods(state.user, DEFAULT_FIELD_ID); } - function test_mowPlantHarvestBlueprint_harvest_fullHarvest_twoFields() public { + function test_automateClaimBlueprint_harvest_fullHarvest_twoFields() public { // Setup test state for harvesting // setupPlant: false, setupHarvest: true, twoFields: true, abovePeg: true - TestState memory state = setupMowPlantHarvestBlueprintTest(false, true, true, true); + TestState memory state = setupAutomateClaimBlueprintTest(false, true, true, true); // add even more liquidity to well to print more beans and clear the podline at fieldId 0 // note: field id 1 has had its harvestable index incremented already @@ -401,7 +401,7 @@ contract MowPlantHarvestBlueprintTest is TractorTestHelper { ); // Setup blueprint for full harvest - (IMockFBeanstalk.Requisition memory req, ) = setupMowPlantHarvestBlueprint( + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( state.user, // account SourceMode.PURE_PINTO, // sourceMode for tip 1 * STALK_DECIMALS, // minMowAmount (1 stalk) diff --git a/test/foundry/utils/TractorTestHelper.sol b/test/foundry/utils/TractorTestHelper.sol index 3da4cdba..69267378 100644 --- a/test/foundry/utils/TractorTestHelper.sol +++ b/test/foundry/utils/TractorTestHelper.sol @@ -9,7 +9,7 @@ import {TractorHelpers} from "contracts/ecosystem/tractor/utils/TractorHelpers.s import {LibSiloHelpers} from "contracts/libraries/Silo/LibSiloHelpers.sol"; import {SiloHelpers} from "contracts/ecosystem/tractor/utils/SiloHelpers.sol"; import {LibTractorHelpers} from "contracts/libraries/Silo/LibTractorHelpers.sol"; -import {MowPlantHarvestBlueprint} from "contracts/ecosystem/MowPlantHarvestBlueprint.sol"; +import {AutomateClaimBlueprint} from "contracts/ecosystem/AutomateClaimBlueprint.sol"; import {BlueprintBase} from "contracts/ecosystem/BlueprintBase.sol"; import {LibTractor} from "contracts/libraries/LibTractor.sol"; import "forge-std/console.sol"; @@ -19,7 +19,7 @@ contract TractorTestHelper is TestHelper { TractorHelpers internal tractorHelpers; SowBlueprint internal sowBlueprint; SiloHelpers internal siloHelpers; - MowPlantHarvestBlueprint internal mowPlantHarvestBlueprint; + AutomateClaimBlueprint internal automateClaimBlueprint; uint256 public constant DEFAULT_FIELD_ID = 0; uint256 public constant PAYBACK_FIELD_ID = 1; @@ -42,8 +42,8 @@ contract TractorTestHelper is TestHelper { siloHelpers = SiloHelpers(_siloHelpers); } - function setMowPlantHarvestBlueprint(address _mowPlantHarvestBlueprint) internal { - mowPlantHarvestBlueprint = MowPlantHarvestBlueprint(_mowPlantHarvestBlueprint); + function setAutomateClaimBlueprint(address _automateClaimBlueprint) internal { + automateClaimBlueprint = AutomateClaimBlueprint(_automateClaimBlueprint); } function createRequisitionWithPipeCall( @@ -172,7 +172,7 @@ contract TractorTestHelper is TestHelper { uint256[] memory harvestablePlotIndexes = _getHarvestablePlotIndexes(account, fieldId); // Create ContractData with key = HARVEST_DATA_KEY + fieldId - uint256 key = mowPlantHarvestBlueprint.HARVEST_DATA_KEY() + fieldId; + uint256 key = automateClaimBlueprint.HARVEST_DATA_KEY() + fieldId; dynamicData[i] = IMockFBeanstalk.ContractData({ key: key, value: abi.encode(harvestablePlotIndexes) @@ -425,9 +425,9 @@ contract TractorTestHelper is TestHelper { return abi.encodeWithSelector(IMockFBeanstalk.advancedFarm.selector, calls); } - //////////////////////////// MowPlantHarvestBlueprint //////////////////////////// + //////////////////////////// AutomateClaimBlueprint //////////////////////////// - function setupMowPlantHarvestBlueprint( + function setupAutomateClaimBlueprint( address account, SourceMode sourceMode, uint256 minMowAmount, @@ -443,11 +443,11 @@ contract TractorTestHelper is TestHelper { internal returns ( IMockFBeanstalk.Requisition memory, - MowPlantHarvestBlueprint.MowPlantHarvestBlueprintStruct memory params + AutomateClaimBlueprint.AutomateClaimBlueprintStruct memory params ) { // build struct params - params = createMowPlantHarvestBlueprintStruct( + params = createAutomateClaimBlueprintStruct( uint8(sourceMode), minMowAmount, minTwaDeltaB, @@ -461,7 +461,7 @@ contract TractorTestHelper is TestHelper { ); // create pipe call data - bytes memory pipeCallData = createMowPlantHarvestBlueprintCallData(params); + bytes memory pipeCallData = createAutomateClaimBlueprintCallData(params); // create requisition IMockFBeanstalk.Requisition memory req = createRequisitionWithPipeCall( @@ -476,8 +476,8 @@ contract TractorTestHelper is TestHelper { return (req, params); } - // Creates and returns the struct params for the mowPlantHarvestBlueprint - function createMowPlantHarvestBlueprintStruct( + // Creates and returns the struct params for the automateClaimBlueprint + function createAutomateClaimBlueprintStruct( uint8 sourceMode, uint256 minMowAmount, uint256 minTwaDeltaB, @@ -488,7 +488,7 @@ contract TractorTestHelper is TestHelper { int256 plantTipAmount, int256 harvestTipAmount, uint256 maxGrownStalkPerBdv - ) internal view returns (MowPlantHarvestBlueprint.MowPlantHarvestBlueprintStruct memory) { + ) internal view returns (AutomateClaimBlueprint.AutomateClaimBlueprintStruct memory) { // Create default whitelisted operators array with msg.sender address[] memory whitelistedOps = new address[](3); whitelistedOps[0] = msg.sender; @@ -509,12 +509,12 @@ contract TractorTestHelper is TestHelper { } // create per-field-id harvest configs - MowPlantHarvestBlueprint.FieldHarvestConfig[] + AutomateClaimBlueprint.FieldHarvestConfig[] memory fieldHarvestConfigs = createFieldHarvestConfigs(minHarvestAmount); - // Create MowPlantHarvestParams struct - MowPlantHarvestBlueprint.MowPlantHarvestParams - memory mowPlantHarvestParams = MowPlantHarvestBlueprint.MowPlantHarvestParams({ + // Create AutomateClaimParams struct + AutomateClaimBlueprint.AutomateClaimParams + memory automateClaimParams = AutomateClaimBlueprint.AutomateClaimParams({ minMowAmount: minMowAmount, minTwaDeltaB: minTwaDeltaB, minPlantAmount: minPlantAmount, @@ -525,7 +525,7 @@ contract TractorTestHelper is TestHelper { }); // create OperatorParamsExtended struct - MowPlantHarvestBlueprint.OperatorParamsExtended + AutomateClaimBlueprint.OperatorParamsExtended memory opParamsExtended = createOperatorParamsExtended( whitelistedOps, tipAddress, @@ -535,8 +535,8 @@ contract TractorTestHelper is TestHelper { ); return - MowPlantHarvestBlueprint.MowPlantHarvestBlueprintStruct({ - mowPlantHarvestParams: mowPlantHarvestParams, + AutomateClaimBlueprint.AutomateClaimBlueprintStruct({ + automateClaimParams: automateClaimParams, opParams: opParamsExtended }); } @@ -546,32 +546,32 @@ contract TractorTestHelper is TestHelper { ) internal view - returns (MowPlantHarvestBlueprint.FieldHarvestConfig[] memory fieldHarvestConfigs) + returns (AutomateClaimBlueprint.FieldHarvestConfig[] memory fieldHarvestConfigs) { - fieldHarvestConfigs = new MowPlantHarvestBlueprint.FieldHarvestConfig[](2); + fieldHarvestConfigs = new AutomateClaimBlueprint.FieldHarvestConfig[](2); // default field id - fieldHarvestConfigs[0] = MowPlantHarvestBlueprint.FieldHarvestConfig({ + fieldHarvestConfigs[0] = AutomateClaimBlueprint.FieldHarvestConfig({ fieldId: DEFAULT_FIELD_ID, minHarvestAmount: minHarvestAmount }); // expected payback field id - fieldHarvestConfigs[1] = MowPlantHarvestBlueprint.FieldHarvestConfig({ + fieldHarvestConfigs[1] = AutomateClaimBlueprint.FieldHarvestConfig({ fieldId: PAYBACK_FIELD_ID, minHarvestAmount: minHarvestAmount }); return fieldHarvestConfigs; } - function createMowPlantHarvestBlueprintCallData( - MowPlantHarvestBlueprint.MowPlantHarvestBlueprintStruct memory params + function createAutomateClaimBlueprintCallData( + AutomateClaimBlueprint.AutomateClaimBlueprintStruct memory params ) internal view returns (bytes memory) { - // create the mowPlantHarvestBlueprint pipe call + // create the automateClaimBlueprint pipe call IMockFBeanstalk.AdvancedPipeCall[] memory pipes = new IMockFBeanstalk.AdvancedPipeCall[](1); pipes[0] = IMockFBeanstalk.AdvancedPipeCall({ - target: address(mowPlantHarvestBlueprint), + target: address(automateClaimBlueprint), callData: abi.encodeWithSelector( - MowPlantHarvestBlueprint.mowPlantHarvestBlueprint.selector, + AutomateClaimBlueprint.automateClaimBlueprint.selector, params ), clipboard: hex"0000" @@ -594,7 +594,7 @@ contract TractorTestHelper is TestHelper { int256 mowTipAmount, int256 plantTipAmount, int256 harvestTipAmount - ) internal view returns (MowPlantHarvestBlueprint.OperatorParamsExtended memory) { + ) internal view returns (AutomateClaimBlueprint.OperatorParamsExtended memory) { // create OperatorParams struct BlueprintBase.OperatorParams memory opParams = BlueprintBase.OperatorParams({ whitelistedOperators: whitelistedOps, @@ -603,8 +603,8 @@ contract TractorTestHelper is TestHelper { }); // create OperatorParamsExtended struct - MowPlantHarvestBlueprint.OperatorParamsExtended - memory opParamsExtended = MowPlantHarvestBlueprint.OperatorParamsExtended({ + AutomateClaimBlueprint.OperatorParamsExtended + memory opParamsExtended = AutomateClaimBlueprint.OperatorParamsExtended({ baseOpParams: opParams, mowTipAmount: mowTipAmount, plantTipAmount: plantTipAmount, From ea71a72b1cb7fba56d549a11c6cf7e111f1b5eb0 Mon Sep 17 00:00:00 2001 From: exTypen <242232957+pocikerim@users.noreply.github.com> Date: Mon, 16 Feb 2026 00:18:37 +0300 Subject: [PATCH 2/4] Add rinse support to AutomateClaimBlueprint with operator sourced fertilizer IDs --- .../ecosystem/AutomateClaimBlueprint.sol | 122 +++- .../ecosystem/AutomateClaimBlueprint.t.sol | 608 ++++++++++++++++-- test/foundry/utils/TractorTestHelper.sol | 156 +++-- 3 files changed, 737 insertions(+), 149 deletions(-) diff --git a/contracts/ecosystem/AutomateClaimBlueprint.sol b/contracts/ecosystem/AutomateClaimBlueprint.sol index a4650f8d..ec59bc2b 100644 --- a/contracts/ecosystem/AutomateClaimBlueprint.sol +++ b/contracts/ecosystem/AutomateClaimBlueprint.sol @@ -7,10 +7,23 @@ import {LibSiloHelpers} from "contracts/libraries/Silo/LibSiloHelpers.sol"; import {SiloHelpers} from "contracts/ecosystem/tractor/utils/SiloHelpers.sol"; import {BlueprintBase} from "./BlueprintBase.sol"; +/** + * @dev Minimal interface for BarnPayback's claim and balance functions. + * We cannot import IBarnPayback directly because it defines its own local LibTransfer + * library, which causes a type conflict with the protocol's LibTransfer.To used here. + */ +interface IBarnPaybackClaim { + function claimFertilized(uint256[] memory ids, LibTransfer.To mode) external; + function balanceOfFertilized( + address account, + uint256[] memory ids + ) external view returns (uint256 beans); +} + /** * @title AutomateClaimBlueprint * @author DefaultJuice - * @notice Contract for mowing, planting and harvesting with Tractor, with a number of conditions + * @notice Contract for mowing, planting, harvesting and rinsing with Tractor, with a number of conditions */ contract AutomateClaimBlueprint is BlueprintBase { /** @@ -21,10 +34,16 @@ contract AutomateClaimBlueprint is BlueprintBase { /** * @dev Key for operator-provided harvest data in transient storage * The key format is: HARVEST_DATA_KEY + fieldId - * Hash: 0x57c0c06c01076b3dedd361eef555163669978891b716ce6c5ef1355fc8ab5a36 + * Hash: 0xad7d503bd76a2177b94db747d4e00459b65eb93e2a4be3b707394f51d084fc4c */ uint256 public constant HARVEST_DATA_KEY = - uint256(keccak256("MowPlantHarvestBlueprint.harvestData")); + uint256(keccak256("AutomateClaimBlueprint.harvestData")); + + /** + * @dev Key for operator-provided rinse data in transient storage + */ + uint256 public constant RINSE_DATA_KEY = + uint256(keccak256("AutomateClaimBlueprint.rinseData")); /** * @notice Main struct for automate claim blueprint @@ -80,6 +99,8 @@ contract AutomateClaimBlueprint is BlueprintBase { uint256 minPlantAmount; // Harvest, per field id FieldHarvestConfig[] fieldHarvestConfigs; + // Rinse (claimFertilized) + uint256 minRinseAmount; // Withdrawal plan parameters for tipping uint8[] sourceTokenIndices; uint256 maxGrownStalkPerBdv; @@ -102,6 +123,7 @@ contract AutomateClaimBlueprint is BlueprintBase { int256 mowTipAmount; int256 plantTipAmount; int256 harvestTipAmount; + int256 rinseTipAmount; } /** @@ -117,18 +139,23 @@ contract AutomateClaimBlueprint is BlueprintBase { bool shouldMow; bool shouldPlant; UserFieldHarvestResults[] userFieldHarvestResults; + uint256[] rinseFertilizerIds; } // Silo helpers for withdrawal functionality SiloHelpers public immutable siloHelpers; + // BarnPayback contract for claiming fertilized beans + IBarnPaybackClaim public immutable barnPayback; constructor( address _beanstalk, address _owner, address _tractorHelpers, - address _siloHelpers + address _siloHelpers, + address _barnPayback ) BlueprintBase(_beanstalk, _owner, _tractorHelpers) { siloHelpers = SiloHelpers(_siloHelpers); + barnPayback = IBarnPaybackClaim(_barnPayback); } /** @@ -145,11 +172,12 @@ contract AutomateClaimBlueprint is BlueprintBase { vars.account = beanstalk.tractorUser(); // get the user state from the protocol and validate against params - (vars.shouldMow, vars.shouldPlant, vars.userFieldHarvestResults) = _getAndValidateUserState( - vars.account, - beanstalk.time().timestamp, - params - ); + ( + vars.shouldMow, + vars.shouldPlant, + vars.userFieldHarvestResults, + vars.rinseFertilizerIds + ) = _getAndValidateUserState(vars.account, beanstalk.time().timestamp, params); // validate order params and revert early if invalid _validateSourceTokens(params.automateClaimParams.sourceTokenIndices); @@ -158,10 +186,14 @@ contract AutomateClaimBlueprint is BlueprintBase { // resolve tip address (defaults to operator if not set) address tipAddress = _resolveTipAddress(params.opParams.baseOpParams.tipAddress); - // Mow, Plant and Harvest - // Check if user should harvest or plant - // In the case a harvest or plant is executed, mow by default - if (vars.shouldPlant || vars.userFieldHarvestResults.length > 0) vars.shouldMow = true; + // Mow, Plant, Harvest and Rinse + // Check if user should harvest, plant or rinse + // In the case a harvest, plant or rinse is executed, mow by default + if ( + vars.shouldPlant || + vars.userFieldHarvestResults.length > 0 || + vars.rinseFertilizerIds.length > 0 + ) vars.shouldMow = true; // Execute operations in order: mow first (if needed), then plant, then harvest if (vars.shouldMow) { @@ -198,6 +230,27 @@ contract AutomateClaimBlueprint is BlueprintBase { vars.totalBeanTip += params.opParams.harvestTipAmount; } + // Rinse (claim fertilized beans) if the conditions are met + if (vars.rinseFertilizerIds.length > 0) { + // Get expected amount before claiming + uint256 expectedRinseAmount = barnPayback.balanceOfFertilized( + vars.account, + vars.rinseFertilizerIds + ); + + require( + expectedRinseAmount >= params.automateClaimParams.minRinseAmount, + "AutomateClaimBlueprint: Rinsed amount below minimum threshold" + ); + + // Claim fertilized beans to user's internal balance + barnPayback.claimFertilized(vars.rinseFertilizerIds, LibTransfer.To.INTERNAL); + + // Rinsed beans are in internal balance, same as harvested beans + vars.totalHarvestedBeans += expectedRinseAmount; + vars.totalBeanTip += params.opParams.rinseTipAmount; + } + // Handle tip payment handleBeansAndTip( vars.account, @@ -220,6 +273,7 @@ contract AutomateClaimBlueprint is BlueprintBase { * @return shouldPlant True if the user should plant * @return userFieldHarvestResults An array of structs containing the harvestable pods * and plots for the user for each field id where operator provided data + * @return rinseFertilizerIds Array of fertilizer IDs to rinse (empty if rinse skipped) */ function _getAndValidateUserState( address account, @@ -231,7 +285,8 @@ contract AutomateClaimBlueprint is BlueprintBase { returns ( bool shouldMow, bool shouldPlant, - UserFieldHarvestResults[] memory userFieldHarvestResults + UserFieldHarvestResults[] memory userFieldHarvestResults, + uint256[] memory rinseFertilizerIds ) { // get user state @@ -241,6 +296,9 @@ contract AutomateClaimBlueprint is BlueprintBase { UserFieldHarvestResults[] memory userFieldHarvestResults ) = _getUserState(account, params.automateClaimParams.fieldHarvestConfigs); + // get rinse data from operator-provided transient storage + rinseFertilizerIds = _getRinseData(account, params.automateClaimParams.minRinseAmount); + // validate params - only revert if none of the conditions are met shouldMow = _checkMowConditions( params.automateClaimParams.minTwaDeltaB, @@ -251,11 +309,11 @@ contract AutomateClaimBlueprint is BlueprintBase { shouldPlant = totalPlantableBeans >= params.automateClaimParams.minPlantAmount; require( - shouldMow || shouldPlant || userFieldHarvestResults.length > 0, + shouldMow || shouldPlant || userFieldHarvestResults.length > 0 || rinseFertilizerIds.length > 0, "AutomateClaimBlueprint: None of the order conditions are met" ); - return (shouldMow, shouldPlant, userFieldHarvestResults); + return (shouldMow, shouldPlant, userFieldHarvestResults, rinseFertilizerIds); } /** @@ -349,6 +407,38 @@ contract AutomateClaimBlueprint is BlueprintBase { return (totalClaimableStalk, totalPlantableBeans, userFieldHarvestResults); } + /** + * @notice Reads rinse data from operator-provided transient storage + * @dev If no data is provided or claimable amount is below minRinseAmount, returns empty array (rinse skipped) + * @param account The account to check fertilized balance for + * @param minRinseAmount The minimum rinsable amount threshold + * @return fertilizerIds Array of fertilizer IDs to rinse (empty if skipped) + */ + function _getRinseData( + address account, + uint256 minRinseAmount + ) internal view returns (uint256[] memory fertilizerIds) { + bytes memory operatorData = beanstalk.getTractorData(RINSE_DATA_KEY); + + if (operatorData.length == 0) { + return new uint256[](0); + } + + fertilizerIds = abi.decode(operatorData, (uint256[])); + + if (fertilizerIds.length == 0) { + return new uint256[](0); + } + + // Check if claimable amount meets minimum threshold + uint256 claimableAmount = barnPayback.balanceOfFertilized(account, fertilizerIds); + if (claimableAmount < minRinseAmount) { + return new uint256[](0); + } + + return fertilizerIds; + } + /** * @notice Handles tip payment * @param account The account to withdraw for diff --git a/test/foundry/ecosystem/AutomateClaimBlueprint.t.sol b/test/foundry/ecosystem/AutomateClaimBlueprint.t.sol index c447a15d..474aff1c 100644 --- a/test/foundry/ecosystem/AutomateClaimBlueprint.t.sol +++ b/test/foundry/ecosystem/AutomateClaimBlueprint.t.sol @@ -11,23 +11,35 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {TractorTestHelper} from "test/foundry/utils/TractorTestHelper.sol"; import {BeanstalkPrice} from "contracts/ecosystem/price/BeanstalkPrice.sol"; import {IBeanstalk} from "contracts/interfaces/IBeanstalk.sol"; -import {AutomateClaimBlueprint} from "contracts/ecosystem/AutomateClaimBlueprint.sol"; +import {AutomateClaimBlueprint, IBarnPaybackClaim} from "contracts/ecosystem/AutomateClaimBlueprint.sol"; +import {BarnPayback} from "contracts/ecosystem/beanstalkShipments/barn/BarnPayback.sol"; +import {BeanstalkFertilizer} from "contracts/ecosystem/beanstalkShipments/barn/BeanstalkFertilizer.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "forge-std/console.sol"; contract AutomateClaimBlueprintTest is TractorTestHelper { address[] farmers; BeanstalkPrice beanstalkPrice; + BarnPayback barnPayback; event Plant(address indexed account, uint256 beans); event Harvest(address indexed account, uint256 fieldId, uint256[] plots, uint256 beans); + event ClaimFertilizer(uint256[] ids, uint256 beans); uint256 STALK_DECIMALS = 1e10; int256 DEFAULT_TIP_AMOUNT = 10e6; // 10 BEAN uint256 constant MAX_GROWN_STALK_PER_BDV = 1000e16; // Stalk is 1e16 uint256 UNEXECUTABLE_MIN_HARVEST_AMOUNT = 1_000_000_000e6; // 1B BEAN + uint256 UNEXECUTABLE_MIN_RINSE_AMOUNT = type(uint256).max; uint256 PODS_FIELD_0 = 1000100000; uint256 PODS_FIELD_1 = 250e6; + // BarnPayback fertilizer constants + uint128 constant INITIAL_BPF = 45e6; + uint128 constant FERT_ID_1 = 50e6; + uint128 constant FERT_ID_2 = 100e6; + uint128 constant FERT_ID_3 = 150e6; + struct TestState { address user; address operator; @@ -62,12 +74,17 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { ); vm.label(address(siloHelpers), "SiloHelpers"); - // Deploy AutomateClaimBlueprint with TractorHelpers and SiloHelpers addresses + // Deploy BarnPayback (proxy pattern) + barnPayback = _deployBarnPayback(); + vm.label(address(barnPayback), "BarnPayback"); + + // Deploy AutomateClaimBlueprint with TractorHelpers, SiloHelpers and BarnPayback addresses automateClaimBlueprint = new AutomateClaimBlueprint( address(bs), address(this), address(tractorHelpers), - address(siloHelpers) + address(siloHelpers), + address(barnPayback) ); vm.label(address(automateClaimBlueprint), "AutomateClaimBlueprint"); @@ -151,17 +168,21 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { // Setup automateClaimBlueprint (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( - state.user, // account - SourceMode.PURE_PINTO, // sourceMode for tip - 1 * STALK_DECIMALS, // minMowAmount (1 stalk) - 10e6, // mintwaDeltaB - type(uint256).max, // minPlantAmount - UNEXECUTABLE_MIN_HARVEST_AMOUNT, // minHarvestAmount (for all fields) - state.operator, // tipAddress - state.mowTipAmount, // mowTipAmount - state.plantTipAmount, // plantTipAmount - state.harvestTipAmount, // harvestTipAmount - MAX_GROWN_STALK_PER_BDV // maxGrownStalkPerBdv + AutomateClaimSetupParams({ + account: state.user, + sourceMode: SourceMode.PURE_PINTO, + minMowAmount: 1 * STALK_DECIMALS, + minTwaDeltaB: 10e6, + minPlantAmount: type(uint256).max, + minHarvestAmount: UNEXECUTABLE_MIN_HARVEST_AMOUNT, + minRinseAmount: UNEXECUTABLE_MIN_RINSE_AMOUNT, + tipAddress: state.operator, + mowTipAmount: state.mowTipAmount, + plantTipAmount: state.plantTipAmount, + harvestTipAmount: state.harvestTipAmount, + rinseTipAmount: 0, + maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV + }) ); // Pre-calculate harvest data BEFORE expectRevert (to avoid consuming the expectation) @@ -201,17 +222,21 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { // Setup blueprint with minPlantAmount greater than total plantable beans (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( - state.user, // account - SourceMode.PURE_PINTO, // sourceMode for tip - 1 * STALK_DECIMALS, // minMowAmount (1 stalk) - 10e6, // mintwaDeltaB - type(uint256).max, // minPlantAmount > (total plantable beans) - UNEXECUTABLE_MIN_HARVEST_AMOUNT, // minHarvestAmount (for all fields) - state.operator, // tipAddress - state.mowTipAmount, // mowTipAmount - state.plantTipAmount, // plantTipAmount - state.harvestTipAmount, // harvestTipAmount - MAX_GROWN_STALK_PER_BDV // maxGrownStalkPerBdv + AutomateClaimSetupParams({ + account: state.user, + sourceMode: SourceMode.PURE_PINTO, + minMowAmount: 1 * STALK_DECIMALS, + minTwaDeltaB: 10e6, + minPlantAmount: type(uint256).max, + minHarvestAmount: UNEXECUTABLE_MIN_HARVEST_AMOUNT, + minRinseAmount: UNEXECUTABLE_MIN_RINSE_AMOUNT, + tipAddress: state.operator, + mowTipAmount: state.mowTipAmount, + plantTipAmount: state.plantTipAmount, + harvestTipAmount: state.harvestTipAmount, + rinseTipAmount: 0, + maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV + }) ); // Pre-calculate harvest data BEFORE expectRevert @@ -240,17 +265,21 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { // Setup blueprint with valid minPlantAmount (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( - state.user, // account - SourceMode.PURE_PINTO, // sourceMode for tip - 1 * STALK_DECIMALS, // minMowAmount (1 stalk) - 10e6, // mintwaDeltaB - 11e6, // minPlantAmount > 10e6 (plant tip amount) - UNEXECUTABLE_MIN_HARVEST_AMOUNT, // minHarvestAmount (for all fields) - state.operator, // tipAddress - state.mowTipAmount, // mowTipAmount - state.plantTipAmount, // plantTipAmount - state.harvestTipAmount, // harvestTipAmount - MAX_GROWN_STALK_PER_BDV // maxGrownStalkPerBdv + AutomateClaimSetupParams({ + account: state.user, + sourceMode: SourceMode.PURE_PINTO, + minMowAmount: 1 * STALK_DECIMALS, + minTwaDeltaB: 10e6, + minPlantAmount: 11e6, + minHarvestAmount: UNEXECUTABLE_MIN_HARVEST_AMOUNT, + minRinseAmount: UNEXECUTABLE_MIN_RINSE_AMOUNT, + tipAddress: state.operator, + mowTipAmount: state.mowTipAmount, + plantTipAmount: state.plantTipAmount, + harvestTipAmount: state.harvestTipAmount, + rinseTipAmount: 0, + maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV + }) ); // Execute requisition, expect plant event @@ -288,17 +317,21 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { // Setup blueprint for partial harvest (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( - state.user, // account - SourceMode.PURE_PINTO, // sourceMode for tip - 1 * STALK_DECIMALS, // minMowAmount (1 stalk) - 10e6, // mintwaDeltaB - 11e6, // minPlantAmount - 11e6, // minHarvestAmount > 10e6 (harvest tip amount) (for all fields) - state.operator, // tipAddress - state.mowTipAmount, // mowTipAmount - state.plantTipAmount, // plantTipAmount - state.harvestTipAmount, // harvestTipAmount - MAX_GROWN_STALK_PER_BDV // maxGrownStalkPerBdv + AutomateClaimSetupParams({ + account: state.user, + sourceMode: SourceMode.PURE_PINTO, + minMowAmount: 1 * STALK_DECIMALS, + minTwaDeltaB: 10e6, + minPlantAmount: 11e6, + minHarvestAmount: 11e6, + minRinseAmount: UNEXECUTABLE_MIN_RINSE_AMOUNT, + tipAddress: state.operator, + mowTipAmount: state.mowTipAmount, + plantTipAmount: state.plantTipAmount, + harvestTipAmount: state.harvestTipAmount, + rinseTipAmount: 0, + maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV + }) ); // Execute requisition, expect harvest event @@ -337,17 +370,21 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { // Setup blueprint for full harvest (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( - state.user, // account - SourceMode.PURE_PINTO, // sourceMode for tip - 1 * STALK_DECIMALS, // minMowAmount (1 stalk) - 10e6, // mintwaDeltaB - 11e6, // minPlantAmount - 11e6, // minHarvestAmount > 10e6 (harvest tip amount) (for all fields) - state.operator, // tipAddress - state.mowTipAmount, // mowTipAmount - state.plantTipAmount, // plantTipAmount - state.harvestTipAmount, // harvestTipAmount - MAX_GROWN_STALK_PER_BDV // maxGrownStalkPerBdv + AutomateClaimSetupParams({ + account: state.user, + sourceMode: SourceMode.PURE_PINTO, + minMowAmount: 1 * STALK_DECIMALS, + minTwaDeltaB: 10e6, + minPlantAmount: 11e6, + minHarvestAmount: 11e6, + minRinseAmount: UNEXECUTABLE_MIN_RINSE_AMOUNT, + tipAddress: state.operator, + mowTipAmount: state.mowTipAmount, + plantTipAmount: state.plantTipAmount, + harvestTipAmount: state.harvestTipAmount, + rinseTipAmount: 0, + maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV + }) ); // Execute requisition, expect harvest event @@ -402,17 +439,21 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { // Setup blueprint for full harvest (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( - state.user, // account - SourceMode.PURE_PINTO, // sourceMode for tip - 1 * STALK_DECIMALS, // minMowAmount (1 stalk) - 10e6, // mintwaDeltaB - 11e6, // minPlantAmount - 11e6, // minHarvestAmount > 10e6 (harvest tip amount) (for all fields) - state.operator, // tipAddress - state.mowTipAmount, // mowTipAmount - state.plantTipAmount, // plantTipAmount - state.harvestTipAmount, // harvestTipAmount - MAX_GROWN_STALK_PER_BDV // maxGrownStalkPerBdv + AutomateClaimSetupParams({ + account: state.user, + sourceMode: SourceMode.PURE_PINTO, + minMowAmount: 1 * STALK_DECIMALS, + minTwaDeltaB: 10e6, + minPlantAmount: 11e6, + minHarvestAmount: 11e6, + minRinseAmount: UNEXECUTABLE_MIN_RINSE_AMOUNT, + tipAddress: state.operator, + mowTipAmount: state.mowTipAmount, + plantTipAmount: state.plantTipAmount, + harvestTipAmount: state.harvestTipAmount, + rinseTipAmount: 0, + maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV + }) ); // Execute requisition, expect harvest events for both fields @@ -551,4 +592,433 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { IMockFBeanstalk.ContractData[] memory dynamicData = getHarvestDynamicDataForUser(user); executeRequisitionWithDynamicData(operator, req, address(bs), dynamicData); } + + /** + * @notice Execute a requisition with harvest and rinse dynamic data + */ + function executeWithHarvestAndRinseData( + address operator, + address user, + IMockFBeanstalk.Requisition memory req, + uint256[] memory fertilizerIds + ) internal { + IMockFBeanstalk.ContractData[] memory harvestData = getHarvestDynamicDataForUser(user); + IMockFBeanstalk.ContractData[] memory rinseData = createRinseDynamicData(fertilizerIds); + IMockFBeanstalk.ContractData[] memory merged = mergeHarvestAndRinseDynamicData( + harvestData, + rinseData + ); + executeRequisitionWithDynamicData(operator, req, address(bs), merged); + } + + /////////////////////////// BARN PAYBACK HELPERS /////////////////////////// + + /** + * @notice Deploy BarnPayback contract via proxy pattern (same as BarnPayback.t.sol) + */ + function _deployBarnPayback() internal returns (BarnPayback) { + BarnPayback implementation = new BarnPayback(); + + BeanstalkFertilizer.InitSystemFertilizer + memory initSystemFert = _createInitSystemFertilizerData(); + + bytes memory data = abi.encodeWithSelector( + BarnPayback.initialize.selector, + address(BEAN), + address(BEANSTALK), + address(0), // contract distributor + initSystemFert + ); + + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(implementation), + address(this), + data + ); + + return BarnPayback(address(proxy)); + } + + function _createInitSystemFertilizerData() + internal + pure + returns (BeanstalkFertilizer.InitSystemFertilizer memory) + { + uint128[] memory fertilizerIds = new uint128[](3); + fertilizerIds[0] = FERT_ID_1; + fertilizerIds[1] = FERT_ID_2; + fertilizerIds[2] = FERT_ID_3; + + uint256[] memory fertilizerAmounts = new uint256[](3); + fertilizerAmounts[0] = 100; + fertilizerAmounts[1] = 50; + fertilizerAmounts[2] = 25; + + uint256 totalFertilizer = (FERT_ID_1 * 100) + (FERT_ID_2 * 50) + (FERT_ID_3 * 25); + + return + BeanstalkFertilizer.InitSystemFertilizer({ + fertilizerIds: fertilizerIds, + fertilizerAmounts: fertilizerAmounts, + activeFertilizer: uint128(175), + fertilizedIndex: uint128(0), + unfertilizedIndex: uint128(totalFertilizer), + fertilizedPaidIndex: uint128(0), + fertFirst: FERT_ID_1, + fertLast: FERT_ID_3, + bpf: INITIAL_BPF, + leftoverBeans: uint128(0) + }); + } + + /** + * @notice Mint fertilizers to a user and send rewards so they have claimable beans + * @dev In the tractor context, BarnPayback's `transferToken` call routes through the + * diamond which uses LibTractor._user() as the sender. This means the protocol pulls + * beans from the tractor publisher's external balance instead of BarnPayback's. + * To make this work, we fund the user's external balance with enough beans to cover + * the claim amount. The user already has max approval on the diamond from test setup. + */ + function _setupRinseConditions(address user) internal { + // Mint fertilizers to the user + BarnPayback.AccountFertilizerData[] + memory accounts = new BarnPayback.AccountFertilizerData[](1); + accounts[0] = BarnPayback.AccountFertilizerData({ + account: user, + amount: 60, + lastBpf: INITIAL_BPF + }); + + BarnPayback.Fertilizers[] memory fertData = new BarnPayback.Fertilizers[](1); + fertData[0] = BarnPayback.Fertilizers({fertilizerId: FERT_ID_1, accountData: accounts}); + + barnPayback.mintFertilizers(fertData); + + // Send rewards to create claimable beans + uint256 rewardAmount = 100e6; + deal(address(BEAN), address(deployer), rewardAmount); + vm.prank(deployer); + IERC20(address(BEAN)).transfer(address(barnPayback), rewardAmount); + vm.prank(address(BEANSTALK)); + barnPayback.barnPaybackReceive(rewardAmount); + + // Fund user's external bean balance to cover claimFertilized via tractor. + // In tractor context, the diamond's transferToken uses LibTractor._user() (the publisher) + // as the sender, so the user needs external beans for the safeTransferFrom to succeed. + uint256[] memory fertIds = new uint256[](1); + fertIds[0] = FERT_ID_1; + uint256 claimable = barnPayback.balanceOfFertilized(user, fertIds); + mintTokensToUser(user, address(BEAN), claimable); + } + + /** + * @notice Setup rinse conditions with multiple fertilizer IDs (FERT_ID_1 and FERT_ID_2) + */ + function _setupRinseConditionsMultipleIds(address user) internal { + // Mint fertilizers from 2 different IDs to the user + BarnPayback.AccountFertilizerData[] + memory accounts = new BarnPayback.AccountFertilizerData[](1); + accounts[0] = BarnPayback.AccountFertilizerData({ + account: user, + amount: 30, + lastBpf: INITIAL_BPF + }); + + BarnPayback.Fertilizers[] memory fertData = new BarnPayback.Fertilizers[](2); + fertData[0] = BarnPayback.Fertilizers({fertilizerId: FERT_ID_1, accountData: accounts}); + fertData[1] = BarnPayback.Fertilizers({fertilizerId: FERT_ID_2, accountData: accounts}); + + barnPayback.mintFertilizers(fertData); + + // Send rewards to create claimable beans + uint256 rewardAmount = 200e6; + deal(address(BEAN), address(deployer), rewardAmount); + vm.prank(deployer); + IERC20(address(BEAN)).transfer(address(barnPayback), rewardAmount); + vm.prank(address(BEANSTALK)); + barnPayback.barnPaybackReceive(rewardAmount); + + // Fund user's external bean balance for both fert IDs + uint256[] memory fertIds = new uint256[](2); + fertIds[0] = FERT_ID_1; + fertIds[1] = FERT_ID_2; + uint256 claimable = barnPayback.balanceOfFertilized(user, fertIds); + mintTokensToUser(user, address(BEAN), claimable); + } + + /////////////////////////// RINSE TESTS /////////////////////////// + + function test_automateClaimBlueprint_rinse_multipleFertilizerIds() public { + // Setup test state (no plant, no harvest, above peg) + TestState memory state = setupAutomateClaimBlueprintTest(false, false, false, true); + + // Setup rinse conditions with multiple fertilizer IDs + _setupRinseConditionsMultipleIds(state.user); + + // Get the fertilizer IDs for the user + uint256[] memory fertIds = new uint256[](2); + fertIds[0] = FERT_ID_1; + fertIds[1] = FERT_ID_2; + + // Verify user has claimable fertilized beans for EACH fert ID individually + uint256[] memory singleId1 = new uint256[](1); + singleId1[0] = FERT_ID_1; + assertGt( + barnPayback.balanceOfFertilized(state.user, singleId1), + 0, + "user should have claimable beans for FERT_ID_1" + ); + uint256[] memory singleId2 = new uint256[](1); + singleId2[0] = FERT_ID_2; + assertGt( + barnPayback.balanceOfFertilized(state.user, singleId2), + 0, + "user should have claimable beans for FERT_ID_2" + ); + + // Get total claimable across both IDs + uint256 totalClaimable = barnPayback.balanceOfFertilized(state.user, fertIds); + assertGt(totalClaimable, 0, "user should have total claimable fertilized beans"); + + // Get user BDV before rinse + uint256 userTotalBdvBefore = bs.balanceOfDepositedBdv(state.user, state.beanToken); + + // Setup blueprint with rinse enabled (minRinseAmount = 1) + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( + AutomateClaimSetupParams({ + account: state.user, + sourceMode: SourceMode.PURE_PINTO, + minMowAmount: 1 * STALK_DECIMALS, + minTwaDeltaB: 10e6, + minPlantAmount: type(uint256).max, + minHarvestAmount: UNEXECUTABLE_MIN_HARVEST_AMOUNT, + minRinseAmount: 1, + tipAddress: state.operator, + mowTipAmount: state.mowTipAmount, + plantTipAmount: state.plantTipAmount, + harvestTipAmount: state.harvestTipAmount, + rinseTipAmount: DEFAULT_TIP_AMOUNT, + maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV + }) + ); + + // Execute with rinse data for multiple IDs + vm.expectEmit(); + emit ClaimFertilizer(fertIds, totalClaimable); + executeWithHarvestAndRinseData(state.operator, state.user, req, fertIds); + + // Verify ALL fertilizer IDs were claimed + assertEq( + barnPayback.balanceOfFertilized(state.user, singleId1), + 0, + "FERT_ID_1 should be fully claimed" + ); + assertEq( + barnPayback.balanceOfFertilized(state.user, singleId2), + 0, + "FERT_ID_2 should be fully claimed" + ); + + // Verify BDV increased + uint256 userTotalBdvAfter = bs.balanceOfDepositedBdv(state.user, state.beanToken); + assertGt(userTotalBdvAfter, userTotalBdvBefore, "userTotalBdv should increase from rinse"); + } + + function test_automateClaimBlueprint_rinse_withHarvest() public { + // Setup test state for harvesting (twoFields=true for deterministic field 1 harvest) + TestState memory state = setupAutomateClaimBlueprintTest(false, true, true, true); + + // Also setup rinse conditions + _setupRinseConditions(state.user); + + // Advance season to print beans for harvest + advanceSeason(); + + // Get user state before operations + uint256 userTotalBdvBefore = bs.balanceOfDepositedBdv(state.user, state.beanToken); + + // Verify harvestable pods exist + (uint256 harvestablePods, ) = _userHarvestablePods(state.user, DEFAULT_FIELD_ID); + assertGt(harvestablePods, 0, "user should have harvestable pods"); + + // Verify claimable rinse beans exist + uint256[] memory fertIds = new uint256[](1); + fertIds[0] = FERT_ID_1; + uint256 claimableRinse = barnPayback.balanceOfFertilized(state.user, fertIds); + assertGt(claimableRinse, 0, "user should have claimable fertilized beans"); + + // Setup blueprint with both harvest and rinse enabled + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( + AutomateClaimSetupParams({ + account: state.user, + sourceMode: SourceMode.PURE_PINTO, + minMowAmount: 1 * STALK_DECIMALS, + minTwaDeltaB: 10e6, + minPlantAmount: type(uint256).max, + minHarvestAmount: 11e6, + minRinseAmount: 1, + tipAddress: state.operator, + mowTipAmount: state.mowTipAmount, + plantTipAmount: state.plantTipAmount, + harvestTipAmount: state.harvestTipAmount, + rinseTipAmount: DEFAULT_TIP_AMOUNT, + maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV + }) + ); + + // Execute with both harvest and rinse data + executeWithHarvestAndRinseData(state.operator, state.user, req, fertIds); + + // Verify BDV increased (both harvested + rinsed beans deposited) + uint256 userTotalBdvAfter = bs.balanceOfDepositedBdv(state.user, state.beanToken); + assertGt( + userTotalBdvAfter, + userTotalBdvBefore, + "userTotalBdv should increase from harvest + rinse" + ); + + // Verify rinse fully claimed + uint256 postClaimBalance = barnPayback.balanceOfFertilized(state.user, fertIds); + assertEq(postClaimBalance, 0, "rinse should be fully claimed"); + + // Verify harvest complete on field 0 + assertNoHarvestablePods(state.user, DEFAULT_FIELD_ID); + } + + function test_automateClaimBlueprint_rinse_success() public { + // Setup test state (no plant, no harvest, above peg) + TestState memory state = setupAutomateClaimBlueprintTest(false, false, false, true); + + // Setup rinse conditions: mint fertilizers and send rewards + _setupRinseConditions(state.user); + + // Get the fertilizer IDs for the user + uint256[] memory fertIds = new uint256[](1); + fertIds[0] = FERT_ID_1; + + // Verify user has claimable fertilized beans + uint256 claimableAmount = barnPayback.balanceOfFertilized(state.user, fertIds); + assertGt(claimableAmount, 0, "user should have claimable fertilized beans"); + + // Get user BDV before rinse + uint256 userTotalBdvBefore = bs.balanceOfDepositedBdv(state.user, state.beanToken); + + // Setup blueprint with rinse enabled (minRinseAmount = 1) + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( + AutomateClaimSetupParams({ + account: state.user, + sourceMode: SourceMode.PURE_PINTO, + minMowAmount: 1 * STALK_DECIMALS, + minTwaDeltaB: 10e6, + minPlantAmount: type(uint256).max, + minHarvestAmount: UNEXECUTABLE_MIN_HARVEST_AMOUNT, + minRinseAmount: 1, + tipAddress: state.operator, + mowTipAmount: state.mowTipAmount, + plantTipAmount: state.plantTipAmount, + harvestTipAmount: state.harvestTipAmount, + rinseTipAmount: DEFAULT_TIP_AMOUNT, + maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV + }) + ); + + // Execute with rinse data + vm.expectEmit(); + emit ClaimFertilizer(fertIds, claimableAmount); + executeWithHarvestAndRinseData(state.operator, state.user, req, fertIds); + + // Verify fertilized beans were claimed (balance should be 0 now) + uint256 postClaimBalance = barnPayback.balanceOfFertilized(state.user, fertIds); + assertEq(postClaimBalance, 0, "user should have no more claimable fertilized beans"); + + // Verify BDV increased (rinsed beans deposited to silo) + uint256 userTotalBdvAfter = bs.balanceOfDepositedBdv(state.user, state.beanToken); + assertGt(userTotalBdvAfter, userTotalBdvBefore, "userTotalBdv should increase from rinse"); + } + + function test_automateClaimBlueprint_rinse_belowMinimum() public { + // Setup test state (no plant, no harvest, above peg) + TestState memory state = setupAutomateClaimBlueprintTest(false, false, false, true); + + // Setup rinse conditions + _setupRinseConditions(state.user); + + uint256[] memory fertIds = new uint256[](1); + fertIds[0] = FERT_ID_1; + + // Verify user has claimable beans + uint256 claimableAmount = barnPayback.balanceOfFertilized(state.user, fertIds); + assertGt(claimableAmount, 0, "user should have claimable fertilized beans"); + + // Setup blueprint with minRinseAmount higher than claimable (rinse skipped) + // Also disable mow/plant/harvest so we expect a full revert + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( + AutomateClaimSetupParams({ + account: state.user, + sourceMode: SourceMode.PURE_PINTO, + minMowAmount: type(uint256).max, + minTwaDeltaB: 10e6, + minPlantAmount: type(uint256).max, + minHarvestAmount: UNEXECUTABLE_MIN_HARVEST_AMOUNT, + minRinseAmount: claimableAmount + 1, + tipAddress: state.operator, + mowTipAmount: state.mowTipAmount, + plantTipAmount: state.plantTipAmount, + harvestTipAmount: state.harvestTipAmount, + rinseTipAmount: DEFAULT_TIP_AMOUNT, + maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV + }) + ); + + // Pre-calculate dynamic data BEFORE expectRevert + IMockFBeanstalk.ContractData[] memory harvestData = getHarvestDynamicDataForUser( + state.user + ); + IMockFBeanstalk.ContractData[] memory rinseData = createRinseDynamicData(fertIds); + IMockFBeanstalk.ContractData[] memory merged = mergeHarvestAndRinseDynamicData( + harvestData, + rinseData + ); + + // Execute - should revert since all conditions are skipped + vm.expectRevert("AutomateClaimBlueprint: None of the order conditions are met"); + executeRequisitionWithDynamicData(state.operator, req, address(bs), merged); + } + + function test_automateClaimBlueprint_rinse_noOperatorData() public { + // Setup test state (no plant, no harvest, above peg) + TestState memory state = setupAutomateClaimBlueprintTest(false, false, false, true); + + // Setup rinse conditions + _setupRinseConditions(state.user); + + // Setup blueprint with rinse enabled but operator provides NO rinse data + // Also disable mow/plant/harvest so we expect a full revert + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( + AutomateClaimSetupParams({ + account: state.user, + sourceMode: SourceMode.PURE_PINTO, + minMowAmount: type(uint256).max, + minTwaDeltaB: 10e6, + minPlantAmount: type(uint256).max, + minHarvestAmount: UNEXECUTABLE_MIN_HARVEST_AMOUNT, + minRinseAmount: 1, + tipAddress: state.operator, + mowTipAmount: state.mowTipAmount, + plantTipAmount: state.plantTipAmount, + harvestTipAmount: state.harvestTipAmount, + rinseTipAmount: DEFAULT_TIP_AMOUNT, + maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV + }) + ); + + // Pre-calculate dynamic data BEFORE expectRevert (harvest data only, no rinse data) + IMockFBeanstalk.ContractData[] memory dynamicData = getHarvestDynamicDataForUser( + state.user + ); + + // Execute without rinse data - should revert since all conditions are skipped + vm.expectRevert("AutomateClaimBlueprint: None of the order conditions are met"); + executeRequisitionWithDynamicData(state.operator, req, address(bs), dynamicData); + } } diff --git a/test/foundry/utils/TractorTestHelper.sol b/test/foundry/utils/TractorTestHelper.sol index 69267378..46a7561b 100644 --- a/test/foundry/utils/TractorTestHelper.sol +++ b/test/foundry/utils/TractorTestHelper.sol @@ -427,18 +427,27 @@ contract TractorTestHelper is TestHelper { //////////////////////////// AutomateClaimBlueprint //////////////////////////// + /** + * @notice Helper struct to bundle automate claim setup parameters and avoid stack too deep + */ + struct AutomateClaimSetupParams { + address account; + SourceMode sourceMode; + uint256 minMowAmount; + uint256 minTwaDeltaB; + uint256 minPlantAmount; + uint256 minHarvestAmount; + uint256 minRinseAmount; + address tipAddress; + int256 mowTipAmount; + int256 plantTipAmount; + int256 harvestTipAmount; + int256 rinseTipAmount; + uint256 maxGrownStalkPerBdv; + } + function setupAutomateClaimBlueprint( - address account, - SourceMode sourceMode, - uint256 minMowAmount, - uint256 minTwaDeltaB, - uint256 minPlantAmount, - uint256 minHarvestAmount, - address tipAddress, - int256 mowTipAmount, - int256 plantTipAmount, - int256 harvestTipAmount, - uint256 maxGrownStalkPerBdv + AutomateClaimSetupParams memory p ) internal returns ( @@ -446,81 +455,47 @@ contract TractorTestHelper is TestHelper { AutomateClaimBlueprint.AutomateClaimBlueprintStruct memory params ) { - // build struct params - params = createAutomateClaimBlueprintStruct( - uint8(sourceMode), - minMowAmount, - minTwaDeltaB, - minPlantAmount, - minHarvestAmount, - tipAddress, - mowTipAmount, - plantTipAmount, - harvestTipAmount, - maxGrownStalkPerBdv - ); + params = _createAutomateClaimBlueprintStructFromParams(p); - // create pipe call data bytes memory pipeCallData = createAutomateClaimBlueprintCallData(params); - // create requisition IMockFBeanstalk.Requisition memory req = createRequisitionWithPipeCall( - account, + p.account, pipeCallData, address(bs) ); - // publish requisition - publishAccountRequisition(account, req); + publishAccountRequisition(p.account, req); return (req, params); } - // Creates and returns the struct params for the automateClaimBlueprint - function createAutomateClaimBlueprintStruct( - uint8 sourceMode, - uint256 minMowAmount, - uint256 minTwaDeltaB, - uint256 minPlantAmount, - uint256 minHarvestAmount, - address tipAddress, - int256 mowTipAmount, - int256 plantTipAmount, - int256 harvestTipAmount, - uint256 maxGrownStalkPerBdv + function _createAutomateClaimBlueprintStructFromParams( + AutomateClaimSetupParams memory p ) internal view returns (AutomateClaimBlueprint.AutomateClaimBlueprintStruct memory) { // Create default whitelisted operators array with msg.sender address[] memory whitelistedOps = new address[](3); whitelistedOps[0] = msg.sender; - whitelistedOps[1] = tipAddress; + whitelistedOps[1] = p.tipAddress; whitelistedOps[2] = address(this); // Create array with single index for the token based on source mode - uint8[] memory sourceTokenIndices = new uint8[](1); - if (sourceMode == uint8(SourceMode.PURE_PINTO)) { - sourceTokenIndices[0] = tractorHelpers.getTokenIndex( - IMockFBeanstalk(address(bs)).getBeanToken() - ); - } else if (sourceMode == uint8(SourceMode.LOWEST_PRICE)) { - sourceTokenIndices[0] = type(uint8).max; - } else { - // LOWEST_SEED - sourceTokenIndices[0] = type(uint8).max - 1; - } + uint8[] memory sourceTokenIndices = _getSourceTokenIndices(p.sourceMode); // create per-field-id harvest configs AutomateClaimBlueprint.FieldHarvestConfig[] - memory fieldHarvestConfigs = createFieldHarvestConfigs(minHarvestAmount); + memory fieldHarvestConfigs = createFieldHarvestConfigs(p.minHarvestAmount); // Create AutomateClaimParams struct AutomateClaimBlueprint.AutomateClaimParams memory automateClaimParams = AutomateClaimBlueprint.AutomateClaimParams({ - minMowAmount: minMowAmount, - minTwaDeltaB: minTwaDeltaB, - minPlantAmount: minPlantAmount, + minMowAmount: p.minMowAmount, + minTwaDeltaB: p.minTwaDeltaB, + minPlantAmount: p.minPlantAmount, fieldHarvestConfigs: fieldHarvestConfigs, + minRinseAmount: p.minRinseAmount, sourceTokenIndices: sourceTokenIndices, - maxGrownStalkPerBdv: maxGrownStalkPerBdv, + maxGrownStalkPerBdv: p.maxGrownStalkPerBdv, slippageRatio: 0.01e18 // 1% }); @@ -528,10 +503,11 @@ contract TractorTestHelper is TestHelper { AutomateClaimBlueprint.OperatorParamsExtended memory opParamsExtended = createOperatorParamsExtended( whitelistedOps, - tipAddress, - mowTipAmount, - plantTipAmount, - harvestTipAmount + p.tipAddress, + p.mowTipAmount, + p.plantTipAmount, + p.harvestTipAmount, + p.rinseTipAmount ); return @@ -541,6 +517,22 @@ contract TractorTestHelper is TestHelper { }); } + function _getSourceTokenIndices( + SourceMode sourceMode + ) internal view returns (uint8[] memory sourceTokenIndices) { + sourceTokenIndices = new uint8[](1); + if (sourceMode == SourceMode.PURE_PINTO) { + sourceTokenIndices[0] = tractorHelpers.getTokenIndex( + IMockFBeanstalk(address(bs)).getBeanToken() + ); + } else if (sourceMode == SourceMode.LOWEST_PRICE) { + sourceTokenIndices[0] = type(uint8).max; + } else { + // LOWEST_SEED + sourceTokenIndices[0] = type(uint8).max - 1; + } + } + function createFieldHarvestConfigs( uint256 minHarvestAmount ) @@ -593,7 +585,8 @@ contract TractorTestHelper is TestHelper { address tipAddress, int256 mowTipAmount, int256 plantTipAmount, - int256 harvestTipAmount + int256 harvestTipAmount, + int256 rinseTipAmount ) internal view returns (AutomateClaimBlueprint.OperatorParamsExtended memory) { // create OperatorParams struct BlueprintBase.OperatorParams memory opParams = BlueprintBase.OperatorParams({ @@ -608,9 +601,44 @@ contract TractorTestHelper is TestHelper { baseOpParams: opParams, mowTipAmount: mowTipAmount, plantTipAmount: plantTipAmount, - harvestTipAmount: harvestTipAmount + harvestTipAmount: harvestTipAmount, + rinseTipAmount: rinseTipAmount }); return opParamsExtended; } + + /** + * @notice Create ContractData for rinse operations + * @param fertilizerIds Array of fertilizer IDs to include in rinse data + * @return dynamicData Array of ContractData containing rinse information + */ + function createRinseDynamicData( + uint256[] memory fertilizerIds + ) internal view returns (IMockFBeanstalk.ContractData[] memory dynamicData) { + dynamicData = new IMockFBeanstalk.ContractData[](1); + dynamicData[0] = IMockFBeanstalk.ContractData({ + key: automateClaimBlueprint.RINSE_DATA_KEY(), + value: abi.encode(fertilizerIds) + }); + } + + /** + * @notice Merge harvest and rinse dynamic data arrays + * @param harvestData Dynamic data for harvest operations + * @param rinseData Dynamic data for rinse operations + * @return merged Combined dynamic data array + */ + function mergeHarvestAndRinseDynamicData( + IMockFBeanstalk.ContractData[] memory harvestData, + IMockFBeanstalk.ContractData[] memory rinseData + ) internal pure returns (IMockFBeanstalk.ContractData[] memory merged) { + merged = new IMockFBeanstalk.ContractData[](harvestData.length + rinseData.length); + for (uint256 i = 0; i < harvestData.length; i++) { + merged[i] = harvestData[i]; + } + for (uint256 i = 0; i < rinseData.length; i++) { + merged[harvestData.length + i] = rinseData[i]; + } + } } From 8d2641aba5842053ff33e59ae24c5f59498966c1 Mon Sep 17 00:00:00 2001 From: exTypen <242232957+pocikerim@users.noreply.github.com> Date: Tue, 17 Feb 2026 00:19:03 +0300 Subject: [PATCH 3/4] Add SiloPayback unripe claim support to AutomateClaimBlueprint --- .../ecosystem/AutomateClaimBlueprint.sol | 68 +++- .../ecosystem/AutomateClaimBlueprint.t.sol | 300 +++++++++++++++++- test/foundry/utils/TractorTestHelper.sol | 12 +- 3 files changed, 368 insertions(+), 12 deletions(-) diff --git a/contracts/ecosystem/AutomateClaimBlueprint.sol b/contracts/ecosystem/AutomateClaimBlueprint.sol index ec59bc2b..149f8c75 100644 --- a/contracts/ecosystem/AutomateClaimBlueprint.sol +++ b/contracts/ecosystem/AutomateClaimBlueprint.sol @@ -20,6 +20,16 @@ interface IBarnPaybackClaim { ) external view returns (uint256 beans); } +/** + * @dev Minimal interface for SiloPayback's claim and earned functions. + * We cannot import ISiloPayback directly because it defines its own local LibTransfer + * library, which causes a type conflict with the protocol's LibTransfer.To used here. + */ +interface ISiloPaybackClaim { + function claim(address recipient, LibTransfer.To toMode) external; + function earned(address account) external view returns (uint256); +} + /** * @title AutomateClaimBlueprint * @author DefaultJuice @@ -99,8 +109,10 @@ contract AutomateClaimBlueprint is BlueprintBase { uint256 minPlantAmount; // Harvest, per field id FieldHarvestConfig[] fieldHarvestConfigs; - // Rinse (claimFertilized) + // Rinse (BarnPayback.claimFertilized) uint256 minRinseAmount; + // Unripe Claim (SiloPayback.claim) + uint256 minUnripeClaimAmount; // Withdrawal plan parameters for tipping uint8[] sourceTokenIndices; uint256 maxGrownStalkPerBdv; @@ -124,6 +136,7 @@ contract AutomateClaimBlueprint is BlueprintBase { int256 plantTipAmount; int256 harvestTipAmount; int256 rinseTipAmount; + int256 unripeClaimTipAmount; } /** @@ -140,22 +153,27 @@ contract AutomateClaimBlueprint is BlueprintBase { bool shouldPlant; UserFieldHarvestResults[] userFieldHarvestResults; uint256[] rinseFertilizerIds; + uint256 unripeClaimAmount; } // Silo helpers for withdrawal functionality SiloHelpers public immutable siloHelpers; // BarnPayback contract for claiming fertilized beans IBarnPaybackClaim public immutable barnPayback; + // SiloPayback contract for claiming unripe silo rewards + ISiloPaybackClaim public immutable siloPayback; constructor( address _beanstalk, address _owner, address _tractorHelpers, address _siloHelpers, - address _barnPayback + address _barnPayback, + address _siloPayback ) BlueprintBase(_beanstalk, _owner, _tractorHelpers) { siloHelpers = SiloHelpers(_siloHelpers); barnPayback = IBarnPaybackClaim(_barnPayback); + siloPayback = ISiloPaybackClaim(_siloPayback); } /** @@ -176,7 +194,8 @@ contract AutomateClaimBlueprint is BlueprintBase { vars.shouldMow, vars.shouldPlant, vars.userFieldHarvestResults, - vars.rinseFertilizerIds + vars.rinseFertilizerIds, + vars.unripeClaimAmount ) = _getAndValidateUserState(vars.account, beanstalk.time().timestamp, params); // validate order params and revert early if invalid @@ -192,7 +211,8 @@ contract AutomateClaimBlueprint is BlueprintBase { if ( vars.shouldPlant || vars.userFieldHarvestResults.length > 0 || - vars.rinseFertilizerIds.length > 0 + vars.rinseFertilizerIds.length > 0 || + vars.unripeClaimAmount > 0 ) vars.shouldMow = true; // Execute operations in order: mow first (if needed), then plant, then harvest @@ -251,6 +271,18 @@ contract AutomateClaimBlueprint is BlueprintBase { vars.totalBeanTip += params.opParams.rinseTipAmount; } + // Claim unripe rewards (SiloPayback) if the conditions are met + if (vars.unripeClaimAmount > 0) { + // Claim to user's internal balance + // Note: In tractor context, transferToken uses the tractor user as sender, + // so the user must have sufficient external Pinto balance to cover the claim. + siloPayback.claim(vars.account, LibTransfer.To.INTERNAL); + + // Claimed amount goes to internal balance, same flow as harvested/rinsed beans + vars.totalHarvestedBeans += vars.unripeClaimAmount; + vars.totalBeanTip += params.opParams.unripeClaimTipAmount; + } + // Handle tip payment handleBeansAndTip( vars.account, @@ -286,7 +318,8 @@ contract AutomateClaimBlueprint is BlueprintBase { bool shouldMow, bool shouldPlant, UserFieldHarvestResults[] memory userFieldHarvestResults, - uint256[] memory rinseFertilizerIds + uint256[] memory rinseFertilizerIds, + uint256 unripeClaimAmount ) { // get user state @@ -299,6 +332,9 @@ contract AutomateClaimBlueprint is BlueprintBase { // get rinse data from operator-provided transient storage rinseFertilizerIds = _getRinseData(account, params.automateClaimParams.minRinseAmount); + // get unripe claim amount from SiloPayback earned balance + unripeClaimAmount = _getUnripeClaimAmount(account, params.automateClaimParams.minUnripeClaimAmount); + // validate params - only revert if none of the conditions are met shouldMow = _checkMowConditions( params.automateClaimParams.minTwaDeltaB, @@ -309,11 +345,11 @@ contract AutomateClaimBlueprint is BlueprintBase { shouldPlant = totalPlantableBeans >= params.automateClaimParams.minPlantAmount; require( - shouldMow || shouldPlant || userFieldHarvestResults.length > 0 || rinseFertilizerIds.length > 0, + shouldMow || shouldPlant || userFieldHarvestResults.length > 0 || rinseFertilizerIds.length > 0 || unripeClaimAmount > 0, "AutomateClaimBlueprint: None of the order conditions are met" ); - return (shouldMow, shouldPlant, userFieldHarvestResults, rinseFertilizerIds); + return (shouldMow, shouldPlant, userFieldHarvestResults, rinseFertilizerIds, unripeClaimAmount); } /** @@ -439,6 +475,24 @@ contract AutomateClaimBlueprint is BlueprintBase { return fertilizerIds; } + /** + * @notice Checks if user has enough earned unripe rewards to claim + * @dev Returns 0 if earned amount is below threshold (soft skip, no revert) + * @param account The account to check earned rewards for + * @param minUnripeClaimAmount The minimum earned amount threshold + * @return earnedAmount The earned amount if above threshold, 0 otherwise + */ + function _getUnripeClaimAmount( + address account, + uint256 minUnripeClaimAmount + ) internal view returns (uint256) { + uint256 earnedAmount = siloPayback.earned(account); + if (earnedAmount < minUnripeClaimAmount) { + return 0; + } + return earnedAmount; + } + /** * @notice Handles tip payment * @param account The account to withdraw for diff --git a/test/foundry/ecosystem/AutomateClaimBlueprint.t.sol b/test/foundry/ecosystem/AutomateClaimBlueprint.t.sol index 474aff1c..c8e048c2 100644 --- a/test/foundry/ecosystem/AutomateClaimBlueprint.t.sol +++ b/test/foundry/ecosystem/AutomateClaimBlueprint.t.sol @@ -14,6 +14,7 @@ import {IBeanstalk} from "contracts/interfaces/IBeanstalk.sol"; import {AutomateClaimBlueprint, IBarnPaybackClaim} from "contracts/ecosystem/AutomateClaimBlueprint.sol"; import {BarnPayback} from "contracts/ecosystem/beanstalkShipments/barn/BarnPayback.sol"; import {BeanstalkFertilizer} from "contracts/ecosystem/beanstalkShipments/barn/BeanstalkFertilizer.sol"; +import {SiloPayback} from "contracts/ecosystem/beanstalkShipments/SiloPayback.sol"; import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "forge-std/console.sol"; @@ -21,16 +22,24 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { address[] farmers; BeanstalkPrice beanstalkPrice; BarnPayback barnPayback; + SiloPayback siloPaybackContract; event Plant(address indexed account, uint256 beans); event Harvest(address indexed account, uint256 fieldId, uint256[] plots, uint256 beans); event ClaimFertilizer(uint256[] ids, uint256 beans); + event SiloPaybackRewardsClaimed( + address indexed account, + address indexed recipient, + uint256 amount, + LibTransfer.To toMode + ); uint256 STALK_DECIMALS = 1e10; int256 DEFAULT_TIP_AMOUNT = 10e6; // 10 BEAN uint256 constant MAX_GROWN_STALK_PER_BDV = 1000e16; // Stalk is 1e16 uint256 UNEXECUTABLE_MIN_HARVEST_AMOUNT = 1_000_000_000e6; // 1B BEAN uint256 UNEXECUTABLE_MIN_RINSE_AMOUNT = type(uint256).max; + uint256 UNEXECUTABLE_MIN_UNRIPE_CLAIM_AMOUNT = type(uint256).max; uint256 PODS_FIELD_0 = 1000100000; uint256 PODS_FIELD_1 = 250e6; @@ -78,13 +87,18 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { barnPayback = _deployBarnPayback(); vm.label(address(barnPayback), "BarnPayback"); - // Deploy AutomateClaimBlueprint with TractorHelpers, SiloHelpers and BarnPayback addresses + // Deploy SiloPayback (proxy pattern) + siloPaybackContract = _deploySiloPayback(); + vm.label(address(siloPaybackContract), "SiloPayback"); + + // Deploy AutomateClaimBlueprint with TractorHelpers, SiloHelpers, BarnPayback and SiloPayback addresses automateClaimBlueprint = new AutomateClaimBlueprint( address(bs), address(this), address(tractorHelpers), address(siloHelpers), - address(barnPayback) + address(barnPayback), + address(siloPaybackContract) ); vm.label(address(automateClaimBlueprint), "AutomateClaimBlueprint"); @@ -176,11 +190,13 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { minPlantAmount: type(uint256).max, minHarvestAmount: UNEXECUTABLE_MIN_HARVEST_AMOUNT, minRinseAmount: UNEXECUTABLE_MIN_RINSE_AMOUNT, + minUnripeClaimAmount: UNEXECUTABLE_MIN_UNRIPE_CLAIM_AMOUNT, tipAddress: state.operator, mowTipAmount: state.mowTipAmount, plantTipAmount: state.plantTipAmount, harvestTipAmount: state.harvestTipAmount, rinseTipAmount: 0, + unripeClaimTipAmount: 0, maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV }) ); @@ -230,11 +246,13 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { minPlantAmount: type(uint256).max, minHarvestAmount: UNEXECUTABLE_MIN_HARVEST_AMOUNT, minRinseAmount: UNEXECUTABLE_MIN_RINSE_AMOUNT, + minUnripeClaimAmount: UNEXECUTABLE_MIN_UNRIPE_CLAIM_AMOUNT, tipAddress: state.operator, mowTipAmount: state.mowTipAmount, plantTipAmount: state.plantTipAmount, harvestTipAmount: state.harvestTipAmount, rinseTipAmount: 0, + unripeClaimTipAmount: 0, maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV }) ); @@ -273,11 +291,13 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { minPlantAmount: 11e6, minHarvestAmount: UNEXECUTABLE_MIN_HARVEST_AMOUNT, minRinseAmount: UNEXECUTABLE_MIN_RINSE_AMOUNT, + minUnripeClaimAmount: UNEXECUTABLE_MIN_UNRIPE_CLAIM_AMOUNT, tipAddress: state.operator, mowTipAmount: state.mowTipAmount, plantTipAmount: state.plantTipAmount, harvestTipAmount: state.harvestTipAmount, rinseTipAmount: 0, + unripeClaimTipAmount: 0, maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV }) ); @@ -325,11 +345,13 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { minPlantAmount: 11e6, minHarvestAmount: 11e6, minRinseAmount: UNEXECUTABLE_MIN_RINSE_AMOUNT, + minUnripeClaimAmount: UNEXECUTABLE_MIN_UNRIPE_CLAIM_AMOUNT, tipAddress: state.operator, mowTipAmount: state.mowTipAmount, plantTipAmount: state.plantTipAmount, harvestTipAmount: state.harvestTipAmount, rinseTipAmount: 0, + unripeClaimTipAmount: 0, maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV }) ); @@ -378,11 +400,13 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { minPlantAmount: 11e6, minHarvestAmount: 11e6, minRinseAmount: UNEXECUTABLE_MIN_RINSE_AMOUNT, + minUnripeClaimAmount: UNEXECUTABLE_MIN_UNRIPE_CLAIM_AMOUNT, tipAddress: state.operator, mowTipAmount: state.mowTipAmount, plantTipAmount: state.plantTipAmount, harvestTipAmount: state.harvestTipAmount, rinseTipAmount: 0, + unripeClaimTipAmount: 0, maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV }) ); @@ -447,11 +471,13 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { minPlantAmount: 11e6, minHarvestAmount: 11e6, minRinseAmount: UNEXECUTABLE_MIN_RINSE_AMOUNT, + minUnripeClaimAmount: UNEXECUTABLE_MIN_UNRIPE_CLAIM_AMOUNT, tipAddress: state.operator, mowTipAmount: state.mowTipAmount, plantTipAmount: state.plantTipAmount, harvestTipAmount: state.harvestTipAmount, rinseTipAmount: 0, + unripeClaimTipAmount: 0, maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV }) ); @@ -793,11 +819,13 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { minPlantAmount: type(uint256).max, minHarvestAmount: UNEXECUTABLE_MIN_HARVEST_AMOUNT, minRinseAmount: 1, + minUnripeClaimAmount: UNEXECUTABLE_MIN_UNRIPE_CLAIM_AMOUNT, tipAddress: state.operator, mowTipAmount: state.mowTipAmount, plantTipAmount: state.plantTipAmount, harvestTipAmount: state.harvestTipAmount, rinseTipAmount: DEFAULT_TIP_AMOUNT, + unripeClaimTipAmount: 0, maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV }) ); @@ -857,11 +885,13 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { minPlantAmount: type(uint256).max, minHarvestAmount: 11e6, minRinseAmount: 1, + minUnripeClaimAmount: UNEXECUTABLE_MIN_UNRIPE_CLAIM_AMOUNT, tipAddress: state.operator, mowTipAmount: state.mowTipAmount, plantTipAmount: state.plantTipAmount, harvestTipAmount: state.harvestTipAmount, rinseTipAmount: DEFAULT_TIP_AMOUNT, + unripeClaimTipAmount: 0, maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV }) ); @@ -913,11 +943,13 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { minPlantAmount: type(uint256).max, minHarvestAmount: UNEXECUTABLE_MIN_HARVEST_AMOUNT, minRinseAmount: 1, + minUnripeClaimAmount: UNEXECUTABLE_MIN_UNRIPE_CLAIM_AMOUNT, tipAddress: state.operator, mowTipAmount: state.mowTipAmount, plantTipAmount: state.plantTipAmount, harvestTipAmount: state.harvestTipAmount, rinseTipAmount: DEFAULT_TIP_AMOUNT, + unripeClaimTipAmount: 0, maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV }) ); @@ -961,11 +993,13 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { minPlantAmount: type(uint256).max, minHarvestAmount: UNEXECUTABLE_MIN_HARVEST_AMOUNT, minRinseAmount: claimableAmount + 1, + minUnripeClaimAmount: UNEXECUTABLE_MIN_UNRIPE_CLAIM_AMOUNT, tipAddress: state.operator, mowTipAmount: state.mowTipAmount, plantTipAmount: state.plantTipAmount, harvestTipAmount: state.harvestTipAmount, rinseTipAmount: DEFAULT_TIP_AMOUNT, + unripeClaimTipAmount: 0, maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV }) ); @@ -1003,11 +1037,13 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { minPlantAmount: type(uint256).max, minHarvestAmount: UNEXECUTABLE_MIN_HARVEST_AMOUNT, minRinseAmount: 1, + minUnripeClaimAmount: UNEXECUTABLE_MIN_UNRIPE_CLAIM_AMOUNT, tipAddress: state.operator, mowTipAmount: state.mowTipAmount, plantTipAmount: state.plantTipAmount, harvestTipAmount: state.harvestTipAmount, rinseTipAmount: DEFAULT_TIP_AMOUNT, + unripeClaimTipAmount: 0, maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV }) ); @@ -1021,4 +1057,264 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { vm.expectRevert("AutomateClaimBlueprint: None of the order conditions are met"); executeRequisitionWithDynamicData(state.operator, req, address(bs), dynamicData); } + + /////////////////////////// SILO PAYBACK HELPERS /////////////////////////// + + /** + * @notice Deploy SiloPayback contract via proxy pattern (same as BarnPayback) + */ + function _deploySiloPayback() internal returns (SiloPayback) { + SiloPayback implementation = new SiloPayback(); + bytes memory data = abi.encodeWithSelector( + SiloPayback.initialize.selector, + address(BEAN), + address(BEANSTALK) + ); + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(implementation), + address(this), + data + ); + return SiloPayback(address(proxy)); + } + + /** + * @notice Setup unripe claim conditions for a user + * @dev Mints UnripeBDV tokens to the user, sends Pinto rewards to SiloPayback, + * and funds the user's external bean balance to cover the claim via tractor. + * Same external balance funding pattern as _setupRinseConditions. + */ + function _setupUnripeClaimConditions(address user) internal { + // Mint UnripeBDV tokens to the user + SiloPayback.UnripeBdvTokenData[] memory data = new SiloPayback.UnripeBdvTokenData[](1); + data[0] = SiloPayback.UnripeBdvTokenData({receipient: user, bdv: 1000e6}); + siloPaybackContract.batchMint(data); + + // Send Pinto rewards to SiloPayback to create earned rewards + uint256 rewardAmount = 100e6; + deal(address(BEAN), address(deployer), rewardAmount); + vm.prank(deployer); + IERC20(address(BEAN)).transfer(address(siloPaybackContract), rewardAmount); + vm.prank(address(BEANSTALK)); + siloPaybackContract.siloPaybackReceive(rewardAmount); + + // Fund user's external bean balance to cover claim via tractor + // (same pattern as _setupRinseConditions - transferToken uses tractor user as sender) + uint256 earnedAmount = siloPaybackContract.earned(user); + mintTokensToUser(user, address(BEAN), earnedAmount); + } + + /////////////////////////// UNRIPE CLAIM TESTS /////////////////////////// + + function test_automateClaimBlueprint_unripeClaim_success() public { + // Setup test state (no plant, no harvest, above peg) + TestState memory state = setupAutomateClaimBlueprintTest(false, false, false, true); + + // Setup unripe claim conditions + _setupUnripeClaimConditions(state.user); + + // Verify user has earned rewards + uint256 earnedBefore = siloPaybackContract.earned(state.user); + assertGt(earnedBefore, 0, "user should have earned unripe rewards"); + + // Get user BDV before claim + uint256 userTotalBdvBefore = bs.balanceOfDepositedBdv(state.user, state.beanToken); + + // Setup blueprint with unripe claim enabled (minUnripeClaimAmount = 1) + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( + AutomateClaimSetupParams({ + account: state.user, + sourceMode: SourceMode.PURE_PINTO, + minMowAmount: 1 * STALK_DECIMALS, + minTwaDeltaB: 10e6, + minPlantAmount: type(uint256).max, + minHarvestAmount: UNEXECUTABLE_MIN_HARVEST_AMOUNT, + minRinseAmount: UNEXECUTABLE_MIN_RINSE_AMOUNT, + minUnripeClaimAmount: 1, + tipAddress: state.operator, + mowTipAmount: state.mowTipAmount, + plantTipAmount: state.plantTipAmount, + harvestTipAmount: state.harvestTipAmount, + rinseTipAmount: 0, + unripeClaimTipAmount: DEFAULT_TIP_AMOUNT, + maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV + }) + ); + + // Execute with harvest data (unripe claim does not need dynamic data) + vm.expectEmit(); + emit SiloPaybackRewardsClaimed(state.user, state.user, earnedBefore, LibTransfer.To.INTERNAL); + executeWithHarvestData(state.operator, state.user, req); + + // Verify earned rewards are now 0 + uint256 earnedAfter = siloPaybackContract.earned(state.user); + assertEq(earnedAfter, 0, "earned rewards should be 0 after claim"); + + // Verify BDV increased (claimed beans deposited to silo) + uint256 userTotalBdvAfter = bs.balanceOfDepositedBdv(state.user, state.beanToken); + assertGt(userTotalBdvAfter, userTotalBdvBefore, "userTotalBdv should increase from unripe claim"); + } + + function test_automateClaimBlueprint_unripeClaim_belowMinimum() public { + // Setup test state (no plant, no harvest, above peg) + TestState memory state = setupAutomateClaimBlueprintTest(false, false, false, true); + + // Setup unripe claim conditions + _setupUnripeClaimConditions(state.user); + + // Verify user has earned rewards + uint256 earnedAmount = siloPaybackContract.earned(state.user); + assertGt(earnedAmount, 0, "user should have earned unripe rewards"); + + // Setup blueprint with minUnripeClaimAmount higher than earned (claim skipped) + // Also disable mow/plant/harvest/rinse so we expect a full revert + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( + AutomateClaimSetupParams({ + account: state.user, + sourceMode: SourceMode.PURE_PINTO, + minMowAmount: type(uint256).max, + minTwaDeltaB: 10e6, + minPlantAmount: type(uint256).max, + minHarvestAmount: UNEXECUTABLE_MIN_HARVEST_AMOUNT, + minRinseAmount: UNEXECUTABLE_MIN_RINSE_AMOUNT, + minUnripeClaimAmount: earnedAmount + 1, + tipAddress: state.operator, + mowTipAmount: state.mowTipAmount, + plantTipAmount: state.plantTipAmount, + harvestTipAmount: state.harvestTipAmount, + rinseTipAmount: 0, + unripeClaimTipAmount: DEFAULT_TIP_AMOUNT, + maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV + }) + ); + + // Pre-calculate dynamic data BEFORE expectRevert + IMockFBeanstalk.ContractData[] memory dynamicData = getHarvestDynamicDataForUser( + state.user + ); + + // Execute - should revert since all conditions are skipped + vm.expectRevert("AutomateClaimBlueprint: None of the order conditions are met"); + executeRequisitionWithDynamicData(state.operator, req, address(bs), dynamicData); + } + + function test_automateClaimBlueprint_unripeClaim_withHarvestAndRinse() public { + // Setup test state for harvesting (twoFields=true for deterministic field 1 harvest) + TestState memory state = setupAutomateClaimBlueprintTest(false, true, true, true); + + // Also setup rinse conditions + _setupRinseConditions(state.user); + + // Also setup unripe claim conditions + _setupUnripeClaimConditions(state.user); + + // Advance season to print beans for harvest + advanceSeason(); + + // Get user state before operations + uint256 userTotalBdvBefore = bs.balanceOfDepositedBdv(state.user, state.beanToken); + + // Verify harvestable pods exist + (uint256 harvestablePods, ) = _userHarvestablePods(state.user, DEFAULT_FIELD_ID); + assertGt(harvestablePods, 0, "user should have harvestable pods"); + + // Verify claimable rinse beans exist + uint256[] memory fertIds = new uint256[](1); + fertIds[0] = FERT_ID_1; + uint256 claimableRinse = barnPayback.balanceOfFertilized(state.user, fertIds); + assertGt(claimableRinse, 0, "user should have claimable fertilized beans"); + + // Verify earned unripe rewards exist + uint256 earnedBefore = siloPaybackContract.earned(state.user); + assertGt(earnedBefore, 0, "user should have earned unripe rewards"); + + // Fund extra external beans to cover both rinse and unripe claim via tractor. + // Both operations pull from user's external balance (transferToken uses tractor user as sender). + // Individual setup helpers fund their own amounts, but when combined we need the total. + mintTokensToUser(state.user, state.beanToken, claimableRinse + earnedBefore); + + // Setup blueprint with harvest + rinse + unripe claim all enabled + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( + AutomateClaimSetupParams({ + account: state.user, + sourceMode: SourceMode.PURE_PINTO, + minMowAmount: 1 * STALK_DECIMALS, + minTwaDeltaB: 10e6, + minPlantAmount: type(uint256).max, + minHarvestAmount: 11e6, + minRinseAmount: 1, + minUnripeClaimAmount: 1, + tipAddress: state.operator, + mowTipAmount: state.mowTipAmount, + plantTipAmount: state.plantTipAmount, + harvestTipAmount: state.harvestTipAmount, + rinseTipAmount: DEFAULT_TIP_AMOUNT, + unripeClaimTipAmount: DEFAULT_TIP_AMOUNT, + maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV + }) + ); + + // Execute with harvest and rinse data + executeWithHarvestAndRinseData(state.operator, state.user, req, fertIds); + + // Verify BDV increased (harvested + rinsed + claimed beans deposited) + uint256 userTotalBdvAfter = bs.balanceOfDepositedBdv(state.user, state.beanToken); + assertGt( + userTotalBdvAfter, + userTotalBdvBefore, + "userTotalBdv should increase from harvest + rinse + unripe claim" + ); + + // Verify rinse fully claimed + uint256 postRinseBalance = barnPayback.balanceOfFertilized(state.user, fertIds); + assertEq(postRinseBalance, 0, "rinse should be fully claimed"); + + // Verify harvest complete on field 0 + assertNoHarvestablePods(state.user, DEFAULT_FIELD_ID); + + // Verify unripe claim rewards are now 0 + uint256 earnedAfter = siloPaybackContract.earned(state.user); + assertEq(earnedAfter, 0, "earned rewards should be 0 after claim"); + } + + function test_automateClaimBlueprint_unripeClaim_noBalance() public { + // Setup test state (no plant, no harvest, above peg) + TestState memory state = setupAutomateClaimBlueprintTest(false, false, false, true); + + // Do NOT setup unripe claim conditions (no UnripeBDV tokens) + // Verify user has no earned rewards + uint256 earnedAmount = siloPaybackContract.earned(state.user); + assertEq(earnedAmount, 0, "user should have no earned unripe rewards"); + + // Setup blueprint with minUnripeClaimAmount = 1 but user has 0 earned + // Also disable mow/plant/harvest/rinse so we expect a full revert + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( + AutomateClaimSetupParams({ + account: state.user, + sourceMode: SourceMode.PURE_PINTO, + minMowAmount: type(uint256).max, + minTwaDeltaB: 10e6, + minPlantAmount: type(uint256).max, + minHarvestAmount: UNEXECUTABLE_MIN_HARVEST_AMOUNT, + minRinseAmount: UNEXECUTABLE_MIN_RINSE_AMOUNT, + minUnripeClaimAmount: 1, + tipAddress: state.operator, + mowTipAmount: state.mowTipAmount, + plantTipAmount: state.plantTipAmount, + harvestTipAmount: state.harvestTipAmount, + rinseTipAmount: 0, + unripeClaimTipAmount: 0, + maxGrownStalkPerBdv: MAX_GROWN_STALK_PER_BDV + }) + ); + + // Pre-calculate dynamic data BEFORE expectRevert + IMockFBeanstalk.ContractData[] memory dynamicData = getHarvestDynamicDataForUser( + state.user + ); + + // Execute - should revert since earned = 0 < minUnripeClaimAmount = 1 + vm.expectRevert("AutomateClaimBlueprint: None of the order conditions are met"); + executeRequisitionWithDynamicData(state.operator, req, address(bs), dynamicData); + } } diff --git a/test/foundry/utils/TractorTestHelper.sol b/test/foundry/utils/TractorTestHelper.sol index 46a7561b..74e792fe 100644 --- a/test/foundry/utils/TractorTestHelper.sol +++ b/test/foundry/utils/TractorTestHelper.sol @@ -438,11 +438,13 @@ contract TractorTestHelper is TestHelper { uint256 minPlantAmount; uint256 minHarvestAmount; uint256 minRinseAmount; + uint256 minUnripeClaimAmount; address tipAddress; int256 mowTipAmount; int256 plantTipAmount; int256 harvestTipAmount; int256 rinseTipAmount; + int256 unripeClaimTipAmount; uint256 maxGrownStalkPerBdv; } @@ -494,6 +496,7 @@ contract TractorTestHelper is TestHelper { minPlantAmount: p.minPlantAmount, fieldHarvestConfigs: fieldHarvestConfigs, minRinseAmount: p.minRinseAmount, + minUnripeClaimAmount: p.minUnripeClaimAmount, sourceTokenIndices: sourceTokenIndices, maxGrownStalkPerBdv: p.maxGrownStalkPerBdv, slippageRatio: 0.01e18 // 1% @@ -507,7 +510,8 @@ contract TractorTestHelper is TestHelper { p.mowTipAmount, p.plantTipAmount, p.harvestTipAmount, - p.rinseTipAmount + p.rinseTipAmount, + p.unripeClaimTipAmount ); return @@ -586,7 +590,8 @@ contract TractorTestHelper is TestHelper { int256 mowTipAmount, int256 plantTipAmount, int256 harvestTipAmount, - int256 rinseTipAmount + int256 rinseTipAmount, + int256 unripeClaimTipAmount ) internal view returns (AutomateClaimBlueprint.OperatorParamsExtended memory) { // create OperatorParams struct BlueprintBase.OperatorParams memory opParams = BlueprintBase.OperatorParams({ @@ -602,7 +607,8 @@ contract TractorTestHelper is TestHelper { mowTipAmount: mowTipAmount, plantTipAmount: plantTipAmount, harvestTipAmount: harvestTipAmount, - rinseTipAmount: rinseTipAmount + rinseTipAmount: rinseTipAmount, + unripeClaimTipAmount: unripeClaimTipAmount }); return opParamsExtended; From 3e34028d633a56fded49be8e32f81d091134868b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 17 Feb 2026 07:22:05 +0000 Subject: [PATCH 4/4] auto-format: prettier formatting for Solidity files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../ecosystem/AutomateClaimBlueprint.sol | 22 ++++++++++++++----- .../ecosystem/AutomateClaimBlueprint.t.sol | 13 +++++++++-- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/contracts/ecosystem/AutomateClaimBlueprint.sol b/contracts/ecosystem/AutomateClaimBlueprint.sol index 149f8c75..988b1a1e 100644 --- a/contracts/ecosystem/AutomateClaimBlueprint.sol +++ b/contracts/ecosystem/AutomateClaimBlueprint.sol @@ -52,8 +52,7 @@ contract AutomateClaimBlueprint is BlueprintBase { /** * @dev Key for operator-provided rinse data in transient storage */ - uint256 public constant RINSE_DATA_KEY = - uint256(keccak256("AutomateClaimBlueprint.rinseData")); + uint256 public constant RINSE_DATA_KEY = uint256(keccak256("AutomateClaimBlueprint.rinseData")); /** * @notice Main struct for automate claim blueprint @@ -333,7 +332,10 @@ contract AutomateClaimBlueprint is BlueprintBase { rinseFertilizerIds = _getRinseData(account, params.automateClaimParams.minRinseAmount); // get unripe claim amount from SiloPayback earned balance - unripeClaimAmount = _getUnripeClaimAmount(account, params.automateClaimParams.minUnripeClaimAmount); + unripeClaimAmount = _getUnripeClaimAmount( + account, + params.automateClaimParams.minUnripeClaimAmount + ); // validate params - only revert if none of the conditions are met shouldMow = _checkMowConditions( @@ -345,11 +347,21 @@ contract AutomateClaimBlueprint is BlueprintBase { shouldPlant = totalPlantableBeans >= params.automateClaimParams.minPlantAmount; require( - shouldMow || shouldPlant || userFieldHarvestResults.length > 0 || rinseFertilizerIds.length > 0 || unripeClaimAmount > 0, + shouldMow || + shouldPlant || + userFieldHarvestResults.length > 0 || + rinseFertilizerIds.length > 0 || + unripeClaimAmount > 0, "AutomateClaimBlueprint: None of the order conditions are met" ); - return (shouldMow, shouldPlant, userFieldHarvestResults, rinseFertilizerIds, unripeClaimAmount); + return ( + shouldMow, + shouldPlant, + userFieldHarvestResults, + rinseFertilizerIds, + unripeClaimAmount + ); } /** diff --git a/test/foundry/ecosystem/AutomateClaimBlueprint.t.sol b/test/foundry/ecosystem/AutomateClaimBlueprint.t.sol index c8e048c2..1a006312 100644 --- a/test/foundry/ecosystem/AutomateClaimBlueprint.t.sol +++ b/test/foundry/ecosystem/AutomateClaimBlueprint.t.sol @@ -1143,7 +1143,12 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { // Execute with harvest data (unripe claim does not need dynamic data) vm.expectEmit(); - emit SiloPaybackRewardsClaimed(state.user, state.user, earnedBefore, LibTransfer.To.INTERNAL); + emit SiloPaybackRewardsClaimed( + state.user, + state.user, + earnedBefore, + LibTransfer.To.INTERNAL + ); executeWithHarvestData(state.operator, state.user, req); // Verify earned rewards are now 0 @@ -1152,7 +1157,11 @@ contract AutomateClaimBlueprintTest is TractorTestHelper { // Verify BDV increased (claimed beans deposited to silo) uint256 userTotalBdvAfter = bs.balanceOfDepositedBdv(state.user, state.beanToken); - assertGt(userTotalBdvAfter, userTotalBdvBefore, "userTotalBdv should increase from unripe claim"); + assertGt( + userTotalBdvAfter, + userTotalBdvBefore, + "userTotalBdv should increase from unripe claim" + ); } function test_automateClaimBlueprint_unripeClaim_belowMinimum() public {