Skip to content

Commit

Permalink
[ETHEREUM-CONTRACTS] proposed changes for the macro forwarder (#1828)
Browse files Browse the repository at this point in the history
* proposed changes for the macro forwarder

* added deploy script for MacroForwarder
  • Loading branch information
d10r authored Feb 13, 2024
1 parent 93c778c commit 072c4fb
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 114 deletions.
2 changes: 1 addition & 1 deletion packages/ethereum-contracts/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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");
}
Expand Down
55 changes: 55 additions & 0 deletions packages/ethereum-contracts/tasks/deploy-macro-forwarder.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -eu

# Usage:
# tasks/deploy-macro-forwarder.sh <network>
#
# 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
159 changes: 159 additions & 0 deletions packages/ethereum-contracts/test/foundry/utils/MacroForwarder.t.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}

This file was deleted.

0 comments on commit 072c4fb

Please sign in to comment.