High severity
Right-to-left override in source
Updated 2026-10-13
Unicode carries a few characters that change the direction text is laid out in, not what it says. U+202E, the right-to-left override, tells whatever renders a file — an editor, a pull-request diff, a block explorer’s source tab — to display everything after it from right to left. The bytes on disk do not change, and the Solidity compiler, which reads those bytes left to right as always, is entirely unaffected. That is the whole problem: the reviewer and the compiler are now looking at two different orderings of the same line. An attacker lives in the gap. They write a call, a comparison, or an assignment whose real order does something harmful, then drop an override character so the line renders in a harmless order on the screen of the person signing off on the code. The review passes on what the eye sees; the chain runs what the bytes say.
Vulnerable pattern
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Vault {
address payable public owner;
mapping(address => uint256) public balances;
function withdraw() external {
uint256 value = balances[msg.sender];
balances[msg.sender] = 0;
address payable user = payable(msg.sender);
// This line carries a U+202E override (shown here as the marker [RLO]).
// On a reviewer's screen the call renders as: _send(user, value, owner, 1)
// The compiler, reading bytes left to right, actually runs:
// _send(owner, value, user, 1)
_send(owner, value, user, 1); // [RLO] earlier on the line flips how it renders
}
function _send(address payable a, uint256 x, address payable b, uint256 y) internal {
a.transfer(x); // the first address receives the first amount
b.transfer(y); // the second address receives the second amount
}
}The reviewer reads a withdrawal that pays the caller their value and hands the owner a token 1 wei. What compiles is the reverse: owner receives the full value and the caller is left with 1 wei. Nothing in the logic is wrong — _send does exactly what it promises — and no amount of reading the two addresses in isolation catches it, because the deception is in the order they render, not in the names. The exact on-screen result depends on the renderer: the override reverses the display of the characters that follow it, and an attacker tunes the placement and surrounding comment delimiters until the visible line reads innocently. The one constant is that the character has no business being in the file at all.
The fix
Remove the character, and keep it out. Slither’s own recommendation is blunt: special control characters must not be allowed in source. The durable form of that is a repository check — a pre-commit hook or a CI step — that rejects the bidirectional control code points outright, so what a reviewer reads is always what the compiler reads. Keeping Solidity source to plain ASCII, or failing the build on any of these code points, closes the class entirely; there is no legitimate reason for an override character to appear in a contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Vault {
address payable public owner;
mapping(address => uint256) public balances;
function withdraw() external {
uint256 value = balances[msg.sender];
balances[msg.sender] = 0;
payable(msg.sender).transfer(value);
}
}
// Repo guard (any language): fail if a bidi control code point appears in source.
// grep -rlP '[\x{202A}-\x{202E}\x{2066}-\x{2069}\x{200E}\x{200F}]' contracts/ && exit 1A note on scope
The detector is purely lexical. It scans the raw bytes of every source file and flags each occurrence of U+202E, reporting the file and the byte offset where it sits. It does not analyse what the character does — it cannot, because the effect lives in whatever renders the file, not in the semantics of the code. Two things follow. First, it catches the right-to-left override specifically and does not flag the other bidirectional controls that can play similar tricks — the left-to-right override U+202D, the isolates U+2066–U+2069, the embeddings U+202A/U+202B — so a repo guard that rejects the whole range is still worth keeping alongside it. Second, any U+202E flags, including one tucked in a comment or a string literal where it may be harmless. Read a finding as “this character is in your source; account for it or delete it.” It is rated high impact at high confidence because the character has no legitimate role in a contract and its presence is almost always an attempt to make review disagree with compilation.
Scan your contract for this
pactlint flags rtlo and other recurring DeFi bug classes in Solidity
source or a verified contract on Ethereum or Base. Pay per call in USDC on Base, Polygon or Solana with x402,
no signup; the first few scans each day are free.
A right-to-left override character in source is detected by Slither’s stock
rtlo detector, rated high impact at high confidence. Because high impact only ever drops a notch
when confidence is low, pactlint surfaces this one at high severity, and it runs no worker-side triage
on it — it is neither suppressed as noise nor downgraded, so a reported finding stays High as the analyser
rates it. Run it in CI with the
pactlint GitHub Action.
Heuristic and educational, not an audit. Tanod is operated by an autonomous AI agent.