Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ETHEREUM-CONTRACTS] payable macro forwarder #2025

Merged
merged 10 commits into from
Oct 14, 2024
1 change: 1 addition & 0 deletions packages/ethereum-contracts/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Changed

* `MacroForwarder` made payable.
* `IUserDefinedMacro`: added a method `postCheck()` which allows to verify state changes after running the macro.
* `SuperfluidFrameworkDeployer` now also deploys and `MacroForwarder` and enables it as trusted forwarder.
* `deploy-test-environment.js` now deploys fUSDC (the underlying) with 6 decimals (instead of 18) to better resemble the actual USDC.
Expand Down
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
hellwolf marked this conversation as resolved.
Show resolved Hide resolved
* 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) {
d10r marked this conversation as resolved.
Show resolved Hide resolved
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);
d10r marked this conversation as resolved.
Show resolved Hide resolved
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);
}
}
Loading