Hierarchy Account 하이어라키 어카운트

— ON-CHAIN OWNERSHIP MODEL —

A precise account of how the user AA wallet and the slot (sub-access account) are structured on-chain — how addresses are derived, what "ownership" means inside dex.sol, and which operations each level of the hierarchy can authorize. 사용자 AA 지갑과 슬롯(하위 접속 어카운트)이 온체인에서 어떻게 구조화되는지 정밀하게 설명합니다 — 주소 파생 방식, dex.sol 내부에서 "소유권"의 의미, 그리고 계층의 각 레벨이 승인할 수 있는 작업들을 다룹니다.

📐 Topics Covered 📐 다루는 주제

1
4-Level Hierarchy 4-레벨 계층 구조
Platform · EOA key (signs for) → User AA (top-level account) → Slot 플랫폼 · EOA 키(서명) → 사용자 AA(최상위 어카운트) → 슬롯
2
Two AA Wallet Types 두 종류의 AA 지갑
W_SALT (user) vs C_SALT (slot) — different owner EOAs W_SALT(사용자) vs C_SALT(슬롯) — 서로 다른 owner EOA
3
dex.sol Ownership dex.sol 소유권
init_permission, _data mapping anatomy init_permission, _data 매핑 해부
4
Ownership Privileges 소유권이 부여하는 권한
Which on-chain calls require owner_addr match owner_addr 일치를 요구하는 온체인 호출 목록
5
Platform EOA Role 플랫폼 EOA의 역할
tx sender vs logical owner — why no msg.sender check tx 발신자 vs 논리적 오너 — msg.sender 체크가 없는 이유
6
On-chain Verification 온체인 검증
Reading ownership state from the contract 컨트랙트에서 소유권 상태 읽기
7
System Capabilities 이 구조가 가능하게 하는 것
OAuth × AA binding, multi-slot, dual auth, self-sovereign transfer OAuth × AA 결합, 멀티슬롯, 이중인가, 자기주권 이전
8
XOR Key Slot Sale Flow XOR 키 슬롯 판매 플로우
approve_slot · revoke_slot · buy_slot platform relay — Race Condition prevention via ERC721 approve pattern approve_slot · revoke_slot · buy_slot 플랫폼 중계 — ERC721 approve 패턴으로 경쟁 조건 방지

01 The 4-Level Account Hierarchy 4-레벨 계정 계층 구조

Every entity in the WebXcom system maps to a unique on-chain address. The relationship between those addresses — and who can authorize actions on behalf of whom — forms a strict 4-level hierarchy. WebXcom 시스템의 모든 엔티티는 고유한 온체인 주소에 매핑됩니다. 주소들 간의 관계와 누가 누구를 대신해 작업을 승인할 수 있는지는 엄격한 4-레벨 계층 구조를 형성합니다.

Level 0 — Platform
Platform EOA  process.env.PRIVATE_KEY → address
Deploys dex.sol, sponsors all gas via VerifyingPaymaster, submits UserOps as bundler. dex.sol을 배포하고, VerifyingPaymaster를 통해 모든 가스를 대납하며, 번들러로서 UserOp을 제출합니다.
Level 1 — EOA Signing Key (off-chain · not an on-chain account)
owner_eoa  Ethers.Wallet.createRandom() → mnemonic → private key
A private key (mnemonic-derived) that authorizes the AA wallet. It is not the user's on-chain account — it is the cryptographic signing mechanism that controls Level 2. Split XOR 3-of-3 across B-server DB, browser session, and user PIN. Never directly visible to the user. AA 지갑을 승인하는 프라이빗 키(니모닉 파생). 사용자의 온체인 어카운트가 아닙니다 — Level 2를 제어하는 암호학적 서명 메커니즘입니다. B서버 DB · 브라우저 세션 · 사용자 PIN으로 XOR 3-of-3 분할. 사용자에게 직접 노출되지 않습니다.
Level 2 — User AA Wallet (SimpleAccount · W_SALT)
db.Wallet.address  factory.getAddress(owner_eoa, account_idx)
The user's canonical on-chain identity. When a UserOp executes, msg.sender on the target contract is this address. This address is registered as dex._data[slot].owner_addr via init_permission. 사용자의 정규 온체인 신원. UserOp이 실행될 때, 대상 컨트랙트의 msg.sender는 이 주소입니다. 이 주소가 init_permission을 통해 dex._data[slot].owner_addr로 등록됩니다.
↓  dex._data[slot].owner_addr
Level 3 — Slot / Content Wallet (SimpleAccount · C_SALT)
db.CWallet.address  factory.getAddress(c_salt_eoa, account_idx)
The slot's on-chain address. Used as the key in dex._data[slot] — not the owner, but the subject being owned. Holds the slot's token balance and tracks used_balances, cuid, and sell status. 슬롯의 온체인 주소. dex._data[slot]에서 로 사용됩니다 — 소유자가 아니라, 소유의 대상입니다. 슬롯의 토큰 잔액을 보유하며 used_balances, cuid, 판매 상태를 추적합니다.
ℹ️ The User's Top-Level On-chain Account is Level 2, Not Level 1 ℹ️ 사용자의 최상위 온체인 어카운트는 Level 2입니다 — Level 1(EOA)이 아닙니다
The EOA (Level 1) is a private key, not an on-chain account. It lives off-chain and its only purpose is to sign operations that prove ownership of the AA wallet. The AA Wallet (Level 2) is the user's actual on-chain identity — it is what dex.sol records as owner_addr, what receives ETH from slot sales, and what other parties on-chain interact with. Think of the EOA as a car key, and the AA wallet as the car. The key is how you control the car, but the car is what exists on the road.

In dex.sol, slot always refers to the content wallet address (Level 3 — key of _data), while owner refers to the user AA wallet address (Level 2 — value stored in _data[slot].owner_addr). These are separate contracts with different owner EOAs and salts.
EOA(Level 1)는 프라이빗 키이지, 온체인 어카운트가 아닙니다. 오프체인에 존재하며 유일한 목적은 AA 지갑의 소유권을 증명하는 서명 생성입니다. AA 지갑(Level 2)가 사용자의 실제 온체인 신원입니다 — dex.solowner_addr로 기록하는 대상이고, 슬롯 판매 시 ETH를 수령하는 주체이며, 다른 온체인 참여자가 상호작용하는 대상입니다. EOA를 자동차 키, AA 지갑을 자동차로 비유하면 — 키는 차를 제어하는 수단이지만, 도로 위에 존재하는 것은 차입니다.

dex.sol에서 slot은 항상 콘텐츠 지갑 주소(Level 3 — _data의 키)를 가리키고, owner사용자 AA 지갑 주소(Level 2 — _data[slot].owner_addr에 저장된 값)를 가리킵니다. 이 두 주소는 서로 다른 owner EOA와 salt를 가진 완전히 별개의 컨트랙트입니다.

02 Two AA Wallet Types: W_SALT vs C_SALT 두 종류의 AA 지갑: W_SALT vs C_SALT

Both the user and the slot have dedicated SimpleAccount contracts on-chain. They are created by the same ERC-4337 factory but with fundamentally different owner EOAs and salts — making their addresses entirely independent. 사용자와 슬롯 모두 온체인에 전용 SimpleAccount 컨트랙트를 가집니다. 동일한 ERC-4337 팩토리로 생성되지만, 근본적으로 다른 owner EOA와 salt를 사용하여 주소가 완전히 독립적입니다.

User AA Wallet 사용자 AA 지갑 Slot / Content Wallet 슬롯 / 콘텐츠 지갑
DB Table user_wallet (db.Wallet) content_wallet (db.CWallet)
Owner EOA source Owner EOA 출처 Ethers.Wallet.createRandom() — random mnemonic, unpredictable private key Ethers.Wallet.createRandom() — 랜덤 니모닉, 예측 불가 프라이빗키 keccak256(account_idx + W_SALT) — deterministic, no mnemonic keccak256(account_idx + W_SALT) — 결정론적, 니모닉 없음
Key management 키 관리 XOR 3-of-3 split (PIN + B서버 DB + browser session) XOR 3-of-3 분할 (PIN + B서버 DB + 브라우저 세션) Stored as-is in B서버 DB (Key table, C_SALT-keyed UUID) B서버 DB에 그대로 저장 (Key 테이블, C_SALT 기반 UUID)
Address formula 주소 공식 factory.getAddress(owner_eoa, account_idx) factory.getAddress(c_salt_eoa, account_idx)
Role in dex.sol dex.sol 내 역할 Owner — stored in _data[slot].owner_addr 소유자_data[slot].owner_addr에 저장됨 Slot key — used as the mapping key _data[this address] 슬롯 키 — 매핑 키 _data[this address]로 사용됨
Holds token balance? 토큰 잔액 보유? Yes — receives ETH from slot sales (buy_slot) 예 — 슬롯 판매 시 ETH 수령 (buy_slot) Yes — receives content tokens from buy_content_token 예 — buy_content_token으로 콘텐츠 토큰 수령
// ── User AA Wallet creation (wallet.service.js) ──
const randomWallet = Ethers.Wallet.createRandom();       // unpredictable
const ownerEoaAddress = randomWallet.address;
const aaAddress = await AAService.computeAAAddress(ownerEoaAddress, account_idx);
// Result: db.Wallet = { address: aaAddress, owner_eoa: ownerEoaAddress, wallet_type: 'AA' }

// ── Slot / Content Wallet creation ──
const hash = Ethers.utils.keccak256(
  Ethers.utils.toUtf8Bytes( crypto_encryp(account_idx + process.env.W_SALT) )
);
const c_salt_eoa = new Ethers.Wallet(hash.substring(0, 66)).address;  // deterministic
const contentAddress = await AAService.computeAAAddress(c_salt_eoa, account_idx);
// Result: db.CWallet = { address: contentAddress, owner_eoa: c_salt_eoa, wallet_type: 'AA' }

// ── Address formula (ERC-4337 CREATE2) ──
// factory.getAddress(owner, salt)
//   → keccak256( 0xff | factory | keccak256(creationCode + abi.encode(owner, salt)) )[12:]
⚠️ The Two owner_eoa Values Are Never the Same ⚠️ 두 owner_eoa는 절대 같지 않습니다
The user AA wallet's owner_eoa is cryptographically random and changes only via key rotation. The slot's owner_eoa is deterministically derived from account_idx and W_SALT — it is server-controlled and never user-facing. Confusing these two is the root cause of AA24 signature errors. 사용자 AA 지갑의 owner_eoa는 암호학적으로 무작위이며 키 교체를 통해서만 변경됩니다. 슬롯의 owner_eoaaccount_idxW_SALT에서 결정론적으로 파생되며 — 서버가 제어하고 사용자에게 노출되지 않습니다. 이 두 값을 혼동하는 것이 AA24 서명 에러의 근본 원인입니다.

03 dex.sol Ownership Model — _data Mapping Anatomy dex.sol 소유권 모델 — _data 매핑 해부

All slot ownership state lives in a single Solidity mapping inside dex.sol. Understanding its structure explains everything about the hierarchy. 모든 슬롯 소유권 상태는 dex.sol 내부의 단일 Solidity 매핑에 저장됩니다. 이 구조를 이해하면 계층 관계의 모든 것이 명확해집니다.

// dex.sol (Solidity)

struct slotdata {
    address payable owner_addr;   // ← user's AA wallet address (Level 2)
    uint256 cuid;                 // ← content unit ID (links slot to provider)
    uint256 used_balances;        // ← accumulated used token balance
    bool    is_sell;              // ← whether the slot is listed for sale
    uint256 price;                // ← listing price (wei)
}

mapping(address => slotdata) private _data;
//      ↑
//      key = slot / content wallet address (Level 3 — db.CWallet.address)

Reading the mapping for a specific slot gives you the full ownership record: 특정 슬롯에 대한 매핑을 읽으면 완전한 소유권 레코드를 얻을 수 있습니다:

// Example state for slot 0xSlotAddress (db.CWallet.address)
_data[0xSlotAddress] = {
    owner_addr:    0xUserAAAddress,  // ← db.Wallet.address (user's SimpleAccount)
    cuid:          42,               // ← content provider ID
    used_balances: 500e18,           // ← 500 tokens used
    is_sell:       false,
    price:         0
}

3-1. init_permission — Registering Ownership 3-1. init_permission — 소유권 등록

init_permission is the on-chain function that establishes the relationship between a user's AA wallet (the owner) and a slot (the subject). It can only be called once per slot — if _data[slot].owner_addr is already non-zero, the call reverts. init_permission은 사용자의 AA 지갑(소유자)과 슬롯(대상) 간의 관계를 온체인에 확립하는 함수입니다. 슬롯당 한 번만 호출 가능합니다 — _data[slot].owner_addr가 이미 0이 아니면 호출이 revert됩니다.

// dex.sol — init_permission (Solidity)
function init_permission(address owner, address slot, uint256 cuid) external returns (bool) {
    require(_data[slot].owner_addr == address(0x0));  // slot must be unclaimed
    require(_data[slot].cuid      == uint256(0x0));   // cuid must be unset
    require(slot != address(0x0));                    // slot must be valid address

    // ⚠️  NO msg.sender check — any account can register any owner
    //     Safety comes from: only platform knows both addresses,
    //     and XOR key verification confirms user identity off-chain

    if (_cuid_count[cuid] == 0) {
        _cuid_releaseTokenEnabled[cuid] = false;
    }
    _cuid_count[cuid]++;

    _data[slot].owner_addr = payable(owner);  // ← user's AA wallet address set here
    _data[slot].cuid       = cuid;
    _data[slot].is_sell    = false;
    return true;
}
ℹ️ Why No msg.sender Check? ℹ️ 왜 msg.sender 체크가 없나?
init_permission intentionally omits a msg.sender check. This allows the platform EOA to act as the transaction sender on behalf of the user — avoiding the AA/UserOperation overhead entirely — while still setting the correct logical owner (owner parameter = user's AA wallet address). The user's identity is verified off-chain by XOR key reconstruction before the platform EOA sends the transaction. init_permission은 의도적으로 msg.sender 체크를 생략합니다. 이를 통해 플랫폼 EOA가 사용자를 대신하여 트랜잭션 발신자 역할을 수행할 수 있습니다 — AA/UserOperation 오버헤드 없이 — 동시에 올바른 논리적 소유자(owner 파라미터 = 사용자 AA 지갑 주소)를 설정합니다. 플랫폼 EOA가 트랜잭션을 전송하기 전, 오프체인에서 XOR 키 복원으로 사용자 신원이 검증됩니다.

init_permission Registration Flow init_permission 등록 플로우

sequenceDiagram participant U as User (Browser) participant W as webxcom (Rails) participant A as A서버 (NestJS) participant B as B서버 (Blockchain) participant DEX as dex.sol U->>W: Enter PIN (slot ownership registration) W->>W: pin_hash = SHA256(pin + salt) W->>A: POST /v1/pin/verify { pin_hash, key_share_user, action: initPermissionAdex } A->>B: gRPC ExecutePinTransaction({ account_idx, pin_hash, key_share_user, slot_addr, cuid }) B->>B: 1. Load share_server from Key table (U_SALT UUID) B->>B: 2. restoreKeyXor3(share_user, pin_hash, share_server) → full private key B->>B: 3. Derive EOA address from key → compare with db.Wallet.owner_eoa B->>B: 4. Identity verified ✓ — key wiped from memory B->>B: 5. Read db.Wallet.address → userAAAddress B->>DEX: Platform EOA signs tx: init_permission(userAAAddress, slotAddr, cuid) DEX->>DEX: _data[slotAddr].owner_addr = userAAAddress ✓ DEX-->>B: transaction receipt B-->>A: { tx_hash } A-->>W: { tx_hash } W-->>U: Ownership confirmed — tx_hash displayed

04 What Ownership Grants — On-chain Privilege Map 소유권이 부여하는 것 — 온체인 권한 지도

Being the owner_addr of a slot in dex.sol is the gating condition for every significant operation on that slot. The contract enforces ownership by checking _data[slot].owner_addr == caller (passed as an explicit owner/sender parameter) before allowing execution. dex.sol에서 슬롯의 owner_addr가 되는 것은 해당 슬롯의 모든 중요한 작업에 대한 게이팅 조건입니다. 컨트랙트는 실행을 허용하기 전에 _data[slot].owner_addr == caller(명시적 owner/sender 파라미터로 전달됨)를 체크하여 소유권을 강제합니다.

Function 함수 Owner Check 소유권 체크 What it does 수행 작업
increaseUsedBal _data[slot].owner_addr == owner Increases the slot's used_balances by amount. Used when content is consumed. 슬롯의 used_balancesamount만큼 증가. 콘텐츠 소비 시 사용.
releaseUsedBal _data[slot].owner_addr == owner Releases a percentage of used_balances back per the provider's release_per. Used during refund/settlement. 프로바이더의 release_per에 따라 used_balances의 일정 비율을 반환. 환불/정산 시 사용.
init_self _data[slot].owner_addr == sender Transfers slot ownership to a new address without payment. Requires the caller to already be the current owner. 결제 없이 슬롯 소유권을 새 주소로 이전. 호출자가 현재 소유자여야 합니다.
init_sell_amount _data[slot].owner_addr == _msgSender() || _platform == _msgSender() Lists the slot for sale at the specified price. Owner or platform can set the price. 지정된 가격으로 슬롯을 판매 목록에 등록. 소유자 또는 플랫폼이 가격을 설정할 수 있습니다.
init_sell_cancle _data[slot].owner_addr == sender Cancels a sale listing. Blocked if _slot_approved[slot] != address(0) — must call revoke_slot first. 판매 목록을 취소. _slot_approved[slot] != address(0)이면 차단 — 먼저 revoke_slot 호출 필요.
approve_slot _data[slot].owner_addr == _msgSender() Grants the operator address the right to call buy_slot on behalf of the owner. Locks init_sell_cancle until revoked. AA wallet must be _msgSender() — signed via seller's XOR-restored EOA key. operator 주소에 소유자를 대신해 buy_slot을 호출할 권한 부여. revoke 전까지 init_sell_cancle을 잠금. AA 지갑이 _msgSender()여야 함 — 판매자 XOR 복원 EOA 키로 서명.
revoke_slot _slot_approved[slot] == _msgSender() || _data[slot].owner_addr == _msgSender() Clears _slot_approved[slot] back to address(0). Callable by the approved operator (platform EOA) or the slot owner. Unlocks init_sell_cancle. _slot_approved[slot]address(0)으로 초기화. 승인된 운영자(플랫폼 EOA) 또는 슬롯 소유자가 호출 가능. init_sell_cancle을 잠금 해제.
transferAdex _data[from].owner_addr == owner Transfers available token balance from the slot to another address. Cannot exceed balance - used_balances. 슬롯에서 다른 주소로 사용 가능한 토큰 잔액을 이전. balance - used_balances를 초과할 수 없습니다.
buy_slot _data[slot].owner_addr != buyer Anyone can buy a listed slot, but the seller (current owner) receives the payment. After purchase, owner_addr changes to the buyer. 누구나 판매 중인 슬롯을 구매할 수 있지만, 결제는 판매자(현재 소유자)에게 전달됩니다. 구매 후 owner_addr가 구매자로 변경됩니다.
// Example: user AA wallet authorizing increaseUsedBal on its slot
// (called via AA UserOperation — msg.sender = user AA wallet)

function increaseUsedBal(
    address owner,      // ← user's AA wallet address
    address slot_addr,  // ← content wallet address (db.CWallet.address)
    uint256 cuid,
    uint256 amount
) public returns (uint256) {
    require(_data[slot_addr].owner_addr == owner, "Not Match Owner"); // ← ownership gate
    require(_data[slot_addr].cuid == cuid, "Not Match cuid");
    require(amount <= balanceOf(slot_addr) - _data[slot_addr].used_balances);

    _data[slot_addr].used_balances += amount;
    _totalUsed += amount;
    return _data[slot_addr].used_balances;
}
ℹ️ owner as Explicit Parameter vs msg.sender ℹ️ 명시적 파라미터 owner vs msg.sender
Most functions in dex.sol accept owner (or sender) as an explicit parameter rather than reading msg.sender. This is by design — it allows the platform to submit transactions via a shared EOA while specifying the logical owner as the user's AA wallet address. The platform is the transaction sender; the user's AA wallet is the logical actor. dex.sol의 대부분 함수는 msg.sender를 읽는 대신 owner(또는 sender)를 명시적 파라미터로 받습니다. 이는 의도된 설계입니다 — 플랫폼이 공유 EOA를 통해 트랜잭션을 제출하면서도 논리적 소유자를 사용자의 AA 지갑 주소로 지정할 수 있게 합니다. 플랫폼은 트랜잭션 발신자이고, 사용자의 AA 지갑은 논리적 행위자입니다.

05 Platform EOA — Transaction Sender vs Logical Owner 플랫폼 EOA — 트랜잭션 발신자 vs 논리적 소유자

The platform EOA (process.env.PRIVATE_KEY) plays a dual role in the system. Understanding which role it plays in each context prevents authorization confusion. 플랫폼 EOA(process.env.PRIVATE_KEY)는 시스템에서 이중 역할을 합니다. 각 컨텍스트에서 어떤 역할을 하는지 이해해야 권한 혼동을 방지할 수 있습니다.

Context 컨텍스트 Platform EOA's role 플랫폼 EOA의 역할 Logical owner passed 전달되는 논리적 소유자
init_permission_by_platform Tx signer (from) Tx 서명자 (from) owner param = user AA wallet address owner 파라미터 = 사용자 AA 지갑 주소
Paymaster signer 페이마스터 서명자 Authorizes gas sponsorship (paymasterAndData) 가스 대납 승인 (paymasterAndData) N/A — off-chain signature only 해당 없음 — 오프체인 서명만
Bundler (handleOps) 번들러 (handleOps) Submits UserOp array to EntryPoint, receives gas refund as beneficiary EntryPoint에 UserOp 배열 제출, beneficiary로 가스 환불 수령 N/A — the actual actor is the user's SimpleAccount 해당 없음 — 실제 행위자는 사용자의 SimpleAccount
dex.sol _platform dex.sol _platform Permanent privileged account set at _mint — receives platform fees, can set fees, can force-list slots _mint 시 설정되는 영구 특권 계정 — 플랫폼 수수료 수령, 수수료 설정, 강제 판매 등록 가능 N/A — inherent contract privilege 해당 없음 — 내재된 컨트랙트 권한
// init_permission_by_platform — B서버 (call.method.js)
async function init_permission_by_platform(userAAAddress, slotAddr, cuid, callback) {
    const platformAddr = web3.eth.accounts
        .privateKeyToAccount(process.env.PRIVATE_KEY).address;

    //  from: platformAddr      ← tx signer (gas payer, msg.sender in dex.sol)
    //  to:   DexInfo.address
    //  data: init_permission(userAAAddress, slotAddr, cuid)
    //                          ↑ logical owner = user's AA wallet, NOT platformAddr

    const tx = {
        from: platformAddr,
        to:   DexInfo.address,
        gas:  300000,
        data: content_contract.methods
                .init_permission(userAAAddress, slotAddr, cuid)
                .encodeABI()
    };
    const signedTx = await web3.eth.accounts.signTransaction(tx, process.env.PRIVATE_KEY);
    const receipt  = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
    return callback(true, receipt);
}

06 Reading Ownership State On-chain 온체인 소유권 상태 읽기

dex.sol exposes a get_slot_owner(address slot) view function. You can call it to verify the current ownership state directly from the blockchain at any time. dex.solget_slot_owner(address slot) view 함수를 제공합니다. 언제든지 블록체인에서 직접 현재 소유권 상태를 확인하기 위해 호출할 수 있습니다.

// Solidity (dex.sol)
function get_slot_owner(address slot) public view returns (address) {
    return _data[slot].owner_addr;
    // Returns: user's AA wallet address (db.Wallet.address)
    //          or address(0) if init_permission was never called
}

// JavaScript (ethers.js)
const dex = new ethers.Contract(DEX_ADDRESS, DEX_ABI, provider);
const ownerAddr = await dex.get_slot_owner(slotAddress);
// ownerAddr === db.Wallet.address (user's SimpleAccount)

// JavaScript (web3.js)
const ownerAddr = await content_contract.methods
    .get_slot_owner(slotAddress)
    .call();

// Verifying the full hierarchy:
// ownerAddr                          → user's AA wallet (SimpleAccount)
// await factory.getAddress(ownerEoa, account_idx) === ownerAddr  → ✓ addresses match
// db.Wallet.owner_eoa                → signs UserOps that act on behalf of ownerAddr

6-1. Expected Ownership State After init_permission 6-1. init_permission 후 예상되는 소유권 상태

Query 조회 Expected value 예상 값 Source 출처
dex.get_slot_owner(slotAddr) User AA wallet address 사용자 AA 지갑 주소 db.Wallet.address
dex._data[slotAddr].cuid Content unit ID (links to provider) 콘텐츠 유닛 ID (프로바이더 연결) oauth_meta.b_cuid
factory.getAddress(owner_eoa, account_idx) Must equal db.Wallet.address db.Wallet.address와 일치해야 함 Computed on-chain via CREATE2 CREATE2로 온체인 계산
simpleAccount.owner() Must equal db.Wallet.owner_eoa db.Wallet.owner_eoa와 일치해야 함 SimpleAccount contract storage SimpleAccount 컨트랙트 스토리지

Complete Hierarchy — Address Relationship Map 전체 계층 구조 — 주소 관계 지도

graph TD subgraph "Level 0 — Platform" P["Platform EOA
(process.env.PRIVATE_KEY)"] end subgraph "Level 1 — User EOA (off-chain key)" E["owner_eoa
random mnemonic-based
XOR 3-of-3 split"] end subgraph "Level 2 — User AA Wallet (on-chain)" AA["SimpleAccount (W_SALT)
factory.getAddress(owner_eoa, account_idx)
= db.Wallet.address"] end subgraph "dex.sol state" D["_data[slotAddr].owner_addr
= User AA Wallet address"] end subgraph "Level 3 — Slot / Content Wallet (on-chain)" CW["SimpleAccount (C_SALT)
factory.getAddress(c_salt_eoa, account_idx)
= db.CWallet.address"] end P -->|"signs tx: init_permission(userAA, slot, cuid)"| D E -->|"signs UserOperations
SimpleAccount.validateUserOp"| AA AA -->|"registered as owner_addr via init_permission"| D D -->|"slot key = CWallet address"| CW CW -->|"holds token balance, cuid, used_balances"| D style P fill:#1a0a0a,stroke:#ef4444,color:#efefef style E fill:#1a1200,stroke:#f59e0b,color:#efefef style AA fill:#0a1a00,stroke:#a3e635,color:#efefef style D fill:#101010,stroke:#555,color:#efefef style CW fill:#00111a,stroke:#38bdf8,color:#efefef
⚠️ Ownership vs Balance — Two Separate Concepts ⚠️ 소유권과 잔액 — 별개의 두 개념
Token balance in dex.sol (_balances[slotAddr]) is held by the slot address (CWallet). Ownership (_data[slotAddr].owner_addr) is held by the user AA wallet. You can transfer tokens from the slot only if you are the owner. The slot's balance does not follow ownership transfers — if the slot is sold via buy_slot, the token balance stays with the slot address while ownership changes to the buyer. dex.sol의 토큰 잔액(_balances[slotAddr])은 슬롯 주소(CWallet)에 보유됩니다. 소유권(_data[slotAddr].owner_addr)은 사용자 AA 지갑에 있습니다. 소유자여야만 슬롯에서 토큰을 이전할 수 있습니다. 슬롯의 잔액은 소유권 이전을 따르지 않습니다 — buy_slot으로 슬롯이 판매되면, 토큰 잔액은 슬롯 주소에 남아 있고 소유권만 구매자로 변경됩니다.

07 Slot = Access Credential — Why This Architecture Exists 슬롯 = 접속 자격증명 — 이 구조가 존재하는 이유

The slot (CWallet AA) is the app access credential.
In standard OAuth, the platform issues an access token tied to a session — the user's right to enter an app lives inside the platform's database and can be revoked at any time. WebXcom separates these two concerns: OAuth handles only identity (who you are), while the slot handles access (what you can enter). The slot is an on-chain entity owned by the user — not a token the platform issues and can revoke.
슬롯(CWallet AA)이 앱 접속 자격증명입니다.
표준 OAuth에서 플랫폼은 세션에 결합된 액세스 토큰을 발급합니다 — 사용자의 앱 접근 권한은 플랫폼의 데이터베이스 안에 있으며 언제든 취소될 수 있습니다. WebXcom은 이 두 가지를 분리합니다: OAuth는 신원만 처리하고(당신이 누구인지), 슬롯이 접속을 처리합니다(무엇에 접근할 수 있는지). 슬롯은 사용자가 소유하는 온체인 엔티티입니다 — 플랫폼이 발급하고 취소할 수 있는 토큰이 아닙니다.

7-0. The Paradigm Shift — Traditional OAuth vs Slot-based Access 7-0. 패러다임 전환 — 기존 OAuth vs 슬롯 기반 접속

In every conventional OAuth-connected service, the access credential is inseparable from the OAuth session. Log out, get banned, or have your token revoked — and access is instantly gone. The user is a tenant in someone else's authorization system. WebXcom's design inverts this: the user is the owner of their access credential, which exists independently of any session. 모든 일반적인 OAuth 연동 서비스에서 접속 자격증명은 OAuth 세션과 분리 불가능합니다. 로그아웃하거나, 밴을 당하거나, 토큰이 취소되면 — 접속은 즉시 사라집니다. 사용자는 다른 누군가의 인가 시스템 안의 임차인입니다. WebXcom의 설계는 이를 뒤집습니다: 사용자는 자신의 접속 자격증명의 소유자이며, 이 자격증명은 어떤 세션과도 독립적으로 존재합니다.

Traditional OAuth 기존 OAuth WebXcom Slot Model WebXcom 슬롯 모델
What is the access credential? 접속 자격증명의 실체 An ephemeral access token — a string in the platform's DB. Expires, can be revoked. 플랫폼 DB의 문자열인 일시적 액세스 토큰. 만료되고 취소 가능. A slot (AA wallet address) on-chain — a contract address with state recorded on the blockchain. Permanent unless transferred. 온체인의 슬롯(AA 지갑 주소) — 블록체인에 상태가 기록된 컨트랙트 주소. 이전하지 않는 한 영구.
Who owns access? 접속 권한의 소유자 The platform — it issues, stores, and can revoke the token at will. 플랫폼 — 토큰을 발급·보관하며 임의로 취소 가능. The user's AA wallet — recorded in dex._data[slot].owner_addr, enforced by EVM consensus. 사용자의 AA 지갑dex._data[slot].owner_addr에 기록, EVM 합의로 집행.
OAuth role OAuth의 역할 Both identity verification AND access gating. OAuth is the single source of truth. 신원 검증과 접속 통제 모두. OAuth가 유일한 진실의 원천. Identity only — "who are you?" Once identity is confirmed, on-chain state determines what you can do. 신원만 — "당신은 누구인가?" 신원이 확인되면, 온체인 상태가 무엇을 할 수 있는지를 결정.
Can platform revoke access? 플랫폼의 접속 취소 가능? Yes — at any time, unilaterally, without user consent. 예 — 언제든, 일방적으로, 사용자 동의 없이. Off-chain session only. On-chain slot ownership is beyond platform control — only the user can transfer it. 오프체인 세션만 취소 가능. 온체인 슬롯 소유권은 플랫폼 통제 밖 — 사용자만 이전 가능.
Is access transferable? 접속 권한 이전 가능? No — tokens are non-transferable by design. Access dies with the account. 불가 — 토큰은 설계상 이전 불가. 접속 권한은 계정과 함께 소멸. Yes — init_self (free) or buy_slot (paid) changes owner_addr to another user's AA wallet atomically. 가능 — init_self(무상) 또는 buy_slot(유상)으로 owner_addr를 다른 사용자 AA로 원자적 변경.
Multiple app access? 복수 앱 접속? Separate tokens per app — all managed and controlled by the platform. 앱별 별도 토큰 — 모두 플랫폼이 관리·통제. Separate slots per app (cuid) — all owned by the user's single AA wallet, independently manageable. 앱별 별도 슬롯(cuid) — 모두 사용자의 단일 AA 지갑이 소유, 독립적으로 관리 가능.
// ── Traditional OAuth flow (access tied to session) ──
User Login → OAuth issues access_token (stored in platform DB)
App calls OAuth server: "Is this token valid?" → Yes/No
Platform can: DELETE token → access gone instantly
User cannot: move, sell, or keep the token if platform decides otherwise

// ── WebXcom Slot flow (access is on-chain ownership) ──
User Login (OAuth) → verifies IDENTITY only: "this is account_idx=42"
B-server checks: dex.get_slot_owner(slotAddr) → 0xUserAAWallet
Platform can: revoke OAuth session (off-chain only)
Platform cannot: change _data[slot].owner_addr — only the owner can
User can: sell, transfer, or keep the slot regardless of platform policy

7-1. What the Slot Represents — Per-App Access Unit 7-1. 슬롯이 나타내는 것 — 앱별 접속 단위

Each slot corresponds to one OAuth-connected application or content provider, identified by cuid. The slot is the on-chain record that the user has access to that specific app. When the connected app wants to verify whether a user is authorized, it resolves the user's AA wallet address and checks dex.get_slot_owner(slotAddr) — not an OAuth token, not a server-side session. 각 슬롯은 cuid로 식별되는 하나의 OAuth 연동 앱 또는 콘텐츠 프로바이더에 대응합니다. 슬롯은 사용자가 해당 특정 앱에 대한 접속 권한을 보유하고 있다는 온체인 레코드입니다. 연동 앱이 사용자의 인가 여부를 확인하려 할 때, 사용자의 AA 지갑 주소를 해석하고 dex.get_slot_owner(slotAddr)를 확인합니다 — OAuth 토큰이 아니라, 서버 사이드 세션이 아니라.

// Connected app authorization check (slot-based, not session-based)
//
// Question: "Does this OAuth user have access to AppX (cuid=7)?"
//
// Step 1: resolve user's AA wallet from OAuth identity
const userAAAddress = db.Wallet.address  // from OAuth account_idx

// Step 2: find the slot address registered for this app
const slotAddress = db.CWallet.address   // user's slot for cuid=7

// Step 3: verify on-chain ownership
const owner = await dex.get_slot_owner(slotAddress)
const hasAccess = (owner.toLowerCase() === userAAAddress.toLowerCase())
//
// hasAccess=true  → user owns the slot → authorized
// hasAccess=false → slot unregistered or transferred → denied
// No OAuth server call needed. No session table lookup. Pure on-chain truth.
Concrete analogy: In traditional SaaS, your subscription is a row in the platform's database. If the database is deleted, you lose access. In this system, your subscription is an entry in an immutable on-chain contract. Even if the platform's servers go offline, the ownership record persists on the blockchain and can be read by any node. 구체적 비유: 기존 SaaS에서 구독은 플랫폼 데이터베이스의 한 행입니다. 데이터베이스가 삭제되면 접속을 잃습니다. 이 시스템에서 구독은 불변의 온체인 컨트랙트의 항목입니다. 플랫폼의 서버가 오프라인이 되더라도, 소유권 레코드는 블록체인에 남아 있고 모든 노드가 읽을 수 있습니다.

7-2. OAuth = Identity Gate · Slot = Access Gate — Clean Separation 7-2. OAuth = 신원 게이트 · 슬롯 = 접속 게이트 — 명확한 분리

The system enforces a strict role separation. OAuth is consulted only to confirm the user is who they claim to be, and to derive which AA wallet address corresponds to them. After that, every access decision is made against on-chain state — not against an OAuth session. 시스템은 엄격한 역할 분리를 강제합니다. OAuth는 사용자가 주장하는 신원이 맞는지 확인하고, 해당 AA 지갑 주소를 파생하는 데만 사용됩니다. 이후 모든 접속 결정은 OAuth 세션이 아닌 온체인 상태를 기준으로 이루어집니다.

sequenceDiagram participant U as User participant APP as OAuth-Connected App participant OAUTH as OAuth Server (A서버) participant CHAIN as Blockchain (dex.sol) U->>APP: "I want to access AppX" APP->>OAUTH: Validate identity (access token) OAUTH-->>APP: account_idx=42, AA wallet=0xUserAA ✓ Note over APP,CHAIN: Identity confirmed. Now check ACCESS separately. APP->>CHAIN: get_slot_owner(slotAddress_for_AppX) CHAIN-->>APP: owner = 0xUserAA Note over APP: owner matches user AA? → YES → Access granted Note over APP: owner is 0x0 or different? → Access denied APP-->>U: Access granted (based on on-chain slot, not session)

7-3. Multi-Slot — One Identity, Many Independent App Credentials 7-3. 멀티슬롯 — 하나의 신원, 여러 앱의 독립 접속 자격증명

A single AA wallet owns one slot per OAuth-connected app (cuid). Each slot is an independent on-chain object — different address, different token balance, different usage history. The user can hold access credentials for many apps simultaneously, each with its own lifecycle, and manage them independently without affecting the others. 하나의 AA 지갑은 OAuth 연동 앱(cuid)별로 하나의 슬롯을 소유합니다. 각 슬롯은 독립적인 온체인 객체입니다 — 다른 주소, 다른 토큰 잔액, 다른 사용 이력. 사용자는 여러 앱의 접속 자격증명을 동시에 보유할 수 있으며, 각각 독립적인 생명주기를 갖고, 다른 것에 영향 없이 개별 관리할 수 있습니다.

// One user's AA wallet owns access credentials for multiple apps
0xUserAA_Wallet
  │
  ├─ Slot_AppX  (cuid=1)  ← access to streaming service AppX
  │     owner_addr = 0xUserAA  ✓
  │     used_balances = 200 tokens (content consumed)
  │     is_sell = false
  │
  ├─ Slot_AppY  (cuid=2)  ← access to gaming platform AppY
  │     owner_addr = 0xUserAA  ✓
  │     used_balances = 0 tokens
  │     is_sell = false
  │
  └─ Slot_AppZ  (cuid=3)  ← access to developer tool AppZ
        owner_addr = 0xUserAA  ✓
        used_balances = 50 tokens
        is_sell = true, price = 0.05 ETH  ← listed for sale

7-4. User Self-Sovereign Management — Platform as Infrastructure, Not Gatekeeper 7-4. 사용자 자기주권 관리 — 플랫폼은 인프라, 게이트키퍼가 아님

Once init_permission registers the slot on-chain, the user's AA wallet becomes the sole on-chain authority over that slot. The user can exercise the following without any platform intermediation — the contract only checks owner_addr: init_permission이 슬롯을 온체인에 등록하면, 사용자의 AA 지갑이 해당 슬롯의 유일한 온체인 권한자가 됩니다. 사용자는 플랫폼 중재 없이 다음을 수행할 수 있습니다 — 컨트랙트는 owner_addr만 확인합니다:

// All of these require only: _data[slot].owner_addr == caller
// Platform cannot block or override any of these.

init_sell_amount(slot, price)       // list slot for sale at chosen price
init_sell_cancle(sender, slot)      // cancel listing anytime
init_self(sender, slot, newOwner)   // give slot to anyone, free
transferAdex(owner, from, to, val)  // move available token balance

// Contrast: in traditional OAuth, none of these actions exist.
// The "access credential" (token) is platform property.
// Here, the slot is user property on an immutable ledger.

7-5. Slot Transfer — Moving App Access Between OAuth Identities 7-5. 슬롯 트랜스퍼 — OAuth 신원 간 앱 접속 자격증명 이전

The most consequential capability: a slot's owner_addr can be changed to point to another user's AA wallet. When this happens, the receiving user's OAuth identity is now the on-chain owner — their AA wallet immediately gains all ownership privileges over the slot. This is the on-chain equivalent of transferring an app membership, license, or subscription to another person — without asking the platform, without the platform's involvement, enforced by the blockchain. 가장 중요한 기능: 슬롯의 owner_addr다른 사용자의 AA 지갑으로 변경할 수 있습니다. 이 경우 수신 사용자의 OAuth 신원이 온체인 소유자가 됩니다 — 그들의 AA 지갑은 즉시 슬롯에 대한 모든 소유자 권한을 획득합니다. 이것은 앱 멤버십, 라이선스, 구독을 다른 사람에게 이전하는 온체인 등가물입니다 — 플랫폼에 묻지 않고, 플랫폼의 개입 없이, 블록체인이 집행합니다.

// Scenario A: Gift / organizational transfer (init_self — free)
dex.init_self(
    sender = UserA_AA,     // current owner — must match _data[slot].owner_addr
    slot   = slotAddress,  // the access credential being transferred
    owner  = UserB_AA      // recipient — their db.Wallet.address
)
// After:  _data[slot].owner_addr = UserB_AA
//         App checks get_slot_owner(slot) → UserB_AA
//         UserA has lost access. UserB has gained access.
//         No database update needed on OAuth side for on-chain truth.

// Scenario B: Marketplace sale (buy_slot — paid)
// Anyone can buy a listed slot:
dex.buy_slot{ value: price }(buyer, slotAddress, cuid, store_manager)
// Atomically:
//   - ETH distributed to platform, provider, store, and seller (UserA_AA)
//   - _data[slot].owner_addr = buyer  (UserB_AA)
//   - is_sell reset to false, price reset to 0
// The buyer inherits the slot's full on-chain state: cuid, used_balances, history.
What transfers with the slot: the cuid (app identity), the token balance in the slot address, and all ownership privileges. What does NOT transfer: off-chain OAuth metadata (oauth_meta records, perm_tx). The platform must reconcile off-chain state by listening for Transfer events or by polling get_slot_owner. The on-chain truth is always the authoritative record. 슬롯과 함께 이전되는 것: cuid(앱 신원), 슬롯 주소의 토큰 잔액, 모든 소유자 권한. 이전되지 않는 것: 오프체인 OAuth 메타데이터(oauth_meta 레코드, perm_tx). 플랫폼은 Transfer 이벤트를 수신하거나 get_slot_owner를 폴링하여 오프체인 상태를 동기화해야 합니다. 온체인 진실이 항상 권위 있는 레코드입니다.

Full System Map — OAuth Identity to On-chain Access Credential Transfer 전체 시스템 구조 — OAuth 신원에서 온체인 접속 자격증명 이전까지

graph TD subgraph "Identity Layer — OAuth (off-chain)" OA["User A · OAuth Account\n(identity only)"] OB["User B · OAuth Account\n(identity only)"] end subgraph "Blockchain Identity (on-chain)" AAA["User A AA Wallet\n= db.Wallet.address"] AAB["User B AA Wallet\n= db.Wallet.address"] end subgraph "Access Credential Layer — dex.sol Slots (on-chain)" S1["Slot · AppX (cuid=1)\nAccess credential to AppX\nowner_addr = UserA AA"] S2["Slot · AppY (cuid=2)\nAccess credential to AppY\nowner_addr = UserA AA"] S3["Slot · AppX (cuid=1)\nAccess credential to AppX\nowner_addr = UserB AA"] end OA -->|"signs for (EOA key)"| AAA OB -->|"signs for (EOA key)"| AAB AAA -->|"init_permission\n→ owns access cred"| S1 AAA -->|"init_permission\n→ owns access cred"| S2 AAB -->|"init_permission\n→ owns access cred"| S3 AAA -->|"init_self (free)\nor buy_slot (paid)\n→ transfer AppX access\nto User B"| AAB AAB -.->|"now owner_addr of S1\nUser B has AppX access\nUser A loses AppX access"| S1 style OA fill:#0a1000,stroke:#a3e635,color:#d4f7a0 style OB fill:#0a1000,stroke:#a3e635,color:#d4f7a0 style AAA fill:#001020,stroke:#38bdf8,color:#bfefff style AAB fill:#001020,stroke:#38bdf8,color:#bfefff style S1 fill:#1a0800,stroke:#f59e0b,color:#ffe4a0 style S2 fill:#10001a,stroke:#a855f7,color:#e8c4ff style S3 fill:#001a08,stroke:#22c55e,color:#a0ffc0
⚠️ On-chain Ownership and Off-chain Session Are Independent ⚠️ 온체인 소유권과 오프체인 세션은 독립적입니다
A user who has lost their OAuth session (logged out, token expired) still owns their slots on-chain. Conversely, a user with a valid OAuth session who has transferred or sold their slot no longer has on-chain access. The on-chain slot state is the ground truth for access; OAuth is consulted only for identity, not for the current authorization state. Apps that use this system must check both independently. OAuth 세션을 잃은(로그아웃, 토큰 만료) 사용자는 여전히 온체인에서 슬롯을 소유합니다. 반대로, 유효한 OAuth 세션이 있어도 슬롯을 이전하거나 판매한 사용자는 더 이상 온체인 접속 권한이 없습니다. 온체인 슬롯 상태가 접속의 근본적 진실입니다. OAuth는 신원 확인에만 사용하고, 현재 인가 상태 확인에는 사용하지 않습니다. 이 시스템을 사용하는 앱은 두 레이어를 독립적으로 확인해야 합니다.

08 XOR Key Slot Sale Flow — approve_slot · buy_slot Platform Relay XOR 키 슬롯 판매 플로우 — approve_slot · buy_slot 플랫폼 중계

The slot sale mechanism introduces a 3-step flow that eliminates race conditions inherent in naive two-party XOR key signing scenarios. The seller's XOR key is used exactly once — to sign approve_slot. After that, the platform EOA relays buy_slot without requiring the buyer's XOR key at all. 슬롯 판매 메커니즘은 두 당사자 XOR 키 서명 시나리오에 내재된 경쟁 조건을 제거하는 3단계 플로우를 도입합니다. 판매자의 XOR 키는 approve_slot 서명에 정확히 한 번만 사용됩니다. 이후 플랫폼 EOA는 구매자의 XOR 키 없이 buy_slot을 중계합니다.

8-0. The Problem — Concurrent XOR Session Race Condition 8-0. 문제 — 동시 XOR 세션 경쟁 조건

⚠️ Before approve_slot: Two XOR Sessions Could Collide ⚠️ approve_slot 이전: 두 XOR 세션의 충돌 가능성
In the original design, purchasing a slot required the buyer's XOR key to sign buy_slot. This meant two XOR sessions were active simultaneously: the seller's key (for init_sell_amount) and the buyer's key (for buy_slot). During that window, the seller could call init_sell_cancle to cancel the listing — racing against the buyer's transaction. The approve_slot mechanism eliminates this entirely. 기존 설계에서 슬롯 구매는 구매자의 XOR 키가 buy_slot에 서명해야 했습니다. 즉 두 XOR 세션이 동시에 활성화됐습니다: 판매자 키(init_sell_amount용)와 구매자 키(buy_slot용). 그 시간 동안 판매자는 init_sell_cancle을 호출해 목록을 취소할 수 있었습니다 — 구매자 트랜잭션과 경쟁 상태. approve_slot 메커니즘이 이를 완전히 제거합니다.

8-1. approve_slot — ERC721-style Operator Approval 8-1. approve_slot — ERC721 방식의 운영자 승인

approve_slot is modeled after ERC721's approve(to, tokenId). The slot owner signs a transaction granting a specific operator address the right to execute buy_slot on their behalf. This is the moment the seller's XOR key is used — once — to confirm sell intent. After this call, _slot_approved[slot] records the approved operator and init_sell_cancle is locked. approve_slot은 ERC721의 approve(to, tokenId)를 모델로 합니다. 슬롯 소유자가 특정 operator 주소에 buy_slot을 대신 실행할 권한을 부여하는 트랜잭션에 서명합니다. 이것이 판매자의 XOR 키가 사용되는 순간입니다 — 단 한 번 — 판매 의사를 확정하기 위해. 호출 후 _slot_approved[slot]에 승인된 운영자가 기록되고 init_sell_cancle이 잠깁니다.

// dex.sol — approve_slot
function approve_slot(address slot, address operator) external returns(bool) {
    // _msgSender() must equal the slot's owner_addr (AA wallet address)
    require(_data[slot].owner_addr == _msgSender(), "approve_slot: Not slot owner");
    require(_data[slot].is_sell == true,  "approve_slot: Slot is not listed for sale");
    require(operator != address(0),       "approve_slot: operator is zero address");

    // Once set, init_sell_cancle reverts until revoke_slot() is called
    _slot_approved[slot] = operator;
    return true;
}

// dex.sol — init_sell_cancle (updated guard)
function init_sell_cancle(address sender, address slot) public returns(bool) {
    require(_data[slot].owner_addr == sender, "init_sell_cancle: Not slot owner");
    require(
        _slot_approved[slot] == address(0),
        "init_sell_cancle: Slot is approved for relay. Call revoke_slot first"
    );
    _data[slot].is_sell = false;
    return true;
}
ℹ️ Why _msgSender()? — AA Wallet Must Sign Directly ℹ️ 왜 _msgSender()? — AA 지갑이 직접 서명해야 하는 이유
Unlike init_permission or init_sell_cancle (which accept an explicit owner/sender parameter that the platform can fill), approve_slot uses _msgSender(). The AA wallet itself must be the transaction originator — enforced via a UserOperation signed by the seller's XOR-restored EOA key. The platform cannot relay this call; the seller's cryptographic identity must directly authorize it. 명시적 owner/sender 파라미터를 받는 init_permission이나 init_sell_cancle과 달리, approve_slot_msgSender()를 사용합니다. AA 지갑 자체가 트랜잭션 발신자여야 합니다 — 판매자의 XOR 복원 EOA 키로 서명한 UserOperation을 통해 집행됩니다. 플랫폼이 이 호출을 중계할 수 없습니다; 판매자의 암호학적 신원이 직접 승인해야 합니다.

8-2. revoke_slot — Cancelling the Approval 8-2. revoke_slot — 승인 취소

revoke_slot mirrors ERC721's ability to rescind an approval. Either the approved operator (platform EOA) or the slot owner (user AA wallet) can revoke. After revocation, _slot_approved[slot] resets to address(0) and init_sell_cancle is unlocked again. revoke_slot은 ERC721의 승인 취소 기능을 반영합니다. 승인된 운영자(플랫폼 EOA) 또는 슬롯 소유자(사용자 AA 지갑) 모두 revoke할 수 있습니다. revoke 후 _slot_approved[slot]address(0)으로 초기화되고 init_sell_cancle이 다시 잠금 해제됩니다.

// dex.sol — revoke_slot
function revoke_slot(address slot) external returns(bool) {
    require(
        _slot_approved[slot] == _msgSender() ||   // approved operator (platform EOA)
        _data[slot].owner_addr == _msgSender(),    // or slot owner (user AA wallet)
        "revoke_slot: Not authorized"
    );
    _slot_approved[slot] = address(0);  // approval cleared → init_sell_cancle unlocked
    return true;
}

// Seller wants to cancel after approve_slot:
//   1. platform.revoke_slot(slot)   ← platform cooperates and revokes
//   2. seller.init_sell_cancle(...)  ← now allowed (slot_approved == address(0))

8-3. _slot_approved Lifecycle — One-shot Approval 8-3. _slot_approved 생명주기 — 1회성 승인

State상태 _slot_approved[slot] init_sell_cancleinit_sell_cancle Platform buy_slot relay플랫폼 buy_slot 중계
Initial / After revoke / After buy초기 / revoke 후 / 구매 후 address(0) Allowed허용 Blocked차단
After approve_slot(slot, platform)approve_slot 후 platform.address Blocked (race condition prevented)차단 (경쟁 조건 방지) Allowed허용
After buy_slot completesbuy_slot 완료 후 address(0) (auto-cleared) Allowed (new owner)허용 (새 소유자) Blocked (price reset to 0)차단 (가격 0 초기화)

8-4. buy_slot — Platform Relay (No Buyer XOR Required) 8-4. buy_slot — 플랫폼 중계 (구매자 XOR 불필요)

After approve_slot(slot, platform), the platform EOA executes buy_slot directly — passing the buyer's AA wallet address as a parameter while acting as _msgSender(). The buyer's XOR key is never activated. ETH distribution executes atomically within the same transaction. approve_slot(slot, platform) 호출 후, 플랫폼 EOA는 buy_slot을 직접 실행합니다 — 구매자의 AA 지갑 주소를 파라미터로 전달하되 _msgSender()로서 작동합니다. 구매자의 XOR 키는 절대 활성화되지 않습니다. ETH 분배는 동일 트랜잭션 내에서 원자적으로 실행됩니다.

// dex.sol — buy_slot caller check
require(
    _msgSender() == buyer ||                   // buyer themselves (direct)
    _slot_approved[slot] == _msgSender(),      // OR: approved operator (platform relay)
    "buy_slot: Not buyer or approved operator"
);

// ETH distribution (sequential, balance-based within same tx):
// 1. platform:      balance × _fee_slot_platform / 100  (default 3%)
// 2. provider:      balance × _fee_slot_provider / 100  (default 4%, skipped if unregistered)
// 3. store_manager: balance × _fee_slot_store    / 100  (default 3%)
// 4. seller (prev owner): remaining balance → call{value:...}()
//
// State after distribution:
// _data[slot].price      = 0           ← reset (must re-register for next sale)
// _data[slot].is_sell    = false       ← reset
// _data[slot].owner_addr = buyer_AA    ← ownership transferred
// _slot_approved[slot]   = address(0)  ← approval auto-cleared (one-shot)

Full XOR Key Slot Sale Flow 전체 XOR 키 슬롯 판매 플로우

sequenceDiagram participant S as Seller (Browser) participant A as A서버 (NestJS) participant B as B서버 (Blockchain) participant DEX as dex.sol Note over S,DEX: STEP 1 — 판매 등록 (platform 또는 seller AA wallet) S->>A: POST /v1/pin/verify { pin_hash, share_user, action: initSellAmount, price } A->>B: gRPC initSellAmount({ account_idx, pin_hash, share_user, slot_addr, price }) B->>B: restoreKeyXor3(share_user, pin_hash, share_server) → seller EOA key ✓ B->>DEX: Platform or Seller AA: init_sell_amount(slot, price) DEX->>DEX: _data[slot].is_sell = true, price = price ✓ B-->>A: tx_hash Note over S,DEX: STEP 2 — 플랫폼 중계 승인 (seller XOR key 필수 — _msgSender 검증) S->>A: POST /v1/pin/verify { pin_hash, share_user, action: approveSlot } A->>B: gRPC approveSlot({ account_idx, pin_hash, share_user, slot_addr }) B->>B: restoreKeyXor3(share_user, pin_hash, share_server) → seller EOA key ✓ B->>B: Sign UserOp: seller EOA key → AA wallet becomes _msgSender B->>DEX: Seller AA wallet: approve_slot(slot, platform_EOA) via UserOp DEX->>DEX: _slot_approved[slot] = platform_EOA ✓ DEX->>DEX: init_sell_cancle now BLOCKED (race condition eliminated) B-->>A: tx_hash Note over S,DEX: STEP 3 — 플랫폼 중계 구매 (buyer XOR key 불필요) B->>DEX: Platform EOA: buy_slot(buyer_AA, slot, cuid, store_mgr) {value: price} DEX->>DEX: ETH split: platform 3% + provider 4% + store 3% + seller remainder DEX->>DEX: owner_addr = buyer_AA ✓ DEX->>DEX: is_sell = false, price = 0, _slot_approved = address(0) (auto-cleared) B-->>A: tx_hash

8-5. Repeated Sale Cycles — Full State Reset After Each Purchase 8-5. 반복 판매 사이클 — 구매마다 완전한 상태 초기화

Each purchase fully resets the slot's sale state. The new owner (buyer) immediately becomes eligible to re-list, re-approve, and re-sell. There is no residual state from previous ownership — each cycle is entirely independent. Verified across 20 consecutive rounds with full state integrity checked at every step. 각 구매는 슬롯의 판매 상태를 완전히 초기화합니다. 새 소유자(구매자)는 즉시 재등록, 재승인, 재판매 자격을 갖습니다. 이전 소유권의 잔여 상태가 없습니다 — 각 사이클은 완전히 독립적입니다. 매 단계마다 상태 무결성을 확인하는 20회 연속 라운드에서 검증 완료.

// State immediately after buy_slot — new owner ready for next cycle
_data[slot].price      → 0          // must call init_sell_amount again
_data[slot].is_sell    → false      // must call init_sell_amount again
_data[slot].owner_addr → buyer_AA   // new owner — signs next approve_slot
_slot_approved[slot]   → address(0) // cleared — new owner re-approves for next sale

// Cycle repeats (N rounds, each independent):
// New owner: init_sell_amount(price)
//          → approve_slot(slot, platform)    ← new owner's XOR key, once
//          → [platform]: buy_slot(nextBuyer) ← no XOR key needed
//          → next_owner owns slot, state reset
ℹ️ Hardhat Test Coverage — 87 tests passing ℹ️ Hardhat 테스트 커버리지 — 87개 테스트 통과
The approve/buy/race-condition flow is covered by test/03_approve_race.test.js (25 tests) and the 20-round repeat stress test in group 3-7, alongside the full XOR integration in test/02_slot_lifecycle.test.js (43 tests). Run with: npx hardhat test --network hardhat. approve/buy/경쟁조건 플로우는 test/03_approve_race.test.js(25개 테스트) 및 그룹 3-7의 20회 반복 스트레스 테스트로 검증됩니다. 전체 XOR 통합은 test/02_slot_lifecycle.test.js(43개 테스트). 실행: npx hardhat test --network hardhat.