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
4 changes: 2 additions & 2 deletions .solhint.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
"imports-on-top": "error",
"ordering": "error",
"visibility-modifier-order": "error",
"code-complexity": ["error", 9],
"code-complexity": ["error", 11],
"function-max-lines": ["error", 80],
"max-line-length": ["error", 150],
"max-line-length": ["error", 170],
filmakarov marked this conversation as resolved.
Show resolved Hide resolved
"no-empty-blocks": "off",
"no-unused-vars": "error",
"payable-fallback": "off",
Expand Down
27 changes: 25 additions & 2 deletions contracts/Nexus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import { ExecutionHelper } from "./base/ExecutionHelper.sol";
import { IValidator } from "./interfaces/modules/IValidator.sol";
import { MODULE_TYPE_VALIDATOR, MODULE_TYPE_EXECUTOR, MODULE_TYPE_FALLBACK, MODULE_TYPE_HOOK, 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";

/// @title Nexus - Smart Account
/// @notice This contract integrates various functionalities to handle modular smart accounts compliant with ERC-7579 and ERC-4337 standards.
Expand Down Expand Up @@ -102,6 +102,8 @@
_handleSingleExecution(executionCalldata, execType);
} else if (callType == CALLTYPE_BATCH) {
_handleBatchExecution(executionCalldata, execType);
} else if (callType == CALLTYPE_DELEGATECALL) {
_handleDelegateCallExecution(executionCalldata, execType);

Check warning on line 106 in contracts/Nexus.sol

View check run for this annotation

Codecov / codecov/patch

contracts/Nexus.sol#L106

Added line #L106 was not covered by tests
} else {
revert UnsupportedCallType(callType);
}
Expand Down Expand Up @@ -140,6 +142,14 @@
if (execType == EXECTYPE_DEFAULT) returnData = _executeBatch(executions);
else if (execType == EXECTYPE_TRY) returnData = _tryExecuteBatch(executions);
else revert UnsupportedExecType(execType);
} else if (callType == CALLTYPE_DELEGATECALL) {
// destructure executionCallData according to single exec
address delegate = address(uint160(bytes20(executionCalldata[0:20])));
bytes calldata callData = executionCalldata[20:];

Check warning on line 148 in contracts/Nexus.sol

View check run for this annotation

Codecov / codecov/patch

contracts/Nexus.sol#L147-L148

Added lines #L147 - L148 were not covered by tests
filmakarov marked this conversation as resolved.
Show resolved Hide resolved
// check if execType is revert or try
if (execType == EXECTYPE_DEFAULT) _executeDelegatecall(delegate, callData);
else if (execType == EXECTYPE_TRY) _tryExecuteDelegatecall(delegate, callData);
else revert UnsupportedExecType(execType);

Check warning on line 152 in contracts/Nexus.sol

View check run for this annotation

Codecov / codecov/patch

contracts/Nexus.sol#L152

Added line #L152 was not covered by tests
filmakarov marked this conversation as resolved.
Show resolved Hide resolved
} else {
revert UnsupportedCallType(callType);
}
Expand Down Expand Up @@ -282,7 +292,9 @@
(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 @@ -551,6 +563,17 @@
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) private {
address delegate = address(uint160(bytes20(executionCalldata[0:20])));
bytes calldata callData = executionCalldata[20:];

Check warning on line 571 in contracts/Nexus.sol

View check run for this annotation

Codecov / codecov/patch

contracts/Nexus.sol#L569-L571

Added lines #L569 - L571 were not covered by tests
filmakarov marked this conversation as resolved.
Show resolved Hide resolved
if (execType == EXECTYPE_DEFAULT) _executeDelegatecall(delegate, callData);
else if (execType == EXECTYPE_TRY) _tryExecuteDelegatecall(delegate, callData);
else revert UnsupportedExecType(execType);

Check warning on line 574 in contracts/Nexus.sol

View check run for this annotation

Codecov / codecov/patch

contracts/Nexus.sol#L574

Added line #L574 was not covered by tests
}

/// @notice Checks if a module is installed on the smart account.
/// @param moduleTypeId The module type ID.
/// @param module The module address.
Expand Down
34 changes: 34 additions & 0 deletions contracts/base/ExecutionHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,38 @@
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 100 in contracts/base/ExecutionHelper.sol

View check run for this annotation

Codecov / codecov/patch

contracts/base/ExecutionHelper.sol#L100

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

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

View check run for this annotation

Codecov / codecov/patch

contracts/base/ExecutionHelper.sol#L102-L103

Added lines #L102 - L103 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 119 in contracts/base/ExecutionHelper.sol

View check run for this annotation

Codecov / codecov/patch

contracts/base/ExecutionHelper.sol#L119

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

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

View check run for this annotation

Codecov / codecov/patch

contracts/base/ExecutionHelper.sol#L121-L122

Added lines #L121 - L122 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 125 in contracts/base/ExecutionHelper.sol

View check run for this annotation

Codecov / codecov/patch

contracts/base/ExecutionHelper.sol#L125

Added line #L125 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.
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,26 +150,6 @@ contract TestAccountExecution_ExecuteFromExecutor is TestAccountExecution_Base {
assertEq(balanceRecipient, amount, "Recipient should have received 0 tokens");
}

/// @notice Tests execution with an unsupported call type via MockExecutor
function test_RevertIf_ExecuteFromExecutor_UnsupportedCallType() public {
ExecutionMode unsupportedMode = ExecutionMode.wrap(bytes32(abi.encodePacked(bytes1(0xff), bytes1(0x00), bytes4(0), bytes22(0))));
bytes memory executionCalldata = abi.encodePacked(address(counter), uint256(0), abi.encodeWithSelector(Counter.incrementNumber.selector));

(CallType callType, , , ) = ModeLib.decode(unsupportedMode);
Execution[] memory execution = new Execution[](1);
execution[0] = Execution(address(mockExecutor), 0, executionCalldata);

vm.expectRevert(abi.encodeWithSelector(UnsupportedCallType.selector, callType));

mockExecutor.customExecuteViaAccount(
unsupportedMode,
BOB_ACCOUNT,
address(counter),
0,
abi.encodeWithSelector(Counter.incrementNumber.selector)
);
}

livingrockrises marked this conversation as resolved.
Show resolved Hide resolved
/// @notice Tests single execution with an unsupported execution type via MockExecutor
function test_RevertIf_ExecuteFromExecutor_UnsupportedExecType_Single() public {
// Create an unsupported execution mode with an invalid execution type
Expand Down
14 changes: 7 additions & 7 deletions test/hardhat/smart-account/Nexus.Basics.specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
CALLTYPE_BATCH,
CALLTYPE_SINGLE,
EXECTYPE_DEFAULT,
EXECTYPE_DELEGATE,
CALLTYPE_DELEGATE,
EXECTYPE_TRY,
MODE_DEFAULT,
MODE_PAYLOAD,
Expand Down Expand Up @@ -247,29 +247,29 @@ describe("Nexus Basic Specs", function () {
expect(
await smartAccount.supportsExecutionMode(
ethers.concat([
ethers.zeroPadValue(toBeHex(EXECTYPE_DELEGATE), 1),
ethers.zeroPadValue(toBeHex(CALLTYPE_SINGLE), 1),
ethers.zeroPadValue(toBeHex(CALLTYPE_DELEGATE), 1),
ethers.zeroPadValue(toBeHex(EXECTYPE_DEFAULT), 1),
ethers.zeroPadValue(toBeHex(UNUSED), 4),
ethers.zeroPadValue(toBeHex(MODE_DEFAULT), 4),
ethers.zeroPadValue(toBeHex(MODE_PAYLOAD), 22),
]),
),
).to.be.false;
).to.be.true;
});

it("Should verify unsupported execution modes", async function () {
// Checks support for predefined module types (e.g., Validation, Execution)
expect(
await smartAccount.supportsExecutionMode(
ethers.concat([
ethers.zeroPadValue(toBeHex(EXECTYPE_DELEGATE), 1),
ethers.zeroPadValue(toBeHex(CALLTYPE_SINGLE), 1),
ethers.zeroPadValue(toBeHex(CALLTYPE_DELEGATE), 1),
ethers.zeroPadValue(toBeHex(EXECTYPE_DEFAULT), 1),
ethers.zeroPadValue(toBeHex(UNUSED), 4),
ethers.zeroPadValue(toBeHex("0x00"), 4),
ethers.zeroPadValue(toBeHex(MODE_PAYLOAD), 22),
]),
),
).to.be.false;
).to.be.true;
});

it("Should return false for unsupported execution mode", async function () {
Expand Down
2 changes: 1 addition & 1 deletion test/hardhat/utils/erc7579Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const CALLTYPE_SINGLE = "0x00"; // 1 byte
export const CALLTYPE_BATCH = "0x01"; // 1 byte
export const EXECTYPE_DEFAULT = "0x00"; // 1 byte
export const EXECTYPE_TRY = "0x01"; // 1 byte
export const EXECTYPE_DELEGATE = "0xFF"; // 1 byte
export const CALLTYPE_DELEGATE = "0xFF"; // 1 byte
export const MODE_DEFAULT = "0x00000000"; // 4 bytes
export const UNUSED = "0x00000000"; // 4 bytes
export const MODE_PAYLOAD = "0x00000000000000000000000000000000000000000000"; // 22 bytes
Expand Down
Loading