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

feat:add support for delegate calltype #108

Merged
merged 12 commits into from
Jul 7, 2024
196 changes: 98 additions & 98 deletions .github/gas_report.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .solhint.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"visibility-modifier-order": "error",
"code-complexity": ["error", 9],
"function-max-lines": ["error", 80],
"max-line-length": ["error", 150],
"max-line-length": ["error", 160],
"no-empty-blocks": "off",
"no-unused-vars": "error",
"payable-fallback": "off",
Expand Down
110 changes: 55 additions & 55 deletions GAS_REPORT.md

Large diffs are not rendered by default.

65 changes: 21 additions & 44 deletions contracts/Nexus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ pragma solidity ^0.8.26;
import { UUPSUpgradeable } from "solady/src/utils/UUPSUpgradeable.sol";
import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol";
import { ExecLib } from "./lib/ExecLib.sol";
import { Execution } from "./types/DataTypes.sol";
import { INexus } from "./interfaces/INexus.sol";
import { IModule } from "./interfaces/modules/IModule.sol";
import { BaseAccount } from "./base/BaseAccount.sol";
Expand All @@ -31,7 +30,17 @@ import {
MODULE_TYPE_MULTI,
VALIDATION_FAILED
} from "./types/Constants.sol";
import { ModeLib, ExecutionMode, ExecType, CallType, CALLTYPE_BATCH, CALLTYPE_SINGLE, EXECTYPE_DEFAULT, EXECTYPE_TRY } from "./lib/ModeLib.sol";
import {
ModeLib,
ExecutionMode,
ExecType,
CallType,
CALLTYPE_BATCH,
CALLTYPE_SINGLE,
CALLTYPE_DELEGATECALL,
EXECTYPE_DEFAULT,
EXECTYPE_TRY
} from "./lib/ModeLib.sol";
import { NonceLib } from "./lib/NonceLib.sol";

/// @title Nexus - Smart Account
Expand Down Expand Up @@ -113,6 +122,8 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra
_handleSingleExecution(executionCalldata, execType);
} else if (callType == CALLTYPE_BATCH) {
_handleBatchExecution(executionCalldata, execType);
} else if (callType == CALLTYPE_DELEGATECALL) {
_handleDelegateCallExecution(executionCalldata, execType);
} else {
revert UnsupportedCallType(callType);
}
Expand All @@ -128,29 +139,13 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra
bytes calldata executionCalldata
) external payable onlyExecutorModule withHook withRegistry(msg.sender, MODULE_TYPE_EXECUTOR) returns (bytes[] memory returnData) {
(CallType callType, ExecType execType) = mode.decodeBasic();

// check if calltype is batch or single
// check if calltype is batch or single or delegate call
if (callType == CALLTYPE_SINGLE) {
// destructure executionCallData according to single exec
(address target, uint256 value, bytes calldata callData) = executionCalldata.decodeSingle();
returnData = new bytes[](1);
bool success;
// check if execType is revert(default) or try
if (execType == EXECTYPE_DEFAULT) {
returnData[0] = _execute(target, value, callData);
} else if (execType == EXECTYPE_TRY) {
(success, returnData[0]) = _tryExecute(target, value, callData);
if (!success) emit TryExecuteUnsuccessful(0, returnData[0]);
} else {
revert UnsupportedExecType(execType);
}
returnData = _handleSingleExecutionAndReturnData(executionCalldata, execType);
} else if (callType == CALLTYPE_BATCH) {
// destructure executionCallData according to batched exec
Execution[] calldata executions = executionCalldata.decodeBatch();
// check if execType is revert or try
if (execType == EXECTYPE_DEFAULT) returnData = _executeBatch(executions);
else if (execType == EXECTYPE_TRY) returnData = _tryExecuteBatch(executions);
else revert UnsupportedExecType(execType);
returnData = _handleBatchExecutionAndReturnData(executionCalldata, execType);
} else if (callType == CALLTYPE_DELEGATECALL) {
returnData = _handleDelegateCallExecutionAndReturnData(executionCalldata, execType);
} else {
revert UnsupportedCallType(callType);
}
Expand Down Expand Up @@ -278,7 +273,9 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra
(CallType callType, ExecType execType) = mode.decodeBasic();

// Return true if both the call type and execution type are supported.
return (callType == CALLTYPE_SINGLE || callType == CALLTYPE_BATCH) && (execType == EXECTYPE_DEFAULT || execType == EXECTYPE_TRY);
return
(callType == CALLTYPE_SINGLE || callType == CALLTYPE_BATCH || callType == CALLTYPE_DELEGATECALL) &&
(execType == EXECTYPE_DEFAULT || execType == EXECTYPE_TRY);
}

/// @notice Determines whether a module is installed on the smart account.
Expand Down Expand Up @@ -527,26 +524,6 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra
version = "1.0.0-beta";
}

/// @dev Executes a single transaction based on the specified execution type.
/// @param executionCalldata The calldata containing the transaction details (target address, value, and data).
/// @param execType The execution type, which can be DEFAULT (revert on failure) or TRY (return on failure).
function _handleSingleExecution(bytes calldata executionCalldata, ExecType execType) private {
(address target, uint256 value, bytes calldata callData) = executionCalldata.decodeSingle();
if (execType == EXECTYPE_DEFAULT) _execute(target, value, callData);
else if (execType == EXECTYPE_TRY) _tryExecute(target, value, callData);
else revert UnsupportedExecType(execType);
}

/// @dev Executes a batch of transactions based on the specified execution type.
/// @param executionCalldata The calldata for a batch of transactions.
/// @param execType The execution type, which can be DEFAULT (revert on failure) or TRY (return on failure).
function _handleBatchExecution(bytes calldata executionCalldata, ExecType execType) private {
Execution[] calldata executions = executionCalldata.decodeBatch();
if (execType == EXECTYPE_DEFAULT) _executeBatch(executions);
else if (execType == EXECTYPE_TRY) _tryExecuteBatch(executions);
else revert UnsupportedExecType(execType);
}

/// @dev For use in `_erc1271HashForIsValidSignatureViaNestedEIP712`,
function _typedDataSignFields() private view returns (bytes32 m) {
(
Expand Down
113 changes: 113 additions & 0 deletions contracts/base/ExecutionHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

import { Execution } from "../types/DataTypes.sol";
import { IExecutionHelperEventsAndErrors } from "../interfaces/base/IExecutionHelper.sol";
import { ExecType, EXECTYPE_DEFAULT, EXECTYPE_TRY } from "../lib/ModeLib.sol";
import { ExecLib } from "../lib/ExecLib.sol";

/// @title Nexus - ExecutionHelper
/// @notice Implements execution management within the Nexus suite, facilitating transaction execution strategies and
Expand All @@ -26,6 +28,8 @@
/// @author @zeroknots | Rhinestone.wtf | zeroknots.eth
/// Special thanks to the Solady team for foundational contributions: https://github.com/Vectorized/solady
contract ExecutionHelper is IExecutionHelperEventsAndErrors {
using ExecLib for bytes;

/// @notice Executes a call to a target address with specified value and data.
/// @param target The address to execute the call on.
/// @param value The amount of wei to send with the call.
Expand Down Expand Up @@ -95,4 +99,113 @@
if (!success) emit TryExecuteUnsuccessful(i, result[i]);
}
}

/// @dev Execute a delegatecall with `delegate` on this account.
function _executeDelegatecall(address delegate, bytes calldata callData) internal returns (bytes memory result) {

Check warning on line 104 in contracts/base/ExecutionHelper.sol

View check run for this annotation

Codecov / codecov/patch

contracts/base/ExecutionHelper.sol#L104

Added line #L104 was not covered by tests
/// @solidity memory-safe-assembly
assembly {
result := mload(0x40)

Check warning on line 107 in contracts/base/ExecutionHelper.sol

View check run for this annotation

Codecov / codecov/patch

contracts/base/ExecutionHelper.sol#L106-L107

Added lines #L106 - L107 were not covered by tests
calldatacopy(result, callData.offset, callData.length)
// Forwards the `data` to `delegate` via delegatecall.
if iszero(delegatecall(gas(), delegate, result, callData.length, codesize(), 0x00)) {
// Bubble up the revert if the call reverts.
returndatacopy(result, 0x00, returndatasize())
revert(result, returndatasize())
}
mstore(result, returndatasize()) // Store the length.
let o := add(result, 0x20)
returndatacopy(o, 0x00, returndatasize()) // Copy the returndata.
mstore(0x40, add(o, returndatasize())) // Allocate the memory.
}
}

/// @dev Execute a delegatecall with `delegate` on this account and catch reverts.
function _tryExecuteDelegatecall(address delegate, bytes calldata callData) internal returns (bool success, bytes memory result) {

Check warning on line 123 in contracts/base/ExecutionHelper.sol

View check run for this annotation

Codecov / codecov/patch

contracts/base/ExecutionHelper.sol#L123

Added line #L123 was not covered by tests
/// @solidity memory-safe-assembly
assembly {
result := mload(0x40)

Check warning on line 126 in contracts/base/ExecutionHelper.sol

View check run for this annotation

Codecov / codecov/patch

contracts/base/ExecutionHelper.sol#L125-L126

Added lines #L125 - L126 were not covered by tests
calldatacopy(result, callData.offset, callData.length)
// Forwards the `data` to `delegate` via delegatecall.
success := delegatecall(gas(), delegate, result, callData.length, codesize(), 0x00)

Check warning on line 129 in contracts/base/ExecutionHelper.sol

View check run for this annotation

Codecov / codecov/patch

contracts/base/ExecutionHelper.sol#L129

Added line #L129 was not covered by tests
mstore(result, returndatasize()) // Store the length.
let o := add(result, 0x20)
returndatacopy(o, 0x00, returndatasize()) // Copy the returndata.
mstore(0x40, add(o, returndatasize())) // Allocate the memory.
}
}

/// @dev Executes a single transaction based on the specified execution type.
/// @param executionCalldata The calldata containing the transaction details (target address, value, and data).
/// @param execType The execution type, which can be DEFAULT (revert on failure) or TRY (return on failure).
function _handleSingleExecution(bytes calldata executionCalldata, ExecType execType) internal {
(address target, uint256 value, bytes calldata callData) = executionCalldata.decodeSingle();
if (execType == EXECTYPE_DEFAULT) _execute(target, value, callData);
else if (execType == EXECTYPE_TRY) _tryExecute(target, value, callData);
else revert UnsupportedExecType(execType);
}

/// @dev Executes a batch of transactions based on the specified execution type.
/// @param executionCalldata The calldata for a batch of transactions.
/// @param execType The execution type, which can be DEFAULT (revert on failure) or TRY (return on failure).
function _handleBatchExecution(bytes calldata executionCalldata, ExecType execType) internal {
Execution[] calldata executions = executionCalldata.decodeBatch();
if (execType == EXECTYPE_DEFAULT) _executeBatch(executions);
else if (execType == EXECTYPE_TRY) _tryExecuteBatch(executions);
else revert UnsupportedExecType(execType);
}

/// @dev Executes a single transaction based on the specified execution type.
/// @param executionCalldata The calldata containing the transaction details (target address, value, and data).
/// @param execType The execution type, which can be DEFAULT (revert on failure) or TRY (return on failure).
function _handleDelegateCallExecution(bytes calldata executionCalldata, ExecType execType) internal {

Check warning on line 160 in contracts/base/ExecutionHelper.sol

View check run for this annotation

Codecov / codecov/patch

contracts/base/ExecutionHelper.sol#L160

Added line #L160 was not covered by tests
(address delegate, bytes calldata callData) = executionCalldata.decodeDelegateCall();
if (execType == EXECTYPE_DEFAULT) _executeDelegatecall(delegate, callData);
else if (execType == EXECTYPE_TRY) _tryExecuteDelegatecall(delegate, callData);
else revert UnsupportedExecType(execType);

Check warning on line 164 in contracts/base/ExecutionHelper.sol

View check run for this annotation

Codecov / codecov/patch

contracts/base/ExecutionHelper.sol#L164

Added line #L164 was not covered by tests
}

/// @dev Executes a single transaction based on the specified execution type.
/// @param executionCalldata The calldata containing the transaction details (target address, value, and data).
/// @param execType The execution type, which can be DEFAULT (revert on failure) or TRY (return on failure).
function _handleSingleExecutionAndReturnData(bytes calldata executionCalldata, ExecType execType) internal returns(bytes[] memory returnData) {
(address target, uint256 value, bytes calldata callData) = executionCalldata.decodeSingle();
returnData = new bytes[](1);
bool success;
// check if execType is revert(default) or try
if (execType == EXECTYPE_DEFAULT) {
returnData[0] = _execute(target, value, callData);
} else if (execType == EXECTYPE_TRY) {
(success, returnData[0]) = _tryExecute(target, value, callData);
if (!success) emit TryExecuteUnsuccessful(0, returnData[0]);
} else {
revert UnsupportedExecType(execType);
}
}

/// @dev Executes a batch of transactions based on the specified execution type.
/// @param executionCalldata The calldata for a batch of transactions.
/// @param execType The execution type, which can be DEFAULT (revert on failure) or TRY (return on failure).
function _handleBatchExecutionAndReturnData(bytes calldata executionCalldata, ExecType execType) internal returns(bytes[] memory returnData){
Execution[] calldata executions = executionCalldata.decodeBatch();
if (execType == EXECTYPE_DEFAULT) returnData = _executeBatch(executions);
else if (execType == EXECTYPE_TRY) returnData = _tryExecuteBatch(executions);
else revert UnsupportedExecType(execType);
}

/// @dev Executes a single transaction based on the specified execution type.
/// @param executionCalldata The calldata containing the transaction details (target address, value, and data).
/// @param execType The execution type, which can be DEFAULT (revert on failure) or TRY (return on failure).
function _handleDelegateCallExecutionAndReturnData(bytes calldata executionCalldata, ExecType execType) internal returns(bytes[] memory returnData) {

Check warning on line 198 in contracts/base/ExecutionHelper.sol

View check run for this annotation

Codecov / codecov/patch

contracts/base/ExecutionHelper.sol#L198

Added line #L198 was not covered by tests
(address delegate, bytes calldata callData) = executionCalldata.decodeDelegateCall();
returnData = new bytes[](1);
bool success;
if (execType == EXECTYPE_DEFAULT) {
returnData[0] = _executeDelegatecall(delegate, callData);
}
else if (execType == EXECTYPE_TRY) {
(success, returnData[0]) = _tryExecuteDelegatecall(delegate, callData);

Check warning on line 206 in contracts/base/ExecutionHelper.sol

View check run for this annotation

Codecov / codecov/patch

contracts/base/ExecutionHelper.sol#L206

Added line #L206 was not covered by tests
if (!success) emit TryDelegateCallUnsuccessful(0, returnData[0]);
}
else revert UnsupportedExecType(execType);

Check warning on line 209 in contracts/base/ExecutionHelper.sol

View check run for this annotation

Codecov / codecov/patch

contracts/base/ExecutionHelper.sol#L209

Added line #L209 was not covered by tests
}
}
6 changes: 1 addition & 5 deletions contracts/interfaces/INexusEventsAndErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pragma solidity ^0.8.26;
// Nexus: A suite of contracts for Modular Smart Accounts compliant with ERC-7579 and ERC-4337, developed by Biconomy.
// Learn more at https://biconomy.io. To report security issues, please contact us at: [email protected]

import { CallType, ExecType } from "../lib/ModeLib.sol";
import { CallType } from "../lib/ModeLib.sol";
import { PackedUserOperation } from "account-abstraction/contracts/interfaces/PackedUserOperation.sol";

/// @title Nexus - INexus Events and Errors
Expand All @@ -36,10 +36,6 @@ interface INexusEventsAndErrors {
/// @param callType The unsupported call type.
error UnsupportedCallType(CallType callType);

/// @notice Error thrown when an execution with an unsupported ExecType was made.
/// @param execType The unsupported execution type.
error UnsupportedExecType(ExecType execType);

/// @notice Error thrown on failed execution.
error ExecutionFailed();

Expand Down
10 changes: 10 additions & 0 deletions contracts/interfaces/base/IExecutionHelperEventsAndErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,17 @@ pragma solidity ^0.8.26;
/// @author @filmakarov | Biconomy | [email protected]
/// @author @zeroknots | Rhinestone.wtf | zeroknots.eth
/// Special thanks to the Solady team for foundational contributions: https://github.com/Vectorized/solady

import { ExecType } from "../../lib/ModeLib.sol";

interface IExecutionHelperEventsAndErrors {
/// @notice Event emitted when a transaction fails to execute successfully.
event TryExecuteUnsuccessful(uint256 batchExecutionindex, bytes result);

/// @notice Event emitted when a transaction fails to execute successfully.
event TryDelegateCallUnsuccessful(uint256 batchExecutionindex, bytes result);

/// @notice Error thrown when an execution with an unsupported ExecType was made.
/// @param execType The unsupported execution type.
error UnsupportedExecType(ExecType execType);
}
6 changes: 6 additions & 0 deletions contracts/lib/ExecLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ library ExecLib {
callData = executionCalldata[52:];
}

function decodeDelegateCall(bytes calldata executionCalldata) internal pure returns (address delegate, bytes calldata callData) {
// destructure executionCallData according to single exec
delegate = address(uint160(bytes20(executionCalldata[0:20])));
callData = executionCalldata[20:];
}

function encodeSingle(address target, uint256 value, bytes memory callData) internal pure returns (bytes memory userOpCalldata) {
userOpCalldata = abi.encodePacked(target, value, callData);
}
Expand Down
8 changes: 8 additions & 0 deletions contracts/mocks/MockDelegateTarget.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract MockDelegateTarget {
function sendValue(address target, uint256 _value) public {
target.call{ value: _value }("");
}
}
30 changes: 29 additions & 1 deletion contracts/mocks/MockExecutor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,19 @@ import { IModule } from "contracts/interfaces/modules/IModule.sol";
import { EncodedModuleTypes } from "contracts/lib/ModuleTypeLib.sol";
import { INexus } from "contracts/interfaces/INexus.sol";
import { MODULE_TYPE_EXECUTOR } from "contracts/types/Constants.sol";
import { ModeLib, ExecutionMode, ExecType, CallType, CALLTYPE_BATCH, CALLTYPE_SINGLE, EXECTYPE_DEFAULT, EXECTYPE_TRY } from "contracts/lib/ModeLib.sol";
import {
ModeLib,
ExecutionMode,
ExecType,
CallType,
CALLTYPE_BATCH,
CALLTYPE_SINGLE,
CALLTYPE_DELEGATECALL,
EXECTYPE_DEFAULT,
EXECTYPE_TRY
} from "contracts/lib/ModeLib.sol";
import { ExecLib } from "contracts/lib/ExecLib.sol";
import { MODE_DEFAULT, ModePayload } from "contracts/lib/ModeLib.sol";

import { IExecutor } from "../../contracts/interfaces/modules/IExecutor.sol";
import "../../contracts/types/DataTypes.sol";
Expand All @@ -27,6 +38,21 @@ contract MockExecutor is IExecutor {
return account.executeFromExecutor(ModeLib.encodeSimpleSingle(), ExecLib.encodeSingle(target, value, callData));
}

function execDelegatecall(
INexus account,
bytes calldata callData
)
external
returns (bytes[] memory returnData)
{
return account.executeFromExecutor(
ModeLib.encode(
CALLTYPE_DELEGATECALL, EXECTYPE_DEFAULT, MODE_DEFAULT, ModePayload.wrap(0x00)
),
callData
);
}

function executeBatchViaAccount(INexus account, Execution[] calldata execs) external returns (bytes[] memory returnData) {
return account.executeFromExecutor(ModeLib.encodeSimpleBatch(), ExecLib.encodeBatch(execs));
}
Expand Down Expand Up @@ -72,4 +98,6 @@ contract MockExecutor is IExecutor {
function isInitialized(address) external pure override returns (bool) {
return false;
}

receive() external payable {}
}
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
[fmt]
bracket_spacing = true
int_types = "long"
line_length = 150
line_length = 160
multiline_func_header = "all"
number_underscore = "thousands"
quote_style = "double"
Expand Down
5 changes: 5 additions & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ const config: HardhatUserConfig = {
},
},
},
networks: {
hardhat: {
allowUnlimitedContractSize: true
},
},
docgen: {
projectName: "Nexus",
projectDescription: "Nexus - Biconomy Modular Smart Account - ERC-7579",
Expand Down
Loading
Loading