Skip to content

Commit

Permalink
Merge pull request #108 from bcnmy/feat/add-delegate-call-type
Browse files Browse the repository at this point in the history
feat:add support for delegate calltype
  • Loading branch information
livingrockrises committed Jul 7, 2024
2 parents 5902a68 + 48176cb commit f6313e6
Show file tree
Hide file tree
Showing 17 changed files with 492 additions and 227 deletions.
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 @@ pragma solidity ^0.8.26;

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 @@ import { IExecutionHelperEventsAndErrors } from "../interfaces/base/IExecutionHe
/// @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 @@ contract ExecutionHelper is IExecutionHelperEventsAndErrors {
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) {
/// @solidity memory-safe-assembly
assembly {
result := mload(0x40)
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) {
/// @solidity memory-safe-assembly
assembly {
result := mload(0x40)
calldatacopy(result, callData.offset, callData.length)
// Forwards the `data` to `delegate` via delegatecall.
success := delegatecall(gas(), delegate, result, callData.length, codesize(), 0x00)
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 {
(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);
}

/// @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) {
(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);
if (!success) emit TryDelegateCallUnsuccessful(0, returnData[0]);
}
else revert UnsupportedExecType(execType);
}
}
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

0 comments on commit f6313e6

Please sign in to comment.