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.
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.
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.
signer
address must be a smart contract wallet
A user with a smart account wallet.
- A user with a smart account wallet calls
VVVVCInvestmentLedger::invest
to invest orVVVVCTokenDistributor::claim
to claim tokens for his/her investment. - 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;
}
Smart contract wallets cannot be used as signers, consider that most VC investments happen via multi-sig wallets(smart accounts).
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;
}
}