diff --git a/contracts/ecosystem/MowPlantHarvestBlueprint.sol b/contracts/ecosystem/AutomateClaimBlueprint.sol similarity index 68% rename from contracts/ecosystem/MowPlantHarvestBlueprint.sol rename to contracts/ecosystem/AutomateClaimBlueprint.sol index 0bf3fcca..988b1a1e 100644 --- a/contracts/ecosystem/MowPlantHarvestBlueprint.sol +++ b/contracts/ecosystem/AutomateClaimBlueprint.sol @@ -8,11 +8,34 @@ import {SiloHelpers} from "contracts/ecosystem/tractor/utils/SiloHelpers.sol"; import {BlueprintBase} from "./BlueprintBase.sol"; /** - * @title MowPlantHarvestBlueprint + * @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); +} + +/** + * @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 - * @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 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 */ @@ -21,18 +44,23 @@ contract MowPlantHarvestBlueprint 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 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 +100,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; @@ -80,6 +108,10 @@ contract MowPlantHarvestBlueprint is BlueprintBase { uint256 minPlantAmount; // Harvest, per field id FieldHarvestConfig[] fieldHarvestConfigs; + // Rinse (BarnPayback.claimFertilized) + uint256 minRinseAmount; + // Unripe Claim (SiloPayback.claim) + uint256 minUnripeClaimAmount; // Withdrawal plan parameters for tipping uint8[] sourceTokenIndices; uint256 maxGrownStalkPerBdv; @@ -102,13 +134,15 @@ contract MowPlantHarvestBlueprint is BlueprintBase { int256 mowTipAmount; int256 plantTipAmount; int256 harvestTipAmount; + int256 rinseTipAmount; + int256 unripeClaimTipAmount; } /** * @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; @@ -117,51 +151,68 @@ contract MowPlantHarvestBlueprint is BlueprintBase { bool shouldMow; 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 _siloHelpers, + address _barnPayback, + address _siloPayback ) BlueprintBase(_beanstalk, _owner, _tractorHelpers) { siloHelpers = SiloHelpers(_siloHelpers); + barnPayback = IBarnPaybackClaim(_barnPayback); + siloPayback = ISiloPaybackClaim(_siloPayback); } /** - * @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(); // 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, + vars.unripeClaimAmount + ) = _getAndValidateUserState(vars.account, beanstalk.time().timestamp, params); // 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) 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.unripeClaimAmount > 0 + ) vars.shouldMow = true; // Execute operations in order: mow first (if needed), then plant, then harvest if (vars.shouldMow) { @@ -188,7 +239,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 @@ -198,17 +249,50 @@ contract MowPlantHarvestBlueprint 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; + } + + // 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, 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 ); } @@ -220,18 +304,21 @@ contract MowPlantHarvestBlueprint 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, uint256 previousSeasonTimestamp, - MowPlantHarvestBlueprintStruct calldata params + AutomateClaimBlueprintStruct calldata params ) internal view returns ( bool shouldMow, bool shouldPlant, - UserFieldHarvestResults[] memory userFieldHarvestResults + UserFieldHarvestResults[] memory userFieldHarvestResults, + uint256[] memory rinseFertilizerIds, + uint256 unripeClaimAmount ) { // get user state @@ -239,23 +326,42 @@ contract MowPlantHarvestBlueprint is BlueprintBase { uint256 totalClaimableStalk, uint256 totalPlantableBeans, UserFieldHarvestResults[] memory userFieldHarvestResults - ) = _getUserState(account, params.mowPlantHarvestParams.fieldHarvestConfigs); + ) = _getUserState(account, params.automateClaimParams.fieldHarvestConfigs); + + // 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.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" + shouldMow || + shouldPlant || + userFieldHarvestResults.length > 0 || + rinseFertilizerIds.length > 0 || + unripeClaimAmount > 0, + "AutomateClaimBlueprint: None of the order conditions are met" ); - return (shouldMow, shouldPlant, userFieldHarvestResults); + return ( + shouldMow, + shouldPlant, + userFieldHarvestResults, + rinseFertilizerIds, + unripeClaimAmount + ); } /** @@ -349,6 +455,56 @@ contract MowPlantHarvestBlueprint 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 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 new file mode 100644 index 00000000..1a006312 --- /dev/null +++ b/test/foundry/ecosystem/AutomateClaimBlueprint.t.sol @@ -0,0 +1,1329 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.0 <0.9.0; +pragma abicoder v2; + +import {TestHelper, LibTransfer, C, IMockFBeanstalk} from "test/foundry/utils/TestHelper.sol"; +import {MockToken} from "contracts/mocks/MockToken.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {TractorHelpers} from "contracts/ecosystem/tractor/utils/TractorHelpers.sol"; +import {SiloHelpers} from "contracts/ecosystem/tractor/utils/SiloHelpers.sol"; +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, 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"; + +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; + + // 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; + address beanToken; + uint256 initialUserBeanBalance; + uint256 initialOperatorBeanBalance; + uint256 mintAmount; + int256 mowTipAmount; + int256 plantTipAmount; + int256 harvestTipAmount; + } + + function setUp() public { + initializeBeanstalkTestState(true, false); + farmers = createUsers(2); + vm.label(farmers[0], "Farmer 1"); + vm.label(farmers[1], "Farmer 2"); + + // Deploy BeanstalkPrice (unused here but needed for TractorHelpers) + beanstalkPrice = new BeanstalkPrice(address(bs)); + vm.label(address(beanstalkPrice), "BeanstalkPrice"); + + // Deploy TractorHelpers (2 args: beanstalk, beanstalkPrice) + tractorHelpers = new TractorHelpers(address(bs), address(beanstalkPrice)); + vm.label(address(tractorHelpers), "TractorHelpers"); + + // Deploy SiloHelpers (3 args: beanstalk, tractorHelpers, priceManipulation) + siloHelpers = new SiloHelpers( + address(bs), + address(tractorHelpers), + address(0) // priceManipulation not needed for this test + ); + vm.label(address(siloHelpers), "SiloHelpers"); + + // Deploy BarnPayback (proxy pattern) + barnPayback = _deployBarnPayback(); + vm.label(address(barnPayback), "BarnPayback"); + + // 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(siloPaybackContract) + ); + vm.label(address(automateClaimBlueprint), "AutomateClaimBlueprint"); + + setTractorHelpers(address(tractorHelpers)); + setAutomateClaimBlueprint(address(automateClaimBlueprint)); + + // Advance season to grow stalk + advanceSeason(); + } + + /** + * @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 setupAutomateClaimBlueprintTest( + bool setupPlant, + bool setupHarvest, + bool twoFields, + bool abovePeg + ) internal returns (TestState memory) { + // Create test state + TestState memory state; + state.user = farmers[0]; + state.operator = address(this); + state.beanToken = bs.getBeanToken(); + state.initialUserBeanBalance = IERC20(state.beanToken).balanceOf(state.user); + state.initialOperatorBeanBalance = bs.getInternalBalance(state.operator, state.beanToken); + state.mintAmount = 110000e6; // 100k for deposit, 10k for sow + state.mowTipAmount = DEFAULT_TIP_AMOUNT; // 10 BEAN + state.plantTipAmount = DEFAULT_TIP_AMOUNT; + state.harvestTipAmount = DEFAULT_TIP_AMOUNT; + + // Mint 2x the amount to ensure we have enough for all test cases + mintTokensToUser(state.user, state.beanToken, state.mintAmount); + // Mint some to farmer 2 for plot tests + mintTokensToUser(farmers[1], state.beanToken, 10000000e6); + + // Deposit beans for user + vm.startPrank(state.user); + IERC20(state.beanToken).approve(address(bs), type(uint256).max); + bs.deposit(state.beanToken, state.mintAmount - 10000e6, uint8(LibTransfer.From.EXTERNAL)); + vm.stopPrank(); + + // For farmer 1, deposit 1000e6 beans, and mint them 1000e6 beans + mintTokensToUser(farmers[1], state.beanToken, 1000e6); + vm.prank(farmers[1]); + bs.deposit(state.beanToken, 1000e6, uint8(LibTransfer.From.EXTERNAL)); + + // Set liquidity in the whitelisted wells to manipulate deltaB + setPegConditions(abovePeg); + + if (setupPlant) skipGermination(); + + if (setupHarvest) setHarvestConditions(state.user, twoFields); + + return state; + } + + /////////////////////////// TESTS /////////////////////////// + + function test_automateClaimBlueprint_Mow() public { + // Setup test state + // setupPlant: false, setupHarvest: false, abovePeg: true + TestState memory state = setupAutomateClaimBlueprintTest(false, false, false, true); + + // Advance season to grow stalk but not enough to plant + advanceSeason(); + vm.warp(block.timestamp + 1 seconds); + + // get user state before mow + uint256 userGrownStalk = bs.balanceOfGrownStalk(state.user, state.beanToken); + // assert user has grown stalk + assertGt(userGrownStalk, 0, "user should have grown stalk to mow"); + // get user total stalk before mow + uint256 userTotalStalkBeforeMow = bs.balanceOfStalk(state.user); + // assert totalDeltaB is greater than 0 + assertGt(bs.totalDeltaB(), 0, "totalDeltaB should be greater than 0"); + + // Setup automateClaimBlueprint + (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: 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 + }) + ); + + // Pre-calculate harvest data BEFORE expectRevert (to avoid consuming the expectation) + IMockFBeanstalk.ContractData[] memory dynamicData = getHarvestDynamicDataForUser( + state.user + ); + + // Try to execute before the last minutes of the season, expect revert + 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 + vm.warp(bs.getNextSeasonStart() - 1 seconds); + executeWithHarvestData(state.operator, state.user, req); + + // assert all grown stalk was mowed + uint256 userGrownStalkAfterMow = bs.balanceOfGrownStalk(state.user, state.beanToken); + assertEq(userGrownStalkAfterMow, 0); + + // get user total stalk after mow + uint256 userTotalStalkAfterMow = bs.balanceOfStalk(state.user); + // assert the user total stalk has increased + assertGt( + userTotalStalkAfterMow, + userTotalStalkBeforeMow, + "userTotalStalk should have increased" + ); + } + + function test_automateClaimBlueprint_plant_revertWhenInsufficientPlantableBeans() public { + // Setup test state for planting + // setupPlant: true, setupHarvest: false, twoFields: true, abovePeg: 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, ) = 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: 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 + }) + ); + + // Pre-calculate harvest data BEFORE expectRevert + IMockFBeanstalk.ContractData[] memory dynamicData = getHarvestDynamicDataForUser( + state.user + ); + + // Execute requisition, expect revert + vm.expectRevert("AutomateClaimBlueprint: None of the order conditions are met"); + executeRequisitionWithDynamicData(state.operator, req, address(bs), dynamicData); + } + + function test_automateClaimBlueprint_plant_success() public { + // Setup test state for planting + // setupPlant: true, setupHarvest: false, twoFields: true, abovePeg: true + TestState memory state = setupAutomateClaimBlueprintTest(true, false, true, true); + + // get user state before plant + uint256 userTotalStalkBeforePlant = bs.balanceOfStalk(state.user); + uint256 userTotalBdvBeforePlant = bs.balanceOfDepositedBdv(state.user, state.beanToken); + + // assert user has grown stalk and initial bdv + assertGt(userTotalStalkBeforePlant, 0, "user should have grown stalk to plant"); + assertEq(userTotalBdvBeforePlant, 100000e6, "user should have the initial bdv"); + assertGt(bs.balanceOfEarnedBeans(state.user), 0, "user should have earned beans to plant"); + + // Setup blueprint with valid minPlantAmount + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( + 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, + 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 + }) + ); + + // Execute requisition, expect plant event + vm.expectEmit(); + emit Plant(state.user, 1933023687); + executeWithHarvestData(state.operator, state.user, req); + + // Verify state changes after successful plant + uint256 userTotalStalkAfterPlant = bs.balanceOfStalk(state.user); + uint256 userTotalBdvAfterPlant = bs.balanceOfDepositedBdv(state.user, state.beanToken); + + assertGt(userTotalStalkAfterPlant, userTotalStalkBeforePlant, "userTotalStalk increase"); + assertGt(userTotalBdvAfterPlant, userTotalBdvBeforePlant, "userTotalBdv increase"); + } + + function test_automateClaimBlueprint_harvest_partialHarvest() public { + // Setup test state for harvesting + // setupPlant: false, setupHarvest: true, twoFields: true, abovePeg: true + TestState memory state = setupAutomateClaimBlueprintTest(false, true, true, true); + + // advance season to print beans + advanceSeason(); + + // get user state before harvest + uint256 userTotalBdvBeforeHarvest = bs.balanceOfDepositedBdv(state.user, state.beanToken); + (, uint256[] memory harvestablePlots) = assertAndGetHarvestablePods( + state.user, + DEFAULT_FIELD_ID, + 1, // expected plots + 488088481 // expected pods + ); + + // assert initial conditions + assertEq(userTotalBdvBeforeHarvest, 100000e6, "user should have the initial bdv"); + + // Setup blueprint for partial harvest + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( + AutomateClaimSetupParams({ + account: state.user, + sourceMode: SourceMode.PURE_PINTO, + minMowAmount: 1 * STALK_DECIMALS, + minTwaDeltaB: 10e6, + 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 + }) + ); + + // Execute requisition, expect harvest event + vm.expectEmit(); + emit Harvest(state.user, bs.activeField(), harvestablePlots, 488088481); + executeWithHarvestData(state.operator, state.user, req); + + // Verify state changes after partial harvest + uint256 userTotalBdvAfterHarvest = bs.balanceOfDepositedBdv(state.user, state.beanToken); + assertGt(userTotalBdvAfterHarvest, userTotalBdvBeforeHarvest, "userTotalBdv increase"); + + // assert user harvestable pods is 0 after harvest + assertNoHarvestablePods(state.user, DEFAULT_FIELD_ID); + } + + function test_automateClaimBlueprint_harvest_fullHarvest() public { + // Setup test state for harvesting + // setupPlant: false, setupHarvest: true, twoFields: false, abovePeg: 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); + addLiquidityToWell(BEAN_WSTETH_WELL, 10000e6, 20 ether); + + // advance season to print beans + advanceSeason(); + + // get user state before harvest + uint256 userTotalBdvBeforeHarvest = bs.balanceOfDepositedBdv(state.user, state.beanToken); + (, uint256[] memory harvestablePlots) = assertAndGetHarvestablePods( + state.user, + DEFAULT_FIELD_ID, + 2, // expected plots + PODS_FIELD_0 // expected pods + ); + + // Setup blueprint for full harvest + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( + AutomateClaimSetupParams({ + account: state.user, + sourceMode: SourceMode.PURE_PINTO, + minMowAmount: 1 * STALK_DECIMALS, + minTwaDeltaB: 10e6, + 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 + }) + ); + + // Execute requisition, expect harvest event + vm.expectEmit(); + emit Harvest(state.user, bs.activeField(), harvestablePlots, 1000100000); + executeWithHarvestData(state.operator, state.user, req); + + // Verify state changes after full harvest + uint256 userTotalBdvAfterHarvest = bs.balanceOfDepositedBdv(state.user, state.beanToken); + assertGt( + userTotalBdvAfterHarvest, + userTotalBdvBeforeHarvest, + "userTotalBdv should increase" + ); + + // get user plots and verify all harvested + IMockFBeanstalk.Plot[] memory plots = bs.getPlotsFromAccount(state.user, bs.activeField()); + assertEq(plots.length, 0, "user should have no plots left"); + + // assert the user has no harvestable pods left + assertNoHarvestablePods(state.user, DEFAULT_FIELD_ID); + } + + function test_automateClaimBlueprint_harvest_fullHarvest_twoFields() public { + // Setup test state for harvesting + // setupPlant: false, setupHarvest: true, twoFields: true, abovePeg: 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 + addLiquidityToWell(BEAN_ETH_WELL, 10000e6, 20 ether); + addLiquidityToWell(BEAN_WSTETH_WELL, 10000e6, 20 ether); + + // advance season to print beans + advanceSeason(); + + // get user state before harvest for fieldId 0 + uint256 userTotalBdvBeforeHarvest = bs.balanceOfDepositedBdv(state.user, state.beanToken); + (, uint256[] memory field0HarvestablePlots) = assertAndGetHarvestablePods( + state.user, + DEFAULT_FIELD_ID, + 2, // expected plots + PODS_FIELD_0 // expected pods + ); + // get user state before harvest for fieldId 1 + (, uint256[] memory field1HarvestablePlots) = assertAndGetHarvestablePods( + state.user, + PAYBACK_FIELD_ID, + 1, // expected plots + PODS_FIELD_1 // expected pods + ); + + // Setup blueprint for full harvest + (IMockFBeanstalk.Requisition memory req, ) = setupAutomateClaimBlueprint( + AutomateClaimSetupParams({ + account: state.user, + sourceMode: SourceMode.PURE_PINTO, + minMowAmount: 1 * STALK_DECIMALS, + minTwaDeltaB: 10e6, + 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 + }) + ); + + // Execute requisition, expect harvest events for both fields + vm.expectEmit(); + emit Harvest(state.user, DEFAULT_FIELD_ID, field0HarvestablePlots, 1000100000); + emit Harvest(state.user, PAYBACK_FIELD_ID, field1HarvestablePlots, 250e6); + executeWithHarvestData(state.operator, state.user, req); + + // Verify state changes after full harvest + uint256 userTotalBdvAfterHarvest = bs.balanceOfDepositedBdv(state.user, state.beanToken); + assertGt(userTotalBdvAfterHarvest, userTotalBdvBeforeHarvest, "userTotalBdv increase"); + + // get user plots and verify all harvested for fieldId 0 + IMockFBeanstalk.Plot[] memory plots = bs.getPlotsFromAccount(state.user, DEFAULT_FIELD_ID); + assertEq(bs.getPlotsFromAccount(state.user, DEFAULT_FIELD_ID).length, 0, "no plots left"); + + // assert the user has no harvestable pods left + assertNoHarvestablePods(state.user, DEFAULT_FIELD_ID); + + // get user plots and verify all harvested for fieldId 1 + plots = bs.getPlotsFromAccount(state.user, PAYBACK_FIELD_ID); + assertEq(plots.length, 0, "no plots left"); + + // assert the user has no harvestable pods left + assertNoHarvestablePods(state.user, PAYBACK_FIELD_ID); + } + + /////////////////////////// HELPER FUNCTIONS /////////////////////////// + + /** + * @notice Assert user has no harvestable pods remaining for a field + */ + function assertNoHarvestablePods(address user, uint256 fieldId) internal { + (uint256 totalPods, uint256[] memory plots) = _userHarvestablePods(user, fieldId); + assertEq(totalPods, 0, "harvestable pods after harvest"); + assertEq(plots.length, 0, "harvestable plots after harvest"); + } + + /** + * @notice Assert user has expected harvestable pods and return them + */ + function assertAndGetHarvestablePods( + address user, + uint256 fieldId, + uint256 expectedPlots, + uint256 expectedPods + ) internal returns (uint256 totalPods, uint256[] memory plots) { + (totalPods, plots) = _userHarvestablePods(user, fieldId); + assertEq( + plots.length, + expectedPlots, + string.concat("harvestable plots for fieldId ", vm.toString(fieldId)) + ); + assertGt( + totalPods, + 0, + string.concat("harvestable pods for fieldId ", vm.toString(fieldId)) + ); + assertEq(totalPods, expectedPods, "harvestable pods for fieldId"); + } + + /// @dev Advance to the next season and update oracles + function advanceSeason() internal { + warpToNextSeasonTimestamp(); + bs.sunrise(); + updateAllChainlinkOraclesWithPreviousData(); + } + + /** + * @notice Set the peg conditions for the whitelisted wells + * @param abovePeg If true, set the conditions for above peg + */ + function setPegConditions(bool abovePeg) internal { + addLiquidityToWell( + BEAN_ETH_WELL, + abovePeg ? 10000e6 : 10010e6, // 10,000 Beans if above peg, 10,010 Beans if below peg + abovePeg ? 11 ether : 10 ether // 11 eth if above peg, 10 ether. if below peg + ); + addLiquidityToWell( + BEAN_WSTETH_WELL, + abovePeg ? 10000e6 : 10010e6, // 10,010 Beans if above peg, 10,000 Beans if below peg + abovePeg ? 11 ether : 10 ether // 11 eth if above peg, 10 ether. if below peg + ); + } + + /** + * @notice Sows beans so that the tractor user can harvest later + * Results in PODS_FIELD_0 for fieldId 0 and optionally PODS_FIELD_1 for fieldId 1 + */ + function setHarvestConditions(address account, bool twoFields) internal { + ////// Set active field harvest by sowing ////// + // set soil to 1000e6 + bs.setSoilE(1000e6); + // sow 1000e6 beans 2 times of 500e6 each + vm.prank(account); + bs.sow(500e6, 0, uint8(LibTransfer.From.EXTERNAL)); + vm.prank(account); + bs.sow(500e6, 0, uint8(LibTransfer.From.EXTERNAL)); + /// Give the user pods in fieldId 1 and increment harvestable index /// + if (twoFields) { + bs.setUserPodsAtField(account, PAYBACK_FIELD_ID, 0, 250e6); + bs.incrementTotalHarvestableE(PAYBACK_FIELD_ID, 250e6); + } + } + + /** + * @notice Skip the germination process by advancing season 2 times + */ + function skipGermination() internal { + advanceSeason(); + advanceSeason(); + } + + /** + * @notice Get harvest dynamic data for both fields + * @dev Must be called BEFORE vm.expectRevert to avoid consuming the revert expectation + */ + function getHarvestDynamicDataForUser( + address user + ) internal view returns (IMockFBeanstalk.ContractData[] memory) { + uint256[] memory fieldIds = new uint256[](2); + fieldIds[0] = DEFAULT_FIELD_ID; + fieldIds[1] = PAYBACK_FIELD_ID; + return createHarvestDynamicData(user, fieldIds); + } + + /** + * @notice Execute a requisition with harvest dynamic data for both fields + * @dev Creates harvest data for DEFAULT_FIELD_ID (0) and PAYBACK_FIELD_ID (1) + */ + function executeWithHarvestData( + address operator, + address user, + IMockFBeanstalk.Requisition memory req + ) internal { + 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, + 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 + }) + ); + + // 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, + 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 + }) + ); + + // 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, + 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 + }) + ); + + // 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, + 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 + }) + ); + + // 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, + 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 + }) + ); + + // 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); + } + + /////////////////////////// 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/ecosystem/MowPlantHarvestBlueprint.t.sol b/test/foundry/ecosystem/MowPlantHarvestBlueprint.t.sol deleted file mode 100644 index 2837b582..00000000 --- a/test/foundry/ecosystem/MowPlantHarvestBlueprint.t.sol +++ /dev/null @@ -1,554 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.6.0 <0.9.0; -pragma abicoder v2; - -import {TestHelper, LibTransfer, C, IMockFBeanstalk} from "test/foundry/utils/TestHelper.sol"; -import {MockToken} from "contracts/mocks/MockToken.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {TractorHelpers} from "contracts/ecosystem/tractor/utils/TractorHelpers.sol"; -import {SiloHelpers} from "contracts/ecosystem/tractor/utils/SiloHelpers.sol"; -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 "forge-std/console.sol"; - -contract MowPlantHarvestBlueprintTest is TractorTestHelper { - address[] farmers; - BeanstalkPrice beanstalkPrice; - - event Plant(address indexed account, uint256 beans); - event Harvest(address indexed account, uint256 fieldId, uint256[] plots, 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 PODS_FIELD_0 = 1000100000; - uint256 PODS_FIELD_1 = 250e6; - - struct TestState { - address user; - address operator; - address beanToken; - uint256 initialUserBeanBalance; - uint256 initialOperatorBeanBalance; - uint256 mintAmount; - int256 mowTipAmount; - int256 plantTipAmount; - int256 harvestTipAmount; - } - - function setUp() public { - initializeBeanstalkTestState(true, false); - farmers = createUsers(2); - vm.label(farmers[0], "Farmer 1"); - vm.label(farmers[1], "Farmer 2"); - - // Deploy BeanstalkPrice (unused here but needed for TractorHelpers) - beanstalkPrice = new BeanstalkPrice(address(bs)); - vm.label(address(beanstalkPrice), "BeanstalkPrice"); - - // Deploy TractorHelpers (2 args: beanstalk, beanstalkPrice) - tractorHelpers = new TractorHelpers(address(bs), address(beanstalkPrice)); - vm.label(address(tractorHelpers), "TractorHelpers"); - - // Deploy SiloHelpers (3 args: beanstalk, tractorHelpers, priceManipulation) - siloHelpers = new SiloHelpers( - address(bs), - address(tractorHelpers), - address(0) // priceManipulation not needed for this test - ); - vm.label(address(siloHelpers), "SiloHelpers"); - - // Deploy MowPlantHarvestBlueprint with TractorHelpers and SiloHelpers addresses - mowPlantHarvestBlueprint = new MowPlantHarvestBlueprint( - address(bs), - address(this), - address(tractorHelpers), - address(siloHelpers) - ); - vm.label(address(mowPlantHarvestBlueprint), "MowPlantHarvestBlueprint"); - - setTractorHelpers(address(tractorHelpers)); - setMowPlantHarvestBlueprint(address(mowPlantHarvestBlueprint)); - - // Advance season to grow stalk - advanceSeason(); - } - - /** - * @notice Setup the test state for the MowPlantHarvestBlueprint 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( - bool setupPlant, - bool setupHarvest, - bool twoFields, - bool abovePeg - ) internal returns (TestState memory) { - // Create test state - TestState memory state; - state.user = farmers[0]; - state.operator = address(this); - state.beanToken = bs.getBeanToken(); - state.initialUserBeanBalance = IERC20(state.beanToken).balanceOf(state.user); - state.initialOperatorBeanBalance = bs.getInternalBalance(state.operator, state.beanToken); - state.mintAmount = 110000e6; // 100k for deposit, 10k for sow - state.mowTipAmount = DEFAULT_TIP_AMOUNT; // 10 BEAN - state.plantTipAmount = DEFAULT_TIP_AMOUNT; - state.harvestTipAmount = DEFAULT_TIP_AMOUNT; - - // Mint 2x the amount to ensure we have enough for all test cases - mintTokensToUser(state.user, state.beanToken, state.mintAmount); - // Mint some to farmer 2 for plot tests - mintTokensToUser(farmers[1], state.beanToken, 10000000e6); - - // Deposit beans for user - vm.startPrank(state.user); - IERC20(state.beanToken).approve(address(bs), type(uint256).max); - bs.deposit(state.beanToken, state.mintAmount - 10000e6, uint8(LibTransfer.From.EXTERNAL)); - vm.stopPrank(); - - // For farmer 1, deposit 1000e6 beans, and mint them 1000e6 beans - mintTokensToUser(farmers[1], state.beanToken, 1000e6); - vm.prank(farmers[1]); - bs.deposit(state.beanToken, 1000e6, uint8(LibTransfer.From.EXTERNAL)); - - // Set liquidity in the whitelisted wells to manipulate deltaB - setPegConditions(abovePeg); - - if (setupPlant) skipGermination(); - - if (setupHarvest) setHarvestConditions(state.user, twoFields); - - return state; - } - - /////////////////////////// TESTS /////////////////////////// - - function test_mowPlantHarvestBlueprint_Mow() public { - // Setup test state - // setupPlant: false, setupHarvest: false, abovePeg: true - TestState memory state = setupMowPlantHarvestBlueprintTest(false, false, false, true); - - // Advance season to grow stalk but not enough to plant - advanceSeason(); - vm.warp(block.timestamp + 1 seconds); - - // get user state before mow - uint256 userGrownStalk = bs.balanceOfGrownStalk(state.user, state.beanToken); - // assert user has grown stalk - assertGt(userGrownStalk, 0, "user should have grown stalk to mow"); - // get user total stalk before mow - uint256 userTotalStalkBeforeMow = bs.balanceOfStalk(state.user); - // assert totalDeltaB is greater than 0 - assertGt(bs.totalDeltaB(), 0, "totalDeltaB should be greater than 0"); - - // Setup mowPlantHarvestBlueprint - (IMockFBeanstalk.Requisition memory req, ) = setupMowPlantHarvestBlueprint( - 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 - ); - - // Pre-calculate harvest data BEFORE expectRevert (to avoid consuming the expectation) - IMockFBeanstalk.ContractData[] memory dynamicData = getHarvestDynamicDataForUser( - state.user - ); - - // Try to execute before the last minutes of the season, expect revert - vm.expectRevert("MowPlantHarvestBlueprint: None of the order conditions are met"); - executeRequisitionWithDynamicData(state.operator, req, address(bs), dynamicData); - - // Try to execute after in last minutes of the season - vm.warp(bs.getNextSeasonStart() - 1 seconds); - executeWithHarvestData(state.operator, state.user, req); - - // assert all grown stalk was mowed - uint256 userGrownStalkAfterMow = bs.balanceOfGrownStalk(state.user, state.beanToken); - assertEq(userGrownStalkAfterMow, 0); - - // get user total stalk after mow - uint256 userTotalStalkAfterMow = bs.balanceOfStalk(state.user); - // assert the user total stalk has increased - assertGt( - userTotalStalkAfterMow, - userTotalStalkBeforeMow, - "userTotalStalk should have increased" - ); - } - - function test_mowPlantHarvestBlueprint_plant_revertWhenInsufficientPlantableBeans() public { - // Setup test state for planting - // setupPlant: true, setupHarvest: false, twoFields: true, abovePeg: true - TestState memory state = setupMowPlantHarvestBlueprintTest(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( - 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 - ); - - // Pre-calculate harvest data BEFORE expectRevert - IMockFBeanstalk.ContractData[] memory dynamicData = getHarvestDynamicDataForUser( - state.user - ); - - // Execute requisition, expect revert - vm.expectRevert("MowPlantHarvestBlueprint: None of the order conditions are met"); - executeRequisitionWithDynamicData(state.operator, req, address(bs), dynamicData); - } - - function test_mowPlantHarvestBlueprint_plant_success() public { - // Setup test state for planting - // setupPlant: true, setupHarvest: false, twoFields: true, abovePeg: true - TestState memory state = setupMowPlantHarvestBlueprintTest(true, false, true, true); - - // get user state before plant - uint256 userTotalStalkBeforePlant = bs.balanceOfStalk(state.user); - uint256 userTotalBdvBeforePlant = bs.balanceOfDepositedBdv(state.user, state.beanToken); - - // assert user has grown stalk and initial bdv - assertGt(userTotalStalkBeforePlant, 0, "user should have grown stalk to plant"); - assertEq(userTotalBdvBeforePlant, 100000e6, "user should have the initial bdv"); - assertGt(bs.balanceOfEarnedBeans(state.user), 0, "user should have earned beans to plant"); - - // Setup blueprint with valid minPlantAmount - (IMockFBeanstalk.Requisition memory req, ) = setupMowPlantHarvestBlueprint( - 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 - ); - - // Execute requisition, expect plant event - vm.expectEmit(); - emit Plant(state.user, 1933023687); - executeWithHarvestData(state.operator, state.user, req); - - // Verify state changes after successful plant - uint256 userTotalStalkAfterPlant = bs.balanceOfStalk(state.user); - uint256 userTotalBdvAfterPlant = bs.balanceOfDepositedBdv(state.user, state.beanToken); - - assertGt(userTotalStalkAfterPlant, userTotalStalkBeforePlant, "userTotalStalk increase"); - assertGt(userTotalBdvAfterPlant, userTotalBdvBeforePlant, "userTotalBdv increase"); - } - - function test_mowPlantHarvestBlueprint_harvest_partialHarvest() public { - // Setup test state for harvesting - // setupPlant: false, setupHarvest: true, twoFields: true, abovePeg: true - TestState memory state = setupMowPlantHarvestBlueprintTest(false, true, true, true); - - // advance season to print beans - advanceSeason(); - - // get user state before harvest - uint256 userTotalBdvBeforeHarvest = bs.balanceOfDepositedBdv(state.user, state.beanToken); - (, uint256[] memory harvestablePlots) = assertAndGetHarvestablePods( - state.user, - DEFAULT_FIELD_ID, - 1, // expected plots - 488088481 // expected pods - ); - - // assert initial conditions - assertEq(userTotalBdvBeforeHarvest, 100000e6, "user should have the initial bdv"); - - // Setup blueprint for partial harvest - (IMockFBeanstalk.Requisition memory req, ) = setupMowPlantHarvestBlueprint( - 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 - ); - - // Execute requisition, expect harvest event - vm.expectEmit(); - emit Harvest(state.user, bs.activeField(), harvestablePlots, 488088481); - executeWithHarvestData(state.operator, state.user, req); - - // Verify state changes after partial harvest - uint256 userTotalBdvAfterHarvest = bs.balanceOfDepositedBdv(state.user, state.beanToken); - assertGt(userTotalBdvAfterHarvest, userTotalBdvBeforeHarvest, "userTotalBdv increase"); - - // assert user harvestable pods is 0 after harvest - assertNoHarvestablePods(state.user, DEFAULT_FIELD_ID); - } - - function test_mowPlantHarvestBlueprint_harvest_fullHarvest() public { - // Setup test state for harvesting - // setupPlant: false, setupHarvest: true, twoFields: false, abovePeg: true - TestState memory state = setupMowPlantHarvestBlueprintTest(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); - addLiquidityToWell(BEAN_WSTETH_WELL, 10000e6, 20 ether); - - // advance season to print beans - advanceSeason(); - - // get user state before harvest - uint256 userTotalBdvBeforeHarvest = bs.balanceOfDepositedBdv(state.user, state.beanToken); - (, uint256[] memory harvestablePlots) = assertAndGetHarvestablePods( - state.user, - DEFAULT_FIELD_ID, - 2, // expected plots - PODS_FIELD_0 // expected pods - ); - - // Setup blueprint for full harvest - (IMockFBeanstalk.Requisition memory req, ) = setupMowPlantHarvestBlueprint( - 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 - ); - - // Execute requisition, expect harvest event - vm.expectEmit(); - emit Harvest(state.user, bs.activeField(), harvestablePlots, 1000100000); - executeWithHarvestData(state.operator, state.user, req); - - // Verify state changes after full harvest - uint256 userTotalBdvAfterHarvest = bs.balanceOfDepositedBdv(state.user, state.beanToken); - assertGt( - userTotalBdvAfterHarvest, - userTotalBdvBeforeHarvest, - "userTotalBdv should increase" - ); - - // get user plots and verify all harvested - IMockFBeanstalk.Plot[] memory plots = bs.getPlotsFromAccount(state.user, bs.activeField()); - assertEq(plots.length, 0, "user should have no plots left"); - - // assert the user has no harvestable pods left - assertNoHarvestablePods(state.user, DEFAULT_FIELD_ID); - } - - function test_mowPlantHarvestBlueprint_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); - - // 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 - addLiquidityToWell(BEAN_ETH_WELL, 10000e6, 20 ether); - addLiquidityToWell(BEAN_WSTETH_WELL, 10000e6, 20 ether); - - // advance season to print beans - advanceSeason(); - - // get user state before harvest for fieldId 0 - uint256 userTotalBdvBeforeHarvest = bs.balanceOfDepositedBdv(state.user, state.beanToken); - (, uint256[] memory field0HarvestablePlots) = assertAndGetHarvestablePods( - state.user, - DEFAULT_FIELD_ID, - 2, // expected plots - PODS_FIELD_0 // expected pods - ); - // get user state before harvest for fieldId 1 - (, uint256[] memory field1HarvestablePlots) = assertAndGetHarvestablePods( - state.user, - PAYBACK_FIELD_ID, - 1, // expected plots - PODS_FIELD_1 // expected pods - ); - - // Setup blueprint for full harvest - (IMockFBeanstalk.Requisition memory req, ) = setupMowPlantHarvestBlueprint( - 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 - ); - - // Execute requisition, expect harvest events for both fields - vm.expectEmit(); - emit Harvest(state.user, DEFAULT_FIELD_ID, field0HarvestablePlots, 1000100000); - emit Harvest(state.user, PAYBACK_FIELD_ID, field1HarvestablePlots, 250e6); - executeWithHarvestData(state.operator, state.user, req); - - // Verify state changes after full harvest - uint256 userTotalBdvAfterHarvest = bs.balanceOfDepositedBdv(state.user, state.beanToken); - assertGt(userTotalBdvAfterHarvest, userTotalBdvBeforeHarvest, "userTotalBdv increase"); - - // get user plots and verify all harvested for fieldId 0 - IMockFBeanstalk.Plot[] memory plots = bs.getPlotsFromAccount(state.user, DEFAULT_FIELD_ID); - assertEq(bs.getPlotsFromAccount(state.user, DEFAULT_FIELD_ID).length, 0, "no plots left"); - - // assert the user has no harvestable pods left - assertNoHarvestablePods(state.user, DEFAULT_FIELD_ID); - - // get user plots and verify all harvested for fieldId 1 - plots = bs.getPlotsFromAccount(state.user, PAYBACK_FIELD_ID); - assertEq(plots.length, 0, "no plots left"); - - // assert the user has no harvestable pods left - assertNoHarvestablePods(state.user, PAYBACK_FIELD_ID); - } - - /////////////////////////// HELPER FUNCTIONS /////////////////////////// - - /** - * @notice Assert user has no harvestable pods remaining for a field - */ - function assertNoHarvestablePods(address user, uint256 fieldId) internal { - (uint256 totalPods, uint256[] memory plots) = _userHarvestablePods(user, fieldId); - assertEq(totalPods, 0, "harvestable pods after harvest"); - assertEq(plots.length, 0, "harvestable plots after harvest"); - } - - /** - * @notice Assert user has expected harvestable pods and return them - */ - function assertAndGetHarvestablePods( - address user, - uint256 fieldId, - uint256 expectedPlots, - uint256 expectedPods - ) internal returns (uint256 totalPods, uint256[] memory plots) { - (totalPods, plots) = _userHarvestablePods(user, fieldId); - assertEq( - plots.length, - expectedPlots, - string.concat("harvestable plots for fieldId ", vm.toString(fieldId)) - ); - assertGt( - totalPods, - 0, - string.concat("harvestable pods for fieldId ", vm.toString(fieldId)) - ); - assertEq(totalPods, expectedPods, "harvestable pods for fieldId"); - } - - /// @dev Advance to the next season and update oracles - function advanceSeason() internal { - warpToNextSeasonTimestamp(); - bs.sunrise(); - updateAllChainlinkOraclesWithPreviousData(); - } - - /** - * @notice Set the peg conditions for the whitelisted wells - * @param abovePeg If true, set the conditions for above peg - */ - function setPegConditions(bool abovePeg) internal { - addLiquidityToWell( - BEAN_ETH_WELL, - abovePeg ? 10000e6 : 10010e6, // 10,000 Beans if above peg, 10,010 Beans if below peg - abovePeg ? 11 ether : 10 ether // 11 eth if above peg, 10 ether. if below peg - ); - addLiquidityToWell( - BEAN_WSTETH_WELL, - abovePeg ? 10000e6 : 10010e6, // 10,010 Beans if above peg, 10,000 Beans if below peg - abovePeg ? 11 ether : 10 ether // 11 eth if above peg, 10 ether. if below peg - ); - } - - /** - * @notice Sows beans so that the tractor user can harvest later - * Results in PODS_FIELD_0 for fieldId 0 and optionally PODS_FIELD_1 for fieldId 1 - */ - function setHarvestConditions(address account, bool twoFields) internal { - ////// Set active field harvest by sowing ////// - // set soil to 1000e6 - bs.setSoilE(1000e6); - // sow 1000e6 beans 2 times of 500e6 each - vm.prank(account); - bs.sow(500e6, 0, uint8(LibTransfer.From.EXTERNAL)); - vm.prank(account); - bs.sow(500e6, 0, uint8(LibTransfer.From.EXTERNAL)); - /// Give the user pods in fieldId 1 and increment harvestable index /// - if (twoFields) { - bs.setUserPodsAtField(account, PAYBACK_FIELD_ID, 0, 250e6); - bs.incrementTotalHarvestableE(PAYBACK_FIELD_ID, 250e6); - } - } - - /** - * @notice Skip the germination process by advancing season 2 times - */ - function skipGermination() internal { - advanceSeason(); - advanceSeason(); - } - - /** - * @notice Get harvest dynamic data for both fields - * @dev Must be called BEFORE vm.expectRevert to avoid consuming the revert expectation - */ - function getHarvestDynamicDataForUser( - address user - ) internal view returns (IMockFBeanstalk.ContractData[] memory) { - uint256[] memory fieldIds = new uint256[](2); - fieldIds[0] = DEFAULT_FIELD_ID; - fieldIds[1] = PAYBACK_FIELD_ID; - return createHarvestDynamicData(user, fieldIds); - } - - /** - * @notice Execute a requisition with harvest dynamic data for both fields - * @dev Creates harvest data for DEFAULT_FIELD_ID (0) and PAYBACK_FIELD_ID (1) - */ - function executeWithHarvestData( - address operator, - address user, - IMockFBeanstalk.Requisition memory req - ) internal { - IMockFBeanstalk.ContractData[] memory dynamicData = getHarvestDynamicDataForUser(user); - executeRequisitionWithDynamicData(operator, req, address(bs), dynamicData); - } -} diff --git a/test/foundry/utils/TractorTestHelper.sol b/test/foundry/utils/TractorTestHelper.sol index 3da4cdba..74e792fe 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,153 +425,149 @@ contract TractorTestHelper is TestHelper { return abi.encodeWithSelector(IMockFBeanstalk.advancedFarm.selector, calls); } - //////////////////////////// MowPlantHarvestBlueprint //////////////////////////// + //////////////////////////// AutomateClaimBlueprint //////////////////////////// - function setupMowPlantHarvestBlueprint( - address account, - SourceMode sourceMode, - uint256 minMowAmount, - uint256 minTwaDeltaB, - uint256 minPlantAmount, - uint256 minHarvestAmount, - address tipAddress, - int256 mowTipAmount, - int256 plantTipAmount, - int256 harvestTipAmount, - uint256 maxGrownStalkPerBdv + /** + * @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; + uint256 minUnripeClaimAmount; + address tipAddress; + int256 mowTipAmount; + int256 plantTipAmount; + int256 harvestTipAmount; + int256 rinseTipAmount; + int256 unripeClaimTipAmount; + uint256 maxGrownStalkPerBdv; + } + + function setupAutomateClaimBlueprint( + AutomateClaimSetupParams memory p ) internal returns ( IMockFBeanstalk.Requisition memory, - MowPlantHarvestBlueprint.MowPlantHarvestBlueprintStruct memory params + AutomateClaimBlueprint.AutomateClaimBlueprintStruct memory params ) { - // build struct params - params = createMowPlantHarvestBlueprintStruct( - uint8(sourceMode), - minMowAmount, - minTwaDeltaB, - minPlantAmount, - minHarvestAmount, - tipAddress, - mowTipAmount, - plantTipAmount, - harvestTipAmount, - maxGrownStalkPerBdv - ); + params = _createAutomateClaimBlueprintStructFromParams(p); - // create pipe call data - bytes memory pipeCallData = createMowPlantHarvestBlueprintCallData(params); + 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 mowPlantHarvestBlueprint - function createMowPlantHarvestBlueprintStruct( - uint8 sourceMode, - uint256 minMowAmount, - uint256 minTwaDeltaB, - uint256 minPlantAmount, - uint256 minHarvestAmount, - address tipAddress, - int256 mowTipAmount, - int256 plantTipAmount, - int256 harvestTipAmount, - uint256 maxGrownStalkPerBdv - ) internal view returns (MowPlantHarvestBlueprint.MowPlantHarvestBlueprintStruct memory) { + 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 - MowPlantHarvestBlueprint.FieldHarvestConfig[] - memory fieldHarvestConfigs = createFieldHarvestConfigs(minHarvestAmount); - - // Create MowPlantHarvestParams struct - MowPlantHarvestBlueprint.MowPlantHarvestParams - memory mowPlantHarvestParams = MowPlantHarvestBlueprint.MowPlantHarvestParams({ - minMowAmount: minMowAmount, - minTwaDeltaB: minTwaDeltaB, - minPlantAmount: minPlantAmount, + AutomateClaimBlueprint.FieldHarvestConfig[] + memory fieldHarvestConfigs = createFieldHarvestConfigs(p.minHarvestAmount); + + // Create AutomateClaimParams struct + AutomateClaimBlueprint.AutomateClaimParams + memory automateClaimParams = AutomateClaimBlueprint.AutomateClaimParams({ + minMowAmount: p.minMowAmount, + minTwaDeltaB: p.minTwaDeltaB, + minPlantAmount: p.minPlantAmount, fieldHarvestConfigs: fieldHarvestConfigs, + minRinseAmount: p.minRinseAmount, + minUnripeClaimAmount: p.minUnripeClaimAmount, sourceTokenIndices: sourceTokenIndices, - maxGrownStalkPerBdv: maxGrownStalkPerBdv, + maxGrownStalkPerBdv: p.maxGrownStalkPerBdv, slippageRatio: 0.01e18 // 1% }); // create OperatorParamsExtended struct - MowPlantHarvestBlueprint.OperatorParamsExtended + AutomateClaimBlueprint.OperatorParamsExtended memory opParamsExtended = createOperatorParamsExtended( whitelistedOps, - tipAddress, - mowTipAmount, - plantTipAmount, - harvestTipAmount + p.tipAddress, + p.mowTipAmount, + p.plantTipAmount, + p.harvestTipAmount, + p.rinseTipAmount, + p.unripeClaimTipAmount ); return - MowPlantHarvestBlueprint.MowPlantHarvestBlueprintStruct({ - mowPlantHarvestParams: mowPlantHarvestParams, + AutomateClaimBlueprint.AutomateClaimBlueprintStruct({ + automateClaimParams: automateClaimParams, opParams: opParamsExtended }); } + 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 ) 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" @@ -593,8 +589,10 @@ contract TractorTestHelper is TestHelper { address tipAddress, int256 mowTipAmount, int256 plantTipAmount, - int256 harvestTipAmount - ) internal view returns (MowPlantHarvestBlueprint.OperatorParamsExtended memory) { + int256 harvestTipAmount, + int256 rinseTipAmount, + int256 unripeClaimTipAmount + ) internal view returns (AutomateClaimBlueprint.OperatorParamsExtended memory) { // create OperatorParams struct BlueprintBase.OperatorParams memory opParams = BlueprintBase.OperatorParams({ whitelistedOperators: whitelistedOps, @@ -603,14 +601,50 @@ 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, - harvestTipAmount: harvestTipAmount + harvestTipAmount: harvestTipAmount, + rinseTipAmount: rinseTipAmount, + unripeClaimTipAmount: unripeClaimTipAmount }); 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]; + } + } }