What Are Arithmetic Operations?
Arithmetic operations are the basic math calculations you learned in school: addition (+), subtraction (-), multiplication (*), division (/), and sometimes bit-shifting (like moving numbers around in binary). In programming, these operations are used to calculate things like fees, balances, or rewards in apps, including blockchain applications.In the Move programming language (designed for secure blockchain smart contracts, these operations are critical because they often deal with money or valuable assets. If the math isn’t handled carefully, small mistakes can lead to big problems, like losing money or giving someone an unfair advantage.
What Is Division Precision, and Why Does It Matter?
When you divide numbers, sometimes the result isn’t a whole number. For example, 7 ÷ 2 = 3.5. In many programming languages, including Move, division between integers (whole numbers like 1, 2, or 100) doesn’t keep the decimal part—it rounds down to the nearest whole number. So, 7 ÷ 2 in Move would give you 3, not 3.5. This is called truncation.
While this might sound harmless, it can cause issues in blockchain applications where precision is critical. For example, if you’re calculating fees for a transaction, rounding down might accidentally make the fee zero, letting someone use a service for free! This loss of precision is called a rounding error, and it can lead to:
Financial imbalances: Someone might pay less (or nothing) when they should owe money.
Data inaccuracies: Incorrect calculations can mess up records or reports.
Flawed decisions: Systems relying on these calculations might make wrong choices, like approving invalid transactions.
Safety risks: In extreme cases (like in medical or automotive systems), errors could cause serious harm.In blockchain, where trust and accuracy are everything, these errors can erode user confidence or even allow bad actors to exploit the system.
Arithmetic in Move:
The Basics Move supports six types of unsigned integers (numbers that are always zero or positive, like 0, 1, 10, or 100).
These are:
u8: Can store numbers from 0 to 255.
u16: Up to 65,535
u32: Up to about 4.3 billion
u64: Up to about 18 quintillion
u128: Even bigger numbers
u256: For extremely large numbers.
When you do math with these numbers in Move, the language has built-in safety features to prevent common errors:
Addition (+) and Multiplication (*): If the result is too big for the integer type (e.g., adding two huge numbers), the program aborts (stops running) to prevent incorrect results.
Subtraction (-): If the result would be negative (e.g., 5 - 10), the program aborts because unsigned integers can’t be negative.
Division (/): If you try to divide by zero, the program aborts.
Left Shift (<<): This operation shifts bits in a number (like multiplying by powers of 2). Unlike the others, it doesn’t abort if the result is too big—it just produces an incorrect value, which can be dangerous if not handled properly.
These safety features are great, but they don’t solve the problem of division precision. Let’s dive into a real-world example to see why this matters.
Real-World Example: Calculating Fees in a Blockchain Protocol
Imagine you’re building a decentralized marketplace on a blockchain where users pay a small fee (say, 1%) for every transaction. The fee is calculated using a formula like this:
Fee = Transaction Size * Fee Percentage / 100
In Move, this might be written as code that uses integers. Let’s say the fee percentage is represented as 100 “basis points” (where 1% = 100 basis points, or PROTOCOL_FEE_BPS = 100).
Here’s what the code might look like:
module 0x42::example {
const PROTOCOL_FEE_BPS: u64 = 100; // 1% fee
public fun calculate_protocol_fees(size: u64): u64 {
return size * PROTOCOL_FEE_BPS / 10000; // 100 basis points = 1%
}
}
What’s the problem? If the size (transaction amount) is small, the division can round down to zero. For example:If size = 50 tokens, the calculation is:50 * 100 / 10000 = 5000 / 10000 = 0.5In Move, this rounds down to 0.
This means a user could make a small transaction and pay no fee at all, which could let them exploit the system by making lots of tiny transactions for free. In a real marketplace, this could lead to thousands of dollars in lost revenue or even crash the system if someone spams it with tiny transactions.
How to Fix It: Secure Coding Practices
To prevent this issue, you need to ensure the fee calculation doesn’t round down to zero.
Here are two ways to fix the code,
explained simply:
Fix 1: Set a Minimum Transaction Size You can require that transactions be large enough so the fee never rounds down to zero.
For example, if the fee is 1% (100 basis points), the minimum transaction size should be at least 10000 / 100 + 1 = 101 to ensure the fee is at least 1.
Here’s the secure code:
module 0x42::example {
const PROTOCOL_FEE_BPS: u64 = 100; // 1% fee
const MIN_ORDER_SIZE: u64 = 10000 / PROTOCOL_FEE_BPS + 1; // 101
public fun calculate_protocol_fees(size: u64): u64 {
assert!(size >= MIN_ORDER_SIZE, 0); // Check size is big enough
return size * PROTOCOL_FEE_BPS / 10000;
}
}
What’s happening here?
The MIN_ORDER_SIZE is calculated as 10000 / 100 + 1 = 101.
The assert! line checks that the transaction size is at least 101. If it’s smaller, the program aborts (stops) with an error.This ensures the fee is always at least 1, preventing free transactions.
Real-world analogy:
Imagine a coffee shop that charges a 1% service fee but requires you to spend at least $10. If you only buy a $1 coffee, they won’t let you pay because the fee would be too small (like 1 cent, which might round to zero in their system). This rule ensures they always collect something.
Fix 2: Check for Non-Zero Fees
Another approach is to calculate the fee and then check if it’s zero. If it is, you can either reject the transaction or set a minimum fee (e.g., 1 token).
Here’s the code:
module 0x42::example {
const PROTOCOL_FEE_BPS: u64 = 100; // 1% fee
public fun calculate_protocol_fees(size: u64): u64 {
let fee = size * PROTOCOL_FEE_BPS / 10000;
assert!(fee > 0, 0); // Ensure fee isn’t zero
return fee;
}
}
What’s happening here?
The code calculates the fee as before.
The assert!(fee > 0, 0) line checks if the fee is zero.
If it is, the program aborts.
This prevents users from making transactions that result in no fee.
Real-world analogy:
Think of a toll booth on a highway. If your toll calculates to zero (maybe because your car is super small), the booth operator says, “Sorry, you can’t pass unless you pay at least something.” This ensures the toll system always collects a fee.
Why Move’s Safety Features Matters
Move’s strict rules about integer operations (like aborting on overflow or division by zero) are designed to make smart contracts safer. For example:
If you try to add two huge numbers and the result is too big for a u64, the program stops instead of giving a wrong answer.If you try to subtract a larger number from a smaller one (e.g., 5 - 10), the program stops because negative numbers aren’t allowed in unsigned integers.
If you try to divide by zero, the program stops to avoid crashing the system.However, the left shift (<<) operation is an exception—it doesn’t abort if the result is too big, which can lead to incorrect calculations. For example, shifting a number too far could “lose” bits, producing a wrong value. Developers need to be extra careful with this operation.
Why This Matters in the Real WorldLet’s
look at a few real-world scenarios where these issues could cause problems.
Decentralized Finance (DeFi)
In a DeFi lending platform, fees are charged on loans. If division rounding causes fees to be zero for small loans, users could borrow tiny amounts repeatedly without paying anything, draining the platform’s revenue.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.