code-423n4 / 2024-03-coinbase-findings

1 stars 0 forks source link

Nonce mechanism lacks uniqueness checks, risking replay attacks and out-of-order execution. #25

Closed c4-bot-1 closed 6 months ago

c4-bot-1 commented 7 months ago

Lines of code

https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/SmartWallet/CoinbaseSmartWallet.sol#L137-L168 https://github.com/code-423n4/2024-03-coinbase/blob/e0573369b865d47fed778de00a7b6df65ab1744e/src/SmartWallet/CoinbaseSmartWallet.sol#L145-L152

Vulnerability details

The nonce mechanism in CoinbaseSmartWallet.sol has potential issue related to nonce uniqueness and ordering for non-replayable user operations. The current implementation does not explicitly check the uniqueness of nonces and does not enforce strict ordering, which could lead to replay attacks and out-of-order execution of user operations within the same chain.

Description:

validateUserOp, which is responsible for validating user operations takes the following parameters:

Loc: validateUserOp

   function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds)
       public
       payable
       virtual
       onlyEntryPoint
       payPrefund(missingAccountFunds)
       returns (uint256 validationData)
   {
       uint256 key = userOp.nonce >> 64;

       // 0xbf6ba1fc = bytes4(keccak256("executeWithoutChainIdValidation(bytes)"))
       if (userOp.callData.length >= 4 && bytes4(userOp.callData[0:4]) == 0xbf6ba1fc) {
           userOpHash = getUserOpHashWithoutChainId(userOp);
           if (key != REPLAYABLE_NONCE_KEY) {
               revert InvalidNonceKey(key);
           }
       } else {
           if (key == REPLAYABLE_NONCE_KEY) {
               revert InvalidNonceKey(key);
           }
       }

   }

The function extracts the key from the upper 192 bits of the userOp.nonce field and uses it to determine whether the user operation is intended for cross-chain replay or not.

a. Nonce Uniqueness:

Loc: #145-L151

   uint256 key = userOp.nonce >> 64;
   // ...
   if (userOp.callData.length >= 4 && bytes4(userOp.callData[0:4]) == 0xbf6ba1fc) {
       // ...
   } else {
       if (key == REPLAYABLE_NONCE_KEY) {
           revert InvalidNonceKey(key);
       }
   }

These lines extract the key from the userOp.nonce and check if it is a replayable or non-replayable user operation, but they do not perform any checks for nonce uniqueness or ordering.

The issue is the current implementation allows the same nonce to be used for multiple non-replayable user operations, making it vulnerable to replay attacks within the same chain.

  • The current implementation does not enforce any specific ordering of nonces for non-replayable user operations, potentially allowing out-of-order execution.

Impact

If the same nonce is used for multiple non-replayable user operations, an attacker can replay a previously executed user operation, potentially leading to unintended or malicious actions.

Recommended Mitigation Steps

a. Nonce Uniqueness:

      mapping(uint256 => bool) private usedNonces;

      function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds)
          public
          payable
          virtual
          onlyEntryPoint
          payPrefund(missingAccountFunds)
          returns (uint256 validationData)
      {
          // ...
          if (key != REPLAYABLE_NONCE_KEY) {
              if (usedNonces[userOp.nonce]) {
                  revert NonceAlreadyUsed(userOp.nonce);
              }
              usedNonces[userOp.nonce] = true;
          }
          // ...
      }

b. Nonce Ordering:

      uint256 private lastNonce;

      function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds)
          public
          payable
          virtual
          onlyEntryPoint
          payPrefund(missingAccountFunds)
          returns (uint256 validationData)
      {
          // ...
          if (key != REPLAYABLE_NONCE_KEY) {
              if (userOp.nonce <= lastNonce) {
                  revert InvalidNonceOrder(userOp.nonce, lastNonce);
              }
              lastNonce = userOp.nonce;
          }
          // ...
      }

Assessed type

Invalid Validation

c4-pre-sort commented 7 months ago

raymondfam marked the issue as insufficient quality report

c4-pre-sort commented 7 months ago

raymondfam marked the issue as primary issue

raymondfam commented 7 months ago

Incremental nonce is not needed here as userOp.nonce is only there to match up with REPLAYABLE_NONCE_KEY after userOpHash is recomputed.

3docSec commented 6 months ago

The logic that the warden expected to find (nonce check + increment) is implemented upstream in the EntryPoint through its parent NonceManager, making the reported attack path not viable by ensuring replay is not possible.

c4-judge commented 6 months ago

3docSec marked the issue as unsatisfactory: Invalid

c4-judge commented 6 months ago

3docSec marked the issue as unsatisfactory: Invalid