Summary
The user's ETH will get stuck in BiconomyMetaFactory.sol
as it's not forwarded to the factory contract in the deployWithFactory()
function.
Vulnerability Details
BiconomyMetaFactory.sol
is an aggregated factory contract that can be used to deploy new Nexus accounts with any of the whitelisted
factories. The function deployWithFactory()
is used to achieve this.
function deployWithFactory(address factory, bytes calldata factoryData) external payable returns (address payable createdAccount) {
require(factoryWhitelist[address(factory)], FactoryNotWhitelisted());
@=> (bool success, bytes memory returnData) = factory.call(factoryData);
require(success, CallToDeployWithFactoryFailed());
assembly {
createdAccount := mload(add(returnData, 0x20))
}
}
As you can see, the function doesn't forward any incoming ETH to the respective factory, causing the deposit to get stuck in the contract.
The user can add some initial deposit to their new Nexus account by sending some ETH with the function call; however, doing so will cause their funds to get stuck in BiconomyMetaFactory.sol
instead, with no method to recover it.
The evidence for this is present in the createAccount()
functions of K1ValidatorFactory.sol
, NexusAccountFactory.sol
and RegistryFactory.sol
.
All three use the LibClone.createDeterministicERC1967()
method, which creates the new Nexus Account Contract with msg.value
amount as an initial deposit.
function createAccount(bytes calldata initData, bytes32 salt) external payable override returns (address payable) {
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)
}
@=> (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);
}
function createDeterministicERC1967(uint256 value, address implementation, bytes32 salt)
internal
returns (bool alreadyDeployed, address instance)
{
assembly {
let m := mload(0x40)
mstore(0x60, 0xcc3735a920a3ca505d382bbc545af43d6000803e6038573d6000fd5b3d6000f3)
mstore(0x40, 0x5155f3363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076)
mstore(0x20, 0x6009)
mstore(0x1e, implementation)
mstore(0x0a, 0x603d3d8160223d3973)
mstore(add(m, 0x35), keccak256(0x21, 0x5f))
mstore(m, shl(88, address()))
mstore8(m, 0xff)
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)
revert(0x1c, 0x04)
}
break
}
alreadyDeployed := 1
if iszero(value) { break }
if iszero(call(gas(), instance, value, codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, 0xb12d13eb)
revert(0x1c, 0x04)
}
break
}
mstore(0x40, m)
mstore(0x60, 0)
}
}
Impact
Permanent loss of funds
Tools Used
Manual Review
Recommendations
- (bool success, bytes memory returnData) = factory.call(factoryData);
+ (bool success, bytes memory returnData) = factory.call{value : msg.value}(factoryData);