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

Fix/finding #2 Fix Calculating Salt #124

Merged
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 14 additions & 19 deletions contracts/factory/K1ValidatorFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ contract K1ValidatorFactory is Stakeable {
/// @notice Creates a new Nexus with a specific validator and initialization data.
/// @param eoaOwner The address of the EOA owner of the Nexus.
/// @param index The index of the Nexus.
/// @param attesters The list of attesters for the Nexus.
/// @param threshold The threshold for the Nexus.
/// @return The address of the newly created Nexus.
function createAccount(
address eoaOwner,
Expand All @@ -80,14 +82,7 @@ contract K1ValidatorFactory is Stakeable {
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)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not quite sure why these two are not the same?

I believe this change was done only for gas optimisation from gaslite suggestion.

Copy link
Contributor

@ankurdubey521 ankurdubey521 Aug 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're effectively calculating keccak256(abi.encode(...params)) while his code is keccak256(abi.encodePacked(...params). Generally encodePack-ing is cheaper but I can't say for sure in this case if re-encoding will be cheaper than copying and hashing pre-encoded data. Best to look at benchmarks.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check how arrays are abi encoded, there's an offset to the data (that can be chosen by the caller as the issue says) and you'd be encoding this offset too along with everything else.

bytes32 actualSalt = keccak256(abi.encodePacked(eoaOwner, index, attesters, threshold));

// Deploy the Nexus contract using the computed salt
(bool alreadyDeployed, address account) = LibClone.createDeterministicERC1967(msg.value, ACCOUNT_IMPLEMENTATION, actualSalt);
Expand All @@ -105,19 +100,19 @@ contract K1ValidatorFactory is Stakeable {
}

/// @notice Computes the expected address of a Nexus contract using the factory's deterministic deployment algorithm.
/// @param - The address of the EOA owner of the Nexus.
/// @param - The index of the Nexus.
/// @param eoaOwner The address of the EOA owner of the Nexus.
/// @param index The index of the Nexus.
/// @param attesters The list of attesters for the Nexus.
/// @param threshold The threshold for the Nexus.
/// @return expectedAddress The expected address at which the Nexus contract will be deployed if the provided parameters are used.
function computeAccountAddress(address, uint256, address[] calldata, uint8) external view returns (address payable expectedAddress) {
function computeAccountAddress(
address eoaOwner,
uint256 index,
address[] calldata attesters,
uint8 threshold
) external view returns (address payable expectedAddress) {
// 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)
}
bytes32 actualSalt = keccak256(abi.encodePacked(eoaOwner, index, attesters, threshold));

// Predict the deterministic address using the LibClone library
expectedAddress = payable(LibClone.predictDeterministicAddressERC1967(ACCOUNT_IMPLEMENTATION, actualSalt, address(this)));
Expand Down
31 changes: 18 additions & 13 deletions contracts/factory/NexusAccountFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,17 @@
/// @param salt Unique salt for the Smart Account creation.
/// @return The address of the newly created Nexus account.
function createAccount(bytes calldata initData, bytes32 salt) external payable override returns (address payable) {
// Compute the actual salt for deterministic deployment
// Compute the actual salt as the hash of initData and salt using assembly
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whats wrong with this comment

// Compute the actual salt for deterministic deployment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to reflect the changes, as we use hash of initData and Salt only (without sig func)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for deterministic deployment

Is nice and consistent..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bytes32 actualSalt;
assembly {
// Load the free memory pointer
let ptr := mload(0x40)
let calldataLength := sub(calldatasize(), 0x04)
mstore(0x40, add(ptr, calldataLength))
calldatacopy(ptr, 0x04, calldataLength)
actualSalt := keccak256(ptr, calldataLength)
// Store initData at the free memory pointer
calldatacopy(ptr, initData.offset, initData.length)

Check warning on line 51 in contracts/factory/NexusAccountFactory.sol

View check run for this annotation

Codecov / codecov/patch

contracts/factory/NexusAccountFactory.sol#L51

Added line #L51 was not covered by tests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not consistent everywhere?
here it would be
bytes32 actualSalt = keccak256(abi.encodePacked(initData, salt))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's what it does but in assembly

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, why cant we just use this abi.encodePacked way everywhere then.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's for gas mostly.

But where else it's not used?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like in other factories we have changed and used like this
bytes32 actualSalt = keccak256(abi.encodePacked(eoaOwner, index, attesters, threshold));

go back to the issue please to also understand test case needs.
we are still hashing everything for salt event without any change, but in the event it would be emitted different value which won't help to reconstruct salt

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Store salt after initData
mstore(add(ptr, initData.length), salt)

Check warning on line 53 in contracts/factory/NexusAccountFactory.sol

View check run for this annotation

Codecov / codecov/patch

contracts/factory/NexusAccountFactory.sol#L53

Added line #L53 was not covered by tests
// Compute the keccak256 hash of the concatenated initData and salt
actualSalt := keccak256(ptr, add(initData.length, 32))

Check warning on line 55 in contracts/factory/NexusAccountFactory.sol

View check run for this annotation

Codecov / codecov/patch

contracts/factory/NexusAccountFactory.sol#L55

Added line #L55 was not covered by tests
}

// Deploy the account using the deterministic address
Expand All @@ -63,20 +66,22 @@
}

/// @notice Computes the expected address of a Nexus contract using the factory's deterministic deployment algorithm.
/// @param - Initialization data to be called on the new Smart Account.
/// @param - Unique salt for the Smart Account creation.
/// @param initData - Initialization data to be called on the new Smart Account.
/// @param salt - Unique salt for the Smart Account creation.
/// @return expectedAddress The expected address at which the Nexus contract will be deployed if the provided parameters are used.
function computeAccountAddress(bytes calldata, bytes32) external view override returns (address payable expectedAddress) {
function computeAccountAddress(bytes calldata initData, bytes32 salt) external view override returns (address payable expectedAddress) {
// Compute the actual salt for deterministic deployment
bytes32 actualSalt;
assembly {
// Load the free memory pointer
let ptr := mload(0x40)
let calldataLength := sub(calldatasize(), 0x04)
mstore(0x40, add(ptr, calldataLength))
calldatacopy(ptr, 0x04, calldataLength)
actualSalt := keccak256(ptr, calldataLength)
// Store initData at the free memory pointer
calldatacopy(ptr, initData.offset, initData.length)

Check warning on line 79 in contracts/factory/NexusAccountFactory.sol

View check run for this annotation

Codecov / codecov/patch

contracts/factory/NexusAccountFactory.sol#L79

Added line #L79 was not covered by tests
// Store salt after initData
mstore(add(ptr, initData.length), salt)

Check warning on line 81 in contracts/factory/NexusAccountFactory.sol

View check run for this annotation

Codecov / codecov/patch

contracts/factory/NexusAccountFactory.sol#L81

Added line #L81 was not covered by tests
// Compute the keccak256 hash of the concatenated initData and salt
actualSalt := keccak256(ptr, add(initData.length, 32))

Check warning on line 83 in contracts/factory/NexusAccountFactory.sol

View check run for this annotation

Codecov / codecov/patch

contracts/factory/NexusAccountFactory.sol#L83

Added line #L83 was not covered by tests
}

expectedAddress = payable(LibClone.predictDeterministicAddressERC1967(ACCOUNT_IMPLEMENTATION, actualSalt, address(this)));
}
}
44 changes: 26 additions & 18 deletions contracts/factory/RegistryFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -96,27 +96,30 @@

// 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));
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(executors[i].module, MODULE_TYPE_EXECUTOR), ModuleNotWhitelisted(executors[i].module));
}

require(isModuleAllowed(hook.module, MODULE_TYPE_HOOK), ModuleNotWhitelisted(hook.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));
require(_isModuleAllowed(fallbacks[i].module, MODULE_TYPE_FALLBACK), ModuleNotWhitelisted(fallbacks[i].module));
}

// Compute the actual salt for deterministic deployment
// Compute the actual salt as the hash of initData and salt using assembly
bytes32 actualSalt;
assembly {
// Load the free memory pointer
let ptr := mload(0x40)
let calldataLength := sub(calldatasize(), 0x04)
mstore(0x40, add(ptr, calldataLength))
calldatacopy(ptr, 0x04, calldataLength)
actualSalt := keccak256(ptr, calldataLength)
// Store initData at the free memory pointer
calldatacopy(ptr, initData.offset, initData.length)

Check warning on line 118 in contracts/factory/RegistryFactory.sol

View check run for this annotation

Codecov / codecov/patch

contracts/factory/RegistryFactory.sol#L118

Added line #L118 was not covered by tests
// Store salt after initData
mstore(add(ptr, initData.length), salt)

Check warning on line 120 in contracts/factory/RegistryFactory.sol

View check run for this annotation

Codecov / codecov/patch

contracts/factory/RegistryFactory.sol#L120

Added line #L120 was not covered by tests
// Compute the keccak256 hash of the concatenated initData and salt
actualSalt := keccak256(ptr, add(initData.length, 32))

Check warning on line 122 in contracts/factory/RegistryFactory.sol

View check run for this annotation

Codecov / codecov/patch

contracts/factory/RegistryFactory.sol#L122

Added line #L122 was not covered by tests
}

// Deploy the account using the deterministic address
Expand All @@ -130,25 +133,30 @@
}

/// @notice Computes the expected address of a Nexus contract using the factory's deterministic deployment algorithm.
/// @param - Initialization data to be called on the new Smart Account.
/// @param - Unique salt for the Smart Account creation.
/// @param initData - Initialization data to be called on the new Smart Account.
/// @param salt - Unique salt for the Smart Account creation.
/// @return expectedAddress The expected address at which the Nexus contract will be deployed if the provided parameters are used.
function computeAccountAddress(bytes calldata, bytes32) external view override returns (address payable expectedAddress) {
function computeAccountAddress(bytes calldata initData, bytes32 salt) external view override returns (address payable expectedAddress) {

Check warning on line 139 in contracts/factory/RegistryFactory.sol

View check run for this annotation

Codecov / codecov/patch

contracts/factory/RegistryFactory.sol#L139

Added line #L139 was not covered by tests
// Compute the actual salt as the hash of initData and salt using assembly
bytes32 actualSalt;
assembly {
// Load the free memory pointer
let ptr := mload(0x40)
let calldataLength := sub(calldatasize(), 0x04)
mstore(0x40, add(ptr, calldataLength))
calldatacopy(ptr, 0x04, calldataLength)
actualSalt := keccak256(ptr, calldataLength)
// Store initData at the free memory pointer
calldatacopy(ptr, initData.offset, initData.length)

Check warning on line 146 in contracts/factory/RegistryFactory.sol

View check run for this annotation

Codecov / codecov/patch

contracts/factory/RegistryFactory.sol#L146

Added line #L146 was not covered by tests
// Store salt after initData
mstore(add(ptr, initData.length), salt)

Check warning on line 148 in contracts/factory/RegistryFactory.sol

View check run for this annotation

Codecov / codecov/patch

contracts/factory/RegistryFactory.sol#L148

Added line #L148 was not covered by tests
// Compute the keccak256 hash of the concatenated initData and salt
actualSalt := keccak256(ptr, add(initData.length, 32))

Check warning on line 150 in contracts/factory/RegistryFactory.sol

View check run for this annotation

Codecov / codecov/patch

contracts/factory/RegistryFactory.sol#L150

Added line #L150 was not covered by tests
}
expectedAddress = payable(LibClone.predictDeterministicAddressERC1967(ACCOUNT_IMPLEMENTATION, actualSalt, address(this)));
}

/// @notice Checks if a module is whitelisted.
/// @param module The address of the module to check.
/// @return True if the module is whitelisted, false otherwise.
function isModuleAllowed(address module, uint256 moduleType) public view returns (bool) {
/// @param moduleType The type of the module to check.
/// @return True if the module is whitelisted, reverts otherwise.
function _isModuleAllowed(address module, uint256 moduleType) private view returns (bool) {

Check warning on line 159 in contracts/factory/RegistryFactory.sol

View check run for this annotation

Codecov / codecov/patch

contracts/factory/RegistryFactory.sol#L159

Added line #L159 was not covered by tests
REGISTRY.check(module, moduleType, attesters, threshold);
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,40 @@ contract TestAccountFactory_Deployments is NexusTest_Base {

// Verify that the validators and hook were installed
assertTrue(
INexus(deployedAccountAddress).isModuleInstalled(MODULE_TYPE_VALIDATOR, address(VALIDATOR_MODULE), ""), "Validator should be installed"
INexus(deployedAccountAddress).isModuleInstalled(MODULE_TYPE_VALIDATOR, address(VALIDATOR_MODULE), ""),
"Validator should be installed"
);
assertTrue(
INexus(deployedAccountAddress).isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), abi.encodePacked(user.addr)),
"Hook should be installed"
);
}

/// @notice Tests that the manually computed address matches the one from computeAccountAddress.
function test_ComputeAccountAddress_ManualComparison() public {
// Prepare the initial data and salt
BootstrapConfig[] memory validators = BootstrapLib.createArrayConfig(address(VALIDATOR_MODULE), initData);
BootstrapConfig memory hook = BootstrapLib.createSingleConfig(address(0), "");
bytes memory saDeploymentIndex = "0";
bytes32 salt = keccak256(saDeploymentIndex);

// Create initcode and salt to be sent to Factory
bytes memory _initData = BOOTSTRAPPER.getInitNexusScopedCalldata(validators, hook, REGISTRY, ATTESTERS, THRESHOLD);

// Manually compute the actual salt
bytes32 actualSalt = keccak256(abi.encodePacked(_initData, salt));
// Compute the expected address using the factory's function
address payable expectedAddress = FACTORY.computeAccountAddress(_initData, salt);

// Manually compute the expected address
address payable manualExpectedAddress = payable(
LibClone.predictDeterministicAddressERC1967(FACTORY.ACCOUNT_IMPLEMENTATION(), actualSalt, address(FACTORY))
);

// Validate that both addresses match
assertEq(expectedAddress, manualExpectedAddress, "Manually computed address mismatch");
}

/// @notice Tests that the Nexus contract constructor reverts if the entry point address is zero.
function test_Constructor_RevertIf_EntryPointIsZero() public {
address zeroAddress = address(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@ TestAccountFactory_Deployments
│ └── it should deploy the account correctly
├── when initializing Nexus with a hook module and deploying an account
│ └── it should deploy the account and install the modules correctly
└── when the Nexus contract constructor is called with a zero entry point address
└── it should revert
├── when the Nexus contract constructor is called with a zero entry point address
│ └── it should revert
└── when manually computing the address using keccak256
└── it should match the address computed by computeAccountAddress
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ contract TestK1ValidatorFactory_Deployments is NexusTest_Base {
vm.deal(user.addr, 1 ether);
initData = abi.encodePacked(user.addr);
bootstrapper = new Bootstrap();
validatorFactory =
new K1ValidatorFactory(address(ACCOUNT_IMPLEMENTATION), address(FACTORY_OWNER.addr), address(VALIDATOR_MODULE), bootstrapper, REGISTRY);
validatorFactory = new K1ValidatorFactory(
address(ACCOUNT_IMPLEMENTATION),
address(FACTORY_OWNER.addr),
address(VALIDATOR_MODULE),
bootstrapper,
REGISTRY
);
}

/// @notice Tests if the constructor correctly initializes the factory with the given implementation, K1 Validator, and Bootstrapper addresses.
Expand Down Expand Up @@ -132,4 +137,26 @@ contract TestK1ValidatorFactory_Deployments is NexusTest_Base {

assertTrue(accountAddress0 != accountAddress1, "Accounts with different indexes should have different addresses");
}

/// @notice Tests that the computed address matches the manually computed address using keccak256.
function test_ComputeAccountAddress_MatchesManualComputation() public {
address eoaOwner = user.addr;
uint256 index = 1;
address[] memory attesters = new address[](1);
attesters[0] = address(0x1234);
uint8 threshold = 1;

// Compute the actual salt manually using keccak256
bytes32 manualSalt = keccak256(abi.encodePacked(eoaOwner, index, attesters, threshold));

address expectedAddress = LibClone.predictDeterministicAddressERC1967(
address(validatorFactory.ACCOUNT_IMPLEMENTATION()),
manualSalt,
address(validatorFactory)
);

address computedAddress = validatorFactory.computeAccountAddress(eoaOwner, index, attesters, threshold);

assertEq(expectedAddress, computedAddress, "Computed address does not match manually computed address");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ TestK1ValidatorFactory_Deployments
│ └── it should return the expected address
├── when creating an account with the same owner and index
│ └── it should result in the same address
└── when creating accounts with different indexes
└── it should result in different addresses
├── when creating accounts with different indexes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think tests should confirm that you're able to compute same account address using the initData and salt index emitted in the events.

or could be different params depending on the factory/method but you get the gist.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The written tests are valid in my opinion

│ └── it should result in different addresses
└── when manually computing the address using keccak256
└── it should match the address computed by computeAccountAddress
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ contract TestNexusAccountFactory_Deployments is NexusTest_Base {

// Verify that the validators and hook were installed
assertTrue(
INexus(deployedAccountAddress).isModuleInstalled(MODULE_TYPE_VALIDATOR, address(VALIDATOR_MODULE), ""), "Validator should be installed"
INexus(deployedAccountAddress).isModuleInstalled(MODULE_TYPE_VALIDATOR, address(VALIDATOR_MODULE), ""),
"Validator should be installed"
);
assertTrue(
INexus(deployedAccountAddress).isModuleInstalled(MODULE_TYPE_HOOK, address(HOOK_MODULE), abi.encodePacked(user.addr)),
Expand Down
Loading
Loading