Skip to main content
This page is a placeholder. All examples on this page are currently AI-generated and are not correct. This documentation will be completed in the future with accurate, tested examples.

Overview

Opcode: 0x07 Introduced: Frontier (EVM genesis) SMOD performs signed modulo operation on two 256-bit values interpreted as two’s complement signed integers. The result has the same sign as the dividend (not the divisor, unlike some languages). Like MOD, modulo by zero returns 0. Additionally, SMOD has special handling for the MIN_INT / -1 edge case.

Specification

Stack Input:
a (top - signed dividend)
b (signed modulus)
Stack Output:
a % b  (if b ≠ 0 and not MIN_INT/-1)
0      (if b = 0 or (a = MIN_INT and b = -1))
Gas Cost: 5 (GasFastStep) Operation:
Two's complement interpretation:
- Range: -2^255 to 2^255 - 1
- Result sign matches dividend

Behavior

SMOD interprets 256-bit values as signed integers using two’s complement:
  • If b = 0: Returns 0 (no exception)
  • If a = MIN_INT and b = -1: Returns 0 (special case)
  • Otherwise: Returns a - (a / b) * b where division is signed
Sign of result:
  • Result always has the same sign as dividend a
  • -7 % 2 = -1 (not 1)
  • 7 % -2 = 1 (not -1)

Examples

Basic Signed Modulo

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

// 10 % 3 = 1
const frame = createFrame({ stack: [10n, 3n] });
const err = smod(frame);

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

Negative Dividend

// -10 % 3 = -1 (result has same sign as dividend)
// -10 in two's complement: 2^256 - 10
const neg10 = (1n << 256n) - 10n;
const frame = createFrame({ stack: [neg10, 3n] });
smod(frame);

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

Negative Modulus

// 10 % -3 = 1 (result has sign of dividend, not modulus)
const neg3 = (1n << 256n) - 3n;
const frame = createFrame({ stack: [10n, neg3] });
smod(frame);

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

Both Negative

// -10 % -3 = -1 (result follows dividend sign)
const neg10 = (1n << 256n) - 10n;
const neg3 = (1n << 256n) - 3n;
const frame = createFrame({ stack: [neg10, neg3] });
smod(frame);

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

MIN_INT % -1 Edge Case

// MIN_INT % -1 = 0 (special case)
const MIN_INT = 1n << 255n;
const negOne = (1n << 256n) - 1n;
const frame = createFrame({ stack: [MIN_INT, negOne] });
smod(frame);

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

Gas Cost

Cost: 5 gas (GasFastStep) SMOD has the same gas cost as MOD and SDIV: Comparison:
  • ADD/SUB: 3 gas
  • MUL/DIV/MOD/SDIV/SMOD/SIGNEXTEND: 5 gas
  • ADDMOD/MULMOD: 8 gas
No gas overhead for sign handling.

Edge Cases

Modulo by Zero

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

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

MIN_INT Special Cases

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

// MIN_INT % 1 = 0
const frame2 = createFrame({ stack: [MIN_INT, 1n] });
smod(frame2);
console.log(frame2.stack); // [0n]

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

Zero Dividend

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

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

Sign Comparison with Other Languages

// EVM SMOD: Result sign matches dividend
// -7 % 2 = -1

// Python: Result sign matches divisor
// -7 % 2 = 1

// C/C++: Result sign matches dividend (like EVM)
// -7 % 2 = -1

Common Usage

Signed Range Wrapping

// Wrap signed value to range
function wrapToRange(int256 value, int256 range)
    pure returns (int256) {
    require(range > 0, "range must be positive");

    assembly {
        let result := smod(value, range)
        mstore(0, result)
        return(0, 32)
    }
}

Signed Parity Check

// Check parity of signed number
function signedParity(int256 n) pure returns (int256) {
    assembly {
        let result := smod(n, 2)
        mstore(0, result)
        return(0, 32)
    }
}

// Examples:
// signedParity(7) = 1
// signedParity(-7) = -1
// signedParity(8) = 0

Cyclic Signed Indexing

// Wrap signed index to array bounds
function cyclicSignedIndex(int256 index, uint256 arrayLength)
    pure returns (uint256) {
    require(arrayLength > 0, "empty array");

    int256 len = int256(arrayLength);
    assembly {
        let mod_result := smod(index, len)
        // If negative, add length to make positive
        if slt(mod_result, 0) {
            mod_result := add(mod_result, len)
        }
        mstore(0, mod_result)
        return(0, 32)
    }
}

Implementation

/**
 * SMOD opcode (0x07) - Signed modulo operation
 */
export function smod(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 = 0
    if (a === MIN_INT && b === MAX_UINT) {
      result = 0n;
    } else {
      // Convert to signed, modulo, convert back
      const aSigned = a < MIN_INT ? a : a - (1n << 256n);
      const bSigned = b < MIN_INT ? b : b - (1n << 256n);
      const remainder = aSigned % bSigned;  // BigInt modulo
      result = remainder < 0n ? (1n << 256n) + remainder : remainder;
    }
  }

  // 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 { smod } from './0x07_SMOD.js';

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

  it('computes positive modulo', () => {
    const frame = createFrame([10n, 3n]);
    expect(smod(frame)).toBeNull();
    expect(frame.stack).toEqual([1n]);
  });

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

  it('handles negative modulus', () => {
    const frame = createFrame([10n, toSigned(-3n)]);
    expect(smod(frame)).toBeNull();
    expect(frame.stack).toEqual([1n]);
  });

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

  it('handles MIN_INT % -1', () => {
    const frame = createFrame([MIN_INT, MAX_UINT]);
    expect(smod(frame)).toBeNull();
    expect(frame.stack).toEqual([0n]);
  });

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

  it('result sign matches dividend', () => {
    // -7 % 2 = -1 (not 1)
    const frame = createFrame([toSigned(-7n), 2n]);
    expect(smod(frame)).toBeNull();
    expect(frame.stack).toEqual([toSigned(-1n)]);
  });
});

Security

Sign Interpretation

// SMOD vs MOD give different results for negative values
uint256 a = type(uint256).max;  // -1 as signed
uint256 b = 10;

// Unsigned: MAX % 10 = 5
uint256 unsignedResult;
assembly { unsignedResult := mod(a, b) }

// Signed: -1 % 10 = -1
uint256 signedResult;
assembly { signedResult := smod(a, b) }

// Results are different!

Cross-Language Differences

// EVM SMOD: Result sign matches dividend
// -7 % 3 = -1

// Python: Result sign matches divisor
// -7 % 3 = 2

// Java/C++: Result sign matches dividend (like EVM)
// -7 % 3 = -1

// Always verify behavior matches expectations

Negative Index Wrapping

// WRONG: Direct SMOD for array indexing
function wrongWrap(int256 index, uint256 length)
    pure returns (uint256) {
    assembly {
        let result := smod(index, length)
        mstore(0, result)
        return(0, 32)
    }
}
// If index is negative, result is negative!

// RIGHT: Convert negative to positive
function correctWrap(int256 index, uint256 length)
    pure returns (uint256) {
    require(length > 0, "empty array");
    int256 len = int256(length);
    int256 mod_result;

    assembly {
        mod_result := smod(index, len)
    }

    if (mod_result < 0) {
        mod_result += len;
    }

    return uint256(mod_result);
}

Safe Signed Modulo

// Solidity 0.8.0+ checks automatically
function safeSmod(int256 a, int256 b) pure returns (int256) {
    return a % b;  // Reverts on b = 0
}

// Explicit checks for assembly usage
function assemblySmod(int256 a, int256 b) pure returns (int256) {
    require(b != 0, "modulo by zero");

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

Benchmarks

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

References