— 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 내부에서 "소유권"의 의미, 그리고 계층의 각 레벨이 승인할 수 있는 작업들을 다룹니다.
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-레벨 계층 구조를 형성합니다.
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] — 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, 판매 상태를 추적합니다.
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.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.sol이 owner_addr로 기록하는 대상이고, 슬롯 판매 시 ETH를 수령하는 주체이며, 다른 온체인 참여자가 상호작용하는 대상입니다. EOA를 자동차 키, AA 지갑을 자동차로 비유하면 — 키는 차를 제어하는 수단이지만, 도로 위에 존재하는 것은 차입니다.dex.sol에서 slot은 항상 콘텐츠 지갑 주소(Level 3 — _data의 키)를 가리키고, owner는 사용자 AA 지갑 주소(Level 2 — _data[slot].owner_addr에 저장된 값)를 가리킵니다. 이 두 주소는 서로 다른 owner EOA와 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:]
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_eoa는 account_idx와 W_SALT에서 결정론적으로 파생되며 — 서버가 제어하고 사용자에게 노출되지 않습니다. 이 두 값을 혼동하는 것이 AA24 서명 에러의 근본 원인입니다.
_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
}
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;
}
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 키 복원으로 사용자 신원이 검증됩니다.
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_balances를 amount만큼 증가. 콘텐츠 소비 시 사용.
|
| 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;
}
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 지갑은 논리적 행위자입니다.
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);
}
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.sol은 get_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
| 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 컨트랙트 스토리지 |
_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으로 슬롯이 판매되면, 토큰 잔액은 슬롯 주소에 남아 있고 소유권만 구매자로 변경됩니다.
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는 신원만 처리하고(당신이 누구인지), 슬롯이 접속을 처리합니다(무엇에 접근할 수 있는지). 슬롯은 사용자가 소유하는 온체인 엔티티입니다 — 플랫폼이 발급하고 취소할 수 있는 토큰이 아닙니다.
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
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.
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 세션이 아닌 온체인 상태를 기준으로 이루어집니다.
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
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.
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.
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를 폴링하여 오프체인 상태를 동기화해야 합니다. 온체인 진실이 항상 권위 있는 레코드입니다.
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을 중계합니다.
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 메커니즘이 이를 완전히 제거합니다.
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;
}
_msgSender()? — AA Wallet Must Sign Directly
ℹ️ 왜 _msgSender()? — AA 지갑이 직접 서명해야 하는 이유
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을 통해 집행됩니다. 플랫폼이 이 호출을 중계할 수 없습니다; 판매자의 암호학적 신원이 직접 승인해야 합니다.
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))
_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 초기화) |
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)
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
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.