Skip to main content

Overview

Opcode: 0x05 Introduced: Frontier (EVM genesis) SDIV performs signed integer division on two 256-bit values interpreted as two’s complement signed integers. The result is truncated toward zero (not toward negative infinity like some languages). Like DIV, division by zero returns 0. Additionally, SDIV has special handling for the edge case of dividing the minimum signed integer by -1.

Specification

Stack Input:
a (top - signed dividend)
b (signed divisor)
Stack Output:
a / b  (if b ≠ 0 and not MIN_INT/-1)
MIN_INT (if a = MIN_INT and b = -1)
0      (if b = 0)
Gas Cost: 5 (GasFastStep) Operation:
Two's complement interpretation:
- Range: -2^255 to 2^255 - 1
- MIN_INT: -2^255 = 0x8000...0000
- -1: 2^256 - 1 = 0xFFFF...FFFF

Behavior

SDIV interprets 256-bit values as signed integers using two’s complement:
  • Bit 255 (MSB) determines sign: 0 = positive, 1 = negative
  • If b = 0: Returns 0 (no exception)
  • If a = MIN_INT and b = -1: Returns MIN_INT (overflow case)
  • Otherwise: Returns a / b truncated toward zero
Truncation toward zero:
  • Positive quotient: rounds down (e.g., 7/2 = 3)
  • Negative quotient: rounds up (e.g., -7/2 = -3, not -4)

Examples

Basic Signed Division

import { sdiv } from '@tevm/voltaire/evm/arithmetic';
import { createFrame } from '@tevm/voltaire/evm/Frame';

// 10 / 2 = 5
const frame = createFrame({ stack: [10n, 2n] });
const err = sdiv(frame);

console.log(frame.stack); // [5n]

Negative Dividend

// -10 / 2 = -5
// -10 in two's complement: 2^256 - 10
const neg10 = (1n << 256n) - 10n;
const frame = createFrame({ stack: [neg10, 2n] });
sdiv(frame);

// Result: -5 in two's complement
const neg5 = (1n << 256n) - 5n;
console.log(frame.stack); // [neg5]

Negative Divisor

// 10 / -2 = -5
const neg2 = (1n << 256n) - 2n;
const frame = createFrame({ stack: [10n, neg2] });
sdiv(frame);

const neg5 = (1n << 256n) - 5n;
console.log(frame.stack); // [neg5]

Both Negative

// -10 / -2 = 5 (negative / negative = positive)
const neg10 = (1n << 256n) - 10n;
const neg2 = (1n << 256n) - 2n;
const frame = createFrame({ stack: [neg10, neg2] });
sdiv(frame);

console.log(frame.stack); // [5n]

Truncation Toward Zero

// 7 / 2 = 3 (not 4)
const frame1 = createFrame({ stack: [7n, 2n] });
sdiv(frame1);
console.log(frame1.stack); // [3n]

// -7 / 2 = -3 (not -4)
// Rounds toward zero, not negative infinity
const neg7 = (1n << 256n) - 7n;
const frame2 = createFrame({ stack: [neg7, 2n] });
sdiv(frame2);

const neg3 = (1n << 256n) - 3n;
console.log(frame2.stack); // [neg3]

MIN_INT / -1 Edge Case

// MIN_INT / -1 would overflow to 2^255 (not representable)
// SDIV returns MIN_INT instead
const MIN_INT = 1n << 255n;
const negOne = (1n << 256n) - 1n;
const frame = createFrame({ stack: [MIN_INT, negOne] });
sdiv(frame);

console.log(frame.stack); // [MIN_INT]

Gas Cost

Cost: 5 gas (GasFastStep) SDIV has the same gas cost as DIV despite additional sign handling: Comparison:
  • ADD/SUB: 3 gas
  • MUL/DIV/MOD/SDIV/SMOD/SIGNEXTEND: 5 gas
  • ADDMOD/MULMOD: 8 gas
The sign interpretation adds no gas overhead.

Edge Cases

Division by Zero

// Signed division by zero returns 0
const neg10 = (1n << 256n) - 10n;
const frame = createFrame({ stack: [neg10, 0n] });
sdiv(frame);

console.log(frame.stack); // [0n]

MIN_INT Special Cases

// MIN_INT / -1 = MIN_INT (overflow case)
const MIN_INT = 1n << 255n;
const negOne = (1n << 256n) - 1n;
const frame1 = createFrame({ stack: [MIN_INT, negOne] });
sdiv(frame1);
console.log(frame1.stack); // [MIN_INT]

// MIN_INT / 1 = MIN_INT (no overflow)
const frame2 = createFrame({ stack: [MIN_INT, 1n] });
sdiv(frame2);
console.log(frame2.stack); // [MIN_INT]

// MIN_INT / MIN_INT = 1
const frame3 = createFrame({ stack: [MIN_INT, MIN_INT] });
sdiv(frame3);
console.log(frame3.stack); // [1n]

Zero Division Results

// 0 / -5 = 0
const neg5 = (1n << 256n) - 5n;
const frame = createFrame({ stack: [0n, neg5] });
sdiv(frame);

console.log(frame.stack); // [0n]

Common Usage

Signed Arithmetic

// Calculate price change (can be negative)
function priceChange(int256 oldPrice, int256 newPrice)
    pure returns (int256) {
    return newPrice - oldPrice;  // Can be negative
}

// Average of signed values
function signedAverage(int256 a, int256 b)
    pure returns (int256) {
    // Must handle negative results correctly
    assembly {
        let sum := add(a, b)
        let result := sdiv(sum, 2)
        mstore(0, result)
        return(0, 32)
    }
}

Directional Calculations

// Calculate slope (can be negative)
function slope(int256 y2, int256 y1, int256 x2, int256 x1)
    pure returns (int256) {
    require(x2 != x1, "vertical line");
    int256 dy = y2 - y1;
    int256 dx = x2 - x1;

    assembly {
        let result := sdiv(dy, dx)
        mstore(0, result)
        return(0, 32)
    }
}

Fixed-Point Signed Math

// Signed fixed-point division
int256 constant FIXED_POINT = 1e18;

function signedWdiv(int256 x, int256 y) pure returns (int256) {
    require(y != 0, "division by zero");

    assembly {
        let result := sdiv(mul(x, FIXED_POINT), y)
        mstore(0, result)
        return(0, 32)
    }
}

Implementation

/**
 * SDIV opcode (0x05) - Signed integer division
 */
export function sdiv(frame: FrameType): EvmError | null {
  // Consume gas (GasFastStep = 5)
  frame.gasRemaining -= 5n;
  if (frame.gasRemaining < 0n) {
    frame.gasRemaining = 0n;
    return { type: "OutOfGas" };
  }

  // Pop operands
  if (frame.stack.length < 2) return { type: "StackUnderflow" };
  const a = frame.stack.pop();
  const b = frame.stack.pop();

  let result: bigint;

  if (b === 0n) {
    result = 0n;
  } else {
    const MIN_INT = 1n << 255n;
    const MAX_UINT = (1n << 256n) - 1n;

    // Special case: MIN_INT / -1 would overflow
    if (a === MIN_INT && b === MAX_UINT) {
      result = MIN_INT;
    } else {
      // Convert to signed, divide, convert back
      const aSigned = a < MIN_INT ? a : a - (1n << 256n);
      const bSigned = b < MIN_INT ? b : b - (1n << 256n);
      const quotient = aSigned / bSigned;  // BigInt division truncates toward zero
      result = quotient < 0n ? (1n << 256n) + quotient : quotient;
    }
  }

  // Push result
  if (frame.stack.length >= 1024) return { type: "StackOverflow" };
  frame.stack.push(result);

  // Increment PC
  frame.pc += 1;

  return null;
}

Testing

Test Coverage

import { describe, it, expect } from 'vitest';
import { sdiv } from './0x05_SDIV.js';

describe('SDIV (0x05)', () => {
  const MIN_INT = 1n << 255n;
  const MAX_UINT = (1n << 256n) - 1n;
  const toSigned = (n: bigint) => n < 0n ? (1n << 256n) + n : n;

  it('divides positive numbers', () => {
    const frame = createFrame([10n, 2n]);
    expect(sdiv(frame)).toBeNull();
    expect(frame.stack).toEqual([5n]);
  });

  it('handles negative dividend', () => {
    const frame = createFrame([toSigned(-10n), 2n]);
    expect(sdiv(frame)).toBeNull();
    expect(frame.stack).toEqual([toSigned(-5n)]);
  });

  it('handles negative divisor', () => {
    const frame = createFrame([10n, toSigned(-2n)]);
    expect(sdiv(frame)).toBeNull();
    expect(frame.stack).toEqual([toSigned(-5n)]);
  });

  it('handles both negative', () => {
    const frame = createFrame([toSigned(-10n), toSigned(-2n)]);
    expect(sdiv(frame)).toBeNull();
    expect(frame.stack).toEqual([5n]);
  });

  it('truncates toward zero (positive)', () => {
    const frame = createFrame([7n, 2n]);
    expect(sdiv(frame)).toBeNull();
    expect(frame.stack).toEqual([3n]);
  });

  it('truncates toward zero (negative)', () => {
    const frame = createFrame([toSigned(-7n), 2n]);
    expect(sdiv(frame)).toBeNull();
    expect(frame.stack).toEqual([toSigned(-3n)]);
  });

  it('handles MIN_INT / -1 overflow case', () => {
    const frame = createFrame([MIN_INT, MAX_UINT]);
    expect(sdiv(frame)).toBeNull();
    expect(frame.stack).toEqual([MIN_INT]);
  });

  it('handles division by zero', () => {
    const frame = createFrame([toSigned(-10n), 0n]);
    expect(sdiv(frame)).toBeNull();
    expect(frame.stack).toEqual([0n]);
  });
});

Security

Sign Interpretation

// Unsigned vs signed division give different results
uint256 a = type(uint256).max;  // Max uint = -1 signed
uint256 b = 2;

// Unsigned: MAX / 2 = 2^255 - 1
uint256 unsignedResult;
assembly { unsignedResult := div(a, b) }

// Signed: -1 / 2 = 0 (truncate toward zero)
uint256 signedResult;
assembly { signedResult := sdiv(a, b) }

// Results are different!

MIN_INT Overflow

// MIN_INT / -1 special case
int256 MIN = type(int256).min;  // -2^255
int256 result = MIN / -1;  // In Solidity, this reverts!

// But in assembly (raw SDIV):
assembly {
    // Returns MIN_INT, does not revert
    result := sdiv(MIN, sub(0, 1))
}

Truncation Behavior

// Different languages handle negative division differently

// EVM SDIV: Truncates toward zero
// -7 / 2 = -3

// Python, Ruby: Floor division (toward negative infinity)
// -7 // 2 = -4

// Always verify truncation direction matches expectations

Safe Signed Division

// Solidity 0.8.0+ automatically checks
function safeSdiv(int256 a, int256 b) pure returns (int256) {
    return a / b;  // Reverts on MIN_INT / -1 or b = 0
}

// Explicit checks for assembly usage
function assemblySdiv(int256 a, int256 b) pure returns (int256) {
    require(b != 0, "division by zero");
    require(!(a == type(int256).min && b == -1), "overflow");

    int256 result;
    assembly {
        result := sdiv(a, b)
    }
    return result;
}

Benchmarks

SDIV performance identical to DIV: Execution time:
  • ADD: 1.0x
  • MUL: 1.2x
  • SDIV: 2.5x (same as DIV)
Gas cost:
  • 5 gas per signed division
  • No overhead for sign handling
  • ~200,000 signed divisions per million gas

References