Skip to content

Commit

Permalink
payable macro forwarder and related use case
Browse files Browse the repository at this point in the history
  • Loading branch information
d10r committed Oct 1, 2024
1 parent 81e7c8d commit 99b216f
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,16 @@ interface IUserDefinedMacro {
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);`
* Additional to the required interface, we recommend to implement one or multiple view functions
* which take operation specific typed arguments and return the abi encoded bytes.
* As a convention, the name of those functions shall start with `params`.
*
* 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:
* Implementing this view function(s) has several advantages:
* - Allows to build more complex macros with internally encapsulated dispatching logic
* - 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
*
* You can consult the related test code in `MacroForwarderTest.t.sol` for examples.
*/
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,17 @@ abstract contract ForwarderBase {
}

function _forwardBatchCall(ISuperfluid.Operation[] memory ops) internal returns (bool) {
return _forwardBatchCall(ops, 0);
}

function _forwardBatchCall(ISuperfluid.Operation[] memory ops, uint256 valueToForward) internal returns (bool) {
bytes memory fwBatchCallData = abi.encodeCall(_host.forwardBatchCall, (ops));

// https://eips.ethereum.org/EIPS/eip-2771
// we encode the msg.sender as the last 20 bytes per EIP-2771 to extract the original txn signer later on
// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory returnedData) = address(_host).call(abi.encodePacked(fwBatchCallData, msg.sender));
(bool success, bytes memory returnedData) = address(_host)
.call{value: valueToForward}(abi.encodePacked(fwBatchCallData, msg.sender));

if (!success) {
CallUtils.revertFromReturnedData(returnedData);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ contract MacroForwarder is ForwarderBase {
* @dev Run the macro defined by the provided macro contract and params.
* @param m Target macro.
* @param params Parameters to run the macro.
* If value (native coins) is provided, it is forwarded.
*/
function runMacro(IUserDefinedMacro m, bytes calldata params) external returns (bool)
function runMacro(IUserDefinedMacro m, bytes calldata params) external payable returns (bool)
{
ISuperfluid.Operation[] memory operations = buildBatchOperations(m, params);
bool retVal = _forwardBatchCall(operations);
bool retVal = _forwardBatchCall(operations, msg.value);
m.postCheck(_host, params, msg.sender);
return retVal;
}
Expand Down
185 changes: 166 additions & 19 deletions packages/ethereum-contracts/test/foundry/utils/MacroForwarder.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ 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) {
function paramsCreateFlows(ISuperToken token, int96 flowRate, address[] calldata recipients) external pure returns (bytes memory) {
return abi.encode(token, flowRate, recipients);
}
}
Expand Down Expand Up @@ -103,7 +103,7 @@ contract MultiFlowDeleteMacro is IUserDefinedMacro {
}

// recommended view function for parameter encoding
function getParams(ISuperToken superToken, address sender, address[] memory receivers, uint256 minBalanceAfter)
function paramsDeleteFlows(ISuperToken superToken, address sender, address[] memory receivers, uint256 minBalanceAfter)
external pure
returns (bytes memory)
{
Expand All @@ -121,7 +121,7 @@ contract MultiFlowDeleteMacro is IUserDefinedMacro {
}

/*
* Example for a macro which has all the state needed, thus needs no additional calldata
* Example for a macro which has auint8 state needed, thus needs no additionalata
* in the context of batch calls.
* Important: state changes do NOT take place in the context of macro calls.
*/
Expand Down Expand Up @@ -169,6 +169,129 @@ contract StatefulMacro is IUserDefinedMacro {
function postCheck(ISuperfluid host, bytes memory params, address msgSender) external view { }
}

/// Example for a macro which takes a fee for CFA operations
contract PaidCFAOpsMacro is IUserDefinedMacro {
uint8 constant OP_CREATE_FLOW = 0;
uint8 constant OP_UPDATE_FLOW = 1;
uint8 constant OP_DELETE_FLOW = 2;

address payable immutable FEE_RECEIVER;
uint256 immutable FEE_AMOUNT;

error UnknownOperation();
error FeeOverpaid();

constructor(address payable feeReceiver, uint256 feeAmount) {
FEE_RECEIVER = feeReceiver;
FEE_AMOUNT = feeAmount;
}

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

// first operation: take fee
operations = new ISuperfluid.Operation[](2);

operations[0] = ISuperfluid.Operation({
operationType: BatchOperation.OPERATION_TYPE_SIMPLE_FORWARD_CALL,
target: address(this),
data: abi.encodeCall(this.takeFee, (FEE_AMOUNT))
});

// second operation: manage flow
// param parsing is now a 2-step process.
// first we parse the op code, then depending on its value the arguments
(uint8 op, bytes memory opArgs) = abi.decode(params, (uint8, bytes));
if (op == OP_CREATE_FLOW) {
(ISuperToken token, address receiver, int96 flowRate) =
abi.decode(opArgs, (ISuperToken, address, int96));
operations[1] = ISuperfluid.Operation({
operationType: BatchOperation.OPERATION_TYPE_SUPERFLUID_CALL_AGREEMENT,
target: address(cfa),
data: abi.encode(
abi.encodeCall(
cfa.createFlow,
(token, receiver, flowRate, new bytes(0))
),
new bytes(0) // userdata
)
});
} else if (op == OP_UPDATE_FLOW) {
(ISuperToken token, address receiver, int96 flowRate) =
abi.decode(opArgs, (ISuperToken, address, int96));
operations[1] = ISuperfluid.Operation({
operationType: BatchOperation.OPERATION_TYPE_SUPERFLUID_CALL_AGREEMENT,
target: address(cfa),
data: abi.encode(
abi.encodeCall(
cfa.updateFlow,
(token, receiver, flowRate, new bytes(0))
),
new bytes(0) // userdata
)
});
} else if (op == OP_DELETE_FLOW) {
(ISuperToken token, address sender, address receiver) =
abi.decode(opArgs, (ISuperToken, address, address));
operations[1] = ISuperfluid.Operation({
operationType: BatchOperation.OPERATION_TYPE_SUPERFLUID_CALL_AGREEMENT,
target: address(cfa),
data: abi.encode(
abi.encodeCall(
cfa.deleteFlow,
(token, sender, receiver, new bytes(0))
),
new bytes(0) // userdata
)
});
} else {
revert UnknownOperation();
}
}

// Forwards a fee in native tokens to the FEE_RECEIVER.
// Will fail if less than `amount` is provided.
function takeFee(uint256 amount) external payable {
FEE_RECEIVER.transfer(amount);
}

// Don't allow native tokens in excess of the required fee
function postCheck(ISuperfluid /*host*/, bytes memory /*params*/, address /*msgSender*/) external view {
if (address(this).balance != 0) revert FeeOverpaid();
}

// recommended view functions for parameter construction
// since this is a multi-method macro, a dispatch logic using op codes is applied.

// view function for getting params for createFlow
function paramsCreateFlow(ISuperToken token, address receiver, int96 flowRate) external pure returns (bytes memory) {
return abi.encode(
OP_CREATE_FLOW, // op
abi.encode(token, receiver, flowRate) // opArgs
);
}

// view function for getting params for updateFlow
function paramsUpdateFlow(ISuperToken token, address receiver, int96 flowRate) external pure returns (bytes memory) {
return abi.encode(
OP_UPDATE_FLOW, // op
abi.encode(token, receiver, flowRate) // opArgs
);
}

// view function for getting params for deleteFlow
function paramsDeleteFlow(ISuperToken token, address sender, address receiver) external pure returns (bytes memory) {
return abi.encode(
OP_DELETE_FLOW, // op
abi.encode(token, sender, receiver) // opArgs
);
}
}

// ============== Test Contract ==============

contract MacroForwarderTest is FoundrySuperfluidTester {
Expand All @@ -195,21 +318,7 @@ contract MacroForwarderTest is FoundrySuperfluidTester {
vm.startPrank(admin);
// NOTE! This is different from abi.encode(superToken, int96(42), [bob, carol]),
// which is a fixed array: address[2].
sf.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 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].
sf.macroForwarder.runMacro(m, m.getParams(superToken, int96(42), recipients));
sf.macroForwarder.runMacro(m, m.paramsCreateFlows(superToken, int96(42), recipients));
assertEq(sf.cfa.getNetFlow(superToken, bob), 42);
assertEq(sf.cfa.getNetFlow(superToken, carol), 42);
vm.stopPrank();
Expand Down Expand Up @@ -244,7 +353,7 @@ contract MacroForwarderTest is FoundrySuperfluidTester {
superToken.createFlow(recipients[i], 42);
}
// now batch-delete them
sf.macroForwarder.runMacro(m, m.getParams(superToken, sender, recipients, 0));
sf.macroForwarder.runMacro(m, m.paramsDeleteFlows(superToken, sender, recipients, 0));

for (uint i = 0; i < recipients.length; ++i) {
assertEq(sf.cfa.getNetFlow(superToken, recipients[i]), 0);
Expand Down Expand Up @@ -279,4 +388,42 @@ contract MacroForwarderTest is FoundrySuperfluidTester {
// reasonable reward expectation: post check passes
sf.macroForwarder.runMacro(m, abi.encode(superToken, alice, recipients, danBalanceBefore + (uint256(uint96(flowRate)) * 600)));
}

function testPaidCFAOps() external {
address payable feeReceiver = payable(address(0x420));
uint256 feeAmount = 1e15;
int96 flowRate1 = 42;
int96 flowRate2 = 42;

// alice needs funds for fee payment
vm.deal(alice, 1 ether);

PaidCFAOpsMacro m = new PaidCFAOpsMacro(feeReceiver, feeAmount);

vm.startPrank(alice);

// alice creates a flow to bob
sf.macroForwarder.runMacro{value: feeAmount}(
m,
m.paramsCreateFlow(superToken, bob, flowRate1)
);
assertEq(feeReceiver.balance, feeAmount, "unexpected fee receiver balance");
assertEq(sf.cfa.getNetFlow(superToken, bob), flowRate1);

// ... then updates that flow
sf.macroForwarder.runMacro{value: feeAmount}(
m,
m.paramsUpdateFlow(superToken, bob, flowRate2)
);
assertEq(feeReceiver.balance, feeAmount * 2, "unexpected fee receiver balance");
assertEq(sf.cfa.getNetFlow(superToken, bob), flowRate2);

// ... and finally deletes it
sf.macroForwarder.runMacro{value: feeAmount}(
m,
m.paramsDeleteFlow(superToken, alice, bob)
);
assertEq(feeReceiver.balance, feeAmount * 3, "unexpected fee receiver balance");
assertEq(sf.cfa.getNetFlow(superToken, bob), 0);
}
}

0 comments on commit 99b216f

Please sign in to comment.