From 04f7b3c22cb1da823bfd378d1a3a4e540f57dcc5 Mon Sep 17 00:00:00 2001 From: Didi Date: Wed, 28 Aug 2024 18:15:19 +0200 Subject: [PATCH] [ETHEREUM-CONTRACTS] User Macro: added postCheck and recommendation for getParams (#1903) --- packages/ethereum-contracts/CHANGELOG.md | 6 ++ .../interfaces/utils/IUserDefinedMacro.sol | 32 +++++++ .../contracts/utils/MacroForwarder.sol | 4 +- .../tasks/deploy-macro-forwarder.sh | 4 +- .../test/foundry/utils/MacroForwarder.t.sol | 92 +++++++++++++++++-- 5 files changed, 128 insertions(+), 10 deletions(-) diff --git a/packages/ethereum-contracts/CHANGELOG.md b/packages/ethereum-contracts/CHANGELOG.md index d9e6aa95b0..82dac18b5f 100644 --- a/packages/ethereum-contracts/CHANGELOG.md +++ b/packages/ethereum-contracts/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to the ethereum-contracts will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +* `IUserDefinedMacro`: added a method `postCheck()` which allows to verify state changes after running the macro. + ## [v1.11.0] ### Breaking diff --git a/packages/ethereum-contracts/contracts/interfaces/utils/IUserDefinedMacro.sol b/packages/ethereum-contracts/contracts/interfaces/utils/IUserDefinedMacro.sol index 50eb17525e..fef43f95a2 100644 --- a/packages/ethereum-contracts/contracts/interfaces/utils/IUserDefinedMacro.sol +++ b/packages/ethereum-contracts/contracts/interfaces/utils/IUserDefinedMacro.sol @@ -18,4 +18,36 @@ 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. + * @param host The host contract set for the executing MacroForwarder. + * @param params The encoded parameters as provided to `MacroForwarder.runMacro()` + * @param msgSender The msg.sender of the call to the MacroForwarder. + */ + function postCheck(ISuperfluid host, bytes memory params, address msgSender) 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 c68a323183..3e8f4e2b03 100644 --- a/packages/ethereum-contracts/contracts/utils/MacroForwarder.sol +++ b/packages/ethereum-contracts/contracts/utils/MacroForwarder.sol @@ -33,6 +33,8 @@ contract MacroForwarder is ForwarderBase { function runMacro(IUserDefinedMacro m, bytes calldata params) external returns (bool) { ISuperfluid.Operation[] memory operations = buildBatchOperations(m, params); - return _forwardBatchCall(operations); + bool retVal = _forwardBatchCall(operations); + m.postCheck(_host, params, msg.sender); + return retVal; } } diff --git a/packages/ethereum-contracts/tasks/deploy-macro-forwarder.sh b/packages/ethereum-contracts/tasks/deploy-macro-forwarder.sh index 71fca058cb..29065255fc 100755 --- a/packages/ethereum-contracts/tasks/deploy-macro-forwarder.sh +++ b/packages/ethereum-contracts/tasks/deploy-macro-forwarder.sh @@ -10,7 +10,7 @@ set -eu # RELEASE_VERSION, MACROFWD_DEPLOYER_PK # # You can use the npm package vanity-eth to get a deployer account for a given contract address: -# Example use: npx vanityeth -i cfa1 --contract +# Example use: npx vanityeth -i fd01 --contract # # For optimism the gas estimation doesn't work, requires setting EST_TX_COST # (the value auto-detected for arbitrum should work). @@ -23,7 +23,7 @@ source .env set -x network=$1 -expectedContractAddr="0xFd017DBC8aCf18B06cff9322fA6cAae2243a5c95" +expectedContractAddr="0xfD01285b9435bc45C243E5e7F978E288B2912de6" deployerPk=$MACROFWD_DEPLOYER_PK tmpfile="/tmp/$(basename "$0").addr" diff --git a/packages/ethereum-contracts/test/foundry/utils/MacroForwarder.t.sol b/packages/ethereum-contracts/test/foundry/utils/MacroForwarder.t.sol index 2e84154ffd..58d9a31bcc 100644 --- a/packages/ethereum-contracts/test/foundry/utils/MacroForwarder.t.sol +++ b/packages/ethereum-contracts/test/foundry/utils/MacroForwarder.t.sol @@ -9,6 +9,9 @@ import { FoundrySuperfluidTester, SuperTokenV1Library } from "../FoundrySuperflu using SuperTokenV1Library for ISuperToken; +// ============== Macro Contracts ============== + +// not overriding IUserDefinedMacro here in order to avoid the compiler enforcing the function to be view-only. contract NaugthyMacro { int naughtyCounter = -1; @@ -26,6 +29,8 @@ contract NaugthyMacro { } return new ISuperfluid.Operation[](0); } + + function postCheck(ISuperfluid host, bytes memory params, address msgSender) external view { } } contract GoodMacro is IUserDefinedMacro { @@ -56,10 +61,19 @@ contract GoodMacro is IUserDefinedMacro { }); } } + + function postCheck(ISuperfluid host, bytes memory params, address msgSender) 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 +// deletes a bunch of flows from one sender to muliple receivers contract MultiFlowDeleteMacro is IUserDefinedMacro { + error InsufficientReward(); + function buildBatchOperations(ISuperfluid host, bytes memory params, address msgSender) external override view returns (ISuperfluid.Operation[] memory operations) { @@ -68,15 +82,15 @@ contract MultiFlowDeleteMacro is IUserDefinedMacro { ))); // parse params - (ISuperToken token, address[] memory receivers) = - abi.decode(params, (ISuperToken, address[])); + (ISuperToken token, address sender, address[] memory receivers, uint256 minBalanceAfter) = + abi.decode(params, (ISuperToken, address, address[], uint256)); // construct batch operations operations = new ISuperfluid.Operation[](receivers.length); for (uint i = 0; i < receivers.length; ++i) { bytes memory callData = abi.encodeCall(cfa.deleteFlow, (token, - msgSender, + sender, receivers[i], new bytes(0) // placeholder )); @@ -87,6 +101,23 @@ contract MultiFlowDeleteMacro is IUserDefinedMacro { }); } } + + // recommended view function for parameter encoding + function getParams(ISuperToken superToken, address sender, address[] memory receivers, uint256 minBalanceAfter) + external pure + returns (bytes memory) + { + return abi.encode(superToken, sender, receivers, minBalanceAfter); + } + + function postCheck(ISuperfluid host, bytes memory params, address msgSender) external view { + // parse params + (ISuperToken superToken,,, uint256 minBalanceAfter) = + abi.decode(params, (ISuperToken, address, address[], uint256)); + if (superToken.balanceOf(msgSender) < minBalanceAfter) { + revert InsufficientReward(); + } + } } /* @@ -134,8 +165,12 @@ contract StatefulMacro is IUserDefinedMacro { }); } } + + function postCheck(ISuperfluid host, bytes memory params, address msgSender) external view { } } +// ============== Test Contract ============== + contract MacroForwarderTest is FoundrySuperfluidTester { MacroForwarder internal macroForwarder; @@ -176,6 +211,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; @@ -193,22 +242,51 @@ contract MacroForwarderTest is FoundrySuperfluidTester { function testMultiFlowDeleteMacro() external { MultiFlowDeleteMacro m = new MultiFlowDeleteMacro(); + address sender = alice; address[] memory recipients = new address[](3); recipients[0] = bob; recipients[1] = carol; recipients[2] = dan; - vm.startPrank(admin); + vm.startPrank(sender); // flows to be deleted need to exist in the first place for (uint i = 0; i < recipients.length; ++i) { superToken.createFlow(recipients[i], 42); } // now batch-delete them - macroForwarder.runMacro(m, abi.encode(superToken, recipients)); + macroForwarder.runMacro(m, m.getParams(superToken, sender, recipients, 0)); for (uint i = 0; i < recipients.length; ++i) { assertEq(sf.cfa.getNetFlow(superToken, recipients[i]), 0); } vm.stopPrank(); } -} + + function testPostCheck() external { + MultiFlowDeleteMacro m = new MultiFlowDeleteMacro(); + address[] memory recipients = new address[](2); + recipients[0] = bob; + recipients[1] = carol; + int96 flowRate = 1e18; + + vm.startPrank(alice); + // flows to be deleted need to exist in the first place + for (uint i = 0; i < recipients.length; ++i) { + superToken.createFlow(recipients[i], flowRate); + } + vm.stopPrank(); + + // fast forward 3000 days + vm.warp(block.timestamp + 86400*3000); + + // alice is now insolvent, dan can batch-delete the flows + vm.startPrank(dan); + uint256 danBalanceBefore = superToken.balanceOf(dan); + // unreasonable reward expectation: post check fails + vm.expectRevert(MultiFlowDeleteMacro.InsufficientReward.selector); + macroForwarder.runMacro(m, abi.encode(superToken, alice, recipients, danBalanceBefore + 1e24)); + + // reasonable reward expectation: post check passes + macroForwarder.runMacro(m, abi.encode(superToken, alice, recipients, danBalanceBefore + (uint256(uint96(flowRate)) * 600))); + } +} \ No newline at end of file