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
// 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.
// 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.
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.