0

I have a function in Solidity that is supposed to check if the recovery hash committed by the Email recovery service matches the hash generated through the token and nonce sent by the user (He receives this from his email). Here's how the code looks:

///@dev Called by the email service which commits the recovery hash
function commitRecoveryHash(bytes32 recoveryHash) external onlyEmailVerificationService {
        emailVerificationHash = recoveryHash;
    }

Here's how the email recovery service generates the recovery hash

 address emailService = address(0xa0Ee7A142d267C1f36714E4a8F75612F20a79720); 
    string userEmail = "[email protected]";
    uint64 nonce = 38;
    

vm.startPrank(emailService);
        bytes memory recoveryToken = (abi.encodePacked(userEmail, emailService));
        emit EmailServiceGeneratingHashUsing(recoveryToken, nonce);
    
        bytes32 emailVerificationHash = keccak256(abi.encodePacked(recoveryToken, nonce));

        accountRecovery.commitRecoveryHash(emailVerificationHash);
        vm.stopPrank();

Now I have a test that calls the function to check if the recovery hashes match.

 address emailService = address(0xa0Ee7A142d267C1f36714E4a8F75612F20a79720); // TODO: To be updated with the wallet address of the actual email
    string userEmail = "[email protected]";
    uint64 nonce = 38;
    

 function testUserVerification() external {
        bytes memory recoveryToken = abi.encodePacked(userEmail, emailService);
        // creating a new Embedded wallet for the user
        address newWallet = makeAddr("newWallet");

        // Act/ Assert
        vm.prank(newWallet); // using the new wallet to send recovery req.
        vm.expectEmit();
        emit AccountRecoveryRequestCreated();
        accountRecovery.verifyUserAsOwnerOfTheAccount(userEmail, recoveryToken, nonce);
    }

Here's the test calling the verifyUserAsOwnerOfTheAccount() using the same recovery token, email, and nonce:

 function verifyUserAsOwnerOfTheAccount(
        string memory email,
        bytes calldata token,
        uint256 nonce
    ) internal returns (bool) {
       
        ///@dev Hashing strings to compare them.
        emit AboutToGenerateHashUsing(token, nonce);

        bytes32 generatedEmailVerificationHash = keccak256(abi.encodePacked(token, nonce));
        emit RecoveryHash(emailVerificationHash);
        emit GeneratedHash(generatedEmailVerificationHash);

        console.log("Do the email hash match:", (generatedEmailVerificationHash == emailVerificationHash));

        if (generatedEmailVerificationHash != emailVerificationHash) {
            revert EmailVerificationFailed();
        }
        return true;
    }

Please note the events EmailServiceGeneratingHashUsing & AboutToGenerateHashUsing prove that the same params went into creating the hashes and the same keccak256(abi.encodePacked(recoveryToken, nonce)) method was used to create the hashes, but still the hashes don't match. Here are the logs:

 VM::stopPrank() 
    │   └─ ← ()
    ├─ [0] VM::startPrank(0xa0Ee7A142d267C1f36714E4a8F75612F20a79720) 
    │   └─ ← ()
    ├─ emit EmailServiceGeneratingHashUsing(token: 0x73686976656e40676d61696c2e636f6da0ee7a142d267c1f36714e4a8f75612f20a79720, nonce: 38)
    ├─ [22408] AccountRecovery::commitRecoveryHash(0x271f87acd2085f2e203d8d983e89f336274e0c2028ebc7340703b3b141b0b6dc) 
    │   └─ ← ()
    ├─ [0] VM::stopPrank() 
    │   └─ ← ()
    └─ ← ()

  [32967] AccountRecoveryTest::testRecoveryRequestGeneration() 
    ├─ [0] VM::addr(<pk>) [staticcall]
    │   └─ ← newWallet: [0xBc1Ce00634302d0DfDF22B8C2b8Dc81011bCA560]
    ├─ [0] VM::label(newWallet: [0xBc1Ce00634302d0DfDF22B8C2b8Dc81011bCA560], newWallet) 
    │   └─ ← ()
    ├─ [0] VM::prank(newWallet: [0xBc1Ce00634302d0DfDF22B8C2b8Dc81011bCA560]) 
    │   └─ ← ()
    ├─ [0] VM::expectEmit() 
    │   └─ ← ()
    ├─ emit AccountRecoveryRequestCreated()
    ├─ [14330] AccountRecovery::verifyUserAsOwnerOfTheAccount([email protected], 0x73686976656e40676d61696c2e636f6da0ee7a142d267c1f36714e4a8f75612f20a79720, 38) 
    │   ├─ emit AboutToGenerateHashUsing(receivedToken: 0x73686976656e40676d61696c2e636f6da0ee7a142d267c1f36714e4a8f75612f20a79720, nonce: 38)
    │   ├─ emit RecoveryHash(recoveryHash: 0x271f87acd2085f2e203d8d983e89f336274e0c2028ebc7340703b3b141b0b6dc)
    │   ├─ emit GeneratedHash(generatedHash: 0x7c3438dd181130e624805b50265942f829f784fc1720b189431743713d7e826c)
    │   ├─ [0] console::log(Do the email hash match:, false) [staticcall]
    │   │   └─ ← ()
    │   └─ ← "EmailVerificationFailed()"
    └─ ← "Log != expected log"

I'm not able to figure out why the emailVerificationHash & generatedEmailVerificationHash don't match.

emit RecoveryHash(recoveryHash: 0x271f87acd2085f2e203d8d983e89f336274e0c2028ebc7340703b3b141b0b6dc)
emit GeneratedHash(generatedHash: 0x7c3438dd181130e624805b50265942f829f784fc1720b189431743713d7e826c)
    

Need help.

2 Answers 2

0

It worked when I created the emailVerificationHash in the contract's commitRecoveryHash(...) instead of sending the hash from the test suite directly.

Here's how the commitRecoveryHash() is modified:

function commitRecoveryHash(
        bytes calldata recoveryToken,
        uint256 recoveryTokenNonce
    ) external onlyEmailVerificationService {
        emailVerificationHash = keccak256(abi.encodePacked(recoveryToken, recoveryTokenNonce)); // generating the hash instead of receiving it
    }

You will notice how the function signature has changed. Instead of receiving the bytes32 recoveryHash the commitRecoveryHash(..) will now receive bytes calldata recoveryToken, uint256 recoveryTokenNonce and will generate the recoveryHash in the contract itself.

Here's how the test setup function will look:

 // commiting the recovery params to create email verification hash (representing the email recovery service)
        vm.startPrank(emailService);
        emit EmailServiceGeneratingHashUsing(recoveryToken, nonce);

        accountRecovery.commitRecoveryHash(recoveryToken, nonce); // sending hash generation params instead of the hash itself
        vm.stopPrank();

With the emailRecoveryHash being produced in the contract itself the test passes.

I still don't understand why Foundry is resulting in different hashes in the test file vs the contract, even when using exactly the same params? I even checked by freezing the solidity versions of both the contract and test file to 0.8.18, but still, different hashes were produced. Does anyone know why this could be happening?

0

The hash was being created using a uint64 nonce in the test file whereas in the contract it was generated using uint256 nonce. Since the data type of nonce was different, the hashes were different.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.