From f68ac04fccfddb569d10303779cb32e092edb0f0 Mon Sep 17 00:00:00 2001 From: pocikerim Date: Thu, 6 Nov 2025 22:41:04 +0300 Subject: [PATCH 1/6] dyanmic data field added to tractorStorage and struct moved to LibTractorStorage.sol --- contracts/libraries/LibTractor.sol | 47 +++++++++++++++-------- contracts/libraries/LibTractorStorage.sol | 17 ++++++++ 2 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 contracts/libraries/LibTractorStorage.sol diff --git a/contracts/libraries/LibTractor.sol b/contracts/libraries/LibTractor.sol index 35c791ec..0eba2161 100644 --- a/contracts/libraries/LibTractor.sol +++ b/contracts/libraries/LibTractor.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.20; +import {LibTractorStorage} from "./LibTractorStorage.sol"; + /** * @title Lib Tractor **/ @@ -29,21 +31,6 @@ library LibTractor { event TractorVersionSet(string version); - struct TractorStorage { - // Number of times the blueprint has been run. - mapping(bytes32 => uint256) blueprintNonce; - // Publisher Address => counter id => counter value. - mapping(address => mapping(bytes32 => uint256)) blueprintCounters; - // Publisher of current operations. Set to address(1) when no active publisher. - address payable activePublisher; - // Version of Tractor. Only Blueprints using current Version can run. - string version; - // Hash of currently executing blueprint - bytes32 currentBlueprintHash; - // Address of the currently executing operator - address operator; - } - // Blueprint stores blueprint related values struct Blueprint { address publisher; @@ -67,13 +54,39 @@ library LibTractor { * @notice Get tractor storage from storage. * @return ts Storage object containing tractor data */ - function _tractorStorage() internal pure returns (TractorStorage storage ts) { + function _tractorStorage() internal pure returns (LibTractorStorage.TractorStorage storage ts) { // keccak256("diamond.storage.tractor") == 0x7efbaaac9214ca1879e26b4df38e29a72561affb741bba775ce66d5bb6a82a07 assembly { ts.slot := 0x7efbaaac9214ca1879e26b4df38e29a72561affb741bba775ce66d5bb6a82a07 } } + /** + * @notice Get tractor data by key. + * @param key The key to get the data for + * @return The data for the key + */ + function _getTractorData(uint256 key) internal view returns (bytes memory) { + return _tractorStorage().data[key]; + } + + /** + * @notice Set tractor data by key. + * @param key The key to set the data for + * @param value The data to set for the key + */ + function _setTractorData(uint256 key, bytes memory value) internal { + _tractorStorage().data[key] = value; + } + + /** + * @notice Clear tractor data by key. + * @param key The key to clear the data for + */ + function _clearTractorData(uint256 key) internal { + delete _tractorStorage().data[key]; + } + /** * @notice Set the tractor hashed version. */ @@ -104,7 +117,7 @@ library LibTractor { * @param publisher blueprint publisher address */ function _setPublisher(address payable publisher) internal { - TractorStorage storage ts = _tractorStorage(); + LibTractorStorage.TractorStorage storage ts = _tractorStorage(); require( uint160(bytes20(address(ts.activePublisher))) <= 1, "LibTractor: publisher already set" diff --git a/contracts/libraries/LibTractorStorage.sol b/contracts/libraries/LibTractorStorage.sol new file mode 100644 index 00000000..e42cc0e4 --- /dev/null +++ b/contracts/libraries/LibTractorStorage.sol @@ -0,0 +1,17 @@ +/** + * SPDX-License-Identifier: MIT + **/ + +pragma solidity ^0.8.20; + +library LibTractorStorage { + struct TractorStorage { + mapping(bytes32 => uint256) blueprintNonce; + mapping(address => mapping(bytes32 => uint256)) blueprintCounters; + address payable activePublisher; + string version; + bytes32 currentBlueprintHash; + address operator; + mapping(uint256 key => bytes value) data; + } +} From a0f6c4314b1dee9eba0f8462d5a55b2fdf554bf8 Mon Sep 17 00:00:00 2001 From: pocikerim Date: Fri, 7 Nov 2025 13:49:59 +0300 Subject: [PATCH 2/6] Replace storage deletion with abi.encode(1) reset for gas optimization. --- contracts/libraries/LibTractor.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/libraries/LibTractor.sol b/contracts/libraries/LibTractor.sol index 0eba2161..17b3199a 100644 --- a/contracts/libraries/LibTractor.sol +++ b/contracts/libraries/LibTractor.sol @@ -81,10 +81,11 @@ library LibTractor { /** * @notice Clear tractor data by key. + * @dev Resets to bytes(abi.encode(1)) instead of zero for gas optimization. * @param key The key to clear the data for */ function _clearTractorData(uint256 key) internal { - delete _tractorStorage().data[key]; + _tractorStorage().data[key] = abi.encode(1); } /** From 23afd531b3b597f7097e03afd07da6909611e800 Mon Sep 17 00:00:00 2001 From: pocikerim Date: Fri, 7 Nov 2025 21:50:59 +0300 Subject: [PATCH 3/6] Update TractorFacet.sol --- .../beanstalk/facets/farm/TractorFacet.sol | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/contracts/beanstalk/facets/farm/TractorFacet.sol b/contracts/beanstalk/facets/farm/TractorFacet.sol index b5561e63..c180d3ef 100644 --- a/contracts/beanstalk/facets/farm/TractorFacet.sol +++ b/contracts/beanstalk/facets/farm/TractorFacet.sol @@ -43,6 +43,11 @@ contract TractorFacet is Invariable, ReentrancyGuard { uint256 gasleft ); + struct ContractData { + uint256 key; + bytes value; + } + /** * @notice Ensure requisition hash matches blueprint data and signer is publisher. */ @@ -298,4 +303,75 @@ contract TractorFacet is Invariable, ReentrancyGuard { function operator() external view returns (address) { return LibTractor._getOperator(); } + + /** + * @notice Get tractor data by key (for blueprint contract access). + * @param key The key to get the data for + * @return The data for the key + */ + function getTractorData(uint256 key) external view returns (bytes memory) { + return LibTractor._getTractorData(key); + } + + /** + * @notice Execute a Tractor blueprint with dynamic data injection. + * @param requisition The blueprint requisition containing signature and blueprint data + * @param operatorInjectorData Operator data for fixed-position injection (like normal tractor) + * @param contractData Array of key-value pairs for dynamic data injection + * @return results Array of results from executed farm calls + */ + function tractorWithData( + LibTractor.Requisition calldata requisition, + bytes memory operatorInjectorData, + ContractData[] memory contractData + ) + external + payable + fundsSafu + nonReentrantFarm + verifyRequisition(requisition) + runBlueprint(requisition) + returns (bytes[] memory results) + { + require(requisition.blueprint.data.length > 0, "TractorWithData: data empty"); + + LibTractor._setCurrentBlueprintHash(requisition.blueprintHash); + + LibTractor._setOperator(msg.sender); + + for (uint256 i = 0; i < contractData.length; i++) { + LibTractor._setTractorData(contractData[i].key, contractData[i].value); + } + + AdvancedFarmCall[] memory calls = abi.decode( + LibBytes.sliceFrom(requisition.blueprint.data, 4), + (AdvancedFarmCall[]) + ); + + for (uint256 i; i < requisition.blueprint.operatorPasteInstrs.length; ++i) { + bytes32 operatorPasteInstr = requisition.blueprint.operatorPasteInstrs[i]; + uint80 pasteCallIndex = operatorPasteInstr.getIndex1(); + require(calls.length > pasteCallIndex, "TractorWithData: pasteCallIndex OOB"); + + LibBytes.pasteBytesTractor( + operatorPasteInstr, + operatorInjectorData, + calls[pasteCallIndex].callData + ); + } + + results = new bytes[](calls.length); + for (uint256 i = 0; i < calls.length; ++i) { + require(calls[i].callData.length != 0, "TractorWithData: empty AdvancedFarmCall"); + results[i] = LibFarm._advancedFarm(calls[i], results); + } + + for (uint256 i = 0; i < contractData.length; i++) { + LibTractor._clearTractorData(contractData[i].key); + } + + LibTractor._resetCurrentBlueprintHash(); + + LibTractor._resetOperator(); + } } From 8853051937e8078ef219a85fa35ad2cb3bc54c96 Mon Sep 17 00:00:00 2001 From: pocikerim Date: Fri, 7 Nov 2025 21:51:04 +0300 Subject: [PATCH 4/6] tests --- contracts/mocks/MockBlueprint.sol | 102 +++++++++ .../ecosystem/MowPlantHarvestBlueprint.t.sol | 203 +++++++++++++++++- 2 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 contracts/mocks/MockBlueprint.sol diff --git a/contracts/mocks/MockBlueprint.sol b/contracts/mocks/MockBlueprint.sol new file mode 100644 index 00000000..66f2196a --- /dev/null +++ b/contracts/mocks/MockBlueprint.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IBeanstalk} from "contracts/interfaces/IBeanstalk.sol"; + +/** + * @title MockBlueprint + * @notice Mock blueprint contract for testing dynamic data injection with tractorWithData + */ +contract MockBlueprint { + IBeanstalk public immutable beanstalk; + + uint256 public constant HARVEST_PLOTS_KEY = 1; + uint256 public constant PLOT_AMOUNTS_KEY = 2; + + event DynamicDataUsed(uint256[] plots, uint256[] amounts); + event HarvestSimulated(address indexed user, uint256 fieldId, uint256[] plots, uint256 totalPods); + + constructor(address _beanstalk) { + beanstalk = IBeanstalk(_beanstalk); + } + + /** + * @notice Main blueprint function that uses dynamic data + * @dev This function demonstrates how a blueprint contract can access + * dynamic data injected via tractorWithData + */ + function executeBlueprint() external { + bytes memory plotData = beanstalk.getTractorData(HARVEST_PLOTS_KEY); + require(plotData.length > 0, "No harvest plot data available"); + + uint256[] memory harvestablePlots = abi.decode(plotData, (uint256[])); + + bytes memory amountData = beanstalk.getTractorData(PLOT_AMOUNTS_KEY); + uint256[] memory plotAmounts; + + if (amountData.length > 0) { + plotAmounts = abi.decode(amountData, (uint256[])); + require( + plotAmounts.length == harvestablePlots.length, + "Plot and amount arrays must match" + ); + } + + emit DynamicDataUsed(harvestablePlots, plotAmounts); + _simulateHarvest(harvestablePlots); + } + + /** + * @notice Simulate harvest operations using dynamic data with realistic validation + * @param plots The harvestable plots from dynamic data + */ + function _simulateHarvest(uint256[] memory plots) internal { + require(plots.length > 0, "MockBlueprint: No plots to harvest"); + + address user = beanstalk.tractorUser(); + uint256 fieldId = beanstalk.activeField(); + uint256 harvestableIndex = beanstalk.harvestableIndex(fieldId); + uint256 totalHarvestablePods; + + for (uint256 i; i < plots.length; ++i) { + uint256 plotIndex = plots[i]; + uint256 plotPods = beanstalk.plot(user, fieldId, plotIndex); + + // Validate plot ownership (like real harvest) + require(plotPods > 0, "MockBlueprint: no plot"); + + // Validate harvestability (like real harvest) + require(plotIndex < harvestableIndex, "MockBlueprint: plot not harvestable"); + + // Calculate harvestable pods (like real harvest) + uint256 harvestablePods = harvestableIndex - plotIndex; + if (harvestablePods > plotPods) harvestablePods = plotPods; + + totalHarvestablePods += harvestablePods; + } + + // Emit simulation event (like real harvest event) + emit HarvestSimulated(user, fieldId, plots, totalHarvestablePods); + + // Note: We don't actually modify state, just validate and emit event + // This simulates the harvest logic without side effects + } + + /** + * @notice Helper function to get current user + * @return The current tractor user + */ + function getCurrentUser() external view returns (address) { + return beanstalk.tractorUser(); + } + + /** + * @notice Helper function to check if data exists for a key + * @param key The data key to check + * @return True if data exists for the key + */ + function hasDataForKey(uint256 key) external view returns (bool) { + bytes memory data = beanstalk.getTractorData(key); + return data.length > 0; + } +} diff --git a/test/foundry/ecosystem/MowPlantHarvestBlueprint.t.sol b/test/foundry/ecosystem/MowPlantHarvestBlueprint.t.sol index 2af0371a..96b3ee19 100644 --- a/test/foundry/ecosystem/MowPlantHarvestBlueprint.t.sol +++ b/test/foundry/ecosystem/MowPlantHarvestBlueprint.t.sol @@ -14,12 +14,15 @@ import {BeanstalkPrice} from "contracts/ecosystem/price/BeanstalkPrice.sol"; import {IBeanstalk} from "contracts/interfaces/IBeanstalk.sol"; import {OperatorWhitelist} from "contracts/ecosystem/OperatorWhitelist.sol"; import {MowPlantHarvestBlueprint} from "contracts/ecosystem/MowPlantHarvestBlueprint.sol"; +import {MockBlueprint} from "contracts/mocks/MockBlueprint.sol"; +import {LibTractor} from "contracts/libraries/LibTractor.sol"; import "forge-std/console.sol"; contract MowPlantHarvestBlueprintTest is TractorHelper { address[] farmers; PriceManipulation priceManipulation; BeanstalkPrice beanstalkPrice; + MockBlueprint mockBlueprint; event Plant(address indexed account, uint256 beans); event Harvest(address indexed account, uint256 fieldId, uint256[] plots, uint256 beans); @@ -77,6 +80,10 @@ contract MowPlantHarvestBlueprintTest is TractorHelper { setTractorHelpers(address(tractorHelpers)); setMowPlantHarvestBlueprint(address(mowPlantHarvestBlueprint)); + // Deploy mock blueprint for dynamic data tests + mockBlueprint = new MockBlueprint(address(bs)); + vm.label(address(mockBlueprint), "MockBlueprint"); + // Advance season to grow stalk advanceSeason(); } @@ -296,7 +303,7 @@ contract MowPlantHarvestBlueprintTest is TractorHelper { advanceSeason(); // assert user has harvestable pods - (uint256 totalHarvestablePods, ) = _userHarvestablePods(state.user, DEFAULT_FIELD_ID); + (uint256 totalHarvestablePods, ) = (state.user, DEFAULT_FIELD_ID); assertGt(totalHarvestablePods, 0, "user should have harvestable pods to harvest"); // Setup blueprint with minHarvestAmount less than harvest tip amount @@ -578,4 +585,198 @@ contract MowPlantHarvestBlueprintTest is TractorHelper { advanceSeason(); advanceSeason(); } + + /** + * @notice Test tractorWithData function with realistic dynamic data injection + */ + function test_tractorWithData_withDynamicData() public { + address farmer = farmers[0]; + address operator = address(this); + + TestState memory state = setupMowPlantHarvestBlueprintTest(false, true, true, true); + advanceSeason(); // Make plots harvestable + + + uint256[] memory harvestablePlots = new uint256[](2); + harvestablePlots[0] = 0; + harvestablePlots[1] = 500e6; + + uint256[] memory plotAmounts = new uint256[](2); + plotAmounts[0] = 500100000; + plotAmounts[1] = 500000000; + + // Create contract data array with real data + TractorFacet.ContractData[] memory contractData = new TractorFacet.ContractData[](2); + contractData[0] = TractorFacet.ContractData({ + key: mockBlueprint.HARVEST_PLOTS_KEY(), + value: abi.encode(harvestablePlots) + }); + contractData[1] = TractorFacet.ContractData({ + key: mockBlueprint.PLOT_AMOUNTS_KEY(), + value: abi.encode(plotAmounts) + }); + + bytes memory blueprintCallData = abi.encodeCall(mockBlueprint.executeBlueprint, ()); + + AdvancedFarmCall[] memory farmCalls = new AdvancedFarmCall[](1); + farmCalls[0] = AdvancedFarmCall({ + callData: abi.encodeCall( + FarmFacet.farm, + (abi.encode([AdvancedFarmCall({callData: blueprintCallData})])) + ) + }); + + LibTractor.Blueprint memory blueprint = LibTractor.Blueprint({ + publisher: farmer, + data: abi.encodePacked(bytes4(0x12345678), abi.encode(farmCalls)), + operatorPasteInstrs: new bytes32[](0), + maxNonce: 10, + startTime: block.timestamp - 1000, + endTime: block.timestamp + 1000 + }); + + bytes32 blueprintHash = bs.getBlueprintHash(blueprint); + bytes memory signature = _createMockSignature(); + + LibTractor.Requisition memory requisition = LibTractor.Requisition({ + blueprint: blueprint, + blueprintHash: blueprintHash, + signature: signature + }); + + vm.startPrank(operator); + + // Expect harvest event to be emitted + uint256 totalExpectedPods = plotAmounts[0] + plotAmounts[1]; + vm.expectEmit(true, true, true, true); + emit Harvest(farmer, bs.activeField(), harvestablePlots, totalExpectedPods); + + bytes[] memory results = bs.tractorWithData( + requisition, + new bytes(0), + contractData + ); + + vm.stopPrank(); + + // Verify execution was successful + assertEq(results.length, 1, "Should have one result"); + + // Verify data was cleared after execution + assertEq(bs.getTractorData(mockBlueprint.HARVEST_PLOTS_KEY()).length, 0, "Harvest plot data should be cleared"); + assertEq(bs.getTractorData(mockBlueprint.PLOT_AMOUNTS_KEY()).length, 0, "Plot amount data should be cleared"); + } + + /** + * @notice Test that blueprint can access dynamic data during execution + */ + function test_blueprintCanAccessDynamicData() public { + address farmer = farmers[0]; + address operator = address(this); + + uint256[] memory testPlots = new uint256[](2); + testPlots[0] = 5000; + testPlots[1] = 6000; + + TractorFacet.ContractData[] memory contractData = new TractorFacet.ContractData[](1); + contractData[0] = TractorFacet.ContractData({ + key: mockBlueprint.HARVEST_PLOTS_KEY(), + value: abi.encode(testPlots) + }); + + // Test hasDataForKey function + bytes memory checkDataCallData = abi.encodeCall( + mockBlueprint.hasDataForKey, + (mockBlueprint.HARVEST_PLOTS_KEY()) + ); + + AdvancedFarmCall[] memory farmCalls = new AdvancedFarmCall[](1); + farmCalls[0] = AdvancedFarmCall({ + callData: abi.encodeCall( + FarmFacet.farm, + (abi.encode([AdvancedFarmCall({callData: checkDataCallData})])) + ) + }); + + LibTractor.Blueprint memory blueprint = LibTractor.Blueprint({ + publisher: farmer, + data: abi.encodePacked(bytes4(0x12345678), abi.encode(farmCalls)), + operatorPasteInstrs: new bytes32[](0), + maxNonce: 10, + startTime: block.timestamp - 1000, + endTime: block.timestamp + 1000 + }); + + bytes32 blueprintHash = bs.getBlueprintHash(blueprint); + bytes memory signature = _createMockSignature(); + + LibTractor.Requisition memory requisition = LibTractor.Requisition({ + blueprint: blueprint, + blueprintHash: blueprintHash, + signature: signature + }); + + vm.prank(operator); + bytes[] memory results = bs.tractorWithData(requisition, new bytes(0), contractData); + + // The mock blueprint's hasDataForKey should return true + bool hasData = abi.decode(results[0], (bool)); + assertTrue(hasData, "Blueprint should be able to access injected data"); + } + + /** + * @notice Test tractorWithData with empty contract data + */ + function test_tractorWithData_emptyContractData() public { + address farmer = farmers[0]; + address operator = address(this); + + TractorFacet.ContractData[] memory contractData = new TractorFacet.ContractData[](0); + + bytes memory checkDataCallData = abi.encodeCall( + mockBlueprint.hasDataForKey, + (mockBlueprint.HARVEST_PLOTS_KEY()) + ); + + AdvancedFarmCall[] memory farmCalls = new AdvancedFarmCall[](1); + farmCalls[0] = AdvancedFarmCall({ + callData: abi.encodeCall( + FarmFacet.farm, + (abi.encode([AdvancedFarmCall({callData: checkDataCallData})])) + ) + }); + + LibTractor.Blueprint memory blueprint = LibTractor.Blueprint({ + publisher: farmer, + data: abi.encodePacked(bytes4(0x12345678), abi.encode(farmCalls)), + operatorPasteInstrs: new bytes32[](0), + maxNonce: 10, + startTime: block.timestamp - 1000, + endTime: block.timestamp + 1000 + }); + + bytes32 blueprintHash = bs.getBlueprintHash(blueprint); + bytes memory signature = _createMockSignature(); + + LibTractor.Requisition memory requisition = LibTractor.Requisition({ + blueprint: blueprint, + blueprintHash: blueprintHash, + signature: signature + }); + + vm.prank(operator); + bytes[] memory results = bs.tractorWithData(requisition, new bytes(0), contractData); + + // Should return false (no data) + bool hasData = abi.decode(results[0], (bool)); + assertFalse(hasData, "Should return false when no data is injected"); + } + + /** + * @notice Helper function to create mock signature for testing + */ + function _createMockSignature() internal pure returns (bytes memory) { + // Return a mock signature for testing purposes + return abi.encodePacked(bytes32(0), bytes32(0), uint8(27)); + } } From d8d1b4a505f8ef3b8215d87084a6a9d083c6eb86 Mon Sep 17 00:00:00 2001 From: pocikerim Date: Fri, 7 Nov 2025 22:02:48 +0300 Subject: [PATCH 5/6] Revert "tests" This reverts commit 8853051937e8078ef219a85fa35ad2cb3bc54c96. --- contracts/mocks/MockBlueprint.sol | 102 --------- .../ecosystem/MowPlantHarvestBlueprint.t.sol | 203 +----------------- 2 files changed, 1 insertion(+), 304 deletions(-) delete mode 100644 contracts/mocks/MockBlueprint.sol diff --git a/contracts/mocks/MockBlueprint.sol b/contracts/mocks/MockBlueprint.sol deleted file mode 100644 index 66f2196a..00000000 --- a/contracts/mocks/MockBlueprint.sol +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {IBeanstalk} from "contracts/interfaces/IBeanstalk.sol"; - -/** - * @title MockBlueprint - * @notice Mock blueprint contract for testing dynamic data injection with tractorWithData - */ -contract MockBlueprint { - IBeanstalk public immutable beanstalk; - - uint256 public constant HARVEST_PLOTS_KEY = 1; - uint256 public constant PLOT_AMOUNTS_KEY = 2; - - event DynamicDataUsed(uint256[] plots, uint256[] amounts); - event HarvestSimulated(address indexed user, uint256 fieldId, uint256[] plots, uint256 totalPods); - - constructor(address _beanstalk) { - beanstalk = IBeanstalk(_beanstalk); - } - - /** - * @notice Main blueprint function that uses dynamic data - * @dev This function demonstrates how a blueprint contract can access - * dynamic data injected via tractorWithData - */ - function executeBlueprint() external { - bytes memory plotData = beanstalk.getTractorData(HARVEST_PLOTS_KEY); - require(plotData.length > 0, "No harvest plot data available"); - - uint256[] memory harvestablePlots = abi.decode(plotData, (uint256[])); - - bytes memory amountData = beanstalk.getTractorData(PLOT_AMOUNTS_KEY); - uint256[] memory plotAmounts; - - if (amountData.length > 0) { - plotAmounts = abi.decode(amountData, (uint256[])); - require( - plotAmounts.length == harvestablePlots.length, - "Plot and amount arrays must match" - ); - } - - emit DynamicDataUsed(harvestablePlots, plotAmounts); - _simulateHarvest(harvestablePlots); - } - - /** - * @notice Simulate harvest operations using dynamic data with realistic validation - * @param plots The harvestable plots from dynamic data - */ - function _simulateHarvest(uint256[] memory plots) internal { - require(plots.length > 0, "MockBlueprint: No plots to harvest"); - - address user = beanstalk.tractorUser(); - uint256 fieldId = beanstalk.activeField(); - uint256 harvestableIndex = beanstalk.harvestableIndex(fieldId); - uint256 totalHarvestablePods; - - for (uint256 i; i < plots.length; ++i) { - uint256 plotIndex = plots[i]; - uint256 plotPods = beanstalk.plot(user, fieldId, plotIndex); - - // Validate plot ownership (like real harvest) - require(plotPods > 0, "MockBlueprint: no plot"); - - // Validate harvestability (like real harvest) - require(plotIndex < harvestableIndex, "MockBlueprint: plot not harvestable"); - - // Calculate harvestable pods (like real harvest) - uint256 harvestablePods = harvestableIndex - plotIndex; - if (harvestablePods > plotPods) harvestablePods = plotPods; - - totalHarvestablePods += harvestablePods; - } - - // Emit simulation event (like real harvest event) - emit HarvestSimulated(user, fieldId, plots, totalHarvestablePods); - - // Note: We don't actually modify state, just validate and emit event - // This simulates the harvest logic without side effects - } - - /** - * @notice Helper function to get current user - * @return The current tractor user - */ - function getCurrentUser() external view returns (address) { - return beanstalk.tractorUser(); - } - - /** - * @notice Helper function to check if data exists for a key - * @param key The data key to check - * @return True if data exists for the key - */ - function hasDataForKey(uint256 key) external view returns (bool) { - bytes memory data = beanstalk.getTractorData(key); - return data.length > 0; - } -} diff --git a/test/foundry/ecosystem/MowPlantHarvestBlueprint.t.sol b/test/foundry/ecosystem/MowPlantHarvestBlueprint.t.sol index 96b3ee19..2af0371a 100644 --- a/test/foundry/ecosystem/MowPlantHarvestBlueprint.t.sol +++ b/test/foundry/ecosystem/MowPlantHarvestBlueprint.t.sol @@ -14,15 +14,12 @@ import {BeanstalkPrice} from "contracts/ecosystem/price/BeanstalkPrice.sol"; import {IBeanstalk} from "contracts/interfaces/IBeanstalk.sol"; import {OperatorWhitelist} from "contracts/ecosystem/OperatorWhitelist.sol"; import {MowPlantHarvestBlueprint} from "contracts/ecosystem/MowPlantHarvestBlueprint.sol"; -import {MockBlueprint} from "contracts/mocks/MockBlueprint.sol"; -import {LibTractor} from "contracts/libraries/LibTractor.sol"; import "forge-std/console.sol"; contract MowPlantHarvestBlueprintTest is TractorHelper { address[] farmers; PriceManipulation priceManipulation; BeanstalkPrice beanstalkPrice; - MockBlueprint mockBlueprint; event Plant(address indexed account, uint256 beans); event Harvest(address indexed account, uint256 fieldId, uint256[] plots, uint256 beans); @@ -80,10 +77,6 @@ contract MowPlantHarvestBlueprintTest is TractorHelper { setTractorHelpers(address(tractorHelpers)); setMowPlantHarvestBlueprint(address(mowPlantHarvestBlueprint)); - // Deploy mock blueprint for dynamic data tests - mockBlueprint = new MockBlueprint(address(bs)); - vm.label(address(mockBlueprint), "MockBlueprint"); - // Advance season to grow stalk advanceSeason(); } @@ -303,7 +296,7 @@ contract MowPlantHarvestBlueprintTest is TractorHelper { advanceSeason(); // assert user has harvestable pods - (uint256 totalHarvestablePods, ) = (state.user, DEFAULT_FIELD_ID); + (uint256 totalHarvestablePods, ) = _userHarvestablePods(state.user, DEFAULT_FIELD_ID); assertGt(totalHarvestablePods, 0, "user should have harvestable pods to harvest"); // Setup blueprint with minHarvestAmount less than harvest tip amount @@ -585,198 +578,4 @@ contract MowPlantHarvestBlueprintTest is TractorHelper { advanceSeason(); advanceSeason(); } - - /** - * @notice Test tractorWithData function with realistic dynamic data injection - */ - function test_tractorWithData_withDynamicData() public { - address farmer = farmers[0]; - address operator = address(this); - - TestState memory state = setupMowPlantHarvestBlueprintTest(false, true, true, true); - advanceSeason(); // Make plots harvestable - - - uint256[] memory harvestablePlots = new uint256[](2); - harvestablePlots[0] = 0; - harvestablePlots[1] = 500e6; - - uint256[] memory plotAmounts = new uint256[](2); - plotAmounts[0] = 500100000; - plotAmounts[1] = 500000000; - - // Create contract data array with real data - TractorFacet.ContractData[] memory contractData = new TractorFacet.ContractData[](2); - contractData[0] = TractorFacet.ContractData({ - key: mockBlueprint.HARVEST_PLOTS_KEY(), - value: abi.encode(harvestablePlots) - }); - contractData[1] = TractorFacet.ContractData({ - key: mockBlueprint.PLOT_AMOUNTS_KEY(), - value: abi.encode(plotAmounts) - }); - - bytes memory blueprintCallData = abi.encodeCall(mockBlueprint.executeBlueprint, ()); - - AdvancedFarmCall[] memory farmCalls = new AdvancedFarmCall[](1); - farmCalls[0] = AdvancedFarmCall({ - callData: abi.encodeCall( - FarmFacet.farm, - (abi.encode([AdvancedFarmCall({callData: blueprintCallData})])) - ) - }); - - LibTractor.Blueprint memory blueprint = LibTractor.Blueprint({ - publisher: farmer, - data: abi.encodePacked(bytes4(0x12345678), abi.encode(farmCalls)), - operatorPasteInstrs: new bytes32[](0), - maxNonce: 10, - startTime: block.timestamp - 1000, - endTime: block.timestamp + 1000 - }); - - bytes32 blueprintHash = bs.getBlueprintHash(blueprint); - bytes memory signature = _createMockSignature(); - - LibTractor.Requisition memory requisition = LibTractor.Requisition({ - blueprint: blueprint, - blueprintHash: blueprintHash, - signature: signature - }); - - vm.startPrank(operator); - - // Expect harvest event to be emitted - uint256 totalExpectedPods = plotAmounts[0] + plotAmounts[1]; - vm.expectEmit(true, true, true, true); - emit Harvest(farmer, bs.activeField(), harvestablePlots, totalExpectedPods); - - bytes[] memory results = bs.tractorWithData( - requisition, - new bytes(0), - contractData - ); - - vm.stopPrank(); - - // Verify execution was successful - assertEq(results.length, 1, "Should have one result"); - - // Verify data was cleared after execution - assertEq(bs.getTractorData(mockBlueprint.HARVEST_PLOTS_KEY()).length, 0, "Harvest plot data should be cleared"); - assertEq(bs.getTractorData(mockBlueprint.PLOT_AMOUNTS_KEY()).length, 0, "Plot amount data should be cleared"); - } - - /** - * @notice Test that blueprint can access dynamic data during execution - */ - function test_blueprintCanAccessDynamicData() public { - address farmer = farmers[0]; - address operator = address(this); - - uint256[] memory testPlots = new uint256[](2); - testPlots[0] = 5000; - testPlots[1] = 6000; - - TractorFacet.ContractData[] memory contractData = new TractorFacet.ContractData[](1); - contractData[0] = TractorFacet.ContractData({ - key: mockBlueprint.HARVEST_PLOTS_KEY(), - value: abi.encode(testPlots) - }); - - // Test hasDataForKey function - bytes memory checkDataCallData = abi.encodeCall( - mockBlueprint.hasDataForKey, - (mockBlueprint.HARVEST_PLOTS_KEY()) - ); - - AdvancedFarmCall[] memory farmCalls = new AdvancedFarmCall[](1); - farmCalls[0] = AdvancedFarmCall({ - callData: abi.encodeCall( - FarmFacet.farm, - (abi.encode([AdvancedFarmCall({callData: checkDataCallData})])) - ) - }); - - LibTractor.Blueprint memory blueprint = LibTractor.Blueprint({ - publisher: farmer, - data: abi.encodePacked(bytes4(0x12345678), abi.encode(farmCalls)), - operatorPasteInstrs: new bytes32[](0), - maxNonce: 10, - startTime: block.timestamp - 1000, - endTime: block.timestamp + 1000 - }); - - bytes32 blueprintHash = bs.getBlueprintHash(blueprint); - bytes memory signature = _createMockSignature(); - - LibTractor.Requisition memory requisition = LibTractor.Requisition({ - blueprint: blueprint, - blueprintHash: blueprintHash, - signature: signature - }); - - vm.prank(operator); - bytes[] memory results = bs.tractorWithData(requisition, new bytes(0), contractData); - - // The mock blueprint's hasDataForKey should return true - bool hasData = abi.decode(results[0], (bool)); - assertTrue(hasData, "Blueprint should be able to access injected data"); - } - - /** - * @notice Test tractorWithData with empty contract data - */ - function test_tractorWithData_emptyContractData() public { - address farmer = farmers[0]; - address operator = address(this); - - TractorFacet.ContractData[] memory contractData = new TractorFacet.ContractData[](0); - - bytes memory checkDataCallData = abi.encodeCall( - mockBlueprint.hasDataForKey, - (mockBlueprint.HARVEST_PLOTS_KEY()) - ); - - AdvancedFarmCall[] memory farmCalls = new AdvancedFarmCall[](1); - farmCalls[0] = AdvancedFarmCall({ - callData: abi.encodeCall( - FarmFacet.farm, - (abi.encode([AdvancedFarmCall({callData: checkDataCallData})])) - ) - }); - - LibTractor.Blueprint memory blueprint = LibTractor.Blueprint({ - publisher: farmer, - data: abi.encodePacked(bytes4(0x12345678), abi.encode(farmCalls)), - operatorPasteInstrs: new bytes32[](0), - maxNonce: 10, - startTime: block.timestamp - 1000, - endTime: block.timestamp + 1000 - }); - - bytes32 blueprintHash = bs.getBlueprintHash(blueprint); - bytes memory signature = _createMockSignature(); - - LibTractor.Requisition memory requisition = LibTractor.Requisition({ - blueprint: blueprint, - blueprintHash: blueprintHash, - signature: signature - }); - - vm.prank(operator); - bytes[] memory results = bs.tractorWithData(requisition, new bytes(0), contractData); - - // Should return false (no data) - bool hasData = abi.decode(results[0], (bool)); - assertFalse(hasData, "Should return false when no data is injected"); - } - - /** - * @notice Helper function to create mock signature for testing - */ - function _createMockSignature() internal pure returns (bytes memory) { - // Return a mock signature for testing purposes - return abi.encodePacked(bytes32(0), bytes32(0), uint8(27)); - } } From b08ba3bdfb65a8b13f44364afb90bf0b0556079e Mon Sep 17 00:00:00 2001 From: pocikerim Date: Fri, 7 Nov 2025 22:07:13 +0300 Subject: [PATCH 6/6] fix: move TractorStorage reference from LibTractor to LibTractorStorage import --- contracts/beanstalk/init/deployment/InitProtocol.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/beanstalk/init/deployment/InitProtocol.sol b/contracts/beanstalk/init/deployment/InitProtocol.sol index 319eb537..b89aa114 100644 --- a/contracts/beanstalk/init/deployment/InitProtocol.sol +++ b/contracts/beanstalk/init/deployment/InitProtocol.sol @@ -10,6 +10,7 @@ import {BeanstalkERC20} from "contracts/tokens/ERC20/BeanstalkERC20.sol"; import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {AppStorage} from "contracts/beanstalk/storage/AppStorage.sol"; import {LibTractor} from "contracts/libraries/LibTractor.sol"; +import {LibTractorStorage} from "contracts/libraries/LibTractorStorage.sol"; import {LibCases} from "contracts/libraries/LibCases.sol"; import {Distribution} from "contracts/beanstalk/facets/sun/abstract/Distribution.sol"; import {C} from "contracts/C.sol"; @@ -195,7 +196,7 @@ contract InitProtocol { * @notice Sets the tractor version and active publisher. */ function setTractor() internal { - LibTractor.TractorStorage storage ts = LibTractor._tractorStorage(); + LibTractorStorage.TractorStorage storage ts = LibTractor._tractorStorage(); ts.activePublisher = payable(address(1)); ts.version = "1.0.0"; }