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: 19/105
Findings: 4
Award: $658.60
🌟 Selected for report: 0
🚀 Solo Findings: 0
22.7235 USDC - $22.72
On calling checkSignatures
with v
signature component as 0 (meaning it is a contract signature) then it is not validated that provided signature is actually signed by owner
checkSignatures
functionfunction checkSignatures( bytes32 dataHash, bytes memory data, bytes memory signatures ) public view virtual { uint8 v; bytes32 r; bytes32 s; uint256 i = 0; address _signer; (v, r, s) = signatureSplit(signatures, i); //review if(v == 0) { // If v is 0 then it is a contract signature // When handling contract signatures the address of the contract is encoded into r _signer = address(uint160(uint256(r))); ... } else if(v > 30) { // If v > 30 then default va (27,28) has been adjusted for eth_sign flow // To support eth_sign and similar we adjust v and hash the messageHash with the Ethereum message prefix before applying ecrecover _signer = ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v - 4, r, s); require(_signer == owner, "INVALID_SIGNATURE"); } else { _signer = ecrecover(dataHash, v, r, s); require(_signer == owner, "INVALID_SIGNATURE"); } }
v=0
then there is no check to validate if provided contract signature is signed by owner. This means// Below check is missing when v=0 require(_signer == owner, "INVALID_SIGNATURE");
checkSignatures
function and unauthorized transaction could get executedRevise the checkSignatures
function as below:
function checkSignatures( bytes32 dataHash, bytes memory data, bytes memory signatures ) public view virtual { uint8 v; bytes32 r; bytes32 s; uint256 i = 0; address _signer; (v, r, s) = signatureSplit(signatures, i); //review if(v == 0) { // If v is 0 then it is a contract signature // When handling contract signatures the address of the contract is encoded into r _signer = address(uint160(uint256(r))); ... } else if(v > 30) { // If v > 30 then default va (27,28) has been adjusted for eth_sign flow // To support eth_sign and similar we adjust v and hash the messageHash with the Ethereum message prefix before applying ecrecover _signer = ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v - 4, r, s); } else { _signer = ecrecover(dataHash, v, r, s); } require(_signer == owner, "INVALID_SIGNATURE"); }
#0 - c4-judge
2023-01-17T16:12:17Z
gzeon-c4 marked the issue as duplicate of #175
#1 - c4-sponsor
2023-01-26T00:07:51Z
livingrockrises marked the issue as sponsor confirmed
#2 - livingrockrises
2023-01-26T00:07:58Z
should be high-risk
#3 - c4-sponsor
2023-01-26T00:08:06Z
livingrockrises requested judge review
#4 - c4-judge
2023-02-10T11:47:31Z
gzeon-c4 changed the severity to 3 (High Risk)
#5 - c4-judge
2023-02-10T12:28:34Z
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
It is possible to replay the transaction by keeping the txHashData same as shown in POC
Victim calls execTransaction function with batchID 1
txHashData
is calculated as below:
bytes memory txHashData = encodeTransactionData( // Transaction info _tx, // Payment info refundInfo, // Signature info nonces[batchId] );
Since nonces[1] is used first time so value will be 0
Attacker replay this transaction with same _tx and refundInfo. He sets batchID as 2
txHashData
is calculated and becomes same as Step 2. Reason is Attacker used same _tx and refundInfo and nonces2.
Add one more field nonce (which increment for each executed transaction). This will ensure txHashData
is always unique
#0 - c4-judge
2023-01-17T07:02:36Z
gzeon-c4 marked the issue as duplicate of #485
#1 - c4-sponsor
2023-01-25T23:52:33Z
livingrockrises marked the issue as sponsor confirmed
#2 - livingrockrises
2023-01-25T23:52:45Z
should be high-risk
#3 - c4-sponsor
2023-01-25T23:52:53Z
livingrockrises requested judge review
#4 - c4-judge
2023-02-10T11:47:02Z
gzeon-c4 changed the severity to 3 (High Risk)
#5 - c4-judge
2023-02-10T12:34:39Z
gzeon-c4 marked the issue as satisfactory
🌟 Selected for report: peakbolt
Also found by: V_B, csanuragjain, zaskoh
449.9602 USDC - $449.96
Attacker can call handleOps
function with lower gas. Any transaction, part of this bundle and requiring higher gas would fail. Even though the fault lies with Attacker, still the victim has to bear the fees
handleOps
function with UserOperation U with gas say Xfunction handleOps(UserOperation[] calldata ops, address payable beneficiary) public { uint256 opslen = ops.length; UserOpInfo[] memory opInfos = new UserOpInfo[](opslen); unchecked { for (uint256 i = 0; i < opslen; i++) { _validatePrepayment(i, ops[i], opInfos[i], address(0)); } uint256 collected = 0; for (uint256 i = 0; i < opslen; i++) { collected += _executeUserOp(i, ops[i], opInfos[i]); } _compensate(beneficiary, collected); } //unchecked }
function _executeUserOp(uint256 opIndex, UserOperation calldata userOp, UserOpInfo memory opInfo) private returns (uint256 collected) { uint256 preGas = gasleft(); bytes memory context = getMemoryBytesFromOffset(opInfo.contextOffset); try this.innerHandleOp(userOp.callData, opInfo, context) returns (uint256 _actualGasCost) { ... } catch { uint256 actualGas = preGas - gasleft() + opInfo.preOpGas; collected = _handlePostOp(opIndex, IPaymaster.PostOpMode.postOpReverted, opInfo, context, actualGas); } }
If Attacker has deliberately provided lower fees then the transaction should revert
Reference: https://github.com/eth-infinitism/account-abstraction/pull/162
#0 - c4-judge
2023-01-18T00:43:29Z
gzeon-c4 marked the issue as duplicate of #303
#1 - c4-judge
2023-01-18T00:43:35Z
gzeon-c4 marked the issue as partial-25
#2 - gzeoneth
2023-01-18T00:43:56Z
fail to mention 1/64 rule otherwise the whole tx will revert oog
#3 - c4-sponsor
2023-02-09T12:21:41Z
livingrockrises marked the issue as sponsor disputed
#4 - c4-judge
2023-02-10T12:16:37Z
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
Issue: Funds can stuck if withdraw address is contract itself
POC:
withdrawTo
function with withdrawAddress
as StakeManager.sol contract itselffunction withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external { DepositInfo storage info = deposits[msg.sender]; require(withdrawAmount <= info.deposit, "Withdraw amount too large"); info.deposit = uint112(info.deposit - withdrawAmount); emit Withdrawn(msg.sender, withdrawAddress, withdrawAmount); (bool success,) = withdrawAddress.call{value : withdrawAmount}(""); require(success, "failed to withdraw"); }
receive
functionreceive() external payable { depositTo(msg.sender); }
Since in this case msg.sender is contract itself so deposit is made for StakeManager.sol contract
Now there is no way to withdraw these funds
Recommendation: Kindly revise the receive function to disallow deposit by contract itself
receive() external payable { require(msg.sender!=address(this), "Incorrect sender"); depositTo(msg.sender); }
Issue:
It seems that currently Staking amount is capped to type(uint112).max
even though deposit cap is set to type(uint112).max
. This means the Staking cap could be revised to also allow type(uint112).max
Recommendation:
Kindly revise the addStake
function
function addStake(uint32 _unstakeDelaySec) public payable { ... require(stake <= type(uint112).max, "stake overflow"); ... }
Issue:
If transaction execution via execTransaction
does not success but has refundInfo.gasPrice != 0 then there is no way to know if the transaction never suceeded
POC:
execTransaction
function is called for transaction Tfalse
as successsuccess = execute(_tx.to, _tx.value, _tx.data, _tx.operation, refundInfo.gasPrice == 0 ? (gasleft() - 2500) : _tx.targetTxGas);
refundInfo.gasPrice != 0
then below condition returns true and function is executed properlyrequire(success || _tx.targetTxGas != 0 || refundInfo.gasPrice != 0, "BSA013"); // We transfer the calculated tx costs to the tx.origin to avoid sending it to intermediate contracts that have made calls uint256 payment = 0; // uint256 extraGas; if (refundInfo.gasPrice > 0) { //console.log("sent %s", startGas - gasleft()); // extraGas = gasleft(); payment = handlePayment(startGas - gasleft(), refundInfo.baseGas, refundInfo.gasPrice, refundInfo.tokenGasPriceFactor, refundInfo.gasToken, refundInfo.refundReceiver); emit WalletHandlePayment(txHash, payment); } // extraGas = extraGas - gasleft(); //console.log("extra gas %s ", extraGas); } }
Recommendation: If success is false then execute an failure event mentioning the same
#0 - c4-judge
2023-01-22T15:20:34Z
gzeon-c4 marked the issue as grade-b
#1 - c4-sponsor
2023-02-09T12:43:30Z
livingrockrises marked the issue as sponsor confirmed