From 072c4fb7f87b0a72c3cecf7123bd0459018272b4 Mon Sep 17 00:00:00 2001 From: Didi Date: Tue, 13 Feb 2024 11:56:16 +0100 Subject: [PATCH] [ETHEREUM-CONTRACTS] proposed changes for the macro forwarder (#1828) * proposed changes for the macro forwarder * added deploy script for MacroForwarder --- packages/ethereum-contracts/CHANGELOG.md | 2 +- .../interfaces/utils/IUserDefinedMacro.sol | 4 +- ...edMacrosVanilla.sol => MacroForwarder.sol} | 18 +- .../ops-scripts/deploy-deterministically.js | 7 + .../tasks/deploy-macro-forwarder.sh | 55 ++++++ .../test/foundry/utils/MacroForwarder.t.sol | 159 ++++++++++++++++++ .../foundry/utils/TrustedMacrosVanilla.t.sol | 97 ----------- 7 files changed, 228 insertions(+), 114 deletions(-) rename packages/ethereum-contracts/contracts/utils/{TrustedMacrosVanilla.sol => MacroForwarder.sol} (56%) create mode 100755 packages/ethereum-contracts/tasks/deploy-macro-forwarder.sh create mode 100644 packages/ethereum-contracts/test/foundry/utils/MacroForwarder.t.sol delete mode 100644 packages/ethereum-contracts/test/foundry/utils/TrustedMacrosVanilla.t.sol diff --git a/packages/ethereum-contracts/CHANGELOG.md b/packages/ethereum-contracts/CHANGELOG.md index 89069ff839..566ae2b0b2 100644 --- a/packages/ethereum-contracts/CHANGELOG.md +++ b/packages/ethereum-contracts/CHANGELOG.md @@ -7,7 +7,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added -- New utility: TrustedMacrosVanilla trusted forwarder. +- New utility: MacroForwarder - a trusted forwarder extensible with permission-less macro contracts. ### Changed diff --git a/packages/ethereum-contracts/contracts/interfaces/utils/IUserDefinedMacro.sol b/packages/ethereum-contracts/contracts/interfaces/utils/IUserDefinedMacro.sol index 3901ca2048..76eb699933 100644 --- a/packages/ethereum-contracts/contracts/interfaces/utils/IUserDefinedMacro.sol +++ b/packages/ethereum-contracts/contracts/interfaces/utils/IUserDefinedMacro.sol @@ -8,7 +8,9 @@ import { ISuperfluid } from "../superfluid/ISuperfluid.sol"; */ interface IUserDefinedMacro { /** - * @dev Build batch operations according the parameters provided by the host contract. + * @dev Build batch operations according to the parameters provided. + * It's up to the macro contract to map the provided params (can also be empty) to any + * valid list of operations. * @param host The executing host contract. * @param params The encoded form of the parameters. * @return operations The batch operations built. diff --git a/packages/ethereum-contracts/contracts/utils/TrustedMacrosVanilla.sol b/packages/ethereum-contracts/contracts/utils/MacroForwarder.sol similarity index 56% rename from packages/ethereum-contracts/contracts/utils/TrustedMacrosVanilla.sol rename to packages/ethereum-contracts/contracts/utils/MacroForwarder.sol index 6b215116b8..bf66a3faa4 100644 --- a/packages/ethereum-contracts/contracts/utils/TrustedMacrosVanilla.sol +++ b/packages/ethereum-contracts/contracts/utils/MacroForwarder.sol @@ -10,29 +10,17 @@ import { ForwarderBase } from "../utils/ForwarderBase.sol"; * @dev This is a trusted forwarder with high degree of extensibility through permission-less and user-defined "macro * contracts". This is a vanilla version without EIP-712 support. */ -contract TrustedMacrosVanilla is ForwarderBase { +contract MacroForwarder is ForwarderBase { constructor(ISuperfluid host) ForwarderBase(host) {} /** - * @dev Simulate the macro run. - * @param m Target macro. - * @param params Parameters to simulate the macro. - * @return operations Operations returned by the macro after the simulation. - */ - function simulateMacro(IUserDefinedMacro m, bytes memory params) public view - returns (ISuperfluid.Operation[] memory operations) - { - operations = m.buildBatchOperations(_host, params); - } - - /** - * @dev Run the macro. + * @dev Run the macro defined by the provided macro contract and params. * @param m Target macro. * @param params Parameters to run the macro. */ function runMacro(IUserDefinedMacro m, bytes memory params) external returns (bool) { - ISuperfluid.Operation[] memory operations = simulateMacro(m, params); + ISuperfluid.Operation[] memory operations = m.buildBatchOperations(_host, params); return _forwardBatchCall(operations); } } diff --git a/packages/ethereum-contracts/ops-scripts/deploy-deterministically.js b/packages/ethereum-contracts/ops-scripts/deploy-deterministically.js index dcd6551dcb..d9aa814b13 100644 --- a/packages/ethereum-contracts/ops-scripts/deploy-deterministically.js +++ b/packages/ethereum-contracts/ops-scripts/deploy-deterministically.js @@ -6,6 +6,7 @@ const Resolver = artifacts.require("Resolver"); const SuperfluidLoader = artifacts.require("SuperfluidLoader"); const CFAv1Forwarder = artifacts.require("CFAv1Forwarder"); const GDAv1Forwarder = artifacts.require("GDAv1Forwarder"); +const MacroForwarder = artifacts.require("MacroForwarder"); /** * @dev Deploy specified contract at a deterministic address (defined by sender, nonce) @@ -87,6 +88,12 @@ module.exports = eval(`(${S.toString()})()`)(async function ( console.log( `setting up GDAv1Forwarder for chainId ${chainId}, host ${hostAddr}` ); + } else if (contractName === "MacroForwarder") { + ContractArtifact = MacroForwarder; + deployArgs = [hostAddr]; + console.log( + `setting up MacroForwarder for chainId ${chainId}, host ${hostAddr}` + ); } else { throw new Error("Contract unknown / not supported"); } diff --git a/packages/ethereum-contracts/tasks/deploy-macro-forwarder.sh b/packages/ethereum-contracts/tasks/deploy-macro-forwarder.sh new file mode 100755 index 0000000000..af2fb5f093 --- /dev/null +++ b/packages/ethereum-contracts/tasks/deploy-macro-forwarder.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -eu + +# Usage: +# tasks/deploy-macro-forwarder.sh +# +# Example: +# tasks/deploy-macro-forwarder.sh optimism-goerli +# +# The invoking account needs to be (co-)owner of the resolver and governance +# +# important ENV vars: +# 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 +# +# For optimism the gas estimation doesn't work, requires setting EST_TX_COST +# (the value auto-detected for arbitrum should work). +# +# On some networks you may need to use override ENV vars for the deployment to succeed + +# shellcheck source=/dev/null +source .env + +set -x + +network=$1 +expectedContractAddr="0xFd017DBC8aCf18B06cff9322fA6cAae2243a5c95" +deployerPk=$MACROFWD_DEPLOYER_PK + +tmpfile="/tmp/$(basename "$0").addr" + +# deploy +DETERMINISTIC_DEPLOYER_PK=$deployerPk npx truffle exec --network "$network" ops-scripts/deploy-deterministically.js : MacroForwarder | tee "$tmpfile" +contractAddr=$(tail -n 1 "$tmpfile") +rm "$tmpfile" + +echo "deployed to $contractAddr" +if [[ $contractAddr != "$expectedContractAddr" ]]; then + echo "oh no!" + exit +fi + +# verify (give it a few seconds to pick up the code) +sleep 5 +npx truffle run --network "$network" verify MacroForwarder@"$contractAddr" + +# set resolver +ALLOW_UPDATE=1 npx truffle exec --network "$network" ops-scripts/resolver-set-key-value.js : MacroForwarder "$contractAddr" + +# create gov action +npx truffle exec --network "$network" ops-scripts/gov-set-trusted-forwarder.js : 0x0000000000000000000000000000000000000000 "$contractAddr" 1 + +# TODO: on mainnets, the resolver entry should be set only after the gov action was signed & executed diff --git a/packages/ethereum-contracts/test/foundry/utils/MacroForwarder.t.sol b/packages/ethereum-contracts/test/foundry/utils/MacroForwarder.t.sol new file mode 100644 index 0000000000..64d850436a --- /dev/null +++ b/packages/ethereum-contracts/test/foundry/utils/MacroForwarder.t.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: AGPLv3 +pragma solidity 0.8.23; + +import { ISuperfluid, BatchOperation } from "../../../contracts/interfaces/superfluid/ISuperfluid.sol"; +import { ISuperToken } from "../../../contracts/superfluid/SuperToken.sol"; +import { IConstantFlowAgreementV1 } from "../../../contracts/interfaces/agreements/IConstantFlowAgreementV1.sol"; +import { MacroForwarder, IUserDefinedMacro } from "../../../contracts/utils/MacroForwarder.sol"; +import { FoundrySuperfluidTester, SuperTokenV1Library } from "../FoundrySuperfluidTester.sol"; + + +contract NaugthyMacro { + int naughtyCounter = -1; + + constructor(bool beNaughty) { + if (beNaughty) naughtyCounter = 0; + } + + // if naughtyCounter >= 0, this changes state, which leads to a rever in the context of a macro call + function buildBatchOperations(ISuperfluid, bytes memory) external + returns (ISuperfluid.Operation[] memory /*operation*/) + { + // Do the naughty thing (updating state as an expected view function) + if (naughtyCounter >= 0) { + naughtyCounter++; + } + } +} + +contract GoodMacro is IUserDefinedMacro { + function buildBatchOperations(ISuperfluid host, bytes memory params) external view + returns (ISuperfluid.Operation[] memory operations) + { + // host-agnostic deployment. alternatively, you may hard code cfa too + IConstantFlowAgreementV1 cfa = IConstantFlowAgreementV1(address(host.getAgreementClass( + keccak256("org.superfluid-finance.agreements.ConstantFlowAgreement.v1") + ))); + // parse params + (ISuperToken token, int96 flowRate, address[] memory recipients) = + abi.decode(params, (ISuperToken, int96, address[])); + // construct batch operations + operations = new ISuperfluid.Operation[](recipients.length); + // Build batch call operations here + for (uint i = 0; i < recipients.length; ++i) { + bytes memory callData = abi.encodeCall(cfa.createFlow, + (token, + recipients[i], + flowRate, + new bytes(0) // placeholder + )); + operations[i] = ISuperfluid.Operation({ + operationType : BatchOperation.OPERATION_TYPE_SUPERFLUID_CALL_AGREEMENT, // type + target: address(cfa), + data: abi.encode(callData, new bytes(0)) + }); + } + } +} + +/* + * Example for a macro which has all the state needed, thus needs no additional calldata + * in the context of batch calls. + * Important: state changes do NOT take place in the context of macro calls. + */ +contract StatefulMacro is IUserDefinedMacro { + struct Config { + MacroForwarder macroForwarder; + ISuperToken superToken; + int96 flowRate; + address[] recipients; + address referrer; + } + Config public config; + + // imagine this to be permissioned, e.g. using Ownable + function setConfig(Config memory config_) public { + config = config_; + } + + function buildBatchOperations(ISuperfluid host, bytes memory /*params*/) external view + returns (ISuperfluid.Operation[] memory operations) + { + // host-agnostic deployment. alternatively, you may hard code cfa too + IConstantFlowAgreementV1 cfa = IConstantFlowAgreementV1(address(host.getAgreementClass( + keccak256("org.superfluid-finance.agreements.ConstantFlowAgreement.v1") + ))); + + // construct batch operations from persisted config + operations = new ISuperfluid.Operation[](config.recipients.length); + for (uint i = 0; i < config.recipients.length; ++i) { + bytes memory callData = abi.encodeCall(cfa.createFlow, + (config.superToken, + config.recipients[i], + config.flowRate, + new bytes(0) // placeholder + )); + operations[i] = ISuperfluid.Operation({ + operationType : BatchOperation.OPERATION_TYPE_SUPERFLUID_CALL_AGREEMENT, // type + target: address(cfa), + data: abi.encode(callData, abi.encode(config.referrer)) + }); + } + } +} + +contract MacroForwarderTest is FoundrySuperfluidTester { + MacroForwarder internal macroForwarder; + + constructor() FoundrySuperfluidTester(5) { + } + + function setUp() public override { + super.setUp(); + macroForwarder = new MacroForwarder(sf.host); + vm.startPrank(address(sf.governance.owner())); + sf.governance.enableTrustedForwarder(sf.host, ISuperToken(address(0)), address(macroForwarder)); + vm.stopPrank(); + } + + function testDummyMacro() external { + NaugthyMacro m = new NaugthyMacro(false /* not naughty */); + macroForwarder.runMacro(IUserDefinedMacro(address(m)), new bytes(0)); + } + + function testNaugtyMacro() external { + NaugthyMacro m = new NaugthyMacro(true /* naughty */); + vm.expectRevert(); + // Note: need to cast the naughty macro + macroForwarder.runMacro(IUserDefinedMacro(address(m)), new bytes(0)); + } + + function testGoodMacro() 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, abi.encode(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; + recipients[1] = carol; + StatefulMacro m = new StatefulMacro(); + m.setConfig(StatefulMacro.Config( + macroForwarder, superToken, 42, recipients, dan + )); + vm.startPrank(admin); + macroForwarder.runMacro(m, new bytes(0)); + assertEq(sf.cfa.getNetFlow(superToken, bob), 42); + assertEq(sf.cfa.getNetFlow(superToken, carol), 42); + vm.stopPrank(); + } +} diff --git a/packages/ethereum-contracts/test/foundry/utils/TrustedMacrosVanilla.t.sol b/packages/ethereum-contracts/test/foundry/utils/TrustedMacrosVanilla.t.sol deleted file mode 100644 index 84ebfa9384..0000000000 --- a/packages/ethereum-contracts/test/foundry/utils/TrustedMacrosVanilla.t.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: AGPLv3 -pragma solidity 0.8.23; - -import { ISuperfluid, BatchOperation } from "../../../contracts/interfaces/superfluid/ISuperfluid.sol"; -import { ISuperToken } from "../../../contracts/superfluid/SuperToken.sol"; -import { IConstantFlowAgreementV1 } from "../../../contracts/interfaces/agreements/IConstantFlowAgreementV1.sol"; -import { TrustedMacrosVanilla, IUserDefinedMacro } from "../../../contracts/utils/TrustedMacrosVanilla.sol"; -import { FoundrySuperfluidTester, SuperTokenV1Library } from "../FoundrySuperfluidTester.sol"; - - -contract NautyMacro { - int naughtyCounter = -1; - - constructor(bool beNaughty) { - if (beNaughty) naughtyCounter = 0; - } - - function buildBatchOperations(ISuperfluid, bytes memory) external - returns (ISuperfluid.Operation[] memory operation) - { - // Do the naughty thing (updating state as an expected view function) - if (naughtyCounter >= 0) { - naughtyCounter++; - } - } -} - -contract GoodMacro is IUserDefinedMacro { - function buildBatchOperations(ISuperfluid host, bytes memory params) external view - returns (ISuperfluid.Operation[] memory operations) - { - // host-agnostic deployment. alternatively, you may hard code cfa too - IConstantFlowAgreementV1 cfa = IConstantFlowAgreementV1(address(host.getAgreementClass( - keccak256("org.superfluid-finance.agreements.ConstantFlowAgreement.v1") - ))); - // parse params - (ISuperToken token, int96 flowRate, address[] memory recipients) = - abi.decode(params, (ISuperToken, int96, address[])); - // construct batch operations - operations = new ISuperfluid.Operation[](recipients.length); - // Build batch call operations here - for (uint i = 0; i < recipients.length; ++i) { - bytes memory callData = abi.encodeCall(cfa.createFlow, - (token, - recipients[i], - flowRate, - new bytes(0) // placeholder - )); - operations[i] = ISuperfluid.Operation({ - operationType : BatchOperation.OPERATION_TYPE_SUPERFLUID_CALL_AGREEMENT, // type - target: address(cfa), - data: abi.encode(callData, new bytes(0)) - }); - } - } -} - -contract TrustedMacrosVanillaTest is FoundrySuperfluidTester { - TrustedMacrosVanilla internal trustedMacros; - - constructor() FoundrySuperfluidTester(5) { - } - - function setUp() public override { - super.setUp(); - trustedMacros = new TrustedMacrosVanilla(sf.host); - vm.startPrank(address(sf.governance.owner())); - sf.governance.enableTrustedForwarder(sf.host, ISuperToken(address(0)), address(trustedMacros)); - vm.stopPrank(); - } - - function testDummyMacro() external { - NautyMacro m = new NautyMacro(false /* not naughty */); - trustedMacros.runMacro(IUserDefinedMacro(address(m)), new bytes(0)); - } - - function testNaugtyMacro() external { - NautyMacro m = new NautyMacro(true /* naughty */); - vm.expectRevert(); - // Note: need to cast the naughty macro - trustedMacros.runMacro(IUserDefinedMacro(address(m)), new bytes(0)); - } - - function testGoodMacro() 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]. - trustedMacros.runMacro(m, abi.encode(superToken, int96(42), recipients)); - assertEq(sf.cfa.getNetFlow(superToken, bob), 42); - assertEq(sf.cfa.getNetFlow(superToken, carol), 42); - vm.stopPrank(); - } -}