Skip to content

Latest commit

 

History

History
110 lines (80 loc) · 4.78 KB

File metadata and controls

110 lines (80 loc) · 4.78 KB

Skinny Blood Mallard

Medium

Incompatibility with smart account wallets as they cannot invest(VVVVCInvestmentLedger::invest) nor claim their rewards(VVVVCTokenDistributor::claim) as calls to VVVVCInvestmentLedger::_isSignatureValid and VVVVCInvestmentLedger::_isSignatureValid will not work for smart account wallets.

Summary

The contract's signature verification mechanism relies solely on ECDSA.recover(), which doesn't support smart contract wallets as it can only recover EOA signatures. The _isSignatureValid function call in both VVVVCInvestmentLedger and VVVVCTokenDistributor call ECDSA.recover to get the signer. This works perfectly fine for EOAs but doesn't work with smart accounts.

Root Cause

VVVVCInvestmentLedger::invest() and VVVVCTokenDistributor::claim() assume that all signers will be EOAs and doesn't implement EIP-1271 for smart contract signature verification. Smart accounts can send transactions but they cannot sign messages like traditional wallets. EOA have private keys which they use for signatures, validating that the message came from that particular wallet. Smart contracts, on the other hand, do not have private keys, so signature validation like ECDSA.recover doesn't work on them.

Internal pre-conditions

  • signer address must be a smart contract wallet

External pre-conditions

A user with a smart account wallet.

Attack Path

  1. A user with a smart account wallet calls VVVVCInvestmentLedger::invest to invest or VVVVCTokenDistributor::claim to claim tokens for his/her investment.
  2. The signature check implemented as below in VVVVCInvestmentLedger
if (!_isSignatureValid(_params)) {
    revert InvalidSignature();
}

and in VVVVCTokenDistributor fails for such users even when valid params are given by them. When _isSignatureValid() attempts to recover the signer using ECDSA.recover(), it fails even for valid signatures from smart account wallets.

/**
     * @notice Checks if the provided signature is valid
     * @param _params An InvestParams struct containing the investment parameters
     * @return true if the signer address is recovered from the signature, false otherwise
     */
    function _isSignatureValid(InvestParams memory _params) internal view returns (bool) {
        bytes32 digest = keccak256(
            abi.encodePacked(
                "\x19\x01",
                DOMAIN_SEPARATOR,
                keccak256(
                    abi.encode(
                        INVESTMENT_TYPEHASH,
                        _params.investmentRound,
                        _params.investmentRoundLimit,
                        _params.investmentRoundStartTimestamp,
                        _params.investmentRoundEndTimestamp,
                        _params.paymentTokenAddress,
                        _params.kycAddress,
                        _params.kycAddressAllocation,
                        _params.exchangeRateNumerator,
                        _params.feeNumerator,
                        _params.deadline
                    )
                )
            )
        );

        address recoveredAddress = ECDSA.recover(digest, _params.signature);   /// @audit-tag doesn't with smart accounts

        bool isSigner = recoveredAddress == signer;
        bool isExpired = block.timestamp > _params.deadline;
        return isSigner && !isExpired;
    }

Impact

Smart contract wallets cannot be used as signers, consider that most VC investments happen via multi-sig wallets(smart accounts).

Mitigation

Consider adding contract signature support by implementing a recovery via the suggested isValidSignature() function of the EIP1271 and comparing the recovered value against the MAGIC_VALUE. The implementation might look something like the below:-

interface IERC1271 {
    function isValidSignature(
        bytes32 hash,
        bytes memory signature
    ) external view returns (bytes4 magicValue);
}

function _isSignatureValid(InvestParams memory _params) internal view returns (bool) {
    bytes32 digest = keccak256(/* ... */);

    // Try EOA recovery
    address recovered = ECDSA.recover(digest, _params.signature);
    if (recovered == signer) {
        /** do something **/
    }

    // Try EIP-1271 verification
    try IERC1271(signer).isValidSignature(digest, _params.signature) returns (bytes4 magicValue) {
        return magicValue == IERC1271.isValidSignature.selector;
    } catch {
        return false;
    }
}