diff --git a/packages/ethereum-contracts/contracts/interfaces/utils/IUserDefinedMacro.sol b/packages/ethereum-contracts/contracts/interfaces/utils/IUserDefinedMacro.sol index 50eb17525e..79b4085ec4 100644 --- a/packages/ethereum-contracts/contracts/interfaces/utils/IUserDefinedMacro.sol +++ b/packages/ethereum-contracts/contracts/interfaces/utils/IUserDefinedMacro.sol @@ -18,4 +18,34 @@ interface IUserDefinedMacro { */ function buildBatchOperations(ISuperfluid host, bytes memory params, address msgSender) external view returns (ISuperfluid.Operation[] memory operations); + + /** + * @dev A post-check function which is called after execution. + * It allows to do arbitrary checks based on the state after execution, + * and to revert if the result is not as expected. + * Can be an empty implementation if no check is needed. + */ + function postCheck() external view; + + /* + * Additional to the required interface, we recommend to implement the following function: + * `function getParams(...) external view returns (bytes memory);` + * + * It shall return abi encoded params as required as second argument of `MacroForwarder.runMacro()`. + * + * The function name shall be `getParams` and the return type shall be `bytes memory`. + * The number, type and name of arguments are free to choose such that they best fit the macro use case. + * + * In conjunction with the name of the Macro contract, the signature should be as self-explanatory as possible. + * + * Example for a contract `MultiFlowDeleteMacro` which lets a user delete multiple flows in one transaction: + * `function getParams(ISuperToken superToken, address[] memory receivers) external view returns (bytes memory)` + * + * + * Implementing this view function has several advantages: + * - Allows to use generic tooling like Explorers to interact with the macro + * - Allows to build auto-generated UIs based on the contract ABI + * - Makes it easier to interface with the macro from Dapps + * + */ } diff --git a/packages/ethereum-contracts/contracts/utils/MacroForwarder.sol b/packages/ethereum-contracts/contracts/utils/MacroForwarder.sol index 8f601e999e..040e86442c 100644 --- a/packages/ethereum-contracts/contracts/utils/MacroForwarder.sol +++ b/packages/ethereum-contracts/contracts/utils/MacroForwarder.sol @@ -33,6 +33,7 @@ contract MacroForwarder is ForwarderBase { function runMacro(IUserDefinedMacro m, bytes calldata params) external returns (bool) { ISuperfluid.Operation[] memory operations = buildBatchOperations(m, params); + m.postCheck(); return _forwardBatchCall(operations); } } diff --git a/packages/ethereum-contracts/test/foundry/utils/MacroForwarder.t.sol b/packages/ethereum-contracts/test/foundry/utils/MacroForwarder.t.sol index e8754b10d8..3eeb40f15c 100644 --- a/packages/ethereum-contracts/test/foundry/utils/MacroForwarder.t.sol +++ b/packages/ethereum-contracts/test/foundry/utils/MacroForwarder.t.sol @@ -9,6 +9,7 @@ import { FoundrySuperfluidTester, SuperTokenV1Library } from "../FoundrySuperflu using SuperTokenV1Library for ISuperToken; +// not overriding IUserDefinedMacro here in order to avoid the compiler enforcing the function to be view-only. contract NaugthyMacro { int naughtyCounter = -1; @@ -26,6 +27,8 @@ contract NaugthyMacro { } return new ISuperfluid.Operation[](0); } + + function postCheck() external view { } } contract GoodMacro is IUserDefinedMacro { @@ -56,6 +59,13 @@ contract GoodMacro is IUserDefinedMacro { }); } } + + function postCheck() external view { } + + // recommended view function for parameter encoding + function getParams(ISuperToken token, int96 flowRate, address[] calldata recipients) external pure returns (bytes memory) { + return abi.encode(token, flowRate, recipients); + } } // deletes a bunch of flows of the msgSender @@ -87,6 +97,20 @@ contract MultiFlowDeleteMacro is IUserDefinedMacro { }); } } + + function postCheck() external view { } +} + +contract MacroWithRevertingPostCheck is IUserDefinedMacro { + function buildBatchOperations(ISuperfluid, bytes memory, address /*msgSender*/) external override pure + returns (ISuperfluid.Operation[] memory /*operation*/) + { + return new ISuperfluid.Operation[](0); + } + + function postCheck() external pure { + revert("I'm a bad macro"); + } } /* @@ -134,6 +158,8 @@ contract StatefulMacro is IUserDefinedMacro { }); } } + + function postCheck() external view { } } contract MacroForwarderTest is FoundrySuperfluidTester { @@ -176,6 +202,20 @@ contract MacroForwarderTest is FoundrySuperfluidTester { vm.stopPrank(); } + function testGoodMacroUsingGetParams() external { + GoodMacro m = new GoodMacro(); + address[] memory recipients = new address[](2); + recipients[0] = bob; + recipients[1] = carol; + vm.startPrank(admin); + // NOTE! This is different from abi.encode(superToken, int96(42), [bob, carol]), + // which is a fixed array: address[2]. + macroForwarder.runMacro(m, m.getParams(superToken, int96(42), recipients)); + assertEq(sf.cfa.getNetFlow(superToken, bob), 42); + assertEq(sf.cfa.getNetFlow(superToken, carol), 42); + vm.stopPrank(); + } + function testStatefulMacro() external { address[] memory recipients = new address[](2); recipients[0] = bob; @@ -211,4 +251,10 @@ contract MacroForwarderTest is FoundrySuperfluidTester { } vm.stopPrank(); } + + function testRevertingPostCheck() external { + MacroWithRevertingPostCheck m = new MacroWithRevertingPostCheck(); + vm.expectRevert(); + macroForwarder.runMacro(m, new bytes(0)); + } }