Skip to main content

Batch transactions with Macros

Superfluid's infrastructure introduces innovative approaches to batching transactions and account abstraction, leveraging the modular architrecture of Superfluid, and specifically the mastermind contract of the protocol called the Superfluid Host. This document provides a guide of how to use the MacroForwarder contract to batch transactions.

Background

The Superfluid Host contract makes it possible to batch transactions from day one through a method called batchCall. Eventually, the Superfluid Host adopted ERC-2771. As opposed to the EIP-4337 which uses a Contract Account (CA) for abstraction, ERC-2771 extends the capabilities of the Host by allowing a trusted forwarder to pass the original msg.sender to the host contract through the method forwardBatchCall.

Macro Forwarder Contract Overview

Introducing a simple and secure way for builders to define their own macros without needing to be directly trusted by the Superfluid host contract. This approach simplifies on-chain logic for batch calls, reduces gas consumption and potentially ameliorates the front-end code, addressing atomicity issues. Today, Superfluid has a contract called MacroForwarder.sol which is a trusted forwarder for user-defined macro interfaces.

Click here to show the MacroForwarder contract

// SPDX-License-Identifier: AGPLv3
pragma solidity 0.8.23;

import { IUserDefinedMacro } from "../interfaces/utils/IUserDefinedMacro.sol";
import { ISuperfluid } from "../interfaces/superfluid/ISuperfluid.sol";
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 MacroForwarder is ForwarderBase {
constructor(ISuperfluid host) ForwarderBase(host) {}

/**
* @dev A convenience view wrapper for building the batch operations using a macro.
* @param m Target macro.
* @param params Parameters to simulate the macro.
* @return operations Operations returned by the macro after the simulation.
*/
function buildBatchOperations(IUserDefinedMacro m, bytes calldata params) public view
returns (ISuperfluid.Operation[] memory operations)
{
operations = m.buildBatchOperations(_host, params, msg.sender);
}

/**
* @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 calldata params) external returns (bool)
{
ISuperfluid.Operation[] memory operations = buildBatchOperations(m, params);
return _forwardBatchCall(operations);
}
}

Macro Forwarder Contract Address

The MacroForwarder contract has the same address on all networks:

0xFD0268E33111565dE546af2675351A4b1587F89F

Macro Forwarder Contract ABI

In order to interact with the MacroForwarder contract from your client application, you can use the following ABI:

Click here to show the MacroForwarder ABI

[
{
"inputs": [
{ "internalType": "contract ISuperfluid", "name": "host", "type": "address" }
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "contract IUserDefinedMacro",
"name": "m",
"type": "address"
},
{ "internalType": "bytes", "name": "params", "type": "bytes" }
],
"name": "buildBatchOperations",
"outputs": [
{
"components": [
{ "internalType": "uint32", "name": "operationType", "type": "uint32" },
{ "internalType": "address", "name": "target", "type": "address" },
{ "internalType": "bytes", "name": "data", "type": "bytes" }
],
"internalType": "struct ISuperfluid.Operation[]",
"name": "operations",
"type": "tuple[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract IUserDefinedMacro",
"name": "m",
"type": "address"
},
{ "internalType": "bytes", "name": "params", "type": "bytes" }
],
"name": "runMacro",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "nonpayable",
"type": "function"
}
]

How to use the Macro Forwarder

In order to understand how to use the MacroForwarder contract, we will use an example contract called MultiFlowDeleteMacro.sol which allows us to batch call delete flow transactions from one account to multiple accounts for a specific Super Token:

Click here to show the MultiFlowDeleteMacro contract

// SPDX-License-Identifier: AGPLv3
pragma solidity 0.8.23;

import { ISuperfluid, BatchOperation, IConstantFlowAgreementV1, ISuperToken }
from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol";
import { IUserDefinedMacro } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/utils/IUserDefinedMacro.sol";


// deletes a bunch of flows of the msgSender
contract MultiFlowDeleteMacro is IUserDefinedMacro {
function buildBatchOperations(ISuperfluid host, bytes memory params, address msgSender) public virtual view
returns (ISuperfluid.Operation[] memory operations)
{
IConstantFlowAgreementV1 cfa = IConstantFlowAgreementV1(address(host.getAgreementClass(
keccak256("org.superfluid-finance.agreements.ConstantFlowAgreement.v1")
)));

// parse params
(ISuperToken token, address[] memory receivers) =
abi.decode(params, (ISuperToken, address[]));

// 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,
receivers[i],
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))
});
}
}

// returns the abi encoded params for the macro, to be used with buildBatchOperations
function getParams(ISuperToken superToken, address[] memory receivers) external pure returns (bytes memory) {
return abi.encode(superToken, receivers);
}
}

The steps in order to use the MacroForwarder contract are as follows:

  1. Create a contract which inherit the User Defined Macro Interface
  2. Implement your Macro Interface
  3. Use the Macro Forwarder to batch call the transactions
Get ready for tests and deployment

Creating your own macro involves testing and deploying a smart contract. If you are not familiar with testing and deployment frameworks on the Ethereum Virtual Machine, you should consider learning about Hardhat or Foundry.

1. Create your contract and inherit the User Defined Macro Interface

As you may have noticed, the MultiFlowDeleteMacro contract inherits the IUserDefinedMacro interface like so:

contract MultiFlowDeleteMacro is IUserDefinedMacro {
...
}

This is an interface that defines the buildBatchOperations method. It is the only required method to be implemented in the contract that inherits it.

Therefore, the first step is to create a new contract which inherits the IUserDefinedMacro interface.

2. Implement your Macro Interface

The buildBatchOperations method is the only required method to be implemented in the contract that inherits the IUserDefinedMacro interface. This method returns an array of ISuperfluid.Operation[] struct which will be consumed by the MacroForwarder contract. This struct is defined as follows:

struct Operation {
operationType operationType;
address target;
bytes data;
}

In the example contract MultiFlowDeleteMacro, you can see that the buildBatchOperations method is implemented as follows:

function buildBatchOperations(ISuperfluid host, bytes memory params, address msgSender) public virtual view
returns (ISuperfluid.Operation[] memory operations)
{
IConstantFlowAgreementV1 cfa = IConstantFlowAgreementV1(address(host.getAgreementClass(
keccak256("org.superfluid-finance.agreements.ConstantFlowAgreement.v1")
)));

// parse params
(ISuperToken token, address[] memory receivers) =
abi.decode(params, (ISuperToken, address[]));

// 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,
receivers[i],
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))
});
}
}

About the method getParams

The MultiFlowDeleteMacro example contract contains a method called getParams. This method is not required to be implemented in the contract that inherits the IUserDefinedMacro interface. However, it is highly recommended to implement this method in order to parse the parameters of the macro on the front-end.

This method is simply implemented by encoding the parameters that will be used to call the method runMacrofrom MacroForwarder contract. It is usually one line of code as such:

function getParams(ISuperToken token, address[] memory receivers) public pure returns (bytes memory) {
return abi.encode(token, receivers);
}

Once, you set up and tested your Macro contract, you can deploy it to your target EVM network and use the MacroForwarder contract to batch call the transactions.

3. Use the Macro Forwarder to batch call the transactions

The MacroForwarder contract is used to batch call the transactions. It is a simple contract that has a method called runMacro which takes the following parameters:

  • IUserDefinedMacro m: The address of the contract that inherits the IUserDefinedMacro interface
  • bytes calldata params: The parameters of the macro

The runMacro method is called by the user and it will batch call the transactions defined in the buildBatchOperations method of the IUserDefinedMacro contract.

To showcase how this works, we use the MacroFowarder contract deployed on OP Sepolia. We deployed an example of our MultiFlowDeleteMacro contract on the same network. We will use the MacroForwarder contract to batch call the transactions.

We showcase below a simple React component which implements all of this:

Click here to show the MacroForwarderComponent

const MacroForwarderComponent = ({
macroForwarderAddress,
userDefinedMacroAddress,
}) => {
const [walletAddress, setWalletAddress] = useState("");
const [superToken, setSuperToken] = useState("");
const [receivers, setReceivers] = useState("");
const [message, setMessage] = useState("");

// ABI for MacroForwarder contract including `runMacro`
const macroForwarderABI = [
//ABI for MacroForwarder contract
];

// ABI for your UserDefinedMacro including `getParams`
const iUserDefinedMacroABI = [
//ABI for your UserDefinedMacro including `getParams`
];

const connectWallet = async () => {
if (window.ethereum) {
try {
const provider = new ethers.providers.Web3Provider(window.ethereum);
await provider.send("eth_requestAccounts", []);
const signer = provider.getSigner();
const address = await signer.getAddress();
setWalletAddress(address);
console.log("Connected to MetaMask");
} catch (error) {
console.error("Error connecting to MetaMask", error);
setMessage("Error connecting to MetaMask");
}
} else {
console.log("Ethereum wallet is not connected or not installed.");
setMessage("Ethereum wallet is not connected or not installed.");
}
};

const executeMacro = async () => {
try {
if (!walletAddress) throw new Error("Wallet not connected.");
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();

const userDefinedMacroContract = new ethers.Contract(
userDefinedMacroAddress,
iUserDefinedMacroABI,
signer
);
const receiversArray = receivers
.split(",")
.map((address) => address.trim());
const params = await userDefinedMacroContract.getParams(
superToken,
receiversArray
);

const macroForwarderContract = new ethers.Contract(
macroForwarderAddress,
macroForwarderABI,
signer
);
const tx = await macroForwarderContract.runMacro(
userDefinedMacroAddress,
params
);
await tx.wait();
setMessage("Macro executed successfully.");
} catch (error) {
console.error("Error executing macro", error);
setMessage(`Error: ${error.message}`);
}
};

return (
<div
style={{
textAlign: "center",
padding: "20px",
fontFamily: "Arial, sans-serif",
}}
>
<h2>Macro Forwarder Interface</h2>
<h3>Connect Wallet to your chosen testnet (e.g. OP Sepolia)</h3>
{walletAddress ? (
<p>
Connected Wallet: <strong>{walletAddress}</strong>
</p>
) : (
<button
onClick={connectWallet}
style={{
backgroundColor: "#168c1e",
color: "white",
padding: "10px 15px",
borderRadius: "5px",
border: "none",
cursor: "pointer",
}}
>
Connect Wallet
</button>
)}
<div style={{ margin: "10px" }}>
{walletAddress && (
<>
<div>
<input
type="text"
placeholder="SuperToken Address"
value={superToken}
onChange={(e) => setSuperToken(e.target.value)}
style={{ margin: "5px", padding: "5px" }}
/>
<input
type="text"
placeholder="Receiver Addresses (comma separated)"
value={receivers}
onChange={(e) => setReceivers(e.target.value)}
style={{ margin: "5px", padding: "5px" }}
/>
</div>
<button onClick={executeMacro} style={{ margin: "10px" }}>
Execute Macro
</button>
<p style={{ marginTop: "20px" }}>{message}</p>
</>
)}
</div>
</div>
);
};

The MacroForwarderComponent is a simple React component that allows you to connect your wallet and execute the macro using EthersJS. If you deployed your own MultiFlowDeleteMacro contract, you can use the MacroForwarderComponent to batch call the transactions by inputing the MacroForwarder and MultiFlowDeleteMacro contract addresses in the playground below.

Live Editor
function MacroComponentExample() {

const macroForwarderAddress="0xFD0268E33111565dE546af2675351A4b1587F89F";
const userMacroAddress="0x997b37Fb47c489CF067421aEeAf7Be0543fA5362";
return (
    <div>
      <MacroForwarderComponent
      macroForwarderAddress={macroForwarderAddress}
      userDefinedMacroAddress={userMacroAddress}
      />
    </div>
  );
}
Result
Loading...

Next Steps - EIP-712 Support

We will provide a guide which laverages EIP-712 for typed structured data hashing and signing, enhancing the security and usability of macro transactions. This will allow for the following features:

  • On-chain verifiable signatures.
  • Multilingual support for transaction signing.
  • Supports meta transactions, allowing for gas-less transactions.
  • And more...