👓Examples

Understanding the Examples: In the "Examples" section, we delve into practical instances of casting vulnerabilities unearthed from actual bug bounty reports, Code Arena analyses, and more. By studying these real-life cases, you'll gain a deeper, more tangible understanding of unsafe casting and its potential impact, enhancing your auditing skills in a practical and impactful way.

Example 1: Unsafe Cast On Oracle Return Value

Real life Cod4rena Bug Find With Payout

Consider a system where oracle.pcvStats returns newProtocolEquity as int256, which is then cast to uint256 in recalculate. The issue lies in the possibility of newProtocolEquity being negative, leading to incorrect conversions. This can be resolved using the SafeCast library, which safely converts signed integers to unsigned integers, and vice versa, preventing possible overflows or underflows.

newProtocolEquity = oracle.pcvStats();
newProtocolEquity = uint256(newProtocolEquity);  // Unsafe cast, no check for negative values

Example 2: Unstafe cast in vesting

Real life Cod4rena Bug Find With Payout

In the function claim(), the variable vestedAmount is subtracted from vester.amount and then the result is being casted to uint192. In Solidity, a uint256 variable can hold a much larger value than a uint192. The value of uint256 can be up to 2^256 - 1, while the value of uint192 can only be up to 2^192 - 1.

The line of code of interest here is:

vester.amount -= uint192(vestedAmount);

This statement does two things:

  1. It subtracts vestedAmount from vester.amount (which reduces the amount of tokens still to be vested for the user).

  2. It casts the result to a uint192 type.

If vester.amount were to hold a value larger than what can be held by a uint192 after subtracting vestedAmount, the behavior would be problematic. This is because casting a larger value to a smaller data type, like uint192, in Solidity does not revert or throw an error. Instead, it will silently overflow, i.e., lose the data exceeding its maximum storage capacity, which may lead to incorrect computations or even allow for potential exploits.

To visualize this, let's say we have a uint256 value which is 2^200, which is too large to fit into a uint192. If we were to cast this uint256 to a uint192, the resulting value would not be 2^200, but 2^200 mod 2^192. This is because the cast operation in Solidity does not revert on overflow; instead, it applies the modulo operation.

This problem can be mitigated by using the SafeCast library from OpenZeppelin. SafeCast provides functions to safely convert unsigned integers to a smaller size. Instead of the cast operation silently overflowing, the SafeCast functions will revert the transaction if an overflow occurs, thus protecting against potential exploits or bugs.

So, instead of the line:

vester.amount -= uint192(vestedAmount);

You could use:

vester.amount = SafeCast.toUint192(SafeMath.sub(vester.amount, vestedAmount));// Some code

The SafeMath.sub method reverts on underflow (if vestedAmount is larger than vester.amount), and the SafeCast.toUint192 method reverts on overflow (if the result of the subtraction operation is larger than what a uint192 can hold).

Using these libraries can help you to ensure that arithmetic and casting operations in your smart contracts are safe from overflow and underflow bugs.

Example 3: Unsafe uint128 casting may overflow

Real life Cod4rena Bug Find With Payout

In the given code, the function _calcRewardIntegral is calculating rewards for users based on some business logic. It makes use of various types of integer values, mainly uint256 and uint128. This function performs several casts from uint256 to uint128 and vice versa.

Let's take a look at these lines:

rewardIntegral = uint128(rewardIntegral) + uint128(((bal - rewardRemaining) * 1e20) / _supply);
reward.reward_integral = uint128(rewardIntegral);

Here, rewardIntegral is a uint256 value. The result of the expression ((bal - rewardRemaining) * 1e20) / _supply is also uint256. The code then casts both rewardIntegral and the result of the expression to uint128 before performing the addition. This could potentially lead to overflow if the uint256 value exceeds the maximum value that can be stored in a uint128 variable.

In Solidity, an overflow would not result in an error or "revert". Instead, the value is calculated using the modulo operation, which would result in a much smaller number than expected. This could lead to a user receiving significantly less rewards than they should have.

To mitigate this, it is recommended to use the SafeCast library from OpenZeppelin. This library provides functions for safely casting between different numeric types without the risk of overflow. The above code snippet would look something like this with SafeCast:

import "@openzeppelin/contracts/utils/math/SafeCast.sol";

// In your contract
using SafeCast for uint256;

// Later in your function
rewardIntegral = rewardIntegral.toUint128() + (((bal - rewardRemaining) * 1e20) / _supply).toUint128();
reward.reward_integral = rewardIntegral.toUint128();

This code performs the same operation as before but now uses the SafeCast library to ensure the operation will not overflow. If the value is too large to fit in a uint128, these functions will revert the transaction, preventing any unexpected behavior.

By using SafeCast, you're ensuring that values that are too large to be stored as a uint128 will cause a transaction to revert, rather than silently overflowing and possibly leading to unexpected behavior in your smart contract.

Example 4: Implicit Under-flows

High Vulnerability

Real life Cod4rena Bug Find With Payout

In the examples provided, there are potential underflows happening due to operations on uint values. An underflow occurs when a variable that has an unsigned integer type is decremented below its minimum value, which wraps around to its maximum value. For example, in the case of uint, the minimum value is 0, and if we try to decrement 0 by 1, the result is 2^256 - 1, the maximum uint value, due to underflow.

The examples show conversions to int256 after subtracting uint values or negating uint values. If the intermediate result is less than 0, it will be represented as a large uint value (close to 2^256 - 1). When this uint value is then cast to int256, the value could become negative.

In Solidity 0.8.x, underflows and overflows are automatically checked and will cause a revert. However, this check is not applied on typecasts, which can lead to unexpected results. Therefore, it's important to ensure underflows don't occur by casting each operand before the operation.

The original problems occur when a subtraction operation is performed between uint values or a uint value is negated, and the result is then cast to int256. This may lead to incorrect values due to underflow.

The mitigation steps in a more generalized form are:

  1. For subtraction operations, ensure that each operand is cast to int256 before performing the operation. So replace int256(a-b) with int256(a)-int256(b).

  2. For negation operations, first cast the uint value to int256 before applying the negation. So replace int256(-x) with -int256(x).

Doing this ensures that the operations are performed on int256 values, thereby preventing any unexpected underflow situations that could arise from the operation being performed on uint values.

Last updated