diff --git a/packages/contracts/src/EmailWalletCore.sol b/packages/contracts/src/EmailWalletCore.sol index a52599fb..4618276b 100644 --- a/packages/contracts/src/EmailWalletCore.sol +++ b/packages/contracts/src/EmailWalletCore.sol @@ -168,21 +168,43 @@ contract EmailWalletCore is Initializable, UUPSUpgradeable, OwnableUpgradeable { } // Validate computed subject = passed subject - (string memory computedSubject, ) = SubjectUtils.computeMaskedSubjectForEmailOp( - emailOp, - accountHandler.getWalletOfSalt(emailOp.emailProof.accountSalt), - this // Core contract to read some states - ); + // (string memory computedSubject, ) = SubjectUtils.computeMaskedSubjectForEmailOp( + // emailOp, + // accountHandler.getWalletOfSalt(emailOp.emailProof.accountSalt), + // this // Core contract to read some states + // ); bytes memory maskedSubjectBytes = bytes(emailOp.emailProof.maskedSubject); require(emailOp.skipSubjectPrefix < maskedSubjectBytes.length, "skipSubjectPrefix too high"); bytes memory skippedSubjectBytes = new bytes(maskedSubjectBytes.length - emailOp.skipSubjectPrefix); for (uint i = 0; i < skippedSubjectBytes.length; i++) { skippedSubjectBytes[i] = maskedSubjectBytes[emailOp.skipSubjectPrefix + i]; } - require( - Strings.equal(computedSubject, string(skippedSubjectBytes)), - string.concat("subject != ", computedSubject) - ); + string memory computedSubject = ""; + for (uint stringCase = 0; stringCase < 3; stringCase++) { + (computedSubject, ) = SubjectUtils.computeMaskedSubjectForEmailOp( + emailOp, + accountHandler.getWalletOfSalt(emailOp.emailProof.accountSalt), + this, // Core contract to read some states + stringCase + ); + if (Strings.equal(computedSubject, string(skippedSubjectBytes))) { + break; + } + if (stringCase == 2) { + revert( + string.concat( + "given subject: ", + string(skippedSubjectBytes), + "!= expected subject: ", + computedSubject + ) + ); + } + } + // require( + // Strings.equal(computedSubject, string(skippedSubjectBytes)), + // string.concat("subject != ", computedSubject) + // ); // Verify proof require(verifier.verifyEmailProof(emailOp.emailProof), "invalid email proof"); diff --git a/packages/contracts/src/libraries/SubjectUtils.sol b/packages/contracts/src/libraries/SubjectUtils.sol index 4eeb222f..7a51d266 100644 --- a/packages/contracts/src/libraries/SubjectUtils.sol +++ b/packages/contracts/src/libraries/SubjectUtils.sol @@ -15,6 +15,18 @@ library SubjectUtils { bytes16 private constant LOWER_HEX_DIGITS = "0123456789abcdef"; bytes16 private constant UPPER_HEX_DIGITS = "0123456789ABCDEF"; + function addressToHexString(address addr, uint stringCase) internal pure returns (string memory) { + if (stringCase == 0) { + return addressToChecksumHexString(addr); + } else if (stringCase == 1) { + return Strings.toHexString(addr); + } else if (stringCase == 2) { + return lowerToUpperCase(Strings.toHexString(addr)); + } else { + revert("invalid stringCase"); + } + } + function addressToChecksumHexString(address addr) internal pure returns (string memory) { string memory lowerCaseAddrWithOx = Strings.toHexString(addr); @@ -54,6 +66,16 @@ library SubjectUtils { return string(result); } + function lowerToUpperCase(string memory hexStr) internal pure returns (string memory) { + bytes memory bytesStr = bytes(hexStr); + for (uint i = 0; i < bytesStr.length; i++) { + if (bytesStr[i] >= 0x61 && bytesStr[i] <= 0x66) { + bytesStr[i] = bytes1(uint8(bytesStr[i]) - 32); + } + } + return string(bytesStr); + } + /// @notice Convert bytes to hex string without 0x prefix /// @param data bytes to convert function bytesToHexString(bytes memory data) public pure returns (string memory) { @@ -77,8 +99,10 @@ library SubjectUtils { function computeMaskedSubjectForEmailOp( EmailOp memory emailOp, address walletAddr, - EmailWalletCore core + EmailWalletCore core, + uint stringCase ) public view returns (string memory maskedSubject, bool isExtension) { + require(stringCase < 3, "invalid stringCase"); ExtensionHandler extensionHandler = ExtensionHandler(core.extensionHandler()); // Sample: Send 1 ETH to recipient@domain.com @@ -100,7 +124,7 @@ library SubjectUtils { ); if (emailOp.recipientETHAddr != address(0)) { - maskedSubject = string.concat(maskedSubject, addressToChecksumHexString(emailOp.recipientETHAddr)); + maskedSubject = string.concat(maskedSubject, addressToHexString(emailOp.recipientETHAddr, stringCase)); } } // Sample: Execute 0x000112aa.. @@ -157,7 +181,7 @@ library SubjectUtils { maskedSubject = string.concat( Commands.EXIT_EMAIL_WALLET, " Email Wallet. Change ownership to ", - addressToChecksumHexString(emailOp.newWalletOwner) + addressToHexString(emailOp.newWalletOwner, stringCase) ); } // Sample: DKIM registry as 0x000112aa.. @@ -167,7 +191,7 @@ library SubjectUtils { maskedSubject = string.concat( Commands.DKIM, " registry set to ", - addressToChecksumHexString(emailOp.newDkimRegistry) + addressToHexString(emailOp.newDkimRegistry, stringCase) ); } // The command is for an extension @@ -230,13 +254,13 @@ library SubjectUtils { // {addres} for wallet address else if (Strings.equal(matcher, Commands.ADDRESS_TEMPLATE)) { address addr = abi.decode(emailOp.extensionParams.subjectParams[nextParamIndex], (address)); - value = addressToChecksumHexString(addr); + value = addressToHexString(addr, stringCase); nextParamIndex++; } // {recipient} is either the recipient's ETH address or zero bytes with the same length of the email address else if (Strings.equal(matcher, Commands.RECIPIENT_TEMPLATE)) { if (!emailOp.emailProof.hasEmailRecipient) { - value = addressToChecksumHexString(emailOp.recipientETHAddr); + value = addressToHexString(emailOp.recipientETHAddr, stringCase); } else { bytes memory zeros = new bytes(emailOp.numRecipientEmailAddrBytes); value = string(zeros); diff --git a/packages/contracts/test/EmailWalletCore.cmd.send.t.sol b/packages/contracts/test/EmailWalletCore.cmd.send.t.sol index a8de25f3..8e6980f5 100644 --- a/packages/contracts/test/EmailWalletCore.cmd.send.t.sol +++ b/packages/contracts/test/EmailWalletCore.cmd.send.t.sol @@ -48,8 +48,8 @@ contract TransferTest is EmailWalletCoreTestHelper { vm.stopPrank(); } - // We only support addres in checksum format in the subject (not all lowercase) - function test_Revert_SendingToEthAddress_WithNonChecksumAddress() public { + // We support addres in lowercase format in the subject + function test_SendingToEthAddress_WithLowercaseAddress() public { address recipient = vm.addr(5); daiToken.freeMint(walletAddr, 1 ether); // vm.addr(5) in lowecase = 0xe1ab8145f7e55dc933d51a18c793f901a3a0b276 @@ -63,7 +63,25 @@ contract TransferTest is EmailWalletCoreTestHelper { emailOp.walletParams.tokenName = "DAI"; vm.startPrank(relayer); - vm.expectRevert("subject != Send 1 DAI to 0xe1AB8145F7E55DC933d51a18c793F901A3A0b276"); + core.validateEmailOp(emailOp); + vm.stopPrank(); + } + + // We support addres in uppercase format in the subject + function test_SendingToEthAddress_WithUppercaseAddress() public { + address recipient = vm.addr(5); + daiToken.freeMint(walletAddr, 1 ether); + // vm.addr(5) in lowecase = 0xE1AB8145F7E55DC933D51A18C793F901A3A0B276 + string memory subject = string.concat("Send 1 DAI to 0xE1AB8145F7E55DC933D51A18C793F901A3A0B276"); + + EmailOp memory emailOp = _getBaseEmailOp(); + emailOp.command = "Send"; + emailOp.emailProof.maskedSubject = subject; + emailOp.recipientETHAddr = recipient; + emailOp.walletParams.amount = 1 ether; + emailOp.walletParams.tokenName = "DAI"; + + vm.startPrank(relayer); core.validateEmailOp(emailOp); vm.stopPrank(); } @@ -274,7 +292,7 @@ contract TransferTest is EmailWalletCoreTestHelper { emailOp.skipSubjectPrefix = 3; vm.startPrank(relayer); - vm.expectRevert("subject != Send 65.4 DAI to "); + vm.expectRevert("given subject: Send 65.4 DAI to != expected subject: Send 65.4 DAI to "); core.validateEmailOp(emailOp); vm.stopPrank(); }