Nonce collision in permit
and delegateBySig
functions
Description
In the Rate token implementation, both permit()
and delegateBySig()
share the same nonce for signature verification:
function permit(address owner, address spender, uint256 rawAmount, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
external
{
// ...
bytes32 domainSeparator =
keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainId(), address(this)));
bytes32 structHash =
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, rawAmount, nonces[owner]++, deadline)); // nonce is same for both permit and delegateBySig
// ...
allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) public {
// ...
require(nonce == nonces[signatory]++, "Rate::delegateBySig: invalid nonce"); // nonce is same for both permit and delegateBySig
require(block.timestamp <= expiry, "Rate::delegateBySig: signature expired");
return _delegate(signatory, delegatee);
}
Note that the two signatures operate in different signature domains yet share the same nonce. This could lead to front-end confusion, since normally, a nonce is tied to a specific signature domain and incremented only when a signature in that domain is consumed.
So, if a user with a zero nonce wishes to do both actions at the same time, a front-end UI that operates under standardized assumptions will sign both messages with the zero nonce. This means that only the first such signature will work and the second will fail since the check will expect the nonce to be one.
Recommendations
Separate the nonce for permit()
and delegateBySig()
to avoid front-end confusion.
Remediation
This issue has been acknowledged by Takara Lend, and a fix was implemented in commit 43c32ffc↗.. The Rate token has been deprecated.