Melted Carrot Swan
High
There is no verification that msg.sender == kycAddress during the execution of the VVVVVVCTokenDistributor::claim
function.
function claim(ClaimParams memory _params) public {
if (claimIsPaused) {
revert ClaimIsPaused();
}
if (_params.projectTokenProxyWallets.length != _params.tokenAmountsToClaim.length) {
revert ArrayLengthMismatch();
}
if (_params.nonce <= nonces[_params.kycAddress]) {
revert InvalidNonce();
}
if (!_isSignatureValid(_params)) {
revert InvalidSignature();
}
// update nonce
nonces[_params.kycAddress] = _params.nonce;
// define token to transfer
IERC20 projectToken = IERC20(_params.projectTokenAddress);
// transfer tokens from each wallet to the caller
for (uint256 i = 0; i < _params.projectTokenProxyWallets.length; i++) {
projectToken.safeTransferFrom(
_params.projectTokenProxyWallets[i],
msg.sender,
_params.tokenAmountsToClaim[i]
);
}
emit VCClaim(
_params.kycAddress,
_params.projectTokenAddress,
_params.projectTokenProxyWallets,
_params.tokenAmountsToClaim,
_params.nonce
);
}
function _isSignatureValid(ClaimParams memory _params) private view returns (bool) {
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(
abi.encode(
CLAIM_TYPEHASH,
_params.kycAddress,
_params.projectTokenAddress,
_params.projectTokenProxyWallets,
_params.tokenAmountsToClaim,
_params.nonce,
_params.deadline
)
)
)
);
address recoveredAddress = ECDSA.recover(digest, _params.signature);
bool isSigner = recoveredAddress == signer;
bool isExpired = block.timestamp > _params.deadline;
return isSigner && !isExpired;
}
Thus, the signature is used on the principle of whoever used it first gets all the tokens, even though the signature is explicitly created at kycAddress.
Thus, using frontrun, or simply recognising the signature in another way - anyone can use it before kycAddress and take the tokens that are intended for him.
- No check that msg.sender == kycAddress
- Tokens transfered to msg.sender
No response
Anyone, who
Anyone who finds out the signature by any means (the most obvious is frontrun) and executes the transaction before kycAddress - will take the tokens that are intended for kycAddress.
Severity: High - broken protocol behaviour and losing funds
paste this test to VVVVCTokenDistributorUnitTests
function testAnyoneClaim() public {
address[] memory thisProjectTokenProxyWallets = new address[](1);
uint256[] memory thisTokenAmountsToClaim = new uint256[](1);
thisProjectTokenProxyWallets[0] = projectTokenProxyWallets[0];
uint256 claimAmount = sampleTokenAmountsToClaim[0];
thisTokenAmountsToClaim[0] = claimAmount;
VVVVCTokenDistributor.ClaimParams memory claimParams = generateClaimParamsWithSignature(
sampleKycAddress,
thisProjectTokenProxyWallets,
thisTokenAmountsToClaim
);
claimAsUser(sampleUser, claimParams);
assertTrue(ProjectTokenInstance.balanceOf(sampleUser) == claimAmount);
}
Add check that msg.sender == kycAddress