Actualites·9 min de lecture·Par Solingo

Solidity 0.8.29 — User-Defined Operators and What They Mean for DeFi

Explore the groundbreaking user-defined operators feature in Solidity 0.8.29 and how it will transform DeFi protocol development.

# Solidity 0.8.29 — User-Defined Operators and What They Mean for DeFi

The Solidity team has released version 0.8.29, introducing one of the most requested features in years: user-defined operators. This feature fundamentally changes how we write financial logic in smart contracts, bringing mathematical elegance and safety to DeFi protocols.

What Are User-Defined Operators?

User-defined operators allow you to overload mathematical and comparison operators for custom types. Instead of calling methods like amount.add(fee), you can write amount + fee with your own types, just like in languages such as Python, C++, and Rust.

// Before 0.8.29

FixedPoint memory result = FixedPointMath.mul(

FixedPointMath.div(price, liquidity),

amount

);

// After 0.8.29 with user-defined operators

FixedPoint result = (price / liquidity) * amount;

This isn't just syntactic sugar — it's a paradigm shift for financial mathematics in smart contracts.

Basic Syntax and Examples

Defining a Custom Type with Operators

// Define a fixed-point decimal type (18 decimals)

type Decimal is uint256;

using {add as +, sub as -, mul as *, div as /} for Decimal global;

function add(Decimal a, Decimal b) pure returns (Decimal) {

return Decimal.wrap(Decimal.unwrap(a) + Decimal.unwrap(b));

}

function sub(Decimal a, Decimal b) pure returns (Decimal) {

return Decimal.wrap(Decimal.unwrap(a) - Decimal.unwrap(b));

}

function mul(Decimal a, Decimal b) pure returns (Decimal) {

return Decimal.wrap(

(Decimal.unwrap(a) * Decimal.unwrap(b)) / 1e18

);

}

function div(Decimal a, Decimal b) pure returns (Decimal) {

return Decimal.wrap(

(Decimal.unwrap(a) * 1e18) / Decimal.unwrap(b)

);

}

Now you can use these types naturally:

contract DEX {

function calculateSwapOutput(

Decimal memory price,

Decimal memory amountIn,

Decimal memory fee

) public pure returns (Decimal memory) {

Decimal memory feeAmount = amountIn * fee;

Decimal memory amountAfterFee = amountIn - feeAmount;

return price * amountAfterFee;

}

}

Real-World DeFi Use Cases

1. Fixed-Point Math in AMMs

Uniswap and other AMMs rely heavily on fixed-point arithmetic. User-defined operators make this code dramatically more readable:

type FixedPoint96 is uint256;

using {

add as +,

sub as -,

mul as *,

div as /,

lt as <,

lte as <=,

gt as >,

gte as >=,

eq as ==,

neq as !=

} for FixedPoint96 global;

// Implement comparison operators

function lt(FixedPoint96 a, FixedPoint96 b) pure returns (bool) {

return FixedPoint96.unwrap(a) < FixedPoint96.unwrap(b);

}

function eq(FixedPoint96 a, FixedPoint96 b) pure returns (bool) {

return FixedPoint96.unwrap(a) == FixedPoint96.unwrap(b);

}

// AMM price calculation becomes intuitive

function getPrice(

FixedPoint96 reserve0,

FixedPoint96 reserve1

) pure returns (FixedPoint96) {

require(reserve1 > FixedPoint96.wrap(0), "Division by zero");

return reserve0 / reserve1;

}

function applyPriceImpact(

FixedPoint96 currentPrice,

FixedPoint96 slippage

) pure returns (FixedPoint96 minPrice, FixedPoint96 maxPrice) {

FixedPoint96 impact = currentPrice * slippage;

minPrice = currentPrice - impact;

maxPrice = currentPrice + impact;

}

2. Interest Rate Calculations in Lending Protocols

Compound and Aave use complex interest rate models. With operators, these become clearer:

type InterestRate is uint256; // Basis points (1e18 = 100%)

using {add as +, sub as -, mul as *, div as /} for InterestRate global;

contract LendingPool {

InterestRate public constant BASE_RATE = InterestRate.wrap(0.02e18); // 2%

InterestRate public constant OPTIMAL_UTIL = InterestRate.wrap(0.8e18); // 80%

function calculateBorrowRate(

uint256 totalBorrows,

uint256 totalLiquidity

) public pure returns (InterestRate) {

if (totalLiquidity == 0) return BASE_RATE;

InterestRate utilization = InterestRate.wrap(

(totalBorrows * 1e18) / totalLiquidity

);

if (utilization <= OPTIMAL_UTIL) {

// Linear increase below optimal

InterestRate slope = InterestRate.wrap(0.1e18); // 10%

return BASE_RATE + (utilization * slope);

} else {

// Steep increase above optimal

InterestRate excessUtil = utilization - OPTIMAL_UTIL;

InterestRate steepSlope = InterestRate.wrap(0.5e18); // 50%

return BASE_RATE + (OPTIMAL_UTIL * InterestRate.wrap(0.1e18))

+ (excessUtil * steepSlope);

}

}

}

3. Options Pricing with Black-Scholes

Complex financial models become implementable:

type Price is uint256;

type Volatility is uint256;

type TimeToExpiry is uint256;

using {mul as *, div as /, add as +, sub as -} for Price global;

using {mul as *, div as /} for Volatility global;

using {mul as *, sqrt} for TimeToExpiry global;

function sqrt(TimeToExpiry t) pure returns (TimeToExpiry) {

uint256 unwrapped = TimeToExpiry.unwrap(t);

uint256 result = unwrapped / 2;

// Newton's method for sqrt

for (uint i = 0; i < 10; i++) {

result = (result + unwrapped / result) / 2;

}

return TimeToExpiry.wrap(result);

}

contract OptionsOracle {

function calculateVolatilityAdjustment(

Price spotPrice,

Volatility vol,

TimeToExpiry timeLeft

) public pure returns (Price) {

// σ * S * √t pattern from Black-Scholes

return spotPrice * Price.wrap(Volatility.unwrap(vol))

* Price.wrap(TimeToExpiry.unwrap(timeLeft.sqrt()));

}

}

Performance and Gas Considerations

User-defined operators compile to regular function calls, so there's no gas overhead compared to calling functions directly:

// These compile to identical bytecode:

Decimal result1 = mul(a, b);

Decimal result2 = a * b;

However, you get significant benefits:

  • Compile-time type safety: Can't accidentally mix different decimal precisions
  • Better optimization: Compiler can inline and optimize operator chains
  • Readability: Fewer bugs due to clearer intent
  • Safety Benefits for DeFi

    Type Safety Prevents Precision Errors

    One of the most common bugs in DeFi is mixing different decimal precisions:

    // Before: Easy to mix up USDC (6 decimals) and DAI (18 decimals)
    

    uint256 totalValue = usdcBalance + daiBalance; // BUG!

    // After: Type system prevents this

    type USDC is uint256; // 6 decimals

    type DAI is uint256; // 18 decimals

    using {add as +} for USDC global;

    using {add as +} for DAI global;

    // This won't compile - different types!

    USDC total = usdcBalance + daiBalance; // ERROR!

    // Must explicitly convert

    function toDAI(USDC amount) pure returns (DAI) {

    return DAI.wrap(USDC.unwrap(amount) * 1e12);

    }

    DAI totalInDAI = toDAI(usdcBalance) + daiBalance; // Correct!

    Preventing Over/Underflows in Custom Math

    type SafeUint is uint256;
    
    

    using {add as +, sub as -} for SafeUint global;

    function add(SafeUint a, SafeUint b) pure returns (SafeUint) {

    uint256 result = SafeUint.unwrap(a) + SafeUint.unwrap(b);

    require(result >= SafeUint.unwrap(a), "Overflow");

    return SafeUint.wrap(result);

    }

    function sub(SafeUint a, SafeUint b) pure returns (SafeUint) {

    require(SafeUint.unwrap(a) >= SafeUint.unwrap(b), "Underflow");

    return SafeUint.wrap(SafeUint.unwrap(a) - SafeUint.unwrap(b));

    }

    Migration Strategy for Existing Protocols

    If you're maintaining a DeFi protocol, here's how to adopt this feature:

    1. Create Type Wrappers

    // Wrapper for existing FixedPointMath library
    

    import "./FixedPointMath.sol";

    type FP is uint256;

    using {

    add as +,

    sub as -,

    mul as *,

    div as /

    } for FP global;

    function add(FP a, FP b) pure returns (FP) {

    return FP.wrap(FixedPointMath.add(FP.unwrap(a), FP.unwrap(b)));

    }

    function mul(FP a, FP b) pure returns (FP) {

    return FP.wrap(FixedPointMath.mul(FP.unwrap(a), FP.unwrap(b)));

    }

    2. Gradual Adoption

    // Old code continues to work
    

    uint256 oldStyleResult = FixedPointMath.mul(a, b);

    // New code uses operators

    FP newStyleResult = FP.wrap(a) * FP.wrap(b);

    // Mix both during transition

    uint256 mixed = FP.unwrap(FP.wrap(a) * FP.wrap(b));

    3. Test Thoroughly

    User-defined operators are new, so comprehensive testing is critical:

    import "forge-std/Test.sol";
    
    

    contract DecimalTest is Test {

    function testAddition() public {

    Decimal a = Decimal.wrap(1.5e18);

    Decimal b = Decimal.wrap(2.3e18);

    Decimal result = a + b;

    assertEq(Decimal.unwrap(result), 3.8e18);

    }

    function testFuzz_Multiplication(uint128 a, uint128 b) public {

    Decimal da = Decimal.wrap(uint256(a));

    Decimal db = Decimal.wrap(uint256(b));

    Decimal result = da * db;

    // Verify against reference implementation

    assertEq(

    Decimal.unwrap(result),

    (uint256(a) * uint256(b)) / 1e18

    );

    }

    }

    The Future of DeFi Development

    User-defined operators represent a maturation of Solidity as a language for financial applications. We'll likely see:

  • Standard libraries of financial types (Decimal, Percentage, BasisPoints)
  • Framework adoption — Foundry, Hardhat adding type-aware tooling
  • Safer protocols — Type systems catching bugs at compile time
  • Better audits — Clearer code means faster, more accurate security reviews
  • Protocols like Uniswap v4, Morpho Blue, and Aave v4 are already exploring these patterns. The DeFi ecosystem is about to get a major upgrade in code quality and safety.

    Conclusion

    Solidity 0.8.29's user-defined operators are more than a convenience feature — they're a fundamental improvement to how we write financial smart contracts. By combining type safety with mathematical clarity, this feature will reduce bugs, improve auditability, and make DeFi protocols safer for users.

    Start experimenting with user-defined operators in your test environment today. The future of DeFi development is here.

    Prêt à mettre en pratique ?

    Applique ces concepts avec des exercices interactifs sur Solingo.

    Commencer gratuitement