vyperlang / vyper

Pythonic Smart Contract Language for the EVM
https://vyperlang.org
Other
4.86k stars 793 forks source link

Request for Comment: `safe` and `unsafe` math operations #3481

Open bout3fiddy opened 1 year ago

bout3fiddy commented 1 year ago

Simple Summary

This VIP introduces an alternative syntax for unsafe math operations. The goal is to improve the readability of complex mathematical expressions without compromising auditability. To that extent we propose safe and unsafe builtin methods for safe as well as unsafe operations.

Motivation

Simple expressions like a: uint256 = unsafe_add(b, c) are straightforward in Vyper. However, readability decreases when complex expressions involve multiple unsafe operations (code snippet taken from CurveCryptoMathOptimized3.vy):

K0 = unsafe_div(
    unsafe_mul(
        unsafe_mul(
            unsafe_div(
                unsafe_mul(
                    unsafe_mul(
                        unsafe_div(
                            unsafe_mul(
                                unsafe_mul(10**18, x[0]), N_COINS
                            ),
                            D,
                        ),
                        x[1],
                    ),
                    N_COINS,
                ),
                D,
            ),
            x[2],
        ),
        N_COINS,
    ),
    D,
)

In such situations, Solidity's inline assembly blocks can be more readable than Vyper's unsafe_ops(...).

Finally, expressions with a mix of safe and unsafe operations are particularly challenging to read (code snippet from CurveCryptoMathOptimized3.vy):

GK0: uint256 = (
    unsafe_div(unsafe_div(2 * K0 * K0, 10**36) * K0, 10**36)
    + pow_mod256(unsafe_add(_A_gamma[1], 10**18), 2)
    - unsafe_div(
        unsafe_div(pow_mod256(K0, 2), 10**36) * unsafe_add(unsafe_mul(2, _A_gamma[1]), 3 * 10**18),
        10**18
    )
)

While long-term compiler optimizations can eventually address these readability issues, we propose a short-term solution here.

Specification

Readable Expressions with Unsafe Operations

For straightforward expressions involving unsafe operations, we retain the existing unsafe_ops syntax for its readability:

a: uint256 = unsafe_add(b, c)

but unsafe can also be used here:

a: uint256 = unsafe(b + c)

Entirely Unsafe Expression

When an entire expression is unsafe, we suggest borrowing Solidity's approach:

K0 = unsafe(10**18 * x[0] * N_COINS / D * x[1] * N_COINS / D * x[2] * N_COINS / D)

such that it is clear the entire expression is evaluated using unsafe math operations.

This would then introduce unsafe builtin that does only unsafe operations with the expression inside it.

Expressions Combining Safe and Unsafe Operations

We propose a safe builtin method that can be used inside unsafe expressions.

GK0: uint256 = (
    unsafe(
        safe(unsafe(safe(2 * K0 * K0) / 10**36) * K0) / 10**36
    )
    + unsafe((_A_gamma[1] + 10**18) ** 2)
    - unsafe(
        unsafe(K0 ** 2 / 10**36) * unsafe(2 * _A_gamma[1] + safe(3 * 10**18)) / 10**18
    )
)

Backwards Compatibility

Syntactically, this would not be a breaking change since it does not change how unsafe_mul etc. are handled, and only introduces unsafe(...expression * safe (...expression)).

michwill commented 1 year ago

Yes ser, bring it!

Vectorized commented 1 year ago

Not sure how it will translate to Python syntax.

Alternatively:

with unsafe_math():
    K0 = 10**18 * x[0] * N_COINS / D * x[1] * N_COINS / D * x[2] * N_COINS / D

The __enter__() and __exit__() method for the unsafe_math() object may be used to turn off the safe math checks for the operator overloads.

It can also allow for optional arguments:

with unsafe_math(allow_zero_denominator=True):
    K0 = 10**18 * x[0] * N_COINS / D * x[1] * N_COINS / D * x[2] * N_COINS / D
z80dev commented 1 year ago

what about +!, -!, /!, and *! as aliases for the unsafe version of each of these operators

so that

GK0: uint256 = (
    unsafe(
        safe(unsafe(safe(2 * K0 * K0) / 10**36) * K0) / 10**36
    )
    + unsafe((_A_gamma[1] + 10**18) ** 2)
    - unsafe(
        unsafe(K0 ** 2 / 10**36) * unsafe(2 * _A_gamma[1] + safe(3 * 10**18)) / 10**18
    )
)

becomes

GK0: uint256 = (((2 * K0 * K0) /! 10**36) * K0) /! 10**36) 
    + ((_A_gamma[1] +! 10**18) ** 2) 
    - ((K0 ** 2 /! 10 ** 36) * (2 *! _A_gamma[1] +! (3 * 10**18) /! 10**18)
bout3fiddy commented 1 year ago

@z80dev Not bad! This looks more readable! Question: why would the ! come before or after the operator? Does that matter?