HardhatFoundry
30,000 USDC
View results
Submission Details
Severity: high
Invalid

Freezing of user funds in factory contracts when `alreadyDeployed` is `true`

Summary

When a user tries to create a Nexus account that has already been deployed, the initial ETH deposit will get stuck in the respective factory contract. Attackers can leverage this to perform griefing attacks on users.

Vulnerability Details

A user can create a new Nexus account using the createAccount() function in NexusAccountFactory.sol, RegistryFactory.sol and K1ValidatorFactory.sol.

For example, let's look at the NexusAccountFactory.sol contract's createAccount() function :

function createAccount(bytes calldata initData, bytes32 salt) external payable override returns (address payable) {
// Compute the actual salt for deterministic deployment
bytes32 actualSalt;
assembly {
let ptr := mload(0x40)
let calldataLength := sub(calldatasize(), 0x04)
mstore(0x40, add(ptr, calldataLength))
calldatacopy(ptr, 0x04, calldataLength)
actualSalt := keccak256(ptr, calldataLength)
}
// Deploy the account using the deterministic address
(bool alreadyDeployed, address account) = LibClone.createDeterministicERC1967(msg.value, ACCOUNT_IMPLEMENTATION, actualSalt);
if (!alreadyDeployed) {
INexus(account).initializeAccount(initData);
emit AccountCreated(account, initData, salt);
}
return payable(account);
}

Here, the salt used for the create2 opcode in LibClone.createDeterministicERC1967() is constructed by user-controlled call data.

function createDeterministicERC1967(uint256 value, address implementation, bytes32 salt)
internal
returns (bool alreadyDeployed, address instance)
{
/// @solidity memory-safe-assembly
assembly {
let m := mload(0x40) // Cache the free memory pointer.
mstore(0x60, 0xcc3735a920a3ca505d382bbc545af43d6000803e6038573d6000fd5b3d6000f3)
mstore(0x40, 0x5155f3363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076)
mstore(0x20, 0x6009)
mstore(0x1e, implementation)
mstore(0x0a, 0x603d3d8160223d3973)
// Compute and store the bytecode hash.
mstore(add(m, 0x35), keccak256(0x21, 0x5f))
mstore(m, shl(88, address()))
mstore8(m, 0xff) // Write the prefix.
mstore(add(m, 0x15), salt)
instance := keccak256(m, 0x55)
for {} 1 {} {
if iszero(extcodesize(instance)) {
instance := create2(value, 0x21, 0x5f, salt)
if iszero(instance) {
mstore(0x00, 0x30116425) // `DeploymentFailed()`.
revert(0x1c, 0x04)
}
break
}
alreadyDeployed := 1
if iszero(value) { break }
if iszero(call(gas(), instance, value, codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
revert(0x1c, 0x04)
}
break
}
mstore(0x40, m) // Restore the free memory pointer.
mstore(0x60, 0) // Restore the zero slot.
}
}

This means that for the same calldata, the generated address will be the same. Consequently, anyone can call createAccount() with particular calldata and deploy a Nexus account with a deterministic address.

This is acceptable as account creation is supposed to be permissionless. As long as the calldata is not malicious, the user is safe, even if someone else deploys that address before them.

The real vulnerability lies in the fact that when the alreadyDeployed flag is activated in the createAccount() function call, the ETH that the user sent as an initial deposit for their Nexus account gets stuck in the factory contract itself and is not refunded to the user or sent to the Nexus account.

This opens up a griefing attack vector, wherein an attacker can front-run a user's createAccount() transaction, deploy the Nexus account with a 0 initial deposit, and when the user's transaction gets executed, the function will exit with the alreadyDeployed flag set to true, causing the msg.value amount of ETH to get stuck in the factory contract.

The attack vector is as follows :

  1. User calls createAccount() with msg.value = X

  2. Attacker observes user's transaction in the mempool and copies the calldata

  3. Attacker front-runs the user's transaction and creates the Nexus account with msg.value = 0

  4. When the user's transaction gets executed, X ETH gets stuck in the factory contract instead of being added to the Nexus account.

Similarly, this vulnerability is present in RegistryFactory.sol and K1ValidatorFactory.sol as well.

RegistryFactory.sol :

function createAccount(bytes calldata initData, bytes32 salt) external payable override returns (address payable) {
// Decode the initData to extract the call target and call data
(, bytes memory callData) = abi.decode(initData, (address, bytes));
// Extract the inner data by removing the first 4 bytes (the function selector)
bytes memory innerData = BytesLib.slice(callData, 4, callData.length - 4);
// Decode the call data to extract the parameters passed to initNexus
(
BootstrapConfig[] memory validators,
BootstrapConfig[] memory executors,
BootstrapConfig memory hook,
BootstrapConfig[] memory fallbacks,
,
,
) = abi.decode(innerData, (BootstrapConfig[], BootstrapConfig[], BootstrapConfig, BootstrapConfig[], address, address[], uint8));
// Ensure all modules are whitelisted
for (uint256 i = 0; i < validators.length; i++) {
require(isModuleAllowed(validators[i].module, MODULE_TYPE_VALIDATOR), ModuleNotWhitelisted(validators[i].module));
}
for (uint256 i = 0; i < executors.length; i++) {
require(isModuleAllowed(executors[i].module, MODULE_TYPE_EXECUTOR), ModuleNotWhitelisted(executors[i].module));
}
require(isModuleAllowed(hook.module, MODULE_TYPE_HOOK), ModuleNotWhitelisted(hook.module));
for (uint256 i = 0; i < fallbacks.length; i++) {
require(isModuleAllowed(fallbacks[i].module, MODULE_TYPE_FALLBACK), ModuleNotWhitelisted(fallbacks[i].module));
}
// Compute the actual salt for deterministic deployment
bytes32 actualSalt;
assembly {
let ptr := mload(0x40)
let calldataLength := sub(calldatasize(), 0x04)
mstore(0x40, add(ptr, calldataLength))
calldatacopy(ptr, 0x04, calldataLength)
actualSalt := keccak256(ptr, calldataLength)
}
// Deploy the account using the deterministic address
(bool alreadyDeployed, address account) = LibClone.createDeterministicERC1967(msg.value, ACCOUNT_IMPLEMENTATION, actualSalt);
if (!alreadyDeployed) {
INexus(account).initializeAccount(initData);
emit AccountCreated(account, initData, salt);
}
return payable(account);
}

K1ValidatorFactory.sol :

function createAccount(
address eoaOwner,
uint256 index,
address[] calldata attesters,
uint8 threshold
) external payable returns (address payable) {
// Compute the actual salt for deterministic deployment
bytes32 actualSalt;
assembly {
let ptr := mload(0x40)
let calldataLength := sub(calldatasize(), 0x04)
mstore(0x40, add(ptr, calldataLength))
calldatacopy(ptr, 0x04, calldataLength)
actualSalt := keccak256(ptr, calldataLength)
}
// Deploy the Nexus contract using the computed salt
(bool alreadyDeployed, address account) = LibClone.createDeterministicERC1967(msg.value, ACCOUNT_IMPLEMENTATION, actualSalt);
// Create the validator configuration using the Bootstrap library
BootstrapConfig memory validator = BootstrapLib.createSingleConfig(K1_VALIDATOR, abi.encodePacked(eoaOwner));
bytes memory initData = BOOTSTRAPPER.getInitNexusWithSingleValidatorCalldata(validator, REGISTRY, attesters, threshold);
// Initialize the account if it was not already deployed
if (!alreadyDeployed) {
INexus(account).initializeAccount(initData);
emit AccountCreated(account, eoaOwner, index);
}
return payable(account);
}

Impact

Permanent loss of funds

Tools Used

Manual Review

Recommendations

Transfer msg.value back to the user or to the Nexus account address if alreadyDeployed flag is active.

Updates

Lead Judging Commences

0xnevi Lead Judge 11 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-front-run-createAccount-fund-loss

Valid high, given it can be executed on any chain that has a public mempool. - These two issues has similar root cause as issue #171 and duplicates, but is the only issue that highlights a valid exploit scenario that can cause a loss of funds - Issue #82 can forcefully cause funds to be stuck within the factory contract, given `createDeterministicERC1967` will not revert when a new Nexus Proxy instance is already deployed. - Issue #91 can forcefully transfer funds out of Nexus Account by front-running a creation, setting a allowance and then self destructing. Duplicating due to the following reasons: - Both issues are contigent on front-running an account creation, which is only possible since the salt use to deploy a new Nexus Proxy instance are user deployed. So the same fix of including `msg.sender` and/or a unique identifier would prevent these attacks. i.e. same root cause --> different attack paths - Both issues have similarities with issue #112 , but I would say is a unique issue given even if #112 is fixed, this issues will not be fixed

Appeal created

adriro Judge
11 months ago
0xnevi Lead Judge
11 months ago
0xnevi Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.