Skip to content

Commit

Permalink
Merge pull request #205 from bcnmy/release/nexus-1.0.0
Browse files Browse the repository at this point in the history
Release/nexus 1.0.0
  • Loading branch information
livingrockrises authored Oct 24, 2024
2 parents 04e88dc + 9a137ae commit fd4db09
Show file tree
Hide file tree
Showing 24 changed files with 170 additions and 91 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ jobs:
uses: crytic/slither-action@v0.4.0
id: slither
with:
slither-version: "0.10.0"
slither-version: "0.10.1"
node-version: "22"
fail-on: "none"
slither-args: '--solc-args="--evm-version cancun" --exclude "assembly|solc-version|low-level-calls|naming-convention|controlled-delegatecall|write-after-write|divide-before-multiply|incorrect-shift" --exclude-informational --exclude-low --filter-paths "contracts/mock|node_modules" --checklist --markdown-root ${{ github.server_url }}/${{ github.repository }}/blob/${{ github.sha }}/contracts/'
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

## [1.0.0] - 2024-10-14
Code Freeze post audit remediations.

## [1.0.0-beta.1] - 2024-09-30

## [1.0.0-beta] - 2024-06-04
Expand Down
Binary file not shown.
2 changes: 1 addition & 1 deletion contracts/Nexus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,6 @@ contract Nexus is INexus, BaseAccount, ExecutionHelper, ModuleManager, UUPSUpgra
/// @dev EIP712 domain name and version.
function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) {
name = "Nexus";
version = "1.0.0-beta.1";
version = "1.0.0";
}
}
2 changes: 1 addition & 1 deletion contracts/base/BaseAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { IBaseAccount } from "../interfaces/base/IBaseAccount.sol";
/// Special thanks to the Solady team for foundational contributions: https://github.com/Vectorized/solady
contract BaseAccount is IBaseAccount {
/// @notice Identifier for this implementation on the network
string internal constant _ACCOUNT_IMPLEMENTATION_ID = "biconomy.nexus.1.0.0-beta.1";
string internal constant _ACCOUNT_IMPLEMENTATION_ID = "biconomy.nexus.1.0.0";

/// @notice The canonical address for the ERC4337 EntryPoint contract, version 0.7.
/// This address is consistent across all supported networks.
Expand Down
52 changes: 47 additions & 5 deletions contracts/base/ExecutionHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ contract ExecutionHelper is IExecutionHelperEventsAndErrors {
using ExecLib for bytes;

/// @notice Executes a call to a target address with specified value and data.
/// @dev calls to an EOA should be counted as successful.
/// @notice calls to an EOA should be counted as successful.
/// @param target The address to execute the call on.
/// @param value The amount of wei to send with the call.
/// @param callData The calldata to send.
Expand All @@ -53,9 +53,25 @@ contract ExecutionHelper is IExecutionHelperEventsAndErrors {
}
}

/// @notice Executes a call to a target address with specified value and data.
/// Same as _execute but without return data for gas optimization.
function _executeNoReturndata(address target, uint256 value, bytes calldata callData) internal virtual {
/// @solidity memory-safe-assembly
assembly {
let result := mload(0x40)
calldatacopy(result, callData.offset, callData.length)
if iszero(call(gas(), target, value, result, callData.length, codesize(), 0x00)) {
// Bubble up the revert if the call reverts.
returndatacopy(result, 0x00, returndatasize())
revert(result, returndatasize())
}
mstore(0x40, add(result, callData.length)) //allocate memory
}
}

/// @notice Tries to execute a call and captures if it was successful or not.
/// @dev Similar to _execute but returns a success boolean and catches reverts instead of propagating them.
/// @dev calls to an EOA should be counted as successful.
/// @notice calls to an EOA should be counted as successful.
/// @param target The address to execute the call on.
/// @param value The amount of wei to send with the call.
/// @param callData The calldata to send.
Expand Down Expand Up @@ -87,6 +103,16 @@ contract ExecutionHelper is IExecutionHelperEventsAndErrors {
}
}

/// @notice Executes a batch of calls without returning the result.
/// @param executions An array of Execution structs each containing target, value, and calldata.
function _executeBatchNoReturndata(Execution[] calldata executions) internal {
Execution calldata exec;
for (uint256 i; i < executions.length; i++) {
exec = executions[i];
_executeNoReturndata(exec.target, exec.value, exec.callData);
}
}

/// @notice Tries to execute a batch of calls and emits an event for each unsuccessful call.
/// @param executions An array of Execution structs.
/// @return result An array of bytes returned from each executed call, with unsuccessful calls marked by events.
Expand Down Expand Up @@ -122,6 +148,22 @@ contract ExecutionHelper is IExecutionHelperEventsAndErrors {
}
}

/// @dev Execute a delegatecall with `delegate` on this account.
/// Same as _executeDelegatecall but without return data for gas optimization.
function _executeDelegatecallNoReturndata(address delegate, bytes calldata callData) internal {
/// @solidity memory-safe-assembly
assembly {
let result := mload(0x40)
calldatacopy(result, callData.offset, callData.length)
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(0x40, add(result, callData.length)) //allocate memory
}
}

/// @dev Execute a delegatecall with `delegate` on this account and catch reverts.
/// @return success True if the delegatecall was successful, false otherwise.
/// @return result The bytes returned from the delegatecall, which contains the returned data from the delegate contract.
Expand All @@ -144,7 +186,7 @@ contract ExecutionHelper is IExecutionHelperEventsAndErrors {
/// @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);
if (execType == EXECTYPE_DEFAULT) _executeNoReturndata(target, value, callData);
else if (execType == EXECTYPE_TRY) {
(bool success, bytes memory result) = _tryExecute(target, value, callData);
if (!success) emit TryExecuteUnsuccessful(callData, result);
Expand All @@ -156,7 +198,7 @@ contract ExecutionHelper is IExecutionHelperEventsAndErrors {
/// @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);
if (execType == EXECTYPE_DEFAULT) _executeBatchNoReturndata(executions);
else if (execType == EXECTYPE_TRY) _tryExecuteBatch(executions);
else revert UnsupportedExecType(execType);
}
Expand All @@ -166,7 +208,7 @@ contract ExecutionHelper is IExecutionHelperEventsAndErrors {
/// @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);
if (execType == EXECTYPE_DEFAULT) _executeDelegatecallNoReturndata(delegate, callData);
else if (execType == EXECTYPE_TRY) {
(bool success, bytes memory result) = _tryExecuteDelegatecall(delegate, callData);
if (!success) emit TryDelegateCallUnsuccessful(callData, result);
Expand Down
2 changes: 1 addition & 1 deletion contracts/base/ModuleManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ abstract contract ModuleManager is Storage, EIP712, IModuleManagerEventsAndError
// Perform the removal first
validators.pop(prev, validator);

// Sentinel pointing to itself means the list is empty, so check this after removal
// Sentinel pointing to itself / zero means the list is empty / uninitialized, so check this after removal
// Below error is very specific to uninstalling validators.
require(_hasValidators(), CanNotRemoveLastValidator());
validator.excessivelySafeCall(gasleft(), 0, 0, abi.encodeWithSelector(IModule.onUninstall.selector, disableModuleData));
Expand Down
30 changes: 20 additions & 10 deletions contracts/modules/validators/K1Validator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@ pragma solidity ^0.8.27;
// 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: security@biconomy.io

import { SignatureCheckerLib } from "solady/utils/SignatureCheckerLib.sol";
import { ECDSA } from "solady/utils/ECDSA.sol";
import { PackedUserOperation } from "account-abstraction/interfaces/PackedUserOperation.sol";
import { ERC7739Validator } from "../../base/ERC7739Validator.sol";
import { IValidator } from "../../interfaces/modules/IValidator.sol";
import { EnumerableSet } from "../../lib/EnumerableSet4337.sol";
import { MODULE_TYPE_VALIDATOR, VALIDATION_SUCCESS, VALIDATION_FAILED } from "../../types/Constants.sol";
import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

/// @title Nexus - K1Validator (ECDSA)
/// @notice Validator module for smart accounts, verifying user operation signatures
Expand All @@ -33,7 +32,7 @@ import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/Mes
/// @author @zeroknots | Rhinestone.wtf | zeroknots.eth
/// Special thanks to the Solady team for foundational contributions: https://github.com/Vectorized/solady
contract K1Validator is IValidator, ERC7739Validator {
using SignatureCheckerLib for address;
using ECDSA for bytes32;
using EnumerableSet for EnumerableSet.AddressSet;

/*//////////////////////////////////////////////////////////////////////////
Expand All @@ -57,6 +56,9 @@ contract K1Validator is IValidator, ERC7739Validator {
/// @notice Error to indicate that the new owner cannot be a contract address
error NewOwnerIsContract();

/// @notice Error to indicate that the owner cannot be the zero address
error OwnerCannotBeZeroAddress();

/// @notice Error to indicate that the data length is invalid
error InvalidDataLength();

Expand All @@ -73,6 +75,7 @@ contract K1Validator is IValidator, ERC7739Validator {
require(data.length != 0, NoOwnerProvided());
require(!_isInitialized(msg.sender), ModuleAlreadyInitialized());
address newOwner = address(bytes20(data[:20]));
require(newOwner != address(0), OwnerCannotBeZeroAddress());
require(!_isContract(newOwner), NewOwnerIsContract());
smartAccountOwners[msg.sender] = newOwner;
if (data.length > 20) {
Expand Down Expand Up @@ -185,7 +188,7 @@ contract K1Validator is IValidator, ERC7739Validator {
/// @notice Returns the version of the module
/// @return The version of the module
function version() external pure returns (string memory) {
return "1.0.0-beta.1";
return "1.0.0";
}

/// @notice Checks if the module is of the specified type
Expand All @@ -199,6 +202,15 @@ contract K1Validator is IValidator, ERC7739Validator {
INTERNAL
//////////////////////////////////////////////////////////////////////////*/

/// @notice Recovers the signer from a signature
/// @param hash The hash of the data to validate
/// @param signature The signature data
/// @return The recovered signer address
/// @notice tryRecover returns address(0) on invalid signature
function _recoverSigner(bytes32 hash, bytes calldata signature) internal view returns (address) {
return hash.tryRecover(signature);
}

/// @dev Returns whether the `hash` and `signature` are valid.
/// Obtains the authorized signer's credentials and calls some
/// module's specific internal function to validate the signature
Expand Down Expand Up @@ -236,12 +248,10 @@ contract K1Validator is IValidator, ERC7739Validator {
return false;
}

if (SignatureCheckerLib.isValidSignatureNowCalldata(owner, hash, signature)) {
return true;
}
if (SignatureCheckerLib.isValidSignatureNowCalldata(owner, MessageHashUtils.toEthSignedMessageHash(hash), signature)) {
return true;
}
// verify signer
// owner can not be zero address in this contract
if (_recoverSigner(hash, signature) == owner) return true;
if (_recoverSigner(hash.toEthSignedMessageHash(), signature) == owner) return true;
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion contracts/utils/NexusBootstrap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,6 @@ contract NexusBootstrap is ModuleManager {
/// @dev EIP712 domain name and version.
function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) {
name = "NexusBootstrap";
version = "1.0.0-beta.1";
version = "1.0.0";
}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"name": "nexus",
"description": "Nexus - ERC7579 Modular Smart Account",
"version": "1.0.0-beta.1",
"version": "1.0.0",
"author": {
"name": "Biconomy",
"url": "https://github.com/bcnmy"
},
"dependencies": {
"@openzeppelin": "https://github.com/OpenZeppelin/openzeppelin-contracts/",
"@openzeppelin": "https://github.com/OpenZeppelin/openzeppelin-contracts",
"dotenv": "^16.4.5",
"solarray": "github:sablier-labs/solarray",
"viem": "2.7.13"
Expand Down
32 changes: 25 additions & 7 deletions scripts/foundry/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,33 @@ pragma solidity >=0.8.0 <0.9.0;
import { Nexus } from "../../contracts/Nexus.sol";

import { BaseScript } from "./Base.s.sol";
import { K1ValidatorFactory } from "../../contracts/factory/K1ValidatorFactory.sol";
import { K1Validator } from "../../contracts/modules/validators/K1Validator.sol";
import { BootstrapLib } from "../../contracts/lib/BootstrapLib.sol";
import { NexusBootstrap } from "../../contracts/utils/NexusBootstrap.sol";
import { MockRegistry } from "../../contracts/mocks/MockRegistry.sol";
import { HelperConfig } from "./HelperConfig.s.sol";

/// @dev See the Solidity Scripting tutorial: https://book.getfoundry.sh/tutorials/solidity-scripting
contract Deploy is BaseScript {
address private constant _ENTRYPOINT = 0x0000000071727De22E5E9d8BAf0edAc6f37da032;
function run() public broadcast returns (Nexus smartAccount) {
smartAccount = new Nexus(_ENTRYPOINT);
}
K1ValidatorFactory private k1ValidatorFactory;
K1Validator private k1Validator;
NexusBootstrap private bootstrapper;
MockRegistry private registry;
HelperConfig private helperConfig;

function test() public {
// This is a test function to exclude this script from the coverage report.
function run() public broadcast returns (Nexus smartAccount) {
helperConfig = new HelperConfig();
require(address(helperConfig.ENTRYPOINT()) != address(0), "ENTRYPOINT is not set");
smartAccount = new Nexus(address(helperConfig.ENTRYPOINT()));
k1Validator = new K1Validator();
bootstrapper = new NexusBootstrap();
registry = new MockRegistry();
k1ValidatorFactory = new K1ValidatorFactory(
address(smartAccount),
msg.sender,
address(k1Validator),
bootstrapper,
registry
);
}
}
31 changes: 31 additions & 0 deletions scripts/foundry/HelperConfig.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0 <0.9.0;
pragma solidity >=0.8.0 <0.9.0;

import { EntryPoint } from "account-abstraction/core/EntryPoint.sol";
import { IEntryPoint } from "account-abstraction/interfaces/IEntryPoint.sol";

import {Script} from "forge-std/Script.sol";

contract HelperConfig is Script {
IEntryPoint public ENTRYPOINT;
address private constant MAINNET_ENTRYPOINT_ADDRESS = 0x0000000071727De22E5E9d8BAf0edAc6f37da032;

constructor() {
if (block.chainid == 31337) {
setupAnvilConfig();
} else {
ENTRYPOINT = IEntryPoint(MAINNET_ENTRYPOINT_ADDRESS);
}
}

function setupAnvilConfig() public {
if(address(ENTRYPOINT) != address(0)){
return;
}
ENTRYPOINT = new EntryPoint();
vm.etch(address(MAINNET_ENTRYPOINT_ADDRESS), address(ENTRYPOINT).code);
ENTRYPOINT = IEntryPoint(MAINNET_ENTRYPOINT_ADDRESS);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ contract ArbitrumSmartAccountUpgradeTest is NexusTest_Base, ArbitrumSettings {
/// @notice Validates the account ID after the upgrade process.
function test_AccountIdValidationAfterUpgrade() public {
test_UpgradeV2ToV3AndInitialize();
string memory expectedAccountId = "biconomy.nexus.1.0.0-beta.1";
string memory expectedAccountId = "biconomy.nexus.1.0.0";
string memory actualAccountId = IAccountConfig(payable(address(smartAccountV2))).accountId();
assertEq(actualAccountId, expectedAccountId, "Account ID does not match after upgrade.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ contract TestAccountConfig_AccountId is Test {

/// @notice Tests if the account ID returns the expected value
function test_WhenCheckingTheAccountID() external givenTheAccountConfiguration {
string memory expected = "biconomy.nexus.1.0.0-beta.1";
string memory expected = "biconomy.nexus.1.0.0";
assertEq(accountConfig.accountId(), expected, "AccountConfig should return the expected account ID.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ contract TestAccountFactory_Deployments is NexusTest_Base {
userOps[0] = buildUserOpWithInitAndCalldata(user, initCode, "", address(VALIDATOR_MODULE));
ENTRYPOINT.depositTo{ value: 1 ether }(address(accountAddress));
ENTRYPOINT.handleOps(userOps, payable(user.addr));
assertEq(IAccountConfig(accountAddress).accountId(), "biconomy.nexus.1.0.0-beta.1", "Not deployed properly");
assertEq(IAccountConfig(accountAddress).accountId(), "biconomy.nexus.1.0.0", "Not deployed properly");
}

/// @notice Tests that deploying an account fails if it already exists.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ contract TestNexusAccountFactory_Deployments is NexusTest_Base {
userOps[0] = buildUserOpWithInitAndCalldata(user, initCode, "", address(VALIDATOR_MODULE));
ENTRYPOINT.depositTo{ value: 1 ether }(address(accountAddress));
ENTRYPOINT.handleOps(userOps, payable(user.addr));
assertEq(IAccountConfig(accountAddress).accountId(), "biconomy.nexus.1.0.0-beta.1", "Not deployed properly");
assertEq(IAccountConfig(accountAddress).accountId(), "biconomy.nexus.1.0.0", "Not deployed properly");
}

/// @notice Tests that deploying an account fails if it already exists.
Expand Down
5 changes: 3 additions & 2 deletions test/foundry/unit/concrete/gas/TestGas_ExecutionHelper.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ contract TestGas_ExecutionHelper is TestAccountExecution_Base {
0
);
ENTRYPOINT.handleOps(userOpsInstall, payable(address(BOB.addr)));
assertTrue(BOB_ACCOUNT.isModuleInstalled(MODULE_TYPE_EXECUTOR, address(mockExecutor), ""), "MockExecutor should be installed");
}

// Execute Tests
Expand Down Expand Up @@ -83,11 +84,11 @@ contract TestGas_ExecutionHelper is TestAccountExecution_Base {

// ExecuteFromExecutor Tests
function test_Gas_ExecuteFromExecutor_Single() public {
prank(address(mockExecutor));

vm.startPrank(address(mockExecutor));
uint256 initialGas = gasleft();
BOB_ACCOUNT.executeFromExecutor(ModeLib.encodeSimpleSingle(), ExecLib.encodeSingle(address(0), 0, ""));
uint256 gasUsed = initialGas - gasleft();
vm.stopPrank();
console.log("Gas used for single empty execution from executor: ", gasUsed);
}

Expand Down
Loading

0 comments on commit fd4db09

Please sign in to comment.