Vyper’s Fixed‑Point Decimal: A Pragmatic Alternative to Solidity’s 1e18 Scaling
Vyper’s fixed‑point <code>decimal</code> type lets you write ratios like 0.05 directly, eliminating 1e18 scaling hacks. This blog walks through a liquidation‑threshold example, compares decimal vs. integer math, and gives practical guidance on when to use each.
17 Sept 2026, 10:55 UTC

Why the 1e18 Scaling Pattern Still Feels Like a Bad Habit
In Solidity you’ll find a lot of code that looks like this:
uint256 constant THRESHOLD = 5 * 10**18; // 5% expressed in “wad” units
function isLiquidatable(uint256 collateral, uint256 debt) public pure returns (bool) {
return collateral <= debt * THRESHOLD / 10**18;
}
While this works, it forces developers to remember that every “ratio” is actually a huge integer that must be scaled back by 1018 when used. A single typo, a forgotten division, or a mismatch in the scaling factor can silently produce wrong results, and auditors spend a lot of time chasing those bugs.
Enter Vyper’s decimal type
Vyper offers a decimal type – a signed fixed‑point number with exactly ten decimal places. Internally it is stored as a 128‑bit integer, but the language lets you write literals like 0.05 or 1.2 directly. The compiler automatically scales the value to the 1010 base, so you can reason about the math in the same units you see on paper.
Key properties:
- Deterministic fixed‑point arithmetic – no floating‑point rounding issues.
- Compile‑time type checking catches unit mismatches.
- Maximum value ≈ 1.7 × 1028 (128‑bit signed).
- Operations use 128‑bit math, so they are safe for moderate‑size numbers but can overflow silently if you exceed the range.
Concrete Use Case: Liquidation Threshold in a Lending Protocol
Let’s walk through a typical health‑factor calculation. In a simplified model you want to check whether the value of a user’s collateral is below a threshold times the value of their debt:
@external
@view
def is_liquidatable(collateral_value: decimal, debt_value: decimal, liquidation_threshold: decimal) -> bool:
# The comparison is mathematically clean – no scaling hacks.
return collateral_value <= debt_value * liquidation_threshold
Because decimal is a distinct type, the compiler will reject a call that mixes a uint256 token balance with a decimal ratio without an explicit conversion. This reduces the chance of accidental unit mismatches.
Practical Steps to Verify the Example
- Compile with a specific Vyper version (e.g., 0.3.9):
vyper --version vyper --output-format=abi example.vyCheck that the ABI shows
decimalfor the function parameters. - Run a unit test that exercises boundary values:
def test_decimal_overflow(): max_val = 1.7e28 # close to the 128‑bit limit contract.is_liquidatable(max_val, 1, 0.05) # Expect an exception or a silent overflow – observe the behavior. - Measure gas on a local node:
eth_sendTransaction({from: addr, to: contract_address, data: contract.is_liquidatable.encode_input(max_val, 1, 0.05)})Compare the gas cost of this
decimalmultiplication against a hand‑scaleduint256version.
Trade‑Offs: Readability vs. Gas & Range
While decimal eliminates the need for 1018 magic numbers, it introduces its own overhead:
- Gas: 128‑bit arithmetic is roughly twice as expensive as 256‑bit operations that Solidity uses for token math.
- Range: The 128‑bit limit means you can’t use
decimalfor large token balances or very high‑precision timestamps. - Precision: Fixed at ten decimal places – if you need 18‑decimal precision for ERC‑20 amounts, you must stay with
uint256and manual scaling.
For hot paths such as AMM swaps or high‑frequency interest accrual, the extra gas can add up. In those cases, sticking with integer math and explicit wad (1018) or ray (1027) scaling libraries is still the pragmatic choice.
When to Prefer decimal Over Integer Scaling
- Configuration constants – liquidation thresholds, risk weights, and other ratios that are read‑only and used in comparisons.
- Off‑chain‑verified parameters – values that are signed by an external oracle and expressed as percentages.
- Auditable business logic – when you want the source code to read like the mathematical formula, making audits faster.
Conversely, keep uint256 for:
- Token balances and amounts that need 18‑decimal precision.
- High‑frequency loops where gas is critical.
- Cross‑chain or cross‑language contracts that rely on Solidity’s standard integer math.
Actionable Checklist Before You Commit
- Pin your Vyper compiler version and read the release notes for
decimalchanges. - Run unit tests covering edge cases (near max, near zero).
- Benchmark gas on a representative workload.
- Document type boundaries in your codebase – clearly separate
decimalratios fromuint256token amounts. - Consider a migration strategy if you already have a Solidity codebase – introduce
decimalin new contracts, not replace existing integer math.
In summary, Vyper’s decimal type is a powerful tool for clarity and safety when dealing with rates and ratios. It shines in configuration and audit‑heavy logic, but for performance‑critical numeric paths, the tried‑and‑true integer scaling remains the better choice.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.