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(); + } } 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"; } diff --git a/contracts/libraries/LibTractor.sol b/contracts/libraries/LibTractor.sol index 35c791ec..17b3199a 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,40 @@ 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. + * @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 { + _tractorStorage().data[key] = abi.encode(1); + } + /** * @notice Set the tractor hashed version. */ @@ -104,7 +118,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; + } +}