
What's happening to Farcaster?
The pivot to a trader-focused-social-wallet, the drama, future of Farcaster and other Clients

Please Stop Making The Audience Fall Asleep At Your IRL Workshop
How to make engaging technical workshops on any topic and make people getting addicted to learning it

Solidity Opcodes,these strangers.
Understanding opcodes gives you deeper insight into gas costs, optimization opportunities, security considerations, and how contracts truly behave on-chain.
<100 subscribers

What's happening to Farcaster?
The pivot to a trader-focused-social-wallet, the drama, future of Farcaster and other Clients

Please Stop Making The Audience Fall Asleep At Your IRL Workshop
How to make engaging technical workshops on any topic and make people getting addicted to learning it

Solidity Opcodes,these strangers.
Understanding opcodes gives you deeper insight into gas costs, optimization opportunities, security considerations, and how contracts truly behave on-chain.


Solidity is designed for secure and deterministic computations on the blockchain. However, one common pitfall developers encounter is precision loss during arithmetic operations. This can lead to unexpected behaviors, financial inaccuracies, or even vulnerabilities.
In this article, we'll explore what precision loss is, why it occurs, common scenarios where it happens, strategies to avoid it, methods to ensure it doesn't affect your code.
Precision loss in Solidity refers to the inaccuracy or truncation of numerical values during computations, particularly when dealing with division or representations of fractional numbers.
Unlike languages with built-in floating-point support, Solidity uses integers for all arithmetic. This means any operation that would produce a fractional result gets rounded down to the nearest whole number, discarding the decimal part.
For example, consider a simple division:
uint256 a = 5;
uint256 b = 2;
uint256 result = a / b; // result = 2 (solidity truncates 2.5 to 2)Here, the expected mathematical result is 2.5, but Solidity outputs 2, losing 0.5 in precision. This isn't a bug. It's how integer arithmetic works. It can accumulate errors in financial calculations, such as token distributions or interest accruals.
Precision loss isn't limited to division; it can also occur in overflows, underflows, or when scaling numbers to simulate decimals (like using 18 decimal places for ERC-20 tokens).
Solidity's design prioritizes security and predictability over flexibility. Floating-point numbers are avoided because they can introduce non-determinism (due to hardware variations in IEEE 754 implementations) and potential exploits in consensus-based systems. Instead, all operations are performed with fixed-size integers, where:
Division is floor division: Any remainder is discarded. For instance, 7 / 3 = 2 (remainder 1 is lost).
No native decimals: To represent fractions, developers must use scaling factors (multiply by 10^decimals), but improper handling leads to truncation.
Overflow/Underflow risks: In versions before Solidity 0.8.0, unchecked arithmetic could wrap around uint8(255) + 1 = 0, exacerbating precision issues. Post version 0.8.0, operations revert on overflow by default.
The root cause is the absence of fractional representation. Numbers are treated as whole units, so operations like averaging or percentage calculations inherently lose detail unless mitigated.
Numerical example: Suppose you're calculating 10% of 123 tokens. Naively:
uint256 amount = 123;
uint256 percentage = 10;
uint256 result = (amount * percentage) / 100; // (1230) / 100 = 12 (loses 0.3, actual 12.3)The true value is 12.3, but you get 12, a 2.4% error relative to the expected amount.
Precision loss often surfaces any contract handling value distributions. Below are typical cases with code examples, followed by avoidance strategies.
In an airdrop, you might divide a total supply among users. Truncation can leave tokens undistributed.
Example Code (with Loss):
contract Airdrop {
function distribute(uint256 totalTokens, uint256 numUsers) public pure returns (uint256 tokensPerUser) {
return totalTokens / numUsers; // E.g., 1000 / 3 = 333 (loses 1 token, actual ~333.333 each)
}
}For 1000 tokens and 3 users: Each gets 333, totaling 999, 1 token lost forever.
Avoidance: Use Remainder Handling or Scaling: Distribute the base amount and handle remainders separately (like send to a treasury). Or scale up before dividing:
contract SafeAirdrop {
function distribute(uint256 totalTokens, uint256 numUsers) public pure returns (uint256 tokensPerUser, uint256 remainder) {
tokensPerUser = totalTokens / numUsers;
remainder = totalTokens % numUsers; // Capture lost part (e.g., 1000 % 3 = 1)
// In practice, add logic to distribute remainder
}
}This ensures accountability for every token.
Calculating compound interest or yields often involves fractions.
Example Code (with Loss):
contract Lending {
function calculateInterest(uint256 principal, uint256 rate) public pure returns (uint256) {
return (principal * rate) / 100; // E.g., 1000 * 5 / 100 = 50 (fine), but 1000 * 4 / 100 = 40 (actual 40)
// Issue amplifies with smaller rates: 1000 * 1 / 100 = 10 (fine), but uneven principals like 999 * 1 / 100 = 9 (loses 0.99)
}
}For 999 principal at 1%: Expected ~9.99, but gets 9.
Avoidance: Multiply by Scaling Factor First Use a higher precision denominator (e.g., basis points: 10000 for 0.01% granularity).
contract SafeLending {
uint256 constant BASIS_POINTS = 10000;
function calculateInterest(uint256 principal, uint256 rateInBasis) public pure returns (uint256) {
return (principal * rateInBasis) / BASIS_POINTS; // E.g., 999 * 100 / 10000 = 9 (for 1%), but for finer: 999 * 99 / 10000 ≈ 9 (still truncates, but closer)
}
}For even better precision, use libraries like ABDKMath64x64 for fixed-point arithmetic, which simulates floats with 64-bit integers.
Swapping assets (e.g., ETH to tokens) using oracles can truncate values.
Example Code (with Loss):
contract Exchange {
function convert(uint256 ethAmount, uint256 rate) public pure returns (uint256 tokenAmount) {
return ethAmount * rate / 1e18; // Assuming rate is scaled; e.g., 1 ETH = 2000 tokens, but if rate=2000e18, division truncates fractions
}
}If ethAmount=1 (1 wei), small fractions vanish.
Avoidance: Order Operations Carefully Always multiply before dividing to minimize loss:
contract SafeExchange {
function convert(uint256 ethAmount, uint256 rateNumerator, uint256 rateDenominator) public pure returns (uint256) {
return (ethAmount * rateNumerator) / rateDenominator; // Multiply first to preserve scale
}
}Check for overflow using SafeMath (from OpenZeppelin) in pre 0.8.0 Solidity:
import "@openzeppelin/contracts/math/SafeMath.sol";
contract WithSafeMath {
using SafeMath for uint256;
function safeOp(uint256 a, uint256 b) public pure returns (uint256) {
return a.mul(b).div(100); // Safe mul/div
}
}To prevent precision loss from slipping into production:
Unit Testing: Write tests asserting expected outputs, including edge cases (e.g., small/large numbers). Fuzz testing is much needed here!
// In a test file
function testDivision() public {
uint256 result = 5 / 2;
assertEq(result, 2); // Expected truncation
// But for safe version: assertEq(safeDivide(5, 2), 2); // Add custom checks
}Static Analysis: Tools like Slither or MythX can flag potential integer overflows or truncations.
Fuzz Testing: Use Echidna to input random values and check invariants (like total distributed == total supply).
Audits and Simulations: Run simulations with real numbers. For example, simulate 1000 iterations of interest accrual and verify cumulative loss < threshold.
Libraries and Best Practices: Always use OpenZeppelin's SafeMath or Math utilities. Monitor for reverts on overflow in Solidity >=0.8.0.
Monitoring: In deployed contracts, emit events for calculations and monitor off-chain for discrepancies.
Ask AI bro: Asking AI is a first approach to quickly scope if there are integer and precision loss issues
By integrating these into your CI/CD pipeline, you can catch issues early.
Precision loss in Solidity is a subtle yet critical issue stemming from its integer-only arithmetic, which prioritizes blockchain safety but demands careful handling from developers.
It commonly arises in distributions, financial computations, and conversions, but can be mitigated through operation ordering, scaling factors, remainder management, and robust libraries. Proactively testing and auditing your code ensures reliability, preventing costly errors in immutable smart contracts.
As DeFi and Web3 evolve, mastering these nuances is essential for building trustless, accurate systems.
Remember, on the blockchain, every wei counts. If you're developing a contract, start with thorough math proofs and end with comprehensive tests to safeguard against precision pitfalls.
Solidity is designed for secure and deterministic computations on the blockchain. However, one common pitfall developers encounter is precision loss during arithmetic operations. This can lead to unexpected behaviors, financial inaccuracies, or even vulnerabilities.
In this article, we'll explore what precision loss is, why it occurs, common scenarios where it happens, strategies to avoid it, methods to ensure it doesn't affect your code.
Precision loss in Solidity refers to the inaccuracy or truncation of numerical values during computations, particularly when dealing with division or representations of fractional numbers.
Unlike languages with built-in floating-point support, Solidity uses integers for all arithmetic. This means any operation that would produce a fractional result gets rounded down to the nearest whole number, discarding the decimal part.
For example, consider a simple division:
uint256 a = 5;
uint256 b = 2;
uint256 result = a / b; // result = 2 (solidity truncates 2.5 to 2)Here, the expected mathematical result is 2.5, but Solidity outputs 2, losing 0.5 in precision. This isn't a bug. It's how integer arithmetic works. It can accumulate errors in financial calculations, such as token distributions or interest accruals.
Precision loss isn't limited to division; it can also occur in overflows, underflows, or when scaling numbers to simulate decimals (like using 18 decimal places for ERC-20 tokens).
Solidity's design prioritizes security and predictability over flexibility. Floating-point numbers are avoided because they can introduce non-determinism (due to hardware variations in IEEE 754 implementations) and potential exploits in consensus-based systems. Instead, all operations are performed with fixed-size integers, where:
Division is floor division: Any remainder is discarded. For instance, 7 / 3 = 2 (remainder 1 is lost).
No native decimals: To represent fractions, developers must use scaling factors (multiply by 10^decimals), but improper handling leads to truncation.
Overflow/Underflow risks: In versions before Solidity 0.8.0, unchecked arithmetic could wrap around uint8(255) + 1 = 0, exacerbating precision issues. Post version 0.8.0, operations revert on overflow by default.
The root cause is the absence of fractional representation. Numbers are treated as whole units, so operations like averaging or percentage calculations inherently lose detail unless mitigated.
Numerical example: Suppose you're calculating 10% of 123 tokens. Naively:
uint256 amount = 123;
uint256 percentage = 10;
uint256 result = (amount * percentage) / 100; // (1230) / 100 = 12 (loses 0.3, actual 12.3)The true value is 12.3, but you get 12, a 2.4% error relative to the expected amount.
Precision loss often surfaces any contract handling value distributions. Below are typical cases with code examples, followed by avoidance strategies.
In an airdrop, you might divide a total supply among users. Truncation can leave tokens undistributed.
Example Code (with Loss):
contract Airdrop {
function distribute(uint256 totalTokens, uint256 numUsers) public pure returns (uint256 tokensPerUser) {
return totalTokens / numUsers; // E.g., 1000 / 3 = 333 (loses 1 token, actual ~333.333 each)
}
}For 1000 tokens and 3 users: Each gets 333, totaling 999, 1 token lost forever.
Avoidance: Use Remainder Handling or Scaling: Distribute the base amount and handle remainders separately (like send to a treasury). Or scale up before dividing:
contract SafeAirdrop {
function distribute(uint256 totalTokens, uint256 numUsers) public pure returns (uint256 tokensPerUser, uint256 remainder) {
tokensPerUser = totalTokens / numUsers;
remainder = totalTokens % numUsers; // Capture lost part (e.g., 1000 % 3 = 1)
// In practice, add logic to distribute remainder
}
}This ensures accountability for every token.
Calculating compound interest or yields often involves fractions.
Example Code (with Loss):
contract Lending {
function calculateInterest(uint256 principal, uint256 rate) public pure returns (uint256) {
return (principal * rate) / 100; // E.g., 1000 * 5 / 100 = 50 (fine), but 1000 * 4 / 100 = 40 (actual 40)
// Issue amplifies with smaller rates: 1000 * 1 / 100 = 10 (fine), but uneven principals like 999 * 1 / 100 = 9 (loses 0.99)
}
}For 999 principal at 1%: Expected ~9.99, but gets 9.
Avoidance: Multiply by Scaling Factor First Use a higher precision denominator (e.g., basis points: 10000 for 0.01% granularity).
contract SafeLending {
uint256 constant BASIS_POINTS = 10000;
function calculateInterest(uint256 principal, uint256 rateInBasis) public pure returns (uint256) {
return (principal * rateInBasis) / BASIS_POINTS; // E.g., 999 * 100 / 10000 = 9 (for 1%), but for finer: 999 * 99 / 10000 ≈ 9 (still truncates, but closer)
}
}For even better precision, use libraries like ABDKMath64x64 for fixed-point arithmetic, which simulates floats with 64-bit integers.
Swapping assets (e.g., ETH to tokens) using oracles can truncate values.
Example Code (with Loss):
contract Exchange {
function convert(uint256 ethAmount, uint256 rate) public pure returns (uint256 tokenAmount) {
return ethAmount * rate / 1e18; // Assuming rate is scaled; e.g., 1 ETH = 2000 tokens, but if rate=2000e18, division truncates fractions
}
}If ethAmount=1 (1 wei), small fractions vanish.
Avoidance: Order Operations Carefully Always multiply before dividing to minimize loss:
contract SafeExchange {
function convert(uint256 ethAmount, uint256 rateNumerator, uint256 rateDenominator) public pure returns (uint256) {
return (ethAmount * rateNumerator) / rateDenominator; // Multiply first to preserve scale
}
}Check for overflow using SafeMath (from OpenZeppelin) in pre 0.8.0 Solidity:
import "@openzeppelin/contracts/math/SafeMath.sol";
contract WithSafeMath {
using SafeMath for uint256;
function safeOp(uint256 a, uint256 b) public pure returns (uint256) {
return a.mul(b).div(100); // Safe mul/div
}
}To prevent precision loss from slipping into production:
Unit Testing: Write tests asserting expected outputs, including edge cases (e.g., small/large numbers). Fuzz testing is much needed here!
// In a test file
function testDivision() public {
uint256 result = 5 / 2;
assertEq(result, 2); // Expected truncation
// But for safe version: assertEq(safeDivide(5, 2), 2); // Add custom checks
}Static Analysis: Tools like Slither or MythX can flag potential integer overflows or truncations.
Fuzz Testing: Use Echidna to input random values and check invariants (like total distributed == total supply).
Audits and Simulations: Run simulations with real numbers. For example, simulate 1000 iterations of interest accrual and verify cumulative loss < threshold.
Libraries and Best Practices: Always use OpenZeppelin's SafeMath or Math utilities. Monitor for reverts on overflow in Solidity >=0.8.0.
Monitoring: In deployed contracts, emit events for calculations and monitor off-chain for discrepancies.
Ask AI bro: Asking AI is a first approach to quickly scope if there are integer and precision loss issues
By integrating these into your CI/CD pipeline, you can catch issues early.
Precision loss in Solidity is a subtle yet critical issue stemming from its integer-only arithmetic, which prioritizes blockchain safety but demands careful handling from developers.
It commonly arises in distributions, financial computations, and conversions, but can be mitigated through operation ordering, scaling factors, remainder management, and robust libraries. Proactively testing and auditing your code ensures reliability, preventing costly errors in immutable smart contracts.
As DeFi and Web3 evolve, mastering these nuances is essential for building trustless, accurate systems.
Remember, on the blockchain, every wei counts. If you're developing a contract, start with thorough math proofs and end with comprehensive tests to safeguard against precision pitfalls.
Share Dialog
Share Dialog
No comments yet