Platform: Code4rena
Start Date: 04/01/2023
Pot Size: $60,500 USDC
Total HM: 15
Participants: 105
Period: 5 days
Judge: gzeon
Total Solo HM: 1
Id: 200
League: ETH
Rank: 11/105
Findings: 6
Award: $865.91
π Selected for report: 0
π Solo Findings: 0
π Selected for report: V_B
Also found by: 0xdeadbeef0x, HE1M, Koolex, Matin, adriro, chaduke, gogo, hihen, jonatascm, kankodu, ro, smit_rajput, spacelord47, taek
125.5131 USDC - $125.51
Uninitialized implementation can lead to self destruction, and stop the proxies from normal working.
The SmartAccount.sol
is not initialized during construction (no constructor is available). So, anybody can call the function init
for the first time and set the important variables _owner
, _entryPointAddress
, and _handler
to a malicious address.
function init(address _owner, address _entryPointAddress, address _handler) public override initializer { require(owner == address(0), "Already initialized"); require(address(_entryPoint) == address(0), "Already initialized"); require(_owner != address(0),"Invalid owner"); require(_entryPointAddress != address(0), "Invalid Entrypoint"); require(_handler != address(0), "Invalid Entrypoint"); owner = _owner; _entryPoint = IEntryPoint(payable(_entryPointAddress)); if (_handler != address(0)) internalSetFallbackHandler(_handler); setupModules(address(0), bytes("")); }
Then, the malicious address can call the function execFromEntryPoint
:
function execFromEntryPoint(address dest, uint value, bytes calldata func, Enum.Operation operation, uint256 gasLimit) external onlyEntryPoint returns (bool success) { success = execute(dest, value, func, operation, gasLimit); require(success, "Userop Failed"); }
This function will call the function execute
in which it delegate calls to an address deployed by the attacker which has selfdestruct function. So, the SmartAccount.sol
will be destructed, and all the proxies who point to this contract as an implementation will not working.
function execute( address to, uint256 value, bytes memory data, Enum.Operation operation, uint256 txGas ) internal returns (bool success) { if (operation == Enum.Operation.DelegateCall) { // solhint-disable-next-line no-inline-assembly assembly { success := delegatecall(txGas, to, add(data, 0x20), mload(data), 0, 0) } } else { // solhint-disable-next-line no-inline-assembly assembly { success := call(txGas, to, value, add(data, 0x20), mload(data), 0, 0) } } // Emit events here.. if (success) emit ExecutionSuccess(to, value, data, operation, txGas); else emit ExecutionFailure(to, value, data, operation, txGas); }
The following piece of code should be added to the contract SmartAccount.sol
:
constructor(){ init(address(this), address(this), address(this)); }
#0 - c4-judge
2023-01-17T14:55:38Z
gzeon-c4 marked the issue as duplicate of #496
#1 - c4-sponsor
2023-01-25T23:31:10Z
livingrockrises marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-10T12:29:57Z
gzeon-c4 marked the issue as satisfactory
π Selected for report: adriro
Also found by: 0x1f8b, 0x73696d616f, 0xdeadbeef0x, BClabs, HE1M, Haipls, Jayus, Kalzak, Lirios, Qeew, V_B, adriro, ast3ros, aviggiano, betweenETHlines, bin2chen, chaduke, dragotanqueray, ey88, giovannidisiena, hihen, horsefacts, ladboy233, wait, zaskoh
26.2582 USDC - $26.26
It is possible to intermediate in creation of a wallet and sets the entryPoint
to a malicious address and then stealing funds or self destructing the wallet by invoking the functions which are only allowed to be accessed by entryPoint
.
Since the smart contract wallet address is counterfactual i.e., it can be generated off-chain without actually deploying the code, user onboarding is super frictionless where the user is shown the smart wallet address as soon as she connects her wallet (EOA).
In the case user pays the wallet creation cost, the user needs to deposit the funds in the smart contract address to be able to pay for the smart contract wallet creation fee.
The user deposits funds using a fiat-onramp, centralized exchange, or from any other EOA on the same chain. In this case, the wallet will be created when the user does his first transaction from the wallet. Biconomy SDK will identify this case and appends a wallet creation transaction along with the userβs first action.
Suppose Alice (an honest user) deposits funds into her smart wallet address (which is not deployed yet. This is a normal behavior of the protocol as described in the documentation and referenced above). After some time, she sends the signed userOp to be handled by the entryPoint.
Bob (a malicious user) immediately deploys the smart wallet of Alice on the expected address with malicious _entryPoint
parameter by calling deployCounterFactualWallet
or deployWallet
.
function deployCounterFactualWallet(address _owner, address _entryPoint, address _handler, uint _index) public returns(address proxy){ bytes32 salt = keccak256(abi.encodePacked(_owner, address(uint160(_index)))); bytes memory deploymentData = abi.encodePacked(type(Proxy).creationCode, uint(uint160(_defaultImpl))); // solhint-disable-next-line no-inline-assembly assembly { proxy := create2(0x0, add(0x20, deploymentData), mload(deploymentData), salt) } require(address(proxy) != address(0), "Create2 call failed"); // EOA + Version tracking emit SmartAccountCreated(proxy,_defaultImpl,_owner, VERSION, _index); BaseSmartAccount(proxy).init(_owner, _entryPoint, _handler); isAccountExist[proxy] = true; }
function deployWallet(address _owner, address _entryPoint, address _handler) public returns(address proxy){ bytes memory deploymentData = abi.encodePacked(type(Proxy).creationCode, uint(uint160(_defaultImpl))); // solhint-disable-next-line no-inline-assembly assembly { proxy := create(0x0, add(0x20, deploymentData), mload(deploymentData)) } BaseSmartAccount(proxy).init(_owner, _entryPoint, _handler); isAccountExist[proxy] = true; }
The parameters provided by Bob are:
_owner
: Alice's address_entryPoint
: Bob's address_handler
: Bob's address_index
: the index used by the protocol when predicting the address of Alice's wallet (it can be captured by Bob also through front-running the relayer during creating the Alice's wallet).Please note that it is possible to give Bob's address as _entryPoint
parameter, because changing _entryPoint
or _handler
does not change the address of the smart wallet. They are only parameters for initializing the wallet.
After Bob deploys the smart wallet for Alice with his own address as entryPoint
, he can call the functions that are only allowed by entryPoint
:
validateUserOp
with the parameters and signatures already provided by Alice to pass the checks inside this function. Then, he can steal the already deposited fund in this wallet by giving a nonzero value to missingAccountFunds
as parameter.function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, address aggregator, uint256 missingAccountFunds) external override virtual returns (uint256 deadline) { _requireFromEntryPoint(); deadline = _validateSignature(userOp, userOpHash, aggregator); if (userOp.initCode.length == 0) { _validateAndUpdateNonce(userOp); } _payPrefund(missingAccountFunds); }
function _payPrefund(uint256 missingAccountFunds) internal virtual { if (missingAccountFunds != 0) { (bool success,) = payable(msg.sender).call{value : missingAccountFunds, gas : type(uint256).max}(""); (success); //ignore failure (its EntryPoint's job to verify, not account.) } }
execFromEntryPoint
to steal all the funds and even self destruct the wallet contract.function execFromEntryPoint(address dest, uint value, bytes calldata func, Enum.Operation operation, uint256 gasLimit) external onlyEntryPoint returns (bool success) { success = execute(dest, value, func, operation, gasLimit); require(success, "Userop Failed"); }
function execute( address to, uint256 value, bytes memory data, Enum.Operation operation, uint256 txGas ) internal returns (bool success) { if (operation == Enum.Operation.DelegateCall) { // solhint-disable-next-line no-inline-assembly assembly { success := delegatecall(txGas, to, add(data, 0x20), mload(data), 0, 0) } } else { // solhint-disable-next-line no-inline-assembly assembly { success := call(txGas, to, value, add(data, 0x20), mload(data), 0, 0) } } // Emit events here.. if (success) emit ExecutionSuccess(to, value, data, operation, txGas); else emit ExecutionFailure(to, value, data, operation, txGas); }
Maybe it is better to include _entryPoint
and _handler
in the salt as well, so if Bob gives his own address as entryPoint, the created address will be different than the address Alice is expecting (and depositing fund to).
function deployCounterFactualWallet(address _owner, address _entryPoint, address _handler, uint _index) public returns(address proxy){ bytes32 salt = keccak256(abi.encodePacked(_owner, address(uint160(_index)), _entryPoint, _handler)); bytes memory deploymentData = abi.encodePacked(type(Proxy).creationCode, uint(uint160(_defaultImpl))); // solhint-disable-next-line no-inline-assembly assembly { proxy := create2(0x0, add(0x20, deploymentData), mload(deploymentData), salt) } require(address(proxy) != address(0), "Create2 call failed"); // EOA + Version tracking emit SmartAccountCreated(proxy,_defaultImpl,_owner, VERSION, _index); BaseSmartAccount(proxy).init(_owner, _entryPoint, _handler); isAccountExist[proxy] = true; }
#0 - c4-judge
2023-01-17T14:55:21Z
gzeon-c4 marked the issue as duplicate of #460
#1 - c4-sponsor
2023-01-26T05:57:24Z
livingrockrises marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-10T12:25:37Z
gzeon-c4 marked the issue as satisfactory
π Selected for report: Tointer
Also found by: 0xdeadbeef0x, HE1M, Haipls, Koolex, PwnedNoMore, Tricko, V_B, csanuragjain, orion, peakbolt, ro, romand, taek
149.4204 USDC - $149.42
Replay attack protection is not used for 2D nonces, so a malicious user can replay the user's transactions many times.
Suppose Alice (an honest user) signed 2 transactions TX0 and TX1, with nonces 0 and 1, respectively.
Then the relayer executes these transactions one by one by assigning the batchId
equal to 10 for example. So, the parameters for these calls will be:
first call:
_tx
: TX0batchId
: 10refundInfo
: the valid data provided by Alicesignatures
: Alice's signature of TX0 with nonce 0second call:
_tx
: TX1batchId
: 10refundInfo
: the valid data provided by Alicesignatures
: Alice's signature of TX1 with nonce 1nonces[batchId] = nonces[10] = 0
and during the second call (as the nonce for the same batch id is incremented by one) we have: nonces[batchId] = nonces[10] = 1
. Since Alice signed the transaction data, refund data, and nonce, the checkSignatures
will pass.{ bytes memory txHashData = encodeTransactionData( // Transaction info _tx, // Payment info refundInfo, // Signature info nonces[batchId] ); // Increase nonce and execute transaction. // Default space aka batchId is 0 nonces[batchId]++; txHash = keccak256(txHashData); checkSignatures(txHash, txHashData, signatures); }
Bob (a malicious user) calls this function with the following parameters:
_tx
: TX0batchId
: 50 (any batchId
that gives nonces[batchId] = 0
, I am assuming the batck id 50 was never used before so it's nonce will be zero)refundInfo
: the valid data provided by Alicesignatures
: Alice's signature of TX0 with nonce 0This transaction will be executed and the checkSignatures
will pass. Although the batch id is different, the signature is passed as the nonce is important (because Alice signs the transaction including the nonce not the batch id).
Bob can replay the second transaction as well with batch id 50 , because the nonce now is 1, so it matches to what Alice singed for TX1.
Bob can replay this transaction many times because the mapping mapping(uint256 => uint256) public nonces;
has a lot of keys with zero value.
The vulnerability is that the replay attack protection is not used for 2D nonces.
Maybe it is better to track the consumed transactions:
function execTransaction( Transaction memory _tx, uint256 batchId, FeeRefund memory refundInfo, bytes memory signatures ) public payable virtual override returns (bool success) { // ... require(!isConsumed[txHash], "it is already consumed"); isConsumed[txHash] = true; }
#0 - c4-judge
2023-01-17T07:10:54Z
gzeon-c4 marked the issue as duplicate of #485
#1 - c4-sponsor
2023-01-25T23:08:47Z
livingrockrises marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-10T12:34:39Z
gzeon-c4 marked the issue as satisfactory
449.9602 USDC - $449.96
In summary, a malicious user can add his UserOperation to the mempool of bundler, and then by front-running the bundler and withdrawing the gas refund required for his operation to be executed, he can cause that the bundler lose huge gas. If his operation is going to be refunded by his smart account, then by front-running and withdrawing his deposit, all the gas consumed for all ops account prepayment validation will be deducted from the bundler without refund. If his operation is going to be refunded by paymaster, then by front-running and withdrawing his deposit as paymaster, all the gas consumed for all ops account prepayment validation and paymaster prepayment validation will be deducted from the bundler without refund.
A bundler is going to relay UserOperation to the EntryPoint contract. The bundler picks UserOperations from the high-level UserOperation mempool (which is not the same as the regular transaction mempool). Suppose there are 50 UserOpertions from various sources (ops1 for user1, ops2 for user2, ...., and ops50 for Bob as a malicious user). So, the function handleOps
will be called to validate and execute these operations.
https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L68
Please note that the function simulateValidation
is to check whether the account and paymaster prepayment are valid before the function handleOps
is called.
https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L226
There are two scenarios:
Bob has already provided the required gas refund for his operation (ops50) by calling the function depositTo(address account)
in which the account
is the address of Bob's smart account. So, we will have deposits[Bob's account].deposit = gas refund
.
Bob has sets paymaster = address(0)
in his ops50 that means the smart account is going to refund the gas not the paymaster. Since there is enough deposit in the EntryPoint contract by Bob, the simulation of validation shows that ops50 is a valid operation. So, the function handleOps
will be called.
Bob, notices this function call in the mempool, and applies front-run attack and calls the function execute
.
He provides the following parameters:
dest
: address of EntryPointvalue
: 0func
: abi.encodeWithSignature("withdrawTo(address,uint256)", Bob's EOA address, the amount was deposited before)By doing so, Bob withdraws the deposits done before. So, we will have deposits[Bob's account].deposit = 0
.
Then, it is the turn of the handleOps
transaction to be executed. It goes over account prepayment validation process of all the ops from 1 to 49, and when it reaches to ops50, it sees that the paymaster is zero-address, so it checks the balance of Bob's account to transfer fund to EntryPoint contract for gas refund is enough or not.
https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L316 https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L319 https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/BaseSmartAccount.sol#L67 https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/BaseSmartAccount.sol#L108
Since the balance of Bob's account is zero, the call will be unsuccessful, so no fund will be transferred to EntryPoint contract. Finally, the validation of Bob's ops50 will fail. This failure, will revert all the validation of the previous ops from 1 to 49. So, the bundler has lost lots of gas during validation of ops 1 to 50. https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L334
Bob (the malicious user) has already provided the required gas refund for his operation (ops50) by calling the function depositFor(address paymasterId)
in which the paymasterId
is the address of Bob's EOA. So, we will have deposits[VerifyingSingletonPaymaster contract].deposit += gas refund
in the EntryPoint contract and paymasterIdBalances[Bob's EOA] = gas refund
in VerifyingSingletonPaymaster contract.
https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/paymasters/verifying/singleton/VerifyingSingletonPaymaster.sol#L48 https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/StakeManager.sol#L48
Bob has sets paymaster = VerifyingSingletonPaymaster contract
in the ops50 that means the paymaster is going to refund the gas not the smart account. Since there is enough deposit in the EntryPoint contract by other paymasters as well as Bob's EOA as paymaster, the simulation of validation shows that it is a valid operation. So, the function handleOps
will be called.
Bob, notices this function call in the mempool, and applies front-run attack and calls the function withdrawTo
to withdraw his deposit as a paymaster. So, we will have deposits[VerifyingSingletonPaymaster contract].deposit -= gas refund
in the EntryPoint contract and paymasterIdBalances[Bob's EOA] = 0
in VerifyingSingletonPaymaster contract.
Then, it is the turn of the handleOps
transaction to be executed. After it goes over account prepayment validation, it goes over paymaster prepayment validation process of all the ops from 1 to 49, and when it reaches to ops50, it checks whether there is enough total deposit in EntryPoint contract or not (The transaction may revert here. But it most probably will not revert here, because there are other paymasters that have already deposited into EntryPoint contract, which are more than enough).
https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L359
After the paymaster prepayment validation is done successfully, the function _executeUserOp
is called for all 50 ops, in which it calls innerHandleOp
, then it calls _handlePostOp
, then postOp
, and finally _postOp
.
https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L48
https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L168
https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L440
https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/paymasters/BasePaymaster.sol#L31
https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/paymasters/verifying/singleton/VerifyingSingletonPaymaster.sol#L119
In the function _postOp
, the transaction will fail because the paymasterIdBalances[Bob's EOA] = 0
, and underflow error happens. This failure reverts all other executed ops 1 to 49, which results in loss of huge gas from bundler.
Bob, notices handleOps
function call in the mempool, and applies front-run attack and calls the function handleOps
to execute only the ops50. After the ops50 is executed, the nonce of Bob's smart account will be incremented. This failure reverts all the account prepayment validation process and the gas consumed will not be refunded to the bundler.
Then, it is the turn of the handleOps
transaction to be executed. It goes over account prepayment validation process of all ops from 1 to 49. When it reaches to ops50, it fails because this operation does not match the expected nonce.
https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L319 https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/BaseSmartAccount.sol#L60 https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L501
try-catch is needed for validation and execution of each UserOperation in the function handleOps
:
https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/aa-4337/core/EntryPoint.sol#L68
#0 - c4-judge
2023-01-18T00:30:07Z
gzeon-c4 marked the issue as duplicate of #90
#1 - c4-judge
2023-01-18T00:30:34Z
gzeon-c4 marked the issue as not a duplicate
#2 - c4-judge
2023-01-18T00:31:16Z
gzeon-c4 marked the issue as duplicate of #499
#3 - livingrockrises
2023-01-26T01:20:23Z
I will discuss internally/community and also ask for Dror/Yoav's opinion from infinitism. same for #511 and #499
#4 - c4-sponsor
2023-02-08T08:14:24Z
livingrockrises marked the issue as sponsor acknowledged
#5 - c4-judge
2023-02-10T12:20:48Z
gzeon-c4 marked the issue as satisfactory
π Selected for report: immeas
Also found by: 0xDave, 0xbepresent, HE1M, Kutu, betweenETHlines, hansfriese, hihen, peanuts, prc, wait
78.2598 USDC - $78.26
https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L460 https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L465 https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/aa-4337/samples/SimpleAccount.sol#L60 https://github.com/code-423n4/2023-01-biconomy/blob/5df2e8f8c0fd3393b9ecdad9ef356955f07fbbdd/scw-contracts/contracts/smart-contract-wallet/aa-4337/samples/SimpleAccount.sol#L68
The wrong extra modifier onlyOwner()
caused that the functions execute
and executeBatch
are not accessible through the entryPoint
.
While this modifier is removed from these functions in the contract SimpleAccount.sol
(which is used for tests), it is not removed from these functions in the contract SmartAccount.sol
(which is the main contract).
The modifier onlyOwner()
only accepts the owner as a sender, so the check _requireFromEntryPointOrOwner
is useless. Since these functions should be allowed to be called by entryPoint
as well, the modifier onlyOwner()
should be removed.
function execute(address dest, uint value, bytes calldata func) external onlyOwner{ _requireFromEntryPointOrOwner(); _call(dest, value, func); } function executeBatch(address[] calldata dest, bytes[] calldata func) external onlyOwner{ _requireFromEntryPointOrOwner(); require(dest.length == func.length, "wrong array lengths"); for (uint i = 0; i < dest.length;) { _call(dest[i], 0, func[i]); unchecked { ++i; } } }
Removing the modifier OnlyOwner()
from these functions.
#0 - c4-judge
2023-01-18T00:36:58Z
gzeon-c4 marked the issue as duplicate of #390
#1 - c4-sponsor
2023-01-26T06:54:58Z
livingrockrises marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-10T12:21:36Z
gzeon-c4 marked the issue as satisfactory
π Selected for report: 0xSmartContract
Also found by: 0x1f8b, 0xAgro, 0xdeadbeef0x, 0xhacksmithh, 2997ms, Atarpara, Bnke0x0, Diana, HE1M, IllIllI, Josiah, Kalzak, Lirios, MalfurionWhitehat, MyFDsYours, Raiders, RaymondFam, Rolezn, SaharDevep, Sathish9098, Udsen, Viktor_Cortess, adriro, ast3ros, betweenETHlines, btk, chaduke, chrisdior4, cryptostellar5, csanuragjain, giovannidisiena, gz627, hl_, horsefacts, joestakey, juancito, ladboy233, lukris02, nadin, oyc_109, pauliax, peanuts, prady, sorrynotsorry, zaskoh
36.5015 USDC - $36.50
Setting the owner should be done through two-step.
function setOwner(address _newOwner) external mixedAuth { require(_newOwner != address(0), "Smart Account:: new Signatory address cannot be zero"); address oldOwner = owner; owner = _newOwner; emit EOAChanged(address(this), oldOwner, _newOwner); }
Should be:
function setPendingOwner(address _newPendingOwner) external mixedAuth { require(_newPendingOwner != address(0)); pendingOwner = _newPendingOwner; } function acceptOwnership() external { require(msg.sender == pendingOwner); owner = msg.sender; pendingOwner = address(0); }
In the normal scenario, Alice sends fund to her smart wallet (which is not created yet) and then after some time sends the dapp transaction to the SDK. The SDK sends batch of transactions (create wallet and dapp transaction) to the relayer. The relayer first creates the wallet by calling the SmartAccountFactory.sol
, and then sends the dapp transaction to the created wallet.
In the malicious scenario, Alice sends fund to her smart wallet (which is not created yet). Then Bob (a malicious user) calls the function deployCounterFactualWallet
to create a smart wallet for Alice (with the same parameters that are used to predict the Alice's smart wallet address), so the smart wallet is deployed for Alice on the expected address. Then after some time Alice sends the dapp transaction to the SDK. The SDK sends batch of transactions (create wallet and dapp transaction) to the relayer. When the ralayer tries to create the wallet for Alice, it reverts, because it is already created by Bob.
Better to modify as:
function deployCounterFactualWallet(address _owner, address _entryPoint, address _handler, uint _index) public returns(address){ bytes32 salt = keccak256(abi.encodePacked(_owner, address(uint160(_index)))); bytes memory deploymentData = abi.encodePacked(type(Proxy).creationCode, uint(uint160(_defaultImpl))); bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(deploymentData))); address _wallet = address(uint160(uint(hash))); if(isAccountExist[_wallet]){ return _wallet; } // solhint-disable-next-line no-inline-assembly address proxy; assembly { proxy := create2(0x0, add(0x20, deploymentData), mload(deploymentData), salt) } require(address(proxy) != address(0), "Create2 call failed"); // EOA + Version tracking emit SmartAccountCreated(proxy,_defaultImpl,_owner, VERSION, _index); BaseSmartAccount(proxy).init(_owner, _entryPoint, _handler); isAccountExist[proxy] = true; return proxy; }
#0 - c4-judge
2023-01-22T15:18:23Z
gzeon-c4 marked the issue as grade-b
#1 - c4-sponsor
2023-02-09T12:44:24Z
livingrockrises marked the issue as sponsor confirmed