-
Notifications
You must be signed in to change notification settings - Fork 0
Dynamic tractor data #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: mow-plant-harvest-blueprint
Are you sure you want to change the base?
Changes from all commits
f68ac04
a0f6c43
23afd53
8853051
d8d1b4a
b08ba3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,11 @@ contract TractorFacet is Invariable, ReentrancyGuard { | |
| uint256 gasleft | ||
| ); | ||
|
|
||
| struct ContractData { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. generally though, we try to adhere to Coinbase's style guide (notably, the exceptions). These are pretty minor though and are only done for readability purposes. https://github.com/coinbase/solidity-style-guide/blob/main/README.md |
||
| 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). | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @notice is generally used to explain "what" the function does (i.e "Get tractor data"), whereas @dev explains the "how" or "why" the function is implemented this way. example: /** Its up to the developer to add the @param or @return, but in this case it's pretty obvious on what the values are, so omitting it is fine. |
||
| * @param key The key to get the data for | ||
| * @return The data for the key | ||
| */ | ||
| function getTractorData(uint256 key) external view returns (bytes memory) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. view functions should be below external state changing functions. https://docs.soliditylang.org/en/latest/style-guide.html#order-of-layout
|
||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would continue to use the same variable naming style in the Sometimes you will have to make decisions on whether to keep variable naming consistent, or changing it in favor for more detail. Given that solidity changes create downstream effects in both the middleware and frontend, we generally lean on to keeping things as consistent as possible. Generally with structs, we reference the struct, rather than describing the struct itself. Below is what i think would be more optimal. |
||
| * @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"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Couple of things: Key principle in coding is to apply DRY (Do not Repeat) principles whenever possible. This is especially important in solidity as duplicate code causes unnecessary duplication of bytecode, which leads to 1) extra costs when deploying smart contracts, 2) may cause us to hit the code size limit. Generally, DRY allows for easier maintainability and readability. To give an example, note that the You can see how from a bird eyes view (i.e a glance), that
You can argue that we can put the setting and resetting of contract data within a modifier. I think both are fine and is up to the developers preference. personally i find this a bit easier to read as all the context is on the function, and we already have 4 modifiers on this function. Obviously you should comment as much as needed to make this easy to understand. A side note: when we make upgrade the protocol, something to keep in mind as solidity developers is that we should avoid the function signature changing for existing functions whenever possible, to allow for backwards compatibility. This is why we created a separate |
||
|
|
||
| LibTractor._setCurrentBlueprintHash(requisition.blueprintHash); | ||
|
|
||
| LibTractor._setOperator(msg.sender); | ||
|
|
||
| for (uint256 i = 0; i < contractData.length; i++) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'd put R342-344 into a internal library function, and have an if statement in the internal function the rule of thumb is that internal functions are useful once you can explain the block of code in a function name. For example, after we add the if check, this section of code will be 5+ lines of code. its easier for other developers to inuit and black-box logic if the function is in one line. ex: |
||
| 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(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this function should be in LibTractorStorage also, lets remove the underscore on this name, i strongly prefer that. |
||
| // 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. per our conversation before, setting the value to bytes(1) saves us little value given that the bytes are dynamic here. I believe we use the |
||
| } | ||
|
|
||
| /** | ||
| * @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" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| /** | ||
| * SPDX-License-Identifier: MIT | ||
| **/ | ||
|
|
||
| pragma solidity ^0.8.20; | ||
|
|
||
| library LibTractorStorage { | ||
| struct TractorStorage { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lets add natspec here on the struct and on LibTractorStorage. You can look at LibAppStorage for reference. |
||
| 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; | ||
| } | ||
| } | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whenever we have a struct, we should have some NatSpec on what the purpose of this struct is. If the struct is quite complex/the names of the variables don't give enough context on what its used for, then we should also try to comment on the names as well.
here is documentation on natspec:
https://docs.soliditylang.org/en/latest/natspec-format.html