15. Approval State Machine (TAP)

Stage 10 Policy 명세 — 3 action · first-match · Unanimous-Veto · 3-tier governance

3 Action Semantics (★ Stage 10)

how-policies-work.md, p.1 — Policy rule 의 action 은 3 종.

Action의미State 전이
Allow자동 approve → designated signer 로 forwardSUBMITTED → QUEUED (PENDING_AUTHORIZATION 우회)
Approved by지정 user / user group 이 approve / deny. 모든 associated role 이 authorized approver 여야 함SUBMITTED → PENDING_AUTHORIZATION (collecting)
Block자동 차단SUBMITTED → BLOCKED (violated rule number 표시)

First-Match Principle

  • Policy 를 top-to-bottom scan
  • 첫 match 에서 scan 중단 + action 실행
  • 모든 Policy 의 마지막 rule = block-all (default, 삭제 불가) → default-deny architecture

Rule Ordering (most restrictive first)

  • Time-period based rule 먼저 (cumulative limit 가 single-tx rule 보다 앞)
  • Overlap 시 더 strict 한 rule 먼저 (예: $10M+ 2-approval rule 이 $1M+ 1-approval 보다 앞)

Approval State Machine

stateDiagram-v2
  [*] --> EVALUATING : tx submitted
  EVALUATING --> AUTO_APPROVED : action = Allow
  EVALUATING --> COLLECTING : action = Approved by
  EVALUATING --> BLOCKED : action = Block (rule violation)
  EVALUATING --> BLOCKED : no rule match (default block-all)

  COLLECTING --> APPROVED : threshold met
  COLLECTING --> REJECTED : 1 reject (Unanimous-Veto)
  COLLECTING --> EXPIRED : 2h timeout (PENDING_AUTHORIZATION)
  COLLECTING --> EXPIRED : 7-day timeout (Add user flow)

  AUTO_APPROVED --> [*]
  APPROVED --> [*]
  REJECTED --> [*]
  BLOCKED --> [*]
  EXPIRED --> [*]
      
Figure 7. Approval state machine — Allow auto-approve / Approved by collecting / Block fail-fast. Unanimous-Veto: 1 reject = 즉시 REJECTED.
Approvers Unanimous-Veto Rule (★)

transaction-lifecycle.md, p.6 직접 인용: "If at least one person chooses to reject a transaction, the transaction is rejected."
Approval group 의 N명 중 1명 reject = 즉시 fail. 다수결 아님.

3-Tier Governance (★ Stage 10 spine)

Admin Quorum  (workspace-level default)
  └── Approval Group  (action-level 위임, 12 actions)
        └── Policy "Approved by" sub-quorum  (rule-level 위임, user group 기반 N-of-M)

how-policies-work.md, p.4: "you can also define approval quorums within a group as part of the Approved by action... requires any two members of a user group called Management to approve."

Default Policy Rules (5)

모든 신규 workspace 자동 적용 (about-policies.md, p.4):

  1. Transfer Policy 1: Allow NFT transfer → whitelisted address
  2. Transfer Policy 2: Allow 모든 USD amount, 모든 asset → whitelisted address
  3. Contract Call Policy: Allow contract call → whitelisted smart contract
  4. Approve Policy: Allow Web3 Approve tx
  5. All Policies (마지막, 삭제 불가): Block any tx not explicitly allowed

Custom Policy 도입 = default 즉시 삭제 (one-way replacement). Custom 전부 삭제 시 default 복원.

DB Schema

CREATE TABLE approval_requests (
  id                BINARY(16) PRIMARY KEY,
  workspace_id      BINARY(16) NOT NULL,
  subject_type      ENUM('transaction', 'add_user', 'edit_user', 'policy_change',
                         'admin_quorum_change', 'whitelist_address') NOT NULL,
  subject_id        BINARY(16) NOT NULL,
  matched_rule_id   BINARY(16),                       -- first-match rule
  action            ENUM('allow', 'approved_by', 'block') NOT NULL,
  state             ENUM('EVALUATING', 'COLLECTING', 'AUTO_APPROVED',
                         'APPROVED', 'REJECTED', 'BLOCKED', 'EXPIRED') NOT NULL,
  threshold         INT,                              -- N of M
  collected_approvals INT NOT NULL DEFAULT 0,
  created_at        DATETIME(6) NOT NULL,
  expires_at        DATETIME(6) NOT NULL,             -- 2h (tx) / 7-day (add_user)
  finalized_at      DATETIME(6),                      -- ★ set-once
  KEY (workspace_id, state),
  KEY (expires_at)
);

CREATE TABLE approval_decisions (
  id                BINARY(16) PRIMARY KEY,
  request_id        BINARY(16) NOT NULL,
  approver_user_id  BINARY(16) NOT NULL,
  decision          ENUM('approve', 'reject') NOT NULL,
  mobile_device_sig BLOB NOT NULL,                    -- ★ set-once, append-only
  decided_at        DATETIME(6) NOT NULL,
  UNIQUE KEY (request_id, approver_user_id)           -- 1 user = 1 decision per request
);
Sticky Terminal · Set-once

approval_requests.state 가 APPROVED / REJECTED / BLOCKED / EXPIRED 도달 시 더 이상 전이 불가 (BEFORE UPDATE trigger).
approval_decisions 의 mobile_device_sig 는 set-once + append-only.