openlang

Solidity reverts on overflow, Rust release builds wrap

Since Solidity 0.8, arithmetic reverts on overflow and underflow. Turning that off takes an explicit unchecked { ... } block. Rust goes the other way. A debug build panics, but build with --release and the default is two's complement wrapping. You only get checks in release if the Cargo profile sets overflow-checks = true. In C signed overflow is undefined behavior, and the compiler may simply delete a check written after the fact.

For code that moves balances I think Solidity has the right default. A wrapped result gets stored as a wrong balance. A revert rolls back the whole transaction. A failure beats a wrong number that stays forever.

Both have the same hole, though. In Solidity an explicit conversion truncates even in checked mode, so uint128(x) does not revert. Rust's as truncates silently too. A checked addition followed by a narrowing cast is unchecked in practice.

So the rule I've settled on. No unchecked in amount arithmetic. Checks on in the Rust release profile. Amount casts only through try_from. Has anyone kept overflow-checks on in release builds? I'm curious whether the cost is noticeable.

后续讨论 4

顺着彼此的依据读下去
  1. 固定链接

    The two languages put the check in different places. In Solidity the syntax sets the default, and the exception shows up in the code as an unchecked block. Rust's overflow-checks is a profile setting, so from the line a + b alone you can't tell whether that operation panics or wraps. The same source becomes a different program depending on build settings.

    For the few lines that move an amount, checked_add beats leaning on the profile. The path where None comes back is visible at the call site, and the behavior stays the same whoever edits the release profile. The extra length is a cost I'd accept for code this narrow. Profile checks can sit underneath as a safety net.

    Casts, as han_seo says, get past either setting. Every as in the amount path should turn up in one search. Count what's left and you can see at once whether the rule holds.

  2. 固定链接

    To a Zig user, unchecked reads as an assertion, not a performance switch. It claims nothing in the block can overflow, and without a reason it has the same problem as Zig's catch unreachable. The outcomes differ. In Zig's ReleaseFast an overflow is unchecked illegal behavior, while Solidity's unchecked leaves a wrapped value. Either way the value is wrong.

    Revert isn't automatically safe either. The Solidity security considerations page says that if an overflow can't be avoided, a contract can end up stuck in a certain state. Once an accumulator that only grows hits its limit, every function that adds to it reverts. Handing the failure to the caller is correct. If the caller can do nothing with it, it hasn't been handled.

    So unchecked needs its reason written next to it. A loop counter bounded by an array length is one, and balances rarely come with anything like it.

  3. 固定链接

    Two tests for anyone who wants to see the profile line in action, with overflow-checks = true under [profile.release].

    use std::hint::black_box;
    
    #[test]
    #[should_panic(expected = "attempt to add with overflow")]
    fn add_overflows() {
        let x: u64 = black_box(u64::MAX);
        let _ = x + 1;
    }
    
    #[test]
    fn cast_truncates() {
        let x: u64 = black_box(1 << 32);
        assert_eq!(x as u32, 0);
    }

    Without black_box it doesn't build. Two constants, and the arithmetic_overflow lint rejects the addition at compile time. With it, both tests pass in release. Remove the profile line and the first test fails, because nothing panics.

    So the profile line does exactly what alden described, and the cast truncates as the root said. The amount path now uses checked_add and try_from anyway, with the profile line left in as the backstop.

  4. 固定链接

    The search for every as that I asked for can be a lint instead.

    #[deny(clippy::cast_possible_truncation)]
    pub mod amount {
        pub fn narrow(x: u64) -> u32 {
            x as u32 // error under cargo clippy
        }
    
        pub fn widen(x: u32) -> u64 {
            x as u64 // passes
        }
    }

    Under cargo clippy the narrowing cast is an error and the help text points at try_from. The widening cast passes. That is the distinction the amount rule needs, and a plain text search cannot make it. cargo build ignores the attribute, so the lint protects the amount path only where clippy actually runs. If clippy runs on every change, the remaining as count stops depending on someone remembering to search.

同一个问题的其他角度

  1. Testing a Go channel handoff without sleep

    Most handoff tests I run into end with a sleep and a hopeful assertion. This one can do without. A request carries an identif…

    回复 23最近活动 go
  2. partial def is the honest answer for Collatz

    Lean has to know a recursive def terminates before it accepts it as a definition. Structural recursion is found automatically…

    回复 10最近活动 lean-4
  3. Why did the compiler delete my signed overflow check?

    if (x + 1 < x) on a signed int is not an overflow guard. Signed overflow is undefined behavior in C, so the compiler may assu…

    回复 4最近活动 c