diff --git a/src/abstract/FlowCommon.sol b/src/abstract/FlowCommon.sol index 92d8b052..59d4adfc 100644 --- a/src/abstract/FlowCommon.sol +++ b/src/abstract/FlowCommon.sol @@ -6,7 +6,7 @@ import {Pointer} from "rain.solmem/lib/LibPointer.sol"; import {IInterpreterCallerV2, SignedContextV1} from "rain.interpreter/src/interface/IInterpreterCallerV2.sol"; import {LibEncodedDispatch} from "rain.interpreter/src/lib/caller/LibEncodedDispatch.sol"; import {LibContext} from "rain.interpreter/src/lib/caller/LibContext.sol"; -import {UnregisteredFlow} from "../interface/unstable/IFlowV4.sol"; +import {UnregisteredFlow, MIN_FLOW_SENTINELS} from "../interface/unstable/IFlowV4.sol"; import { DeployerDiscoverableMetaV2, DeployerDiscoverableMetaV2ConstructionConfig @@ -38,10 +38,6 @@ import {LibUint256Matrix} from "rain.solmem/lib/LibUint256Matrix.sol"; /// @param flowMinOutputs The min outputs for the flow. error BadMinStackLength(uint256 flowMinOutputs); -/// @dev The number of sentinels required by `FlowCommon`. An evaluable can never -/// have fewer minimum outputs than required sentinels. -uint256 constant MIN_FLOW_SENTINELS = 3; - /// @dev The entrypoint for a flow is always `0` because each flow has its own /// evaluable with its own entrypoint. Running multiple flows involves evaluating /// several expressions in sequence. @@ -220,7 +216,11 @@ abstract contract FlowCommon is } /// TODO merge both flowStack functions into one. - function _flowStack(Evaluable memory evaluable, uint256[] memory callerContext, SignedContextV1[] memory signedContexts) internal returns (Pointer, Pointer, uint256[] memory) { + function _flowStack( + Evaluable memory evaluable, + uint256[] memory callerContext, + SignedContextV1[] memory signedContexts + ) internal returns (Pointer, Pointer, uint256[] memory) { uint256[][] memory context = LibContext.build(callerContext.matrixFrom(), signedContexts); emit Context(msg.sender, context); return _flowStack(evaluable, context); diff --git a/src/concrete/basic/Flow.sol b/src/concrete/basic/Flow.sol index 594794cf..c9931735 100644 --- a/src/concrete/basic/Flow.sol +++ b/src/concrete/basic/Flow.sol @@ -2,13 +2,9 @@ pragma solidity =0.8.19; import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "rain.factory/src/interface/ICloneableV2.sol"; -import { - FlowCommon, - DeployerDiscoverableMetaV2ConstructionConfig, - LibContext, - MIN_FLOW_SENTINELS -} from "../../abstract/FlowCommon.sol"; -import {IFlowV4, LibFlow} from "../../lib/LibFlow.sol"; +import {FlowCommon, DeployerDiscoverableMetaV2ConstructionConfig, LibContext} from "../../abstract/FlowCommon.sol"; +import {IFlowV4, MIN_FLOW_SENTINELS} from "../../interface/unstable/IFlowV4.sol"; +import {LibFlow} from "../../lib/LibFlow.sol"; import {LibUint256Matrix} from "rain.solmem/lib/LibUint256Matrix.sol"; import {Pointer} from "rain.solmem/lib/LibPointer.sol"; import {LibUint256Array} from "rain.solmem/lib/LibUint256Array.sol"; @@ -56,7 +52,8 @@ contract Flow is ICloneableV2, IFlowV4, FlowCommon { nonReentrant returns (FlowTransferV1 memory) { - (Pointer stackBottom, Pointer stackTop, uint256[] memory kvs) = _flowStack(evaluable, callerContext, signedContexts); + (Pointer stackBottom, Pointer stackTop, uint256[] memory kvs) = + _flowStack(evaluable, callerContext, signedContexts); FlowTransferV1 memory flowTransfer = LibFlow.stackToFlow(stackBottom, stackTop); LibFlow.flow(flowTransfer, evaluable.store, kvs); return flowTransfer; diff --git a/src/concrete/erc1155/FlowERC1155.sol b/src/concrete/erc1155/FlowERC1155.sol index faf4dce5..9385f132 100644 --- a/src/concrete/erc1155/FlowERC1155.sol +++ b/src/concrete/erc1155/FlowERC1155.sol @@ -15,7 +15,11 @@ import { SignedContextV1, FlowERC1155ConfigV2, ERC1155SupplyChange, - RAIN_FLOW_SENTINEL + RAIN_FLOW_SENTINEL, + FLOW_ERC1155_HANDLE_TRANSFER_ENTRYPOINT, + FLOW_ERC1155_HANDLE_TRANSFER_MAX_OUTPUTS, + FLOW_ERC1155_HANDLE_TRANSFER_MIN_OUTPUTS, + FLOW_ERC1155_MIN_FLOW_SENTINELS } from "../../interface/unstable/IFlowERC1155V4.sol"; import {LibBytecode} from "lib/rain.interpreter/src/lib/bytecode/LibBytecode.sol"; import {IInterpreterV1} from "rain.interpreter/src/interface/IInterpreterV1.sol"; @@ -25,28 +29,27 @@ import {Pointer} from "rain.solmem/lib/LibPointer.sol"; import {LibFlow} from "../../lib/LibFlow.sol"; import {SourceIndex} from "rain.interpreter/src/interface/IInterpreterV1.sol"; import { - MIN_FLOW_SENTINELS, - FlowCommon, - DeployerDiscoverableMetaV2ConstructionConfig, - ERC1155Receiver + FlowCommon, DeployerDiscoverableMetaV2ConstructionConfig, ERC1155Receiver } from "../../abstract/FlowCommon.sol"; import {LibContext} from "rain.interpreter/src/lib/caller/LibContext.sol"; +/// @dev The hash of the meta data expected by the `FlowCommon` constructor. bytes32 constant CALLER_META_HASH = bytes32(0x7ea70f837234357ec1bb5b777e04453ebaf3ca778a98805c4bb20a738d559a21); -SourceIndex constant HANDLE_TRANSFER_ENTRYPOINT = SourceIndex.wrap(0); -uint256 constant HANDLE_TRANSFER_MIN_OUTPUTS = 0; -uint16 constant HANDLE_TRANSFER_MAX_OUTPUTS = 0; - -uint256 constant FLOW_ERC1155_MIN_OUTPUTS = MIN_FLOW_SENTINELS + 2; - +/// @title FlowERC1155 +/// See `IFlowERC1155V4` for documentation. contract FlowERC1155 is ICloneableV2, IFlowERC1155V4, FlowCommon, ERC1155 { using LibStackSentinel for Pointer; using LibUint256Matrix for uint256[]; + using LibUint256Array for uint256[]; + /// True if the evaluable needs to be called on every transfer. bool private sEvalHandleTransfer; + + /// The `Evaluable` that handles transfers. Evaluable internal sEvaluable; + /// Forwards the `FlowCommon` constructor. constructor(DeployerDiscoverableMetaV2ConstructionConfig memory config) FlowCommon(CALLER_META_HASH, config) {} /// Overloaded typed initialize function MUST revert with this error. @@ -64,11 +67,11 @@ contract FlowERC1155 is ICloneableV2, IFlowERC1155V4, FlowCommon, ERC1155 { // Set state before external calls here. bool evalHandleTransfer = LibBytecode.sourceCount(flowERC1155Config.evaluableConfig.bytecode) > 0 && LibBytecode.sourceOpsLength( - flowERC1155Config.evaluableConfig.bytecode, SourceIndex.unwrap(HANDLE_TRANSFER_ENTRYPOINT) + flowERC1155Config.evaluableConfig.bytecode, SourceIndex.unwrap(FLOW_ERC1155_HANDLE_TRANSFER_ENTRYPOINT) ) > 0; sEvalHandleTransfer = evalHandleTransfer; - flowCommonInit(flowERC1155Config.flowConfig, FLOW_ERC1155_MIN_OUTPUTS); + flowCommonInit(flowERC1155Config.flowConfig, FLOW_ERC1155_MIN_FLOW_SENTINELS); if (evalHandleTransfer) { (IInterpreterV1 interpreter, IInterpreterStoreV1 store, address expression) = flowERC1155Config @@ -77,7 +80,7 @@ contract FlowERC1155 is ICloneableV2, IFlowERC1155V4, FlowCommon, ERC1155 { .deployExpression( flowERC1155Config.evaluableConfig.bytecode, flowERC1155Config.evaluableConfig.constants, - LibUint256Array.arrayFrom(HANDLE_TRANSFER_MIN_OUTPUTS) + LibUint256Array.arrayFrom(FLOW_ERC1155_HANDLE_TRANSFER_MIN_OUTPUTS) ); // There's no way to set this before the external call because the // output of the `deployExpression` call is the input to `Evaluable`. @@ -92,10 +95,6 @@ contract FlowERC1155 is ICloneableV2, IFlowERC1155V4, FlowCommon, ERC1155 { return ICLONEABLE_V2_SUCCESS; } - function _dispatchHandleTransfer(address expression) internal pure returns (EncodedDispatch) { - return LibEncodedDispatch.encode(expression, HANDLE_TRANSFER_ENTRYPOINT, HANDLE_TRANSFER_MAX_OUTPUTS); - } - /// Needed here to fix Open Zeppelin implementing `supportsInterface` on /// multiple base contracts. function supportsInterface(bytes4 interfaceId) @@ -140,7 +139,14 @@ contract FlowERC1155 is ICloneableV2, IFlowERC1155V4, FlowCommon, ERC1155 { } (uint256[] memory stack, uint256[] memory kvs) = evaluable.interpreter.eval( - evaluable.store, DEFAULT_STATE_NAMESPACE, _dispatchHandleTransfer(evaluable.expression), context + evaluable.store, + DEFAULT_STATE_NAMESPACE, + LibEncodedDispatch.encode( + evaluable.expression, + FLOW_ERC1155_HANDLE_TRANSFER_ENTRYPOINT, + FLOW_ERC1155_HANDLE_TRANSFER_MAX_OUTPUTS + ), + context ); (stack); if (kvs.length > 0) { @@ -150,15 +156,36 @@ contract FlowERC1155 is ICloneableV2, IFlowERC1155V4, FlowCommon, ERC1155 { } } - function _previewFlow(Evaluable memory evaluable, uint256[][] memory context) - internal - view - returns (FlowERC1155IOV1 memory, uint256[] memory) + /// @inheritdoc IFlowERC1155V4 + function stackToFlow(uint256[] memory stack) + external + pure + override + returns (FlowERC1155IOV1 memory flowERC1155IO) + { + return _stackToFlow(stack.dataPointer(), stack.endPointer()); + } + + /// @inheritdoc IFlowERC1155V4 + function flow(Evaluable memory evaluable, uint256[] memory callerContext, SignedContextV1[] memory signedContexts) + external + virtual + returns (FlowERC1155IOV1 memory) { + return _flow(evaluable, callerContext, signedContexts); + } + + /// Wraps the standard `LibFlow.stackToFlow` function with the addition of + /// consuming the mint/burn sentinels from the stack and returning them in + /// the `FlowERC1155IOV1`. + /// @param stackBottom The bottom of the stack. + /// @param stackTop The top of the stack. + /// @return flowERC1155IO The `FlowERC1155IOV1` representation of the stack. + function _stackToFlow(Pointer stackBottom, Pointer stackTop) internal pure returns (FlowERC1155IOV1 memory) { ERC1155SupplyChange[] memory mints; ERC1155SupplyChange[] memory burns; Pointer tuplesPointer; - (Pointer stackBottom, Pointer stackTop, uint256[] memory kvs) = _flowStack(evaluable, context); + // mints // https://github.com/crytic/slither/issues/2126 //slither-disable-next-line unused-return @@ -173,9 +200,13 @@ contract FlowERC1155 is ICloneableV2, IFlowERC1155V4, FlowCommon, ERC1155 { assembly ("memory-safe") { burns := tuplesPointer } - return (FlowERC1155IOV1(mints, burns, LibFlow.stackToFlow(stackBottom, stackTop)), kvs); + return FlowERC1155IOV1(mints, burns, LibFlow.stackToFlow(stackBottom, stackTop)); } + /// Wraps the standard `LibFlow.flow` function to handle minting and burning + /// of the flow contract itself. This involves consuming the mint/burn + /// sentinels from the stack and minting/burning the tokens accordingly, then + /// calling `LibFlow.flow` to handle the rest of the flow. function _flow(Evaluable memory evaluable, uint256[] memory callerContext, SignedContextV1[] memory signedContexts) internal virtual @@ -183,36 +214,18 @@ contract FlowERC1155 is ICloneableV2, IFlowERC1155V4, FlowCommon, ERC1155 { returns (FlowERC1155IOV1 memory) { unchecked { - uint256[][] memory context = LibContext.build(callerContext.matrixFrom(), signedContexts); - emit Context(msg.sender, context); - (FlowERC1155IOV1 memory flowIO, uint256[] memory kvs) = _previewFlow(evaluable, context); - for (uint256 i = 0; i < flowIO.mints.length; i++) { + (Pointer stackBottom, Pointer stackTop, uint256[] memory kvs) = + _flowStack(evaluable, callerContext, signedContexts); + FlowERC1155IOV1 memory flowERC1155IO = _stackToFlow(stackBottom, stackTop); + for (uint256 i = 0; i < flowERC1155IO.mints.length; i++) { // @todo support data somehow. - _mint(flowIO.mints[i].account, flowIO.mints[i].id, flowIO.mints[i].amount, ""); + _mint(flowERC1155IO.mints[i].account, flowERC1155IO.mints[i].id, flowERC1155IO.mints[i].amount, ""); } - for (uint256 i = 0; i < flowIO.burns.length; i++) { - _burn(flowIO.burns[i].account, flowIO.burns[i].id, flowIO.burns[i].amount); + for (uint256 i = 0; i < flowERC1155IO.burns.length; i++) { + _burn(flowERC1155IO.burns[i].account, flowERC1155IO.burns[i].id, flowERC1155IO.burns[i].amount); } - LibFlow.flow(flowIO.flow, evaluable.store, kvs); - return flowIO; + LibFlow.flow(flowERC1155IO.flow, evaluable.store, kvs); + return flowERC1155IO; } } - - function previewFlow( - Evaluable memory evaluable, - uint256[] memory callerContext, - SignedContextV1[] memory signedContexts - ) external view virtual returns (FlowERC1155IOV1 memory) { - uint256[][] memory context = LibContext.build(callerContext.matrixFrom(), signedContexts); - (FlowERC1155IOV1 memory flowERC1155IO,) = _previewFlow(evaluable, context); - return flowERC1155IO; - } - - function flow(Evaluable memory evaluable, uint256[] memory callerContext, SignedContextV1[] memory signedContexts) - external - virtual - returns (FlowERC1155IOV1 memory) - { - return _flow(evaluable, callerContext, signedContexts); - } } diff --git a/src/concrete/erc20/FlowERC20.sol b/src/concrete/erc20/FlowERC20.sol index 6553148c..a7f28626 100644 --- a/src/concrete/erc20/FlowERC20.sol +++ b/src/concrete/erc20/FlowERC20.sol @@ -14,18 +14,18 @@ import { SignedContextV1, FLOW_ERC20_HANDLE_TRANSFER_ENTRYPOINT, FLOW_ERC20_HANDLE_TRANSFER_MIN_OUTPUTS, - FLOW_ERC20_HANDLE_TRANSFER_MAX_OUTPUTS + FLOW_ERC20_HANDLE_TRANSFER_MAX_OUTPUTS, + RAIN_FLOW_SENTINEL, + FLOW_ERC20_MIN_FLOW_SENTINELS } from "../../interface/unstable/IFlowERC20V4.sol"; import {LibBytecode} from "lib/rain.interpreter/src/lib/bytecode/LibBytecode.sol"; import {EncodedDispatch, LibEncodedDispatch} from "rain.interpreter/src/lib/caller/LibEncodedDispatch.sol"; -import {RAIN_FLOW_SENTINEL} from "../../interface/unstable/IFlowERC20V4.sol"; import {Sentinel, LibStackSentinel} from "rain.solmem/lib/LibStackSentinel.sol"; import {LibFlow} from "../../lib/LibFlow.sol"; import { FlowCommon, DeployerDiscoverableMetaV2, - DeployerDiscoverableMetaV2ConstructionConfig, - MIN_FLOW_SENTINELS + DeployerDiscoverableMetaV2ConstructionConfig } from "../../abstract/FlowCommon.sol"; import {SourceIndex, IInterpreterV1} from "rain.interpreter/src/interface/IInterpreterV1.sol"; import {IInterpreterStoreV1} from "rain.interpreter/src/interface/IInterpreterStoreV1.sol"; @@ -53,7 +53,7 @@ contract FlowERC20 is ICloneableV2, IFlowERC20V4, FlowCommon, ERC20 { /// every transfer. This is only set if `sEvalHandleTransfer` is true. Evaluable internal sEvaluable; - /// Forwards the `FlowCommon` constructor arguments to the `FlowCommon` + /// Forwards the `FlowCommon` constructor arguments to the `FlowCommon`. constructor(DeployerDiscoverableMetaV2ConstructionConfig memory config) FlowCommon(CALLER_META_HASH, config) {} /// Overloaded typed initialize function MUST revert with this error. @@ -75,7 +75,7 @@ contract FlowERC20 is ICloneableV2, IFlowERC20V4, FlowCommon, ERC20 { ) > 0; sEvalHandleTransfer = evalHandleTransfer; - flowCommonInit(flowERC20Config.flowConfig, MIN_FLOW_SENTINELS + 2); + flowCommonInit(flowERC20Config.flowConfig, FLOW_ERC20_MIN_FLOW_SENTINELS); if (evalHandleTransfer) { (IInterpreterV1 interpreter, IInterpreterStoreV1 store, address expression) = flowERC20Config @@ -162,6 +162,7 @@ contract FlowERC20 is ICloneableV2, IFlowERC20V4, FlowCommon, ERC20 { ERC20SupplyChange[] memory mints; ERC20SupplyChange[] memory burns; Pointer tuplesPointer; + // mints // https://github.com/crytic/slither/issues/2126 //slither-disable-next-line unused-return @@ -191,7 +192,8 @@ contract FlowERC20 is ICloneableV2, IFlowERC20V4, FlowCommon, ERC20 { returns (FlowERC20IOV1 memory) { unchecked { - (Pointer stackBottom, Pointer stackTop, uint256[] memory kvs) = _flowStack(evaluable, callerContext, signedContexts); + (Pointer stackBottom, Pointer stackTop, uint256[] memory kvs) = + _flowStack(evaluable, callerContext, signedContexts); FlowERC20IOV1 memory flowERC20IO = _stackToFlow(stackBottom, stackTop); for (uint256 i = 0; i < flowERC20IO.mints.length; ++i) { _mint(flowERC20IO.mints[i].account, flowERC20IO.mints[i].amount); diff --git a/src/concrete/erc721/FlowERC721.sol b/src/concrete/erc721/FlowERC721.sol index e8e89cb4..b2c5e941 100644 --- a/src/concrete/erc721/FlowERC721.sol +++ b/src/concrete/erc721/FlowERC721.sol @@ -14,7 +14,14 @@ import { FlowERC721IOV1, SignedContextV1, FlowERC721ConfigV2, - ERC721SupplyChange + ERC721SupplyChange, + FLOW_ERC721_TOKEN_URI_MIN_OUTPUTS, + FLOW_ERC721_TOKEN_URI_MAX_OUTPUTS, + FLOW_ERC721_HANDLE_TRANSFER_MIN_OUTPUTS, + FLOW_ERC721_HANDLE_TRANSFER_MAX_OUTPUTS, + FLOW_ERC721_TOKEN_URI_ENTRYPOINT, + FLOW_ERC721_HANDLE_TRANSFER_ENTRYPOINT, + FLOW_ERC721_MIN_FLOW_SENTINELS } from "../../interface/unstable/IFlowERC721V4.sol"; import {LibBytecode} from "lib/rain.interpreter/src/lib/bytecode/LibBytecode.sol"; import {SourceIndex} from "rain.interpreter/src/interface/IInterpreterV1.sol"; @@ -23,39 +30,60 @@ import { FlowCommon, DeployerDiscoverableMetaV2ConstructionConfig, LibContext, - MIN_FLOW_SENTINELS, ERC1155Receiver } from "../../abstract/FlowCommon.sol"; import {Evaluable, DEFAULT_STATE_NAMESPACE} from "rain.interpreter/src/lib/caller/LibEvaluable.sol"; import {IInterpreterV1} from "rain.interpreter/src/interface/IInterpreterV1.sol"; import {IInterpreterStoreV1} from "rain.interpreter/src/interface/IInterpreterStoreV1.sol"; import {Pointer} from "rain.solmem/lib/LibPointer.sol"; -import {RAIN_FLOW_SENTINEL} from "../../interface/unstable/IFlowERC721V4.sol"; - -/// Thrown when burner of tokens is not the owner of tokens. -error BurnerNotOwner(); +import {RAIN_FLOW_SENTINEL, BurnerNotOwner} from "../../interface/unstable/IFlowERC721V4.sol"; +/// @dev The hash of the meta data expected to be passed to `FlowCommon`'s +/// constructor. bytes32 constant CALLER_META_HASH = bytes32(0x7f7944a4b89741668c06a27ffde94e19be970cd0506786de91aee01c2893d4ef); -SourceIndex constant HANDLE_TRANSFER_ENTRYPOINT = SourceIndex.wrap(0); -SourceIndex constant TOKEN_URI_ENTRYPOINT = SourceIndex.wrap(1); -uint256 constant HANDLE_TRANSFER_MIN_OUTPUTS = 0; -uint256 constant TOKEN_URI_MIN_OUTPUTS = 1; -uint16 constant HANDLE_TRANSFER_MAX_OUTPUTS = 0; -uint16 constant TOKEN_URI_MAX_OUTPUTS = 1; - /// @title FlowERC721 +/// See `IFlowERC721V4` for documentation. contract FlowERC721 is ICloneableV2, IFlowERC721V4, FlowCommon, ERC721 { using LibUint256Matrix for uint256[]; + using LibUint256Array for uint256[]; using LibStackSentinel for Pointer; + /// @dev True if we need to eval `handleTransfer` on every transfer. For many + /// tokens this will be false, so we don't want to invoke the external + /// interpreter call just to cause a noop. bool private sEvalHandleTransfer; + + /// @dev True if we need to eval `tokenURI` to build the token URI. For many + /// tokens this will be false, so we don't want to invoke the external + /// interpreter call just to cause a noop. bool private sEvalTokenURI; + + /// @dev The evaluable that contains the entrypoints for `handleTransfer` and + /// `tokenURI`. This is only set if `sEvalHandleTransfer` or `sEvalTokenURI` + /// is true. Evaluable internal sEvaluable; + + /// @dev The base URI for all token URIs. This is set during initialization + /// and cannot be changed. The token URI evaluable can be used for dynamic + /// token URIs from the base URI. string private sBaseURI; + /// Forwards the `FlowCommon` constructor arguments to the `FlowCommon`. constructor(DeployerDiscoverableMetaV2ConstructionConfig memory config) FlowCommon(CALLER_META_HASH, config) {} + /// Needed here to fix Open Zeppelin implementing `supportsInterface` on + /// multiple base contracts. + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC721, ERC1155Receiver) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + /// Overloaded typed initialize function MUST revert with this error. /// As per `ICloneableV2` interface. function initialize(FlowERC721ConfigV2 memory) external pure { @@ -73,23 +101,23 @@ contract FlowERC721 is ICloneableV2, IFlowERC721V4, FlowCommon, ERC721 { uint256 sourceCount = LibBytecode.sourceCount(flowERC721Config.evaluableConfig.bytecode); bool evalHandleTransfer = sourceCount > 0 && LibBytecode.sourceOpsLength( - flowERC721Config.evaluableConfig.bytecode, SourceIndex.unwrap(HANDLE_TRANSFER_ENTRYPOINT) + flowERC721Config.evaluableConfig.bytecode, SourceIndex.unwrap(FLOW_ERC721_HANDLE_TRANSFER_ENTRYPOINT) ) > 0; bool evalTokenURI = sourceCount > 1 && LibBytecode.sourceOpsLength( - flowERC721Config.evaluableConfig.bytecode, SourceIndex.unwrap(TOKEN_URI_ENTRYPOINT) + flowERC721Config.evaluableConfig.bytecode, SourceIndex.unwrap(FLOW_ERC721_TOKEN_URI_ENTRYPOINT) ) > 0; sEvalHandleTransfer = evalHandleTransfer; sEvalTokenURI = evalTokenURI; - flowCommonInit(flowERC721Config.flowConfig, MIN_FLOW_SENTINELS + 2); + flowCommonInit(flowERC721Config.flowConfig, FLOW_ERC721_MIN_FLOW_SENTINELS); if (evalHandleTransfer) { // Include the token URI min outputs if we expect to eval it, // otherwise only include the handle transfer min outputs. uint256[] memory minOutputs = evalTokenURI - ? LibUint256Array.arrayFrom(HANDLE_TRANSFER_MIN_OUTPUTS, TOKEN_URI_MIN_OUTPUTS) - : LibUint256Array.arrayFrom(HANDLE_TRANSFER_MIN_OUTPUTS); + ? LibUint256Array.arrayFrom(FLOW_ERC721_HANDLE_TRANSFER_MIN_OUTPUTS, FLOW_ERC721_TOKEN_URI_MIN_OUTPUTS) + : LibUint256Array.arrayFrom(FLOW_ERC721_HANDLE_TRANSFER_MIN_OUTPUTS); (IInterpreterV1 interpreter, IInterpreterStoreV1 store, address expression) = flowERC721Config .evaluableConfig @@ -110,17 +138,29 @@ contract FlowERC721 is ICloneableV2, IFlowERC721V4, FlowCommon, ERC721 { return ICLONEABLE_V2_SUCCESS; } + /// Overrides the Open Zeppelin `_baseURI` hook to return the base URI set + /// during initialization. + /// @inheritdoc ERC721 function _baseURI() internal view virtual override returns (string memory) { return sBaseURI; } + /// Overrides the Open Zeppelin `tokenURI` function to return the token URI + /// calculated by the token URI evaluable. Currently the token URI evaluable + /// can only return a single token ID value, and the token URI is built from + /// that according to default Open Zeppelin logic. If the token URI evaluable + /// is not set, then the default Open Zeppelin logic is used with the token + /// ID passed in. + /// @inheritdoc ERC721 function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { if (sEvalTokenURI) { Evaluable memory evaluable = sEvaluable; (uint256[] memory stack, uint256[] memory kvs) = evaluable.interpreter.eval( evaluable.store, DEFAULT_STATE_NAMESPACE, - _dispatchTokenURI(evaluable.expression), + LibEncodedDispatch.encode( + evaluable.expression, FLOW_ERC721_TOKEN_URI_ENTRYPOINT, FLOW_ERC721_TOKEN_URI_MAX_OUTPUTS + ), LibContext.build(LibUint256Array.arrayFrom(tokenId).matrixFrom(), new SignedContextV1[](0)) ); // @todo it would be nice if we could do something with the kvs here, @@ -132,26 +172,25 @@ contract FlowERC721 is ICloneableV2, IFlowERC721V4, FlowCommon, ERC721 { return super.tokenURI(tokenId); } - function _dispatchHandleTransfer(address expression) internal pure returns (EncodedDispatch) { - return LibEncodedDispatch.encode(expression, HANDLE_TRANSFER_ENTRYPOINT, HANDLE_TRANSFER_MAX_OUTPUTS); - } - - function _dispatchTokenURI(address expression) internal pure returns (EncodedDispatch) { - return LibEncodedDispatch.encode(expression, TOKEN_URI_ENTRYPOINT, TOKEN_URI_MAX_OUTPUTS); + /// @inheritdoc IFlowERC721V4 + function stackToFlow(uint256[] memory stack) external pure virtual override returns (FlowERC721IOV1 memory) { + return _stackToFlow(stack.dataPointer(), stack.endPointer()); } - /// Needed here to fix Open Zeppelin implementing `supportsInterface` on - /// multiple base contracts. - function supportsInterface(bytes4 interfaceId) - public - view + /// @inheritdoc IFlowERC721V4 + function flow(Evaluable memory evaluable, uint256[] memory callerContext, SignedContextV1[] memory signedContexts) + external virtual - override(ERC721, ERC1155Receiver) - returns (bool) + returns (FlowERC721IOV1 memory) { - return super.supportsInterface(interfaceId); + return _flow(evaluable, callerContext, signedContexts); } + /// Exposes the Open Zeppelin `_afterTokenTransfer` hook as an evaluable + /// entrypoint so that the deployer of the token can use it to implement + /// custom transfer logic. The stack is ignored, so if the expression author + /// wants to prevent some kind of transfer, they can just revert within the + /// expression evaluation. /// @inheritdoc ERC721 function _afterTokenTransfer(address from, address to, uint256 tokenId, uint256 batchSize) internal @@ -168,7 +207,11 @@ contract FlowERC721 is ICloneableV2, IFlowERC721V4, FlowCommon, ERC721 { (uint256[] memory stack, uint256[] memory kvs) = evaluable.interpreter.eval( evaluable.store, DEFAULT_STATE_NAMESPACE, - _dispatchHandleTransfer(evaluable.expression), + LibEncodedDispatch.encode( + evaluable.expression, + FLOW_ERC721_HANDLE_TRANSFER_ENTRYPOINT, + FLOW_ERC721_HANDLE_TRANSFER_MAX_OUTPUTS + ), LibContext.build( // Transfer information. // Does NOT include `batchSize` because handle @@ -185,16 +228,19 @@ contract FlowERC721 is ICloneableV2, IFlowERC721V4, FlowCommon, ERC721 { } } - function _previewFlow(Evaluable memory evaluable, uint256[][] memory context) - internal - view - returns (FlowERC721IOV1 memory, uint256[] memory) - { + /// Wraps the standard `LibFlow.stackToFlow` with the additional logic to + /// convert the stack to a `FlowERC721IOV1` struct. This involves consuming + /// the mints and burns from the stack as additional sentinel separated + /// tuples. The mints are consumed first, then the burns, then the remaining + /// stack is converted to a flow as normal. + /// @param stackBottom The bottom of the stack. + /// @param stackTop The top of the stack. + /// @return flowERC721IO The `FlowERC721IOV1` representation of the stack. + function _stackToFlow(Pointer stackBottom, Pointer stackTop) internal pure returns (FlowERC721IOV1 memory) { ERC721SupplyChange[] memory mints; ERC721SupplyChange[] memory burns; Pointer tuplesPointer; - (Pointer stackBottom, Pointer stackTop, uint256[] memory kvs) = _flowStack(evaluable, context); // mints // https://github.com/crytic/slither/issues/2126 //slither-disable-next-line unused-return @@ -209,9 +255,15 @@ contract FlowERC721 is ICloneableV2, IFlowERC721V4, FlowCommon, ERC721 { assembly ("memory-safe") { burns := tuplesPointer } - return (FlowERC721IOV1(mints, burns, LibFlow.stackToFlow(stackBottom, stackTop)), kvs); + + return FlowERC721IOV1(mints, burns, LibFlow.stackToFlow(stackBottom, stackTop)); } + /// Wraps the standard `LibFlow.flow` with the additional logic to handle + /// self minting and burning. This involves consuming the mints and burns + /// from the stack as additional sentinel separated tuples. The mints are + /// consumed first, then the burns, then the remaining stack is converted to + /// a flow as normal. function _flow(Evaluable memory evaluable, uint256[] memory callerContext, SignedContextV1[] memory signedContexts) internal virtual @@ -219,39 +271,21 @@ contract FlowERC721 is ICloneableV2, IFlowERC721V4, FlowCommon, ERC721 { returns (FlowERC721IOV1 memory) { unchecked { - uint256[][] memory context = LibContext.build(callerContext.matrixFrom(), signedContexts); - emit Context(msg.sender, context); - (FlowERC721IOV1 memory flowIO, uint256[] memory kvs) = _previewFlow(evaluable, context); - for (uint256 i = 0; i < flowIO.mints.length; i++) { - _safeMint(flowIO.mints[i].account, flowIO.mints[i].id); + (Pointer stackBottom, Pointer stackTop, uint256[] memory kvs) = + _flowStack(evaluable, callerContext, signedContexts); + FlowERC721IOV1 memory flowERC721IO = _stackToFlow(stackBottom, stackTop); + for (uint256 i = 0; i < flowERC721IO.mints.length; i++) { + _safeMint(flowERC721IO.mints[i].account, flowERC721IO.mints[i].id); } - for (uint256 i = 0; i < flowIO.burns.length; i++) { - uint256 burnId = flowIO.burns[i].id; - if (ERC721.ownerOf(burnId) != flowIO.burns[i].account) { + for (uint256 i = 0; i < flowERC721IO.burns.length; i++) { + uint256 burnId = flowERC721IO.burns[i].id; + if (ERC721.ownerOf(burnId) != flowERC721IO.burns[i].account) { revert BurnerNotOwner(); } _burn(burnId); } - LibFlow.flow(flowIO.flow, evaluable.store, kvs); - return flowIO; + LibFlow.flow(flowERC721IO.flow, evaluable.store, kvs); + return flowERC721IO; } } - - function previewFlow( - Evaluable memory evaluable, - uint256[] memory callerContext, - SignedContextV1[] memory signedContexts - ) external view virtual returns (FlowERC721IOV1 memory) { - uint256[][] memory context = LibContext.build(callerContext.matrixFrom(), signedContexts); - (FlowERC721IOV1 memory flowERC721IO,) = _previewFlow(evaluable, context); - return flowERC721IO; - } - - function flow(Evaluable memory evaluable, uint256[] memory callerContext, SignedContextV1[] memory signedContexts) - external - virtual - returns (FlowERC721IOV1 memory) - { - return _flow(evaluable, callerContext, signedContexts); - } } diff --git a/src/interface/IFlowERC1155V3.sol b/src/interface/IFlowERC1155V3.sol index c3c4243f..7176c231 100644 --- a/src/interface/IFlowERC1155V3.sol +++ b/src/interface/IFlowERC1155V3.sol @@ -6,21 +6,54 @@ import {EvaluableConfigV2} from "rain.interpreter/src/lib/caller/LibEvaluable.so import "./IFlowV3.sol"; +/// @dev Entrypont of the `handleTransfer` evaluation. +SourceIndex constant FLOW_ERC1155_HANDLE_TRANSFER_ENTRYPOINT = SourceIndex.wrap(0); + +/// @dev Minimum number of outputs of the `handleTransfer` evaluation. +/// This is 0 because the return stack is ignored. +uint256 constant FLOW_ERC1155_HANDLE_TRANSFER_MIN_OUTPUTS = 0; + +/// @dev Maximum number of outputs of the `handleTransfer` evaluation. +/// This is 0 because the return stack is ignored. +uint16 constant FLOW_ERC1155_HANDLE_TRANSFER_MAX_OUTPUTS = 0; + +/// @dev Minimum number of sentinels required by `FlowERC1155`. +/// This is 2 more than the minimum required by `FlowCommon` because the +/// mints and burns are included in the stack. +uint256 constant FLOW_ERC1155_MIN_FLOW_SENTINELS = MIN_FLOW_SENTINELS + 2; + +/// @dev v3 of `FlowERC1155` expected a sentinel different to +/// `RAIN_FLOW_SENTINEL`, but this was generally more confusing than helpful. Sentinel constant RAIN_FLOW_ERC1155_SENTINEL = Sentinel.wrap(uint256(keccak256(bytes("RAIN_FLOW_ERC1155_SENTINEL")) | SENTINEL_HIGH_BITS)); +/// Initializer config. +/// @param uri As per Open Zeppelin `ERC1155Upgradeable`. +/// @param evaluableConfig The `EvaluableConfigV2` to use to build the +/// `evaluable` that can be used to handle transfers. +/// @param flowConfig Constructor config for the `Evaluable`s that define the +/// flow behaviours including self mints/burns. struct FlowERC1155Config { string uri; EvaluableConfig evaluableConfig; EvaluableConfig[] flowConfig; } +/// Represents a single mint or burn of a single ERC1155 token. Whether this is +/// a mint or burn must be implied by the context. +/// @param account The address the token is being minted/burned to/from. +/// @param id The id of the token being minted/burned. +/// @param amount The amount of the token being minted/burned. struct ERC1155SupplyChange { address account; uint256 id; uint256 amount; } +/// Represents a set of ERC1155 transfers, including self mints/burns. +/// @param mints The mints that occurred. +/// @param burns The burns that occurred. +/// @param flow The transfers that occured. struct FlowERC1155IOV1 { ERC1155SupplyChange[] mints; ERC1155SupplyChange[] burns; @@ -28,18 +61,44 @@ struct FlowERC1155IOV1 { } /// @title IFlowERC1155V3 +/// Conceptually identical to `IFlowV3`, but the flow contract itself is an +/// ERC1155 token. This means that ERC1155 self mints and burns are included in +/// the stack that the flows must evaluate to. As stacks are processed by flow +/// from bottom to top, this means that the self mint/burn will be the last thing +/// evaluated, with mints at the bottom and burns next, followed by the flows. +/// +/// As the flow is an ERC1155 token it also includes an evaluation to be run on +/// every token transfer. This is the `handleTransfer` entrypoint. The return +/// stack of this evaluation is ignored, but reverts MUST be respected. This +/// allows expression authors to prevent transfers from occurring if they don't +/// want them to, by reverting within the expression. +/// +/// Otherwise the flow contract is identical to `IFlowV3`. interface IFlowERC1155V3 { + /// Contract has initialized. event Initialize(address sender, FlowERC1155Config config); + /// As per `IFlowV3` but returns a `FlowERC1155IOV1` instead of a + /// `FlowTransferV1`. + /// @param evaluable The `Evaluable` that is flowing. + /// @param callerContext The context of the caller. + /// @param signedContexts The signed contexts of the caller. + /// @return flowERC1155IO The `FlowERC1155IOV1` that occurred. function previewFlow( Evaluable calldata evaluable, uint256[] calldata callerContext, SignedContextV1[] calldata signedContexts - ) external view returns (FlowERC1155IOV1 calldata); + ) external view returns (FlowERC1155IOV1 calldata flowERC1155IO); + /// As per `IFlowV3` but returns a `FlowERC1155IOV1` instead of a + /// `FlowTransferV1` and mints/burns itself as an ERC1155 accordingly. + /// @param evaluable The `Evaluable` that is flowing. + /// @param callerContext The context of the caller. + /// @param signedContexts The signed contexts of the caller. + /// @return flowERC1155IO The `FlowERC1155IOV1` that occurred. function flow( Evaluable calldata evaluable, uint256[] calldata callerContext, SignedContextV1[] calldata signedContexts - ) external returns (FlowERC1155IOV1 calldata); + ) external returns (FlowERC1155IOV1 calldata flowERC1155IO); } diff --git a/src/interface/IFlowERC20V3.sol b/src/interface/IFlowERC20V3.sol index 73f52cfd..5c19abb2 100644 --- a/src/interface/IFlowERC20V3.sol +++ b/src/interface/IFlowERC20V3.sol @@ -4,31 +4,55 @@ pragma solidity ^0.8.18; import "rain.interpreter/src/interface/IInterpreterCallerV2.sol"; import "rain.interpreter/src/lib/caller/LibEvaluable.sol"; import {Sentinel} from "rain.solmem/lib/LibStackSentinel.sol"; -import "./IFlowV3.sol"; +import {MIN_FLOW_SENTINELS, SENTINEL_HIGH_BITS, FlowTransferV1} from "./IFlowV3.sol"; +/// @dev v3 of `FlowERC20` expected a sentinel different to +/// `RAIN_FLOW_SENTINEL`, but this was generally more confusing than helpful. Sentinel constant RAIN_FLOW_ERC20_SENTINEL = Sentinel.wrap(uint256(keccak256(bytes("RAIN_FLOW_ERC20_SENTINEL")) | SENTINEL_HIGH_BITS)); +/// @dev Entrypont of the `handleTransfer` evaluation. SourceIndex constant FLOW_ERC20_HANDLE_TRANSFER_ENTRYPOINT = SourceIndex.wrap(0); + +/// @dev Minimum number of outputs of the `handleTransfer` evaluation. +/// This is 0 because the return stack is ignored. uint256 constant FLOW_ERC20_HANDLE_TRANSFER_MIN_OUTPUTS = 0; + +/// @dev Maximum number of outputs of the `handleTransfer` evaluation. +/// This is 0 because the return stack is ignored. uint16 constant FLOW_ERC20_HANDLE_TRANSFER_MAX_OUTPUTS = 0; +/// @dev Minimum number of sentinels required by `FlowERC20`. +/// This is 2 more than the minimum required by `FlowCommon` because the +/// mints and burns are included in the stack. +uint256 constant FLOW_ERC20_MIN_FLOW_SENTINELS = MIN_FLOW_SENTINELS + 2; + +/// Represents a single mint or burn of a single ERC20 token. Whether this is +/// a mint or burn must be implied by the context. +/// @param account The address the token is being minted/burned to/from. +/// @param amount The amount of the token being minted/burned. struct ERC20SupplyChange { address account; uint256 amount; } +/// Represents a set of ERC20 transfers, including self mints/burns. +/// @param mints The mints that occurred. +/// @param burns The burns that occurred. +/// @param flow The transfers that occured. struct FlowERC20IOV1 { ERC20SupplyChange[] mints; ERC20SupplyChange[] burns; FlowTransferV1 flow; } -/// Constructor config. -/// @param Constructor config for the ERC20 token minted according to flow -/// schedule in `flow`. -/// @param Constructor config for the `ImmutableSource` that defines the -/// emissions schedule for claiming. +/// Initializer config. +/// @param name As per Open Zeppelin `ERC20Upgradeable`. +/// @param symbol As per Open Zeppelin `ERC20Upgradeable`. +/// @param evaluableConfig The `EvaluableConfigV2` to use to build the +/// `evaluable` that can be used to handle transfers. +/// @param flowConfig Initializer config for the `Evaluable`s that define the +/// flow behaviours including self mints/burns. struct FlowERC20Config { string name; string symbol; @@ -55,15 +79,27 @@ interface IFlowERC20V3 { /// @param config All initialized config. event Initialize(address sender, FlowERC20Config config); + /// As per `IFlowV3` but returns a `FlowERC20IOV1` instead of a + /// `FlowTransferV1`. + /// @param evaluable The `Evaluable` to use to evaluate the flow. + /// @param callerContext The caller context to use to evaluate the flow. + /// @param signedContexts The signed contexts to use to evaluate the flow. + /// @return flowERC20IO The `FlowERC20IOV1` that occurred. function previewFlow( Evaluable calldata evaluable, uint256[] calldata callerContext, SignedContextV1[] calldata signedContexts - ) external view returns (FlowERC20IOV1 calldata); + ) external view returns (FlowERC20IOV1 calldata flowERC20IO); + /// As per `IFlowV3` but returns a `FlowERC20IOV1` instead of a + /// `FlowTransferV1` and mints/burns itself as an ERC20 accordingly. + /// @param evaluable The `Evaluable` to use to evaluate the flow. + /// @param callerContext The caller context to use to evaluate the flow. + /// @param signedContexts The signed contexts to use to evaluate the flow. + /// @return flowERC20IO The `FlowERC20IOV1` that occurred. function flow( Evaluable calldata evaluable, uint256[] calldata callerContext, SignedContextV1[] calldata signedContexts - ) external returns (FlowERC20IOV1 calldata); + ) external returns (FlowERC20IOV1 calldata flowERC20IO); } diff --git a/src/interface/IFlowERC721V3.sol b/src/interface/IFlowERC721V3.sol index a61b5363..a8223a30 100644 --- a/src/interface/IFlowERC721V3.sol +++ b/src/interface/IFlowERC721V3.sol @@ -6,14 +6,44 @@ import "rain.interpreter/src/lib/caller/LibEvaluable.sol"; import "./IFlowV3.sol"; +/// @dev Entrypont of the `handleTransfer` evaluation. +SourceIndex constant FLOW_ERC721_HANDLE_TRANSFER_ENTRYPOINT = SourceIndex.wrap(0); +/// @dev Entrypont of the `tokenURI` evaluation. +SourceIndex constant FLOW_ERC721_TOKEN_URI_ENTRYPOINT = SourceIndex.wrap(1); + +/// @dev Minimum number of outputs of the `handleTransfer` evaluation. +/// This is 0 because the return stack is ignored. +uint256 constant FLOW_ERC721_HANDLE_TRANSFER_MIN_OUTPUTS = 0; +/// @dev Minimum number of outputs of the `tokenURI` evaluation. +/// This is 1 because we can only handle a single token ID value. +uint256 constant FLOW_ERC721_TOKEN_URI_MIN_OUTPUTS = 1; + +/// @dev Maximum number of outputs of the `handleTransfer` evaluation. +/// This is 0 because the return stack is ignored. +uint16 constant FLOW_ERC721_HANDLE_TRANSFER_MAX_OUTPUTS = 0; +/// @dev Maximum number of outputs of the `tokenURI` evaluation. +/// This is 1 because we can only handle a single token ID value. +uint16 constant FLOW_ERC721_TOKEN_URI_MAX_OUTPUTS = 1; + +/// @dev v3 of `FlowERC721` expected a sentinel different to +/// `RAIN_FLOW_SENTINEL`, but this was generally more confusing than helpful. Sentinel constant RAIN_FLOW_ERC721_SENTINEL = Sentinel.wrap(uint256(keccak256(bytes("RAIN_FLOW_ERC721_SENTINEL")) | SENTINEL_HIGH_BITS)); -/// Constructor config. -/// @param Constructor config for the ERC721 token minted according to flow -/// schedule in `flow`. -/// @param Constructor config for the `ImmutableSource` that defines the -/// emissions schedule for claiming. +/// @dev Minimum number of sentinels required by `FlowERC721`. +/// This is 2 more than the minimum required by `FlowCommon` because the +/// mints and burns are included in the stack. +uint256 constant FLOW_ERC721_MIN_FLOW_SENTINELS = MIN_FLOW_SENTINELS + 2; + +/// Initializer config. +/// @param name As per Open Zeppelin `ERC721Upgradeable`. +/// @param symbol As per Open Zeppelin `ERC721Upgradeable`. +/// @param baseURI As per Open Zeppelin `ERC721Upgradeable`. +/// @param evaluableConfig The `EvaluableConfigV2` to use to build the +/// `evaluable` that can be used to handle transfers and token URIs. The token +/// URI entrypoint is optional. +/// @param flowConfig Constructor config for the `Evaluable`s that define the +/// flow behaviours including self mints/burns. struct FlowERC721Config { string name; string symbol; @@ -22,11 +52,19 @@ struct FlowERC721Config { EvaluableConfig[] flowConfig; } +/// Represents a single mint or burn of a single ERC721 token. Whether this is +/// a mint or burn must be implied by the context. +/// @param account The address the token is being minted/burned to/from. +/// @param id The id of the token being minted/burned. struct ERC721SupplyChange { address account; uint256 id; } +/// Represents a set of ERC721 transfers, including self mints/burns. +/// @param mints The mints that occurred. +/// @param burns The burns that occurred. +/// @param flow The transfers that occured. struct FlowERC721IOV1 { ERC721SupplyChange[] mints; ERC721SupplyChange[] burns; @@ -34,21 +72,55 @@ struct FlowERC721IOV1 { } /// @title IFlowERC721V3 +/// Conceptually identical to `IFlowV3`, but the flow contract itself is an +/// ERC721 token. This means that ERC721 self mints and burns are included in +/// the stack. +/// +/// As the flow is an ERC721 token, there are two entrypoints in addition to +/// the flows: +/// - `handleTransfer` is called when the flow is transferred. +/// - `tokenURI` is called when the token URI is requested. +/// +/// The `handleTransfer` entrypoint is mandatory, but the `tokenURI` entrypoint +/// is optional. If the `tokenURI` entrypoint is not provided, the default +/// Open Zeppelin implementation will be used. +/// +/// The `handleTransfer` entrypoint may be used to restrict transfers of the +/// flow token. For example, it may be used to restrict transfers to only +/// occur when the flow is in a certain state. +/// +/// The `tokenURI` entrypoint may be used to provide a custom token ID to build +/// a token URI for the flow token. +/// +/// Otherwise the flow contract behaves identically to `IFlowV3`. interface IFlowERC721V3 { /// Contract has initialized. /// @param sender `msg.sender` initializing the contract (factory). /// @param config All initialized config. event Initialize(address sender, FlowERC721Config config); + /// As per `IFlowV3` but returns a `FlowERC721IOV1` instead of a + /// `FlowTransferV1`. + /// @param evaluable The `Evaluable` that is flowing. + /// @param callerContext The context of the caller. + /// @param signedContexts The signed contexts of the caller. + /// @return flowERC721IO The `FlowERC721IOV1` that would occur if the flow + /// was executed. function previewFlow( Evaluable calldata evaluable, uint256[] calldata callerContext, SignedContextV1[] calldata signedContexts - ) external view returns (FlowERC721IOV1 calldata); + ) external view returns (FlowERC721IOV1 calldata flowERC721IO); + /// As per `IFlowV3` but returns a `FlowERC721IOV1` instead of a + /// `FlowTransferV1` and mints/burns itself as an ERC721 accordingly. + /// @param evaluable The `Evaluable` that is flowing. + /// @param callerContext The context of the caller. + /// @param signedContexts The signed contexts of the caller. + /// @return flowERC721IO The `FlowERC721IOV1` that occurred. function flow( Evaluable calldata evaluable, uint256[] calldata callerContext, SignedContextV1[] calldata signedContexts - ) external returns (FlowERC721IOV1 calldata); + ) external returns (FlowERC721IOV1 calldata flowERC721IO); } diff --git a/src/interface/IFlowV3.sol b/src/interface/IFlowV3.sol index 4153955c..1f262621 100644 --- a/src/interface/IFlowV3.sol +++ b/src/interface/IFlowV3.sol @@ -21,6 +21,10 @@ error UnsupportedERC721Flow(); /// Thrown for unsupported erc1155 transfers. error UnsupportedERC1155Flow(); +/// @dev The number of sentinels required by `FlowCommon`. An evaluable can never +/// have fewer minimum outputs than required sentinels. +uint256 constant MIN_FLOW_SENTINELS = 3; + /// @dev Sets the high bits of all flow sentinels to guarantee that the numeric /// value of the sentinel will never collide with a token amount or address. This /// guarantee holds as long as the token supply is less than 2^252, and the @@ -38,12 +42,20 @@ bytes32 constant SENTINEL_HIGH_BITS = bytes32(0xF0000000000000000000000000000000 Sentinel constant RAIN_FLOW_SENTINEL = Sentinel.wrap(uint256(keccak256(bytes("RAIN_FLOW_SENTINEL")) | SENTINEL_HIGH_BITS)); +/// Wraps `EvaluableConfig[]` to workaround a Solidity bug. +/// https://github.com/ethereum/solidity/issues/13597 +/// @param dummyConfig A dummy config to workaround a Solidity bug. +/// @param config The list of evaluable configs that define the flows. struct FlowConfig { - // https://github.com/ethereum/solidity/issues/13597 EvaluableConfig dummyConfig; EvaluableConfig[] config; } +/// Represents a single transfer of a single ERC20 token. +/// @param token The address of the ERC20 token being transferred. +/// @param from The address the token is being transferred from. +/// @param to The address the token is being transferred to. +/// @param amount The amount of the token being transferred. struct ERC20Transfer { address token; address from; @@ -51,6 +63,11 @@ struct ERC20Transfer { uint256 amount; } +/// Represents a single transfer of a single ERC721 token. +/// @param token The address of the ERC721 token being transferred. +/// @param from The address the token is being transferred from. +/// @param to The address the token is being transferred to. +/// @param id The id of the token being transferred. struct ERC721Transfer { address token; address from; @@ -58,6 +75,12 @@ struct ERC721Transfer { uint256 id; } +/// Represents a single transfer of a single ERC1155 token. +/// @param token The address of the ERC1155 token being transferred. +/// @param from The address the token is being transferred from. +/// @param to The address the token is being transferred to. +/// @param id The id of the token being transferred. +/// @param amount The amount of the token being transferred. struct ERC1155Transfer { address token; address from; @@ -66,21 +89,53 @@ struct ERC1155Transfer { uint256 amount; } +/// Represents an ordered set of transfers that will be or have been executed. +/// Supports ERC20, ERC721, and ERC1155 transfers. +/// @param erc20 An array of ERC20 transfers. +/// @param erc721 An array of ERC721 transfers. +/// @param erc1155 An array of ERC1155 transfers. struct FlowTransferV1 { ERC20Transfer[] erc20; ERC721Transfer[] erc721; ERC1155Transfer[] erc1155; } +/// @title IFlowV3 +/// At a high level, identical to `IFlowV4` but with an older, less flexible +/// previewing system, and the older `FlowConfig` struct that was used with +/// older versions of the interpreter. interface IFlowV3 { + /// MUST be emitted when the flow contract is initialized. + /// @param sender The EOA that deployed the flow contract. + /// @param config The list of evaluable configs that define the flows. event Initialize(address sender, FlowConfig config); + /// "Dry run" of a flow, returning the resulting token transfers without + /// actually executing them. + /// @param evaluable The evaluable to evaluate. + /// @param callerContext The caller context to use when evaluating the + /// flow. + /// @param signedContexts The signed contexts to use when evaluating the + /// flow. function previewFlow( Evaluable calldata evaluable, uint256[] calldata callerContext, SignedContextV1[] calldata signedContexts ) external view returns (FlowTransferV1 calldata flowTransfer); + /// Given an evaluable, caller context, and signed contexts, evaluate the + /// evaluable and return the resulting flow transfer. MUST process the + /// flow transfer atomically, either all of it succeeds or none of it + /// succeeds. MUST revert if the evaluable is not registered with the flow + /// contract. MUST revert if the evaluable reverts. MUST revert if the + /// evaluable returns a stack that is malformed. MUST revert if the evaluable + /// returns a stack that contains a token transfer that is not allowed by + /// the flow contract (e.g. if a token is being moved from an address that + /// is not the caller or the flow contract). + /// @param evaluable The evaluable to evaluate. + /// @param callerContext The caller context to pass to the evaluable. + /// @param signedContexts The signed contexts to pass to the evaluable. + /// @return flowTransfer The resulting flow transfer. function flow( Evaluable calldata evaluable, uint256[] calldata callerContext, diff --git a/src/interface/unstable/IFlowERC1155V4.sol b/src/interface/unstable/IFlowERC1155V4.sol index 948d95ce..7a02d296 100644 --- a/src/interface/unstable/IFlowERC1155V4.sol +++ b/src/interface/unstable/IFlowERC1155V4.sol @@ -6,23 +6,60 @@ import {Evaluable, EvaluableConfigV2} from "rain.interpreter/src/lib/caller/LibE import {Sentinel} from "rain.solmem/lib/LibStackSentinel.sol"; import {RAIN_FLOW_SENTINEL} from "./IFlowV4.sol"; -import {FlowERC1155IOV1, ERC1155SupplyChange} from "../IFlowERC1155V3.sol"; +import { + FlowERC1155IOV1, + ERC1155SupplyChange, + FLOW_ERC1155_HANDLE_TRANSFER_MAX_OUTPUTS, + FLOW_ERC1155_HANDLE_TRANSFER_ENTRYPOINT, + FLOW_ERC1155_HANDLE_TRANSFER_MIN_OUTPUTS, + FLOW_ERC1155_MIN_FLOW_SENTINELS +} from "../IFlowERC1155V3.sol"; +/// Constructor config. +/// @param uri As per Open Zeppelin `ERC1155Upgradeable`. +/// @param evaluableConfig The `EvaluableConfigV2` to use to build the +/// `evaluable` that can be used to handle transfers. +/// @param flowConfig Constructor config for the `Evaluable`s that define the +/// flow behaviours outside self mints/burns. struct FlowERC1155ConfigV2 { string uri; EvaluableConfigV2 evaluableConfig; EvaluableConfigV2[] flowConfig; } +/// @title IFlowERC1155V4 +/// Conceptually identical to `IFlowV4`, but the flow contract itself is an +/// ERC1155 token. This means that ERC1155 self mints and burns are included in +/// the stack that the flows must evaluate to. As stacks are processed by flow +/// from bottom to top, this means that the self mint/burn will be the last thing +/// evaluated, with mints at the bottom and burns next, followed by the flows. +/// +/// As the flow is an ERC1155 token it also includes an evaluation to be run on +/// every token transfer. This is the `handleTransfer` entrypoint. The return +/// stack of this evaluation is ignored, but reverts MUST be respected. This +/// allows expression authors to prevent transfers from occurring if they don't +/// want them to, by reverting within the expression. +/// +/// Otherwise the flow contract is identical to `IFlowV4`. interface IFlowERC1155V4 { + /// Contract has initialized. + /// @param sender `msg.sender` initializing the contract (factory). + /// @param config All initialized config. event Initialize(address sender, FlowERC1155ConfigV2 config); - function previewFlow( - Evaluable calldata evaluable, - uint256[] calldata callerContext, - SignedContextV1[] calldata signedContexts - ) external view returns (FlowERC1155IOV1 calldata); + /// As per `IFlowV4` but returns a `FlowERC1155IOV1` instead of a + /// `FlowTransferV1`. + /// @param stack The stack to convert to a `FlowERC1155IOV1`. + /// @return flowERC1155IO The `FlowERC1155IOV1` representation of the stack. + function stackToFlow(uint256[] memory stack) external pure returns (FlowERC1155IOV1 memory flowERC1155IO); + /// As per `IFlowV4` but returns a `FlowERC1155IOV1` instead of a + /// `FlowTransferV1` and mints/burns itself as an ERC1155 accordingly. + /// @param evaluable The `Evaluable` to use to evaluate the flow. + /// @param callerContext The caller context to use to evaluate the flow. + /// @param signedContexts The signed contexts to use to evaluate the flow. + /// @return flowERC1155IO The `FlowERC1155IOV1` representing all token + /// mint/burns and transfers that occurred during the flow. function flow( Evaluable calldata evaluable, uint256[] calldata callerContext, diff --git a/src/interface/unstable/IFlowERC20V4.sol b/src/interface/unstable/IFlowERC20V4.sol index 4a3fc55d..a414b09a 100644 --- a/src/interface/unstable/IFlowERC20V4.sol +++ b/src/interface/unstable/IFlowERC20V4.sol @@ -9,7 +9,8 @@ import { ERC20SupplyChange, FLOW_ERC20_HANDLE_TRANSFER_ENTRYPOINT, FLOW_ERC20_HANDLE_TRANSFER_MIN_OUTPUTS, - FLOW_ERC20_HANDLE_TRANSFER_MAX_OUTPUTS + FLOW_ERC20_HANDLE_TRANSFER_MAX_OUTPUTS, + FLOW_ERC20_MIN_FLOW_SENTINELS } from "../IFlowERC20V3.sol"; import {RAIN_FLOW_SENTINEL} from "./IFlowV4.sol"; @@ -28,12 +29,38 @@ struct FlowERC20ConfigV2 { } /// @title IFlowERC20V4 -/// Conceptually identical to `IFlowV4`, but with the addition of th +/// Conceptually identical to `IFlowV4`, but the flow contract itself is an +/// ERC20 token. This means that ERC20 self mints and burns are included in the +/// stack that the flows must evaluate to. As stacks are processed by flow from +/// bottom to top, this means that the self mint/burn will be the last thing +/// evaluated, with mints at the bottom and burns next, followed by the flows. +/// +/// As the flow is an ERC20 token it also includes an evaluation to be run on +/// every token transfer. This is the `handleTransfer` entrypoint. The return +/// stack of this evaluation is ignored, but reverts MUST be respected. This +/// allows expression authors to prevent transfers from occurring if they don't +/// want them to, by reverting within the expression. +/// +/// Otherwise the flow contract is identical to `IFlowV4`. interface IFlowERC20V4 { + /// Contract has initialized. + /// @param sender `msg.sender` initializing the contract (factory). + /// @param config All initialized config. event Initialize(address sender, FlowERC20ConfigV2 config); + /// As per `IFlowV4` but returns a `FlowERC20IOV1` instead of a + /// `FlowTransferV1`. + /// @param stack The stack to convert to a `FlowERC20IOV1`. + /// @return flowERC20IO The `FlowERC20IOV1` representation of the stack. function stackToFlow(uint256[] memory stack) external pure returns (FlowERC20IOV1 memory flowERC20IO); + /// As per `IFlowV4` but returns a `FlowERC20IOV1` instead of a + /// `FlowTransferV1` and mints/burns itself as an ERC20 accordingly. + /// @param evaluable The `Evaluable` to use to evaluate the flow. + /// @param callerContext The caller context to use to evaluate the flow. + /// @param signedContexts The signed contexts to use to evaluate the flow. + /// @return flowERC20IO The `FlowERC20IOV1` representing all token mint/burns + /// and transfers that occurred during the flow. function flow( Evaluable calldata evaluable, uint256[] calldata callerContext, diff --git a/src/interface/unstable/IFlowERC721V4.sol b/src/interface/unstable/IFlowERC721V4.sol index bb3d55af..2a3c3d72 100644 --- a/src/interface/unstable/IFlowERC721V4.sol +++ b/src/interface/unstable/IFlowERC721V4.sol @@ -4,17 +4,31 @@ pragma solidity ^0.8.18; import "rain.interpreter/src/interface/IInterpreterCallerV2.sol"; import "rain.interpreter/src/lib/caller/LibEvaluable.sol"; -import {FlowERC721IOV1, ERC721SupplyChange} from "../IFlowERC721V3.sol"; +import { + FlowERC721IOV1, + ERC721SupplyChange, + FLOW_ERC721_TOKEN_URI_MIN_OUTPUTS, + FLOW_ERC721_TOKEN_URI_MAX_OUTPUTS, + FLOW_ERC721_HANDLE_TRANSFER_MIN_OUTPUTS, + FLOW_ERC721_HANDLE_TRANSFER_MAX_OUTPUTS, + FLOW_ERC721_TOKEN_URI_ENTRYPOINT, + FLOW_ERC721_HANDLE_TRANSFER_ENTRYPOINT, + FLOW_ERC721_MIN_FLOW_SENTINELS +} from "../IFlowERC721V3.sol"; import {RAIN_FLOW_SENTINEL} from "./IFlowV4.sol"; +/// Thrown when burner of tokens is not the owner of tokens. +error BurnerNotOwner(); + /// Constructor config. /// @param name As per Open Zeppelin `ERC721Upgradeable`. /// @param symbol As per Open Zeppelin `ERC721Upgradeable`. /// @param baseURI As per Open Zeppelin `ERC721Upgradeable`. -/// @param evaluableConfig Constructor config for the `Evaluable` that defines -/// the mints/burn schedule. -/// @param flowConfig Constructor config for the `Evaluable` that defines the +/// @param evaluableConfig The `EvaluableConfigV2` to use to build the +/// `evaluable` that can be used to handle transfers and build token IDs for the +/// token URI. +/// @param flowConfig Constructor config for the `Evaluable`s that define the /// flow behaviours outside self mints/burns. struct FlowERC721ConfigV2 { string name; @@ -25,21 +39,45 @@ struct FlowERC721ConfigV2 { } /// @title IFlowERC721V4 +/// Conceptually identical to `IFlowV4`, but the flow contract itself is an +/// ERC721 token. This means that ERC721 self mints and burns are included in the +/// stack that the flows must evaluate to. As stacks are processed by flow from +/// bottom to top, this means that the self mint/burn will be the last thing +/// evaluated, with mints at the bottom and burns next, followed by the flows. +/// +/// As the flow is an ERC721 token it also includes an evaluation to be run on +/// every token transfer. This is the `handleTransfer` entrypoint. The return +/// stack of this evaluation is ignored, but reverts MUST be respected. This +/// allows expression authors to prevent transfers from occurring if they don't +/// want them to, by reverting within the expression. +/// +/// The flow contract also includes an evaluation to be run on every token URI +/// request. This is the `tokenURI` entrypoint. The return value of this +/// evaluation is the token ID to use for the token URI. This entryoint is +/// optional, and if not provided the token URI will be the default Open Zeppelin +/// token URI logic. +/// +/// Otherwise the flow contract is identical to `IFlowV4`. interface IFlowERC721V4 { /// Contract has initialized. /// @param sender `msg.sender` initializing the contract (factory). /// @param config All initialized config. event Initialize(address sender, FlowERC721ConfigV2 config); - function previewFlow( - Evaluable calldata evaluable, - uint256[] calldata callerContext, - SignedContextV1[] calldata signedContexts - ) external view returns (FlowERC721IOV1 calldata); + /// As per `IFlowV4` but returns a `FlowERC721IOV1` instead of a + /// `FlowTransferV1`. + function stackToFlow(uint256[] memory stack) external pure returns (FlowERC721IOV1 memory flowERC721IO); + /// As per `IFlowV4` but returns a `FlowERC721IOV1` instead of a + /// `FlowTransferV1` and mints/burns itself as an ERC721 accordingly. + /// @param evaluable The `Evaluable` to use to evaluate the flow. + /// @param callerContext The caller context to use to evaluate the flow. + /// @param signedContexts The signed contexts to use to evaluate the flow. + /// @return flowERC721IO The `FlowERC721IOV1` representing all token + /// mint/burns and transfers that occurred during the flow. function flow( Evaluable calldata evaluable, uint256[] calldata callerContext, SignedContextV1[] calldata signedContexts - ) external returns (FlowERC721IOV1 calldata); + ) external returns (FlowERC721IOV1 calldata flowERC721IO); } diff --git a/src/interface/unstable/IFlowV4.sol b/src/interface/unstable/IFlowV4.sol index 4ed9b5a5..7a4fd40a 100644 --- a/src/interface/unstable/IFlowV4.sol +++ b/src/interface/unstable/IFlowV4.sol @@ -15,7 +15,8 @@ import { UnregisteredFlow, UnsupportedERC20Flow, UnsupportedERC721Flow, - UnsupportedERC1155Flow + UnsupportedERC1155Flow, + MIN_FLOW_SENTINELS } from "../IFlowV3.sol"; /// @title IFlowV4 diff --git a/src/lib/LibFlow.sol b/src/lib/LibFlow.sol index bf3f857a..40d1199e 100644 --- a/src/lib/LibFlow.sol +++ b/src/lib/LibFlow.sol @@ -20,11 +20,25 @@ import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/Safe import {IERC721} from "openzeppelin-contracts/contracts/token/ERC721/IERC721.sol"; import {IERC1155} from "openzeppelin-contracts/contracts/token/ERC1155/IERC1155.sol"; +/// @title LibFlow +/// Standard processing used by all variants of `Flow`. These utilities can't +/// be directly embedded in `FlowCommon` because each variant of `Flow` has +/// slightly different requirements to incorporate mints and burns as well as +/// the basic transfer handling. library LibFlow { using SafeERC20 for IERC20; using LibStackSentinel for Pointer; using LibFlow for FlowTransferV1; + /// Converts pointers bounding an evaluated stack to a `FlowTransferV1`. + /// Works by repeatedly consuming sentinel tuples from the stack, where the + /// tuple size is 4 for ERC20, 4 for ERC721 and 5 for ERC1155. The sentinels + /// are consumed from the stack from top to bottom, so the first sentinels + /// consumed are the ERC20 transfers, followed by the ERC721 transfers and + /// finally the ERC1155 transfers. + /// @param stackBottom The bottom of the stack. + /// @param stackTop The top of the stack. + /// @return The `FlowTransferV1` representing the transfers in the stack. function stackToFlow(Pointer stackBottom, Pointer stackTop) internal pure returns (FlowTransferV1 memory) { unchecked { ERC20Transfer[] memory erc20; @@ -50,6 +64,12 @@ library LibFlow { } } + /// Processes the ERC20 transfers in the flow. + /// Reverts if the `from` address is not either the `msg.sender` or the + /// flow contract. Uses `IERC20.safeTransferFrom` to transfer the tokens to + /// ensure that reverts from the token are respected. + /// @param flowTransfer The `FlowTransferV1` to process. Tokens other than + /// ERC20 tokens are ignored. function flowERC20(FlowTransferV1 memory flowTransfer) internal { unchecked { ERC20Transfer memory transfer; @@ -68,10 +88,16 @@ library LibFlow { } } + /// Processes the ERC721 transfers in the flow. + /// Reverts if the `from` address is not either the `msg.sender` or the + /// flow contract. Uses `IERC721.safeTransferFrom` to transfer the tokens to + /// ensure that reverts from the token are respected. + /// @param flowTransfer The `FlowTransferV1` to process. Tokens other than + /// ERC721 tokens are ignored. function flowERC721(FlowTransferV1 memory flowTransfer) internal { unchecked { ERC721Transfer memory transfer; - for (uint256 i = 0; i < flowTransfer.erc721.length; i++) { + for (uint256 i = 0; i < flowTransfer.erc721.length; ++i) { transfer = flowTransfer.erc721[i]; if (transfer.from != msg.sender && transfer.from != address(this)) { revert UnsupportedERC721Flow(); @@ -81,6 +107,12 @@ library LibFlow { } } + /// Processes the ERC1155 transfers in the flow. + /// Reverts if the `from` address is not either the `msg.sender` or the + /// flow contract. Uses `IERC1155.safeTransferFrom` to transfer the tokens to + /// ensure that reverts from the token are respected. + /// @param flowTransfer The `FlowTransferV1` to process. Tokens other than + /// ERC1155 tokens are ignored. function flowERC1155(FlowTransferV1 memory flowTransfer) internal { unchecked { ERC1155Transfer memory transfer; @@ -96,6 +128,13 @@ library LibFlow { } } + /// Processes a flow transfer. Firstly sets state for the interpreter on the + /// interpreter store. Then processes the ERC20, ERC721 and ERC1155 transfers + /// in the flow. Guarantees ordering of the transfers but DOES NOT prevent + /// reentrancy attacks. This is the responsibility of the caller. + /// @param flowTransfer The `FlowTransferV1` to process. + /// @param interpreterStore The `IInterpreterStoreV1` to set state on. + /// @param kvs The key value pairs to set on the interpreter store. function flow(FlowTransferV1 memory flowTransfer, IInterpreterStoreV1 interpreterStore, uint256[] memory kvs) internal {