Workspace IndexDev Notes › The code was right and the layout was wrong

#93PoC

The code was right and the layout was wrong

An attacker took Enjin's admin rights through a storage slot collision — adapters and the contract managing them used the same storage for different meanings — and drained about $162,000. Every function was individually correct. The defect lived in the space between modules.

Not yet scoped — and the first task is an inventory, not an audit. For any system where code executes in another contract's storage context, list every such module, compute each one's storage layout, and assert that no two claim the same slot for different meanings. Layout-diff tooling already exists, but it is aimed at successive versions of one contract during an upgrade; the gap here is that a pluggable adapter set is not a sequence of versions, so nothing checks it. Second task, cheaper still: write the invariant that says the admin address may only change inside an admin-change transaction, and measure how long an unauthorised change would go unnoticed today. Source: SlowMist via X, and Digital Asset, 2026-08-26 — roughly $162,000 taken, ENJ down 6.39% to $0.02625, no statement from Enjin at the time of reporting. To confirm against the post-mortem when one appears.

Why

Nothing broke. No function reverted, no assertion failed, no key leaked, and no line of code was individually wrong. Two modules wrote to the same storage slot meaning two different things, and one of those meanings happened to be who is the administrator. That is a bug class where reading any single file carefully finds nothing, because the defect is not in a file — it is in the relationship between files, and it only exists once they are composed.

This is the price of the adapter pattern, stated in one incident. Enjin's design connects separate programs that run as part of the host, which is exactly what makes such a system extensible: new capability without redeploying the core. The same mechanism means every added module is another claim on one shared address space. Extensibility and storage safety pull against each other directly, and the pull gets stronger with each adapter, which is the opposite of how most teams think about adding a plugin.

The tooling gap is specific and worth naming. Storage-layout checking is a solved problem for upgrades: compare version N to version N+1 of one contract and refuse a mismatch. An adapter set is not a sequence of versions. It is a set of peers sharing one layout, and the pairwise question — do any two of these disagree about slot k — is not what the standard tools are pointed at. So the failure is not that nobody has the technique. It is that the technique is aimed at a different shape of the same problem.

And it lands on the row written for it. plumbing-skills-buyback says surveillance has to be code that runs with nobody watching, and the admin address changed outside an admin-change transaction is about as simple as an on-chain invariant gets. The loss here was $162,000, which is small enough to be a cheap lesson; the same bug class under a settlement contract is not. governance-capture-cost found an exploit that looked like a market order; this one looks like a storage write. Both are attacks that pass every check because they are, procedurally, ordinary.

How it works

Why a single-file review cannot find it

Layer What a reviewer sees Where the defect actually is
One adapter Correct code, sensible variables Nowhere — it is fine on its own
The manager contract Correct code, an admin variable Nowhere — also fine on its own
The pair, composed Not represented in any file Both claim slot k, one means admin
The exploit transaction An ordinary state-changing call The write that reassigns the administrator

The two checks, in order of cost

Cheap and immediate — the invariant. The admin address changes only inside an admin-change transaction. One line, no new infrastructure, catches this exact class after the fact, and answers a question every system should already be able to answer: how long would an unauthorised admin change go unnoticed here. Detection came from an outside security firm, and no statement had been issued at the time of reporting — which is itself the answer for this system.

Structural — the pairwise layout assertion. Enumerate every module that executes in the host's storage context, compute each layout, and assert that no two assign different meanings to the same slot. The tooling exists but is pointed at upgrade sequences rather than peer sets, so the work is mostly repointing it, not inventing it.

The generalisation worth keeping

A shared mutable address space is the oldest source of composition bugs there is, and a chain does not make it new — it makes it expensive and public. Any design that lets code run in someone else's storage context inherits it: proxies, delegatecall modules, plugin systems, diamond facets, hook architectures. The safety question is never is this module correct but does this module agree with every other module about what memory means — and that question has no owner unless someone is assigned to it.

Reading the price honestly

$162,000 taken and a 6.39% move in ENJ. The token move is the part not to over-read: a six percent day is inside normal range for a small-cap, so it is a coincident fact rather than a measurement of the damage. The number that matters is the one nobody publishes — how many adapters shared that space, and how many of the remaining ones were checked afterwards.

← All Dev Notes · Workspace Index · Top ↑

코드는 맞았고 배치가 틀렸다

공격자가 스토리지 슬롯 충돌로 Enjin의 관리자 권한을 탈취해 약 16만 2천 달러를 빼갔습니다 — 어댑터들과 그것을 관리하는 컨트랙트가 같은 저장 공간을 다른 의미로 썼습니다. 개별 함수는 전부 올바랐습니다. 결함은 모듈들 사이의 공간에 있었습니다.

아직 범위 미정 — 첫 작업은 감사가 아니라 목록 만들기입니다. 다른 컨트랙트의 스토리지 문맥에서 코드가 실행되는 시스템이라면, 그런 모듈을 전부 나열하고 각각의 스토리지 레이아웃을 계산해, 어느 두 모듈도 같은 슬롯을 다른 의미로 주장하지 않는다를 단언합니다. 레이아웃 대조 도구는 이미 있지만 업그레이드 시 한 컨트랙트의 연속된 버전을 겨냥합니다 — 여기서의 빈틈은 꽂아 넣는 어댑터 집합은 버전의 연속이 아니라서 아무도 검사하지 않는다는 것입니다. 더 싼 두 번째 작업: 관리자 주소는 관리자 변경 트랜잭션 안에서만 바뀔 수 있다는 불변식을 쓰고, 지금이라면 무단 변경이 얼마나 오래 눈에 띄지 않을지를 재는 것. 출처: SlowMist(X), 디지털애셋 2026-08-26 — 약 16만 2천 달러 탈취, ENJ 6.39% 하락해 $0.02625, 보도 시점 Enjin 입장 없음. 사후 보고서가 나오면 확인할 것.

아무것도 고장 나지 않았습니다. revert 도 없고, assertion 실패도 없고, 키 유출도 없고, 개별적으로 틀린 코드 한 줄도 없습니다. 두 모듈이 같은 스토리지 슬롯에 서로 다른 의미로 썼고, 그 의미 중 하나가 하필 누가 관리자인가 였습니다. 파일 하나를 아무리 꼼꼼히 읽어도 아무것도 못 찾는 버그 종류입니다 — 결함이 파일 안에 있지 않고 파일들 사이의 관계에 있으며, 조합된 뒤에야 존재하기 때문입니다.

어댑터 패턴의 대가가 사건 하나로 진술된 것입니다. Enjin의 설계는 호스트의 일부로 실행되는 별도 프로그램을 연결합니다. 바로 그것이 시스템을 확장 가능하게 만듭니다 — 코어를 재배포하지 않고 기능 추가. 같은 메커니즘이 뜻하는 것은 추가되는 모든 모듈이 하나의 공유 주소 공간에 대한 또 하나의 주장이라는 것입니다. 확장성과 스토리지 안전성은 서로 정면으로 잡아당기고, 어댑터가 늘수록 그 힘이 세집니다 — 플러그인 추가를 생각하는 보통의 방식과 정반대입니다.

도구의 빈틈은 구체적이고 이름 붙일 값이 있습니다. 스토리지 레이아웃 검사는 업그레이드에 대해서는 풀린 문제입니다 — 한 컨트랙트의 버전 N과 N+1을 비교해 불일치를 거부합니다. 어댑터 집합은 버전의 연속이 아닙니다. 하나의 레이아웃을 공유하는 동료들의 집합이고, 이 중 어느 둘이 슬롯 k에 대해 의견이 다른가 라는 쌍별 질문은 표준 도구가 겨냥하는 것이 아닙니다. 그러니 실패는 기법이 없어서가 아닙니다. 기법이 같은 문제의 다른 모양을 겨냥하고 있어서입니다.

그리고 이건 그것을 위해 쓴 줄에 정확히 떨어집니다. plumbing-skills-buyback 은 감시가 아무도 지켜보지 않아도 도는 코드여야 한다고 적었고, 관리자 주소가 관리자 변경 트랜잭션 밖에서 바뀌었다 는 온체인 불변식 중 가장 단순한 축입니다. 여기 피해는 16만 2천 달러로, 싸게 배우는 교훈이 될 만큼 작습니다. 같은 버그 종류가 정산 컨트랙트 아래에 있으면 그렇지 않습니다. governance-capture-cost시장가 주문처럼 보이는 익스플로잇을 찾았고, 이건 스토리지 쓰기처럼 보입니다. 둘 다 절차적으로 평범하기 때문에 모든 검사를 통과하는 공격입니다.

동작 방식

왜 파일 하나를 리뷰해서는 못 찾는가

층위 리뷰어에게 보이는 것 결함이 실제로 있는 곳
어댑터 하나 올바른 코드, 합리적인 변수 없음 — 단독으로는 문제없음
관리 컨트랙트 올바른 코드, admin 변수 없음 — 이쪽도 단독으로는 문제없음
둘을 조합한 것 어떤 파일에도 표현되지 않음 둘 다 슬롯 k를 주장하고, 한쪽 의미가 admin
익스플로잇 트랜잭션 평범한 상태 변경 호출 관리자를 재지정하는 그 쓰기

검사 둘, 비용 순서로

싸고 즉시 — 불변식. 관리자 주소는 관리자 변경 트랜잭션 안에서만 바뀐다. 한 줄, 새 인프라 없음, 이 종류를 사후에 정확히 잡고, 모든 시스템이 이미 답할 수 있어야 하는 질문에 답합니다 — 여기서 무단 관리자 변경은 얼마나 오래 눈에 안 띄는가. 탐지는 외부 보안 업체에서 왔고 보도 시점까지 입장이 없었습니다 — 그 자체가 이 시스템의 답입니다.

구조적 — 쌍별 레이아웃 단언. 호스트의 스토리지 문맥에서 실행되는 모든 모듈을 나열하고, 각 레이아웃을 계산해, 어느 둘도 같은 슬롯에 다른 의미를 주지 않는다를 단언합니다. 도구는 있지만 동료 집합이 아니라 업그레이드 연속을 겨냥하므로, 일의 대부분은 발명이 아니라 겨냥을 바꾸는 것입니다.

남겨 둘 일반화

공유된 가변 주소 공간은 조합 버그의 가장 오래된 원천이고, 체인이 그걸 새롭게 만들지 않습니다 — 비싸고 공개적으로 만들 뿐입니다. 남의 스토리지 문맥에서 코드가 돌게 하는 모든 설계가 이걸 물려받습니다 — 프록시, delegatecall 모듈, 플러그인 시스템, 다이아몬드 패싯, 훅 아키텍처. 안전성 질문은 결코 이 모듈이 올바른가 가 아니라 이 모듈이 메모리의 의미에 대해 다른 모든 모듈과 합의하는가 이고, 누군가를 지정해 두지 않으면 그 질문에는 주인이 없습니다.

피해액을 정직하게 읽기

16만 2천 달러 탈취, ENJ 6.39% 하락. 토큰 움직임은 과대 해석하지 말아야 할 부분입니다 — 소형주에 6%는 정상 범위 안이므로 피해의 측정치가 아니라 동시에 일어난 사실입니다. 중요한 숫자는 아무도 발표하지 않는 쪽입니다 — 그 공간을 공유한 어댑터가 몇 개였고, 남은 것 중 몇 개를 사후에 점검했는가.

← 전체 개발 노트 · 워크스페이스 인덱스 · 맨 위 ↑