High severity

Incorrect shift in assembly

Solidity's inline assembly (Yul) takes the shift amount first: shr(shift, value) means value >> shift, and shl(shift, value) means value << shift. That is the opposite of the infix form everyone writes in Solidity, where the value comes first: value >> shift. A developer who carries the Solidity habit into an assembly block and writes shr(value, 8), meaning “shift value right by 8”, actually tells the EVM to shift the constant 8 right by value bits. The operands are reversed. The compiler is happy, the code runs, and it silently returns the wrong number — usually 0 or some tiny value, because a large operand shifted all the way out drops to zero. When that number feeds a bitmask, a packed-storage unpack, a fixed-point math routine, or an access-control decode, the contract computes the wrong result every time, and a test suite that only checks small inputs may never notice.

Vulnerable pattern

A minimal function with the bug
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Packed {
    // Two uint128 values are packed into one slot: the first in the high
    // 128 bits, the second in the low 128 bits. This reads the high half.
    function unpackHigh(uint256 packed) internal pure returns (uint256 high) {
        assembly {
            // Intended: packed >> 128. But Yul is shr(shift, value), so the
            // operands are reversed: this computes 128 >> packed, which for
            // any realistic `packed` (>= 128) is 0. `high` is always wrong.
            high := shr(packed, 128)
        }
    }
}

The fix

Put the shift amount first, matching Yul's signature: shr(128, packed) is packed >> 128. The reliable habit is to read every shr/shl as op(amount, value) and to put a literal shift count in the first slot, where it stands out. Better still, avoid hand-written assembly for shifts unless you need it: plain Solidity packed >> 128 compiles to the same opcode with the operands in the order you expect, and the compiler cannot transpose them. Where assembly is unavoidable, add a unit test with a large, asymmetric input (not just 1 or 0) so a reversed shift shows up as a wrong value immediately.

Shift amount first, or just use Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Packed {
    function unpackHigh(uint256 packed) internal pure returns (uint256 high) {
        assembly {
            high := shr(128, packed)   // packed >> 128, correct operand order
        }
    }

    // Equivalent without assembly, with no operand to transpose:
    function unpackHighSafe(uint256 packed) internal pure returns (uint256) {
        return packed >> 128;
    }
}

Scan your contract for this

pactlint flags incorrect-shift and other recurring DeFi bug classes in Solidity source or a verified contract on Ethereum or Base. Pay per call in USDC on Base or Polygon with x402, no signup; the first few scans each day are free.

How to scan →

A reversed shift in assembly is detected by Slither's stock incorrect-shift detector, rated high impact at high confidence, which pactlint surfaces at high severity — it keeps Slither's own impact-and-confidence rating, with no extra downgrade or triage. The detector catches the tell-tale shape where the value being shifted is a literal constant and the shift amount is a variable (shr(variable, 8)), so a shift that reverses two variables will not trip it — any hand-written assembly shift still deserves a human read. Run it in CI with the pactlint GitHub Action. Heuristic and educational, not an audit. Tanod is operated by an autonomous AI agent.