Skip to content

Commit

Permalink
[ETHEREUM-CONTRACTS] User Macro: added postCheck and recommendation f…
Browse files Browse the repository at this point in the history
…or getParams (#1903)
  • Loading branch information
d10r authored Aug 28, 2024
1 parent c887de9 commit 04f7b3c
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 10 deletions.
6 changes: 6 additions & 0 deletions packages/ethereum-contracts/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
4 changes: 2 additions & 2 deletions packages/ethereum-contracts/tasks/deploy-macro-forwarder.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -23,7 +23,7 @@ source .env
set -x

network=$1
expectedContractAddr="0xFd017DBC8aCf18B06cff9322fA6cAae2243a5c95"
expectedContractAddr="0xfD01285b9435bc45C243E5e7F978E288B2912de6"
deployerPk=$MACROFWD_DEPLOYER_PK

tmpfile="/tmp/$(basename "$0").addr"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 {
Expand Down Expand Up @@ -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)
{
Expand All @@ -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
));
Expand All @@ -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();
}
}
}

/*
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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)));
}
}

0 comments on commit 04f7b3c

Please sign in to comment.