Infinite token approval is triggered outside the spec
Description
The Rate contract is an ERC-20--compliant token that implements EIP-717 unlimited authorization for permits and regular allowances:
function approve(address spender, uint256 rawAmount) external returns (bool) {
uint96 amount;
if (rawAmount == type(uint256).max) {
amount = type(uint96).max;
} else {
amount = safe96(rawAmount, "Rate::approve: amount exceeds 96 bits");
}
allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
However, the recordkeeping of token amounts is implemented in uint96
, instead of uint256
. This implementation decision leaks a corner case into the interface due to the overlap in meaning between unlimited approval and the maximum amount of approval. If the caller sets the uint256 rawAmount
parameter to exactly type(uint96).max
, then the amount approved will be unlimited, instead of limited to type(uint96).max
.
The unadopted EIP-717 specification↗ is the current standard for unlimited token allowance, and it specifies exactly type(uint256).max
as the only sentinel allowance value that means unlimited allowance. So, although it is unlikely, a caller that manipulates arbitrary ERC-20 tokens under the assumption that EIP-717 is strictly how unlimited approvals work can be misled into unintentionally setting unlimited allowance for the Rate token.
Impact
The impact is fairly low, since the token-balance recordkeeping is done in uint96
, which means that an allowance that is that high can be interpreted as effectively infinite since it is large relative to the total supply. However, the impact is not zero, as this edge case may contribute to DeFi-exploit chains that include two or more independent systems.
Recommendations
In order to fully comply with the specification, except for allowance and token balances not representable as uint96
, an extra boolean flag must be added to the state to represent unlimited approval.
Alternatively, the representable region can be reduced by one, down to type(uint96).max - 1
. This can be done by slightly tightening the range of the safe96
internal function and adding associated logic.
Remediation
This issue has been acknowledged by Takara Lend, and a fix was implemented in commit 43c32ffc↗. The Rate token has been deprecated.