diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 0000000..dc2b7ff --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,39 @@ +# CLA Assistant GitHub Action +# Copy this file to: .github/workflows/cla.yml in each repo +# +# Prerequisites: +# 1. Go to repo Settings → Secrets → add secret named CLA_TOKEN +# (a Personal Access Token with repo scope from an org admin account) +# 2. The CLA text lives at: +# https://github.com/AAStarCommunity/Brood/blob/main/protocol/CLA.md + +name: CLA Assistant + +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened, closed, synchronize] + +permissions: + actions: write + contents: write + pull-requests: write + statuses: write + +jobs: + cla-check: + runs-on: ubuntu-latest + steps: + - name: CLA Assistant + uses: contributor-assistant/github-action@v2.6.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_TOKEN }} + with: + path-to-signatures: 'signatures/cla.json' + path-to-document: 'https://github.com/AAStarCommunity/Brood/blob/main/protocol/CLA.md' + branch: 'cla-signatures' + allowlist: bot*,dependabot[bot],github-actions[bot] + remote-organization-name: AAStarCommunity + remote-repository-name: Brood diff --git a/CLAUDE.md b/CLAUDE.md index 72f7458..22840af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,10 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +## Mycelium Protocol 生态上下文 + +@/Users/jason/Dev/Brood/protocol/MISSION.md +@/Users/jason/Dev/Brood/orgs/aastar/PROFILE.md +@/Users/jason/Dev/Brood/orgs/aastar/INTERFACES.md ## Project Overview diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8ed062e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing Guide + +> Part of [Mycelium Protocol](https://github.com/AAStarCommunity/Brood) ecosystem. +> Full contribution guide & CLA text: [protocol/CONTRIBUTING.md](https://github.com/AAStarCommunity/Brood/blob/main/protocol/CONTRIBUTING.md) + +--- + +## Apache 2.0 — 2 分钟白话版 + +本项目使用 Apache License 2.0,对所有人开放: + +**可以做**:免费用、商业用、修改、集成进闭源产品、分发 +**必须做**:保留版权行 · 保留 NOTICE 文件 · 修改文件须注明 · 不能蹭品牌(见 TRADEMARK.md) +**不要求**:改了代码不用开源(这是和 GPL 最大的区别) + +--- + +## 签署 CLA + +本项目要求所有贡献者签署 **CLA(贡献者许可协议)**,以确保项目对所有贡献代码有清晰的法律授权。 + +> **CLA 是什么**:你签一次的声明:"我提交的代码是我的原创(或我有权提交),授权本项目在 Apache 2.0 下使用。" +> **为什么需要**:没有明确授权,项目分发你的代码存在法律漏洞。 +> **怎么签**:提交 PR 后,`@cla-assistant` 机器人自动评论并引导你签名,只需 1 分钟,永久有效,只签一次。 + +完整 CLA 协议文本:[CLA.md](https://github.com/AAStarCommunity/Brood/blob/main/protocol/CLA.md) | [中文参考译本](https://github.com/AAStarCommunity/Brood/blob/main/protocol/CLA-zh.md) + +--- + +## 贡献流程 + +``` +Fork → 新建分支 → 写代码 → 提交 PR → 签 CLA → Review → Merge +``` + +- 分支命名:`feat/xxx` · `fix/xxx` · `docs/xxx` +- Commit 规范:[Conventional Commits](https://www.conventionalcommits.org/) +- 问题反馈:在本仓库提 Issue + +## License + +Contributions are licensed under [Apache License 2.0](LICENSE). +See [NOTICE](./NOTICE) · [TRADEMARK.md](./TRADEMARK.md) · [LICENSE-zh.md](./LICENSE-zh.md) · [TRADEMARK-zh.md](./TRADEMARK-zh.md) for details. diff --git a/LICENSE-zh.md b/LICENSE-zh.md new file mode 100644 index 0000000..a92c6ac --- /dev/null +++ b/LICENSE-zh.md @@ -0,0 +1,80 @@ +# Apache 许可证 2.0 版 — 非官方中文参考译本 + +> **重要声明**:本文件为 Apache License 2.0 的非官方中文参考译本,仅供理解之用。 +> 本译本不具有法律效力,以英文原版 `LICENSE` 文件为准。 +> 英文原版:http://www.apache.org/licenses/LICENSE-2.0 +> +> **IMPORTANT**: This is an unofficial Chinese translation of the Apache License 2.0, +> provided for reference only. It is NOT legally binding. The authoritative version +> is the English original in the `LICENSE` file. + +--- + +## Apache 许可证 + +版本 2.0,2004 年 1 月 + +http://www.apache.org/licenses/ + +### 使用、复制和分发的条款和条件 + +**1. 定义** + +"许可证"是指本文件第 1 至第 9 节所定义的使用、复制和分发的条款和条件。 + +"许可方"是指授予本许可证的版权所有者或由版权所有者授权的实体。 + +"法律实体"是指行为实体与所有控制该实体、被该实体控制或与该实体处于共同控制之下的其他实体的联合体。就此定义而言,"控制"是指 (i) 通过合同或其他方式直接或间接引导或管理该实体的权力,或 (ii) 持有百分之五十 (50%) 或更多的已发行股份,或 (iii) 该实体的受益所有权。 + +"您"(或"您的")是指行使本许可证授予的权限的个人或法律实体。 + +"源"形式是指进行修改的首选形式,包括但不限于软件源代码、文档源和配置文件。 + +"目标"形式是指由源形式经机械转换或翻译产生的任何形式,包括但不限于编译的目标代码、生成的文档以及转换为其他媒体类型的内容。 + +"作品"是指根据本许可证提供的、以源形式或目标形式表现的著作权作品,如附在作品中或与作品关联的版权声明所示。 + +"衍生作品"是指基于(或派生自)该作品的、以源形式或目标形式表现的任何作品,其中的编辑修订、注释、阐述或其他修改作为整体构成原创性著作。就本许可证而言,衍生作品不包括与该作品及其衍生作品的接口保持分离或仅通过名称链接(或绑定)的作品。 + +"贡献"是指任何著作权作品,包括该作品的原始版本及对该作品或其衍生作品的任何修改或补充,由版权所有者或由获得版权所有者授权的个人或法律实体有意提交给许可方以纳入该作品。就此定义而言,"提交"是指发送给许可方或其代表的任何形式的电子、口头或书面通信,包括但不限于由许可方或代表许可方管理的、以讨论和改进该作品为目的的电子邮件列表、源代码控制系统和问题跟踪系统上的通信,但不包括版权所有者以书面形式明确标记或以其他方式指定为"非贡献"的通信。 + +"贡献者"是指许可方和其贡献已被许可方接收并纳入该作品的任何个人或法律实体。 + +**2. 版权许可的授予。** 在遵守本许可证条款和条件的前提下,每位贡献者特此授予您永久的、全球性的、非排他性的、免费的、免版税的、不可撤销的版权许可,以复制、准备衍生作品、公开展示、公开表演、再许可和分发该作品及其衍生作品(以源形式或目标形式)。 + +**3. 专利许可的授予。** 在遵守本许可证条款和条件的前提下,每位贡献者特此授予您永久的、全球性的、非排他性的、免费的、免版税的、不可撤销的(本节另有规定的除外)专利许可,以制造、委托制造、使用、许诺销售、销售、进口和以其他方式转让该作品,该许可仅适用于该贡献者可许可的、因其贡献单独或其贡献与提交贡献时的作品组合而必然被侵犯的专利权利要求。如果您对任何实体(包括诉讼中的交叉索赔或反诉)提起专利诉讼,声称该作品或纳入该作品的贡献构成直接或间接专利侵权,则根据本许可证授予您的关于该作品的任何专利许可自该诉讼提起之日起终止。 + +**4. 再分发。** 您可以在任何媒介中复制和分发该作品或其衍生作品的副本(无论是否经过修改,以源形式或目标形式),前提是您满足以下条件: + +(a) 您必须向该作品或衍生作品的任何其他接收者提供本许可证的副本;且 + +(b) 您必须使任何修改过的文件带有显著的声明,说明您更改了这些文件;且 + +(c) 您必须在您分发的任何衍生作品的源形式中保留来自该作品源形式的所有版权、专利、商标和归属声明,但不包括与衍生作品的任何部分无关的声明;且 + +(d) 如果该作品包含 "NOTICE" 文本文件作为其分发的一部分,则您分发的任何衍生作品必须在以下至少一个位置包含该 NOTICE 文件中所含归属声明的可读副本(不包括与衍生作品的任何部分无关的声明):作为衍生作品一部分分发的 NOTICE 文本文件中;衍生作品附带的源形式或文档中;或者,在衍生作品生成的显示中(如果此类第三方声明通常出现在此处)。NOTICE 文件的内容仅供参考,不修改本许可证。 + +**5. 贡献的提交。** 除非您另有明确声明,否则您有意提交给许可方以纳入该作品的任何贡献均应遵守本许可证的条款和条件,不附加任何额外条款或条件。 + +**6. 商标。** 本许可证不授予使用许可方的商号、商标、服务标志或产品名称的许可,除非在描述该作品的来源和复制 NOTICE 文件内容时合理和惯常使用。 + +**7. 免责声明。** 除非适用法律要求或以书面形式同意,许可方按"原样"提供该作品(每位贡献者按"原样"提供其贡献),不附带任何明示或暗示的保证或条件,包括但不限于所有权、不侵权、适销性或特定用途适用性的保证或条件。 + +**8. 责任限制。** 在任何情况下,无论基于何种法律理论,无论是侵权(包括过失)、合同还是其他理论,除非适用法律要求(如故意和重大过失行为)或以书面形式同意,任何贡献者均不对您承担损害赔偿责任。 + +**9. 接受保证或附加责任。** 在再分发该作品或其衍生作品时,您可以选择提供并收取费用,以接受与本许可证一致的支持、保证、赔偿或其他责任义务和/或权利。但在接受此类义务时,您只能代表您自己并由您自行承担责任,不得代表任何其他贡献者。 + +--- + +条款和条件结束 + +版权所有 2024-至今 MushroomDAO 贡献者 + +根据 Apache 许可证 2.0 版(以下简称"许可证")获得许可; +除非遵守本许可证,否则您不得使用本文件。 +您可以在以下网址获取许可证副本: + +http://www.apache.org/licenses/LICENSE-2.0 + +除非适用法律要求或以书面形式同意,根据本许可证分发的软件按"原样"分发, +不附带任何明示或暗示的保证或条件。请参阅许可证以了解管辖权限和限制的具体语言。 diff --git a/NOTICE b/NOTICE index 6c5825a..737aba3 100644 --- a/NOTICE +++ b/NOTICE @@ -6,13 +6,16 @@ This product includes software developed by MushroomDAO Licensed under the Apache License, Version 2.0. See LICENSE file for full terms. +本产品使用 Apache 许可证 2.0 版,参见 LICENSE 文件(英文原版)及 LICENSE-zh.md(中文参考译本)。 -== Attribution == +== Attribution / 归属声明 == Per Apache 2.0 Section 4(d), derivative works that distribute this software must retain this NOTICE file. +根据 Apache 2.0 第 4(d) 条,分发本软件的衍生作品必须保留本 NOTICE 文件。 -== Trademark == +== Trademark / 商标 == MushroomDAO, Mycelium Protocol, GToken, PNTs, and related names are trademarks -of MushroomDAO. Forks must rebrand. See TRADEMARK.md for full policy. +of MushroomDAO. Forks must rebrand. See TRADEMARK.md (English) / TRADEMARK-zh.md (中文). +MushroomDAO、Mycelium Protocol、GToken、PNTs 及相关名称为 MushroomDAO 的商标。分叉须更名。 diff --git a/README.md b/README.md index e038869..dffbba0 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,12 @@ WalletBeat evaluates wallets across Stage 0, 1, 2. AirAccount is a **smart contr ## Documentation +### Feature Reference + +| Document | Description | +|----------|-------------| +| [docs/feature-list.md](docs/feature-list.md) | **Complete feature list M1–M7** — per-milestone tables with characteristics, user value, and active/passive classification | + ### Architecture & Design | Document | Description | @@ -205,3 +211,9 @@ forge test --summary # per-suite breakdown - **Session key revocation** — nonce-based, prior grant signatures invalidated on revoke - **EIP-7212 P256** — hardware-bound passkey authentication, available on OP Mainnet (Fjord) - **Audit reports** — see `docs/2026-03-*-audit-report.md` + +## License + +This project is licensed under the [Apache License, Version 2.0](LICENSE). +Copyright 2024-present MushroomDAO Contributors. +See [NOTICE](./NOTICE) · [TRADEMARK.md](./TRADEMARK.md) · [LICENSE-zh.md](./LICENSE-zh.md) · [TRADEMARK-zh.md](./TRADEMARK-zh.md) for details. diff --git a/TRADEMARK-zh.md b/TRADEMARK-zh.md new file mode 100644 index 0000000..ebae466 --- /dev/null +++ b/TRADEMARK-zh.md @@ -0,0 +1,143 @@ +# MushroomDAO 商标政策 + +> 最后更新:2026-04-15 +> 英文版(法律效力等同):[TRADEMARK.md](TRADEMARK.md) + +--- + +## 目的 + +MushroomDAO 的源代码根据 Apache 许可证 2.0 版开放——这是一个完全宽松的、经 OSI 批准的开源许可证。任何人都可以使用、修改和分发代码,包括用于商业目的。 + +但是,MushroomDAO 及其产品的**名称、标志和品牌**不在 Apache 2.0 许可证的覆盖范围内。本商标政策说明了您可以和不可以对 MushroomDAO 的标识做什么。 + +本政策参照 [Mozilla 基金会商标政策](https://www.mozilla.org/en-US/foundation/trademarks/policy/)制定。该政策确立了一个先例:开源代码可以自由分叉,但要求分叉版本使用不同的品牌名称(例如 Debian 将 Firefox 重新命名为 Iceweasel)。 + +--- + +## 受保护的名称 + +以下名称是 MushroomDAO 及其关联实体的商标: + +### 项目与协议名称 +- **MushroomDAO** +- **Mycelium Protocol** / **Mycelium**(菌丝协议) +- **Park Protocol** +- **CityOS** +- **Cos72** +- **Sin90** + +### 代币与积分名称 +- **GToken** +- **PNTs** / **xPNTs** / **aPNTs** +- **Spores** + +### 产品与服务名称 +- **AAStar** +- **AirAccount** +- **SuperPaymaster** +- **OpenNest** +- **AuraAI** + +### 公司与组织名称 +- **HyperCapital** + +--- + +## 受保护的标志 + +以下标志受到保护(实际文件待添加): + +- MushroomDAO 标志(蘑菇图标 + 文字标识) +- AAStar 标志 +- AuraAI 标志 +- Mycelium Protocol 标志 +- GToken 符号 + +*(标志文件将在品牌设计完成后存放于此目录,预计 2026 年第三季度。)* + +--- + +## 使用规则 + +### 您可以: + +1. **自由使用代码** —— 根据 Apache 2.0 许可证,商标政策不限制代码的使用。 + +2. **说明兼容性。** 您可以在描述性文字、文档和营销材料中如实声明您的项目"兼容 Mycelium Protocol"或"基于 MushroomDAO 技术构建"。 + +3. **链接到 MushroomDAO。** 您可以链接到 MushroomDAO 的仓库、网站和文档。 + +4. **讨论 MushroomDAO。** 您可以在文章、博客文章、演讲和其他关于该项目的评论中使用这些名称。 + +### "Powered By" / 兼容性声明: + +分叉和衍生作品**可以**在描述性文字(如 README、文档、营销文案)中使用 **"Compatible with Mycelium Protocol"**(兼容菌丝协议)或 **"Powered by Mycelium"**(由菌丝驱动)等短语,**前提是**它们确实与 Mycelium Protocol 网络互操作。这些属于许可的**指示性合理使用**——它们描述事实关系,而非品牌隶属。 + +这与下面的分叉更名要求是分开的:您必须从**产品名称和界面**中移除 MushroomDAO 商标,但可以在**文档和营销文字**中使用描述性兼容声明。 + +所有分叉和衍生作品**必须**根据 Apache 2.0 第 4(d) 条保留 NOTICE 文件。 + +### 您不可以: + +1. **在产品名称中使用受保护名称。** 您不得将产品命名为"Mycelium Wallet"、"MushroomDAO Enterprise"、"GToken Exchange"或任何暗示官方隶属的类似名称。 + +2. **未经书面许可使用受保护标志。** + +3. **暗示背书或隶属关系。** 您不得暗示您的项目受到 MushroomDAO 的背书、与其有关联或是其官方组成部分(除非获得明确的书面授权)。 + +4. **注册易混淆的类似名称。** 您不得注册与上述受保护名称容易混淆的域名、社交媒体账户或商标。 + +5. **删除 NOTICE 文件。** 您不得从再分发或衍生作品中删除 NOTICE 文件。这是 Apache 2.0 许可证(第 4 条)的要求,与本商标政策无关。 + +### 分叉规则: + +如果您分叉了 MushroomDAO 的仓库: + +- 您**必须**从分叉中移除所有 MushroomDAO 商标,包括: + - 界面、文档和配置中的产品名称 + - 标志、图标和品牌资产 + - 暗示该分叉是 MushroomDAO 官方产品的引用 +- 您**可以**声明:"本项目是 MushroomDAO [原始仓库名] 的分叉" +- 您**必须**为分叉选择不包含任何受保护名称的新名称 + +这与 Debian 将 Firefox 重新命名为 Iceweasel 的模式相同:代码在 Mozilla 公共许可证下自由可用,但 Firefox 的名称和标志需要单独的商标许可。 + +--- + +## 请求许可 + +如果您希望以上述规则未涵盖的方式使用 MushroomDAO 商标,请联系我们: + +- **邮箱:** jason@aastar.io +- **GitHub:** 在 [MushroomDAO/.github](https://github.com/MushroomDAO/.github/issues) 提交 issue + +我们通常支持社区使用,并将在 14 天内回复。 + +--- + +## 执行 + +MushroomDAO 保留以下权利: + +- 要求移除未经授权的商标使用 +- 提前 30 天通知撤销先前授予的商标许可 +- 在故意侵权的情况下采取法律行动 + +我们倾向于在采取正式行动之前通过友好沟通解决商标问题。 + +--- + +## 开源承诺 + +MushroomDAO 致力于遵守由[开源促进会](https://opensource.org)维护的[开源定义](https://opensource.org/osd)。 + +我们选择真正的开源([Apache 2.0](https://opensource.org/licenses/Apache-2.0),经 OSI 批准)作为构建数字公共物品的行为。我们请求您尊重使这一切成为可能的归属声明。 + +--- + +## 联系方式 + +- **商标咨询:** jason@aastar.io +- **项目一般问题:** https://github.com/MushroomDAO/.github/issues +- **Telegram:** https://t.me/Account_Abstraction_Community diff --git a/TRADEMARK.md b/TRADEMARK.md index 1b9029a..4297f67 100644 --- a/TRADEMARK.md +++ b/TRADEMARK.md @@ -1,6 +1,7 @@ # MushroomDAO Trademark Policy -> Last updated: 2026-04-15 +> Last updated: 2026-04-28 +> 中文版 / Chinese version: [TRADEMARK-zh.md](TRADEMARK-zh.md) --- diff --git a/docs/feature-list.md b/docs/feature-list.md new file mode 100644 index 0000000..ab2851c --- /dev/null +++ b/docs/feature-list.md @@ -0,0 +1,105 @@ +# AirAccount Feature List — M1 to M7 + +Complete feature reference by milestone. Each feature includes its characteristics, user value, and whether the user needs to actively trigger it or it works passively in the background. + +**Active** = user must explicitly trigger or configure +**Passive** = works automatically as a safety/infrastructure layer + +--- + +## M1 — ERC-4337 ECDSA 基础账户 + +| Feature | 特征 | 用户价值 | 主动/被动 | +|---------|------|---------|---------| +| **ERC-4337 UserOperation** | 兼容标准 EntryPoint,Bundler 转发,无需用户持有 ETH 发交易 | Gasless 体验,邮箱即账户,无助记词 | 被动 | +| **ECDSA 签名验证**(algId 0x02) | 65 字节标准 secp256k1,向后兼容以太坊生态 | 现有以太坊钱包可直接使用,无迁移成本 | 被动 | +| **CREATE2 确定性部署** | 同一 owner + salt → 同一地址,多链一致 | 账户地址可提前预测,跨链身份统一 | 被动 | +| **不可升级设计** | 无代理模式,无 UUPS,逻辑固化在合约里 | 无管理员后门,合约不会被偷偷修改 | 被动 | + +--- + +## M2 — BLS 三重聚合签名 + +| Feature | 特征 | 用户价值 | 主动/被动 | +|---------|------|---------|---------| +| **BLS 三重签名**(algId 0x01) | ECDSA×2 + BLS 聚合一次验证,链上单次 pairing | 大额交易多方联防,不被单点攻击 | 主动(大额触发) | +| **Gas 节省 50%** | vs YetAA 参考实现:523k → 259k gas | 高频大额操作成本减半 | 被动 | +| **BLS 聚合器合约** | `AAStarBLSAggregator`,多 UserOp 共享一次 pairing | 多用户打包时进一步节省 gas | 被动 | +| **algId 路由体系** | 签名首字节即路由标识,零歧义分发给对应验证器 | 未来扩展新算法不改主合约 | 被动 | + +--- + +## M3 — 安全加固 & Gas 优化 + +| Feature | 特征 | 用户价值 | 主动/被动 | +|---------|------|---------|---------| +| **Gas 再降 51%** | M2 259k → M3 127k,Solc 0.8.33 + Cancun EVM | 同等安全性下交易费用减半 | 被动 | +| **签名绑定哈希** | 签名覆盖 calldata hash,防签名重放与替换 | 无法用旧签名发起新攻击 | 被动 | +| **Transient storage** | EIP-1153 传递 algId 跨调用栈,无持久化 gas 成本 | 更低 gas,无状态污染 | 被动 | + +--- + +## M4 — 分级累积签名 + 社会恢复 + +| Feature | 特征 | 用户价值 | 主动/被动 | +|---------|------|---------|---------| +| **Tier 1**(algId 0x02/0x03) | 单因素:ECDSA 或 P256 WebAuthn | < $100 交易,指纹一点即过 | 主动(日常路径) | +| **Tier 2**(algId 0x04) | P256 + BLS DVT 共签 | $100–$1000,双设备确认,防单点被盗 | 主动(触发门槛) | +| **Tier 3**(algId 0x05) | P256 + BLS + Guardian ECDSA 三重 | > $1000,多方联防,极高安全 | 主动(大额必须) | +| **社会恢复** | 3 个 guardian(亲友/设备/社区),2-of-3 投票,48h timelock | 手机丢失后可恢复账户,任意两方即可 | 主动(紧急场景) | +| **cancelRecovery 保护** | 取消恢复需 2-of-3 guardian 投票,owner 无法单独取消 | 私钥被盗后攻击者无法阻止正常恢复 | 被动 | + +--- + +## M5 — ERC20 守卫 + 治理 + 零信任 T1 + +| Feature | 特征 | 用户价值 | 主动/被动 | +|---------|------|---------|---------| +| **GlobalGuard ETH 限额** | 合约底层硬编码每日 ETH 消费上限,任何签名组合不可绕过 | 即使私钥被盗,每日损失有上限 | 被动 | +| **ERC20 token 独立限额** | 每个 token 配置 Tier1/Tier2/每日限额 | 稳定币、DeFi token 分开设防 | 主动(用户配置) | +| **CalldataParser** | Railgun、Uniswap V3 calldata 解析,提取实际转账 token 和金额 | DeFi 操作也被守卫覆盖,不只是普通转账 | 被动 | +| **单调安全配置** | 每日限额只能降低不能提高,算法只能增加不能删除 | 不存在"通过提高限额绕过守卫"的攻击面 | 被动 | +| **Guardian accept 机制** | Guardian 需主动签名接受才生效 | 不会在不知情的情况下背负他人账户恢复责任 | 主动(guardian 确认) | +| **零信任 T1**(algId 0x06) | P256 AND ECDSA 同时验证,Tier1 下双因素同时出具 | 高价值小额操作(如授权签名)也可强制双因素 | 主动(手动开启) | + +--- + +## M6 — Session Key + 加权多签 + EIP-7702 + +| Feature | 特征 | 用户价值 | 主动/被动 | +|---------|------|---------|---------| +| **Session Key**(algId 0x08) | 时限性临时密钥,过期自动失效,限额独立 | DApp 授权后免反复确认,类似"登录态" | 主动(用户授权) | +| **Weighted MultiSig**(algId 0x07) | 每个签名源(P256/BLS/guardian)配置权重,阈值可定制 | 灵活的多签策略,2 个 guardian 可等于 1 个 owner | 主动(用户配置) | +| **弱化变更 timelock** | 降低安全强度(权重减少)需 2-of-3 guardian + 48h 等待 | 攻击者拿到私钥也无法立刻降低防护 | 被动 | +| **EIP-7702 AirAccountDelegate** | EOA 可通过 Type 4 交易临时委托给 AirAccount 逻辑 | 无需部署合约即享受部分 AA 功能,迁移门槛极低 | 主动(EOA 主动委托) | +| **ForceExitModule** | 2-of-3 guardian 授权后可强制转出全部资产 | 极端情况(合约 bug)也有逃生通道 | 主动(紧急使用) | + +--- + +## M7 — ERC-7579 模块 + Agent Economy + 隐私标准 + +| Feature | 特征 | 用户价值 | 主动/被动 | +|---------|------|---------|---------| +| **ERC-7579 模块系统** | 支持 Validator(1)/Executor(2)/Hook(3) 三类模块,guardian 授权安装/卸载 | 账户可按需安装功能插件,不改主合约 | 主动(用户安装) | +| **AirAccountCompositeValidator** | 复合验证模块,多签策略外挂 | 更复杂的签名策略无需升级主合约 | 主动 | +| **TierGuardHook** | ERC-7579 Hook,每次执行前强制 tier 检查 | 模块路径下的交易同样受 tier 守卫保护 | 被动 | +| **AgentSessionKeyValidator** | Agent 会话密钥:过期时间 + 消费上限 + 调用速率(velocity)限制 | 授权 AI Agent 操作账户,限额可控,随时撤销 | 主动(用户授权) | +| **delegateSession 子委托** | Agent 可向子 Agent 委托,子配置不可超过父配置(速率用 cross-multiply 比较) | Agent 可安全再授权,不会超出原始授权范围 | 被动(自动校验) | +| **ERC-5564 Stealth Address** | 发送方发布一次性隐匿收款地址公告,接收方链下扫描 | 隐匿收款,无法从链上追踪资金归属 | 主动(用户发起) | +| **ERC-7828 链限定地址** | `keccak256(addr \|\| chainId)`,同地址跨链有不同标识 | 防止跨链重放和地址混淆攻击 | 被动 | + +--- + +## 尚未实现 / 规划中 + +| Feature | 说明 | 预计里程碑 | +|---------|------|---------| +| **Railgun 隐私池接入** | `RailgunParser` 合约已实现(能解析 calldata),但实际接入 shielded pool 存取款流程尚未完成 | M8+ | +| **OAPD 子账户隔离** | 每 DApp 一个隔离账户的完整产品流程,合约层已预留接口,SDK/产品层待实现 | M8+ | +| **抗量子签名**(algId 0x10) | 接口和 algId 已预留(ML-DSA/Dilithium),验证合约待开发 | M8+ | +| **EIP-8130 Native AA 兼容** | 等 Hegota fork EIP 确认后执行 | 2026 Q3 | + +--- + +*Last updated: 2026-05-21* +*Source: M1–M7 contract layer — `src/core/`, `src/validators/`, `src/aggregator/`* diff --git a/scripts/deploy-agent-registry.ts b/scripts/deploy-agent-registry.ts new file mode 100644 index 0000000..494f6eb --- /dev/null +++ b/scripts/deploy-agent-registry.ts @@ -0,0 +1,119 @@ +/** + * deploy-agent-registry.ts — Deploy AgentRegistry (M8.1) + * + * Deploys the AgentRegistry contract that maps agent execution wallets to their + * human AirAccount owners. Used by AirAccount.setAgentWallet() and SuperPaymaster + * for sponsorship eligibility checks. + * + * Usage: + * pnpm tsx scripts/deploy-agent-registry.ts + * + * Prerequisites: + * - .env.sepolia with PRIVATE_KEY and SEPOLIA_RPC_URL + * - Run `forge build` first to generate out/AgentRegistry.sol/AgentRegistry.json + */ + +import { config } from "dotenv"; +import { resolve } from "path"; +import { readFileSync } from "fs"; +import { + createPublicClient, + createWalletClient, + http, + encodeDeployData, + formatEther, + type Address, + type Hex, +} from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { sepolia } from "viem/chains"; + +config({ path: resolve(import.meta.dirname, "../.env.sepolia") }); + +const PRIVATE_KEY = process.env.PRIVATE_KEY as Hex; + +const RPC_URLS = [ + process.env.SEPOLIA_RPC_URL, + process.env.SEPOLIA_RPC_URL2, + process.env.SEPOLIA_RPC_URL3, +].filter(Boolean) as string[]; + +function loadArtifact(name: string) { + const artifact = JSON.parse( + readFileSync(resolve(import.meta.dirname, `../out/${name}.sol/${name}.json`), "utf-8") + ); + return { abi: artifact.abi as unknown[], bytecode: artifact.bytecode.object as Hex }; +} + +async function waitTx( + pub: ReturnType, + hash: Hex, + label: string +) { + console.log(` TX(${label}): https://sepolia.etherscan.io/tx/${hash}`); + const receipt = await pub.waitForTransactionReceipt({ hash, timeout: 300_000 }); + if (receipt.status !== "success") throw new Error(`${label} reverted`); + console.log(` Gas used: ${receipt.gasUsed} Block: ${receipt.blockNumber}`); + return receipt; +} + +async function tryDeploy(rpcUrl: string, owner: ReturnType) { + const transport = http(rpcUrl, { timeout: 300_000 }); + const pub = createPublicClient({ chain: sepolia, transport }); + const wal = createWalletClient({ account: owner, chain: sepolia, transport }); + + const art = loadArtifact("AgentRegistry"); + const hash = await wal.sendTransaction({ + data: encodeDeployData({ abi: art.abi, bytecode: art.bytecode, args: [] }), + gas: 800_000n, + }); + const receipt = await waitTx(pub, hash, "AgentRegistry"); + return receipt.contractAddress as Address; +} + +async function main() { + if (!PRIVATE_KEY) { + console.error("Missing PRIVATE_KEY in .env.sepolia"); + process.exit(1); + } + if (RPC_URLS.length === 0) { + console.error("Missing SEPOLIA_RPC_URL in .env.sepolia"); + process.exit(1); + } + + const owner = privateKeyToAccount(PRIVATE_KEY); + + console.log("=== Deploy AgentRegistry (M8.1) ==="); + console.log(`Deployer: ${owner.address}`); + + // Check balance on first RPC + const pub0 = createPublicClient({ chain: sepolia, transport: http(RPC_URLS[0]) }); + const bal = await pub0.getBalance({ address: owner.address }); + console.log(`Balance: ${formatEther(bal)} ETH\n`); + + let agentRegistryAddr: Address | undefined; + + for (const rpcUrl of RPC_URLS) { + try { + console.log(`Trying RPC: ${rpcUrl.slice(0, 60)}...`); + agentRegistryAddr = await tryDeploy(rpcUrl, owner); + break; + } catch (err: any) { + console.warn(` Failed: ${err.message?.slice(0, 100)}`); + } + } + + if (!agentRegistryAddr) { + console.error("All RPCs failed."); + process.exit(1); + } + + console.log(`\n✓ AgentRegistry deployed at: ${agentRegistryAddr}`); + console.log(` Verify: https://sepolia.etherscan.io/address/${agentRegistryAddr}`); + console.log(`\nNext step: pass this address as agentRegistry to setAgentWallet()`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/src/core/AAStarAirAccountBase.sol b/src/core/AAStarAirAccountBase.sol index 3868591..c16ef2e 100644 --- a/src/core/AAStarAirAccountBase.sol +++ b/src/core/AAStarAirAccountBase.sol @@ -256,6 +256,8 @@ abstract contract AAStarAirAccountBase is Initializable { error WeightChangeNotApproved(); error NoWeightChangeProposal(); error WeightChangeAlreadyApproved(); + // M8.1 AgentRegistry + error AgentRegistrationFailed(); // ─── Events ─────────────────────────────────────────────────────── @@ -1026,20 +1028,33 @@ abstract contract AAStarAirAccountBase is Initializable { bytes4 private constant _PRECHECK_SEL = bytes4(keccak256("preCheck(address,uint256,bytes)")); /// @dev Dispatch ERC-7579 preCheck to the active hook module (if any). - /// Uses compact assembly encoding to avoid abi.encodeWithSignature bytecode cost. + /// Forwards the full execute() calldata (msg.data) as the `bytes msgData` parameter so + /// hook modules can inspect call target and inner selector for scope enforcement. + /// msg.data layout for execute(address,uint256,bytes): + /// [0:4] execute() selector + /// [4:36] dest (address padded) + /// [36:68] value (uint256) + /// [68:100] offset for func bytes param (= 0x60 relative to args start) + /// [100:132] func length + /// [132:] func data /// Reverts HookReverted() if hook call fails. function _dispatchHook(uint256 ethValue) private { address hook = _activeHook; bytes4 sel = _PRECHECK_SEL; bool ok; assembly { + let cdSize := calldatasize() let m := mload(0x40) - mstore(m, sel) // selector at [0:4] - mstore(add(m, 4), caller()) // address at [4:36] - mstore(add(m, 36), ethValue) // value at [36:68] - mstore(add(m, 68), 0x60) // bytes offset = 96 - mstore(add(m,100), 0) // bytes length = 0 - ok := call(gas(), hook, 0, m, 132, 0, 0) + mstore(m, sel) // [0:4] preCheck selector + mstore(add(m, 4), caller()) // [4:36] msgSender + mstore(add(m, 36), ethValue) // [36:68] msgValue + mstore(add(m, 68), 0x60) // [68:100] bytes offset = 96 (relative to args start) + mstore(add(m,100), cdSize) // [100:132] bytes length = calldatasize (= full execute calldata) + calldatacopy(add(m, 132), 0, cdSize) // [132:] = full execute calldata (selector+args) + // Total size: 4 + 32 + 32 + 32 + 32 + cdSize = 132 + cdSize + // Round up to 32-byte boundary for safety (required by ABI) + let totalSize := add(132, cdSize) + ok := call(gas(), hook, 0, m, totalSize, 0, 0) } if (!ok) revert HookReverted(); } @@ -1147,6 +1162,18 @@ abstract contract AAStarAirAccountBase is Initializable { } } + /// @notice Peek at the next session key in the transient queue without consuming it. + /// Called by TierGuardHook.preCheck() for session scope enforcement. + /// Returns bytes32(0) if the queue is empty (no session key stored in this batch). + /// Top byte of returned value: 0x01 = ECDSA session (lower 20 bytes = address), + /// 0x02 = P256 session (lower 31 bytes = key hash). + function getCurrentSessionKey() external view returns (bytes32 taggedId) { + assembly { + let readIdx := tload(add(SESSION_KEY_SLOT_BASE, 1)) + taggedId := tload(add(add(SESSION_KEY_SLOT_BASE, 2), readIdx)) + } + } + /// @dev Push validated algId to transient storage queue. /// Called during validateUserOp (validation phase). function _storeValidatedAlgId(uint8 algId) internal { @@ -1527,24 +1554,28 @@ abstract contract AAStarAirAccountBase is Initializable { // ─── ERC-8004 Agent Identity Binding (M7.16) ───────────────────────── - /// @notice Link an ERC-8004 agent NFT to a session key address on this account. - /// @param agentId ERC-8004 agent NFT token ID - /// @param agentWallet Session key address that this agent uses for transactions - /// @param erc8004Registry ERC-8004 Identity Registry contract address - /// @dev Only owner can set agent wallet bindings. This is a metadata operation — - /// the actual spending limits are still enforced by session key scopes. + /// @notice Link an agent wallet to this AirAccount by registering it in AgentRegistry. + /// @param agentId Logical agent identifier (used for event indexing only) + /// @param agentWallet Execution wallet address that the agent uses for transactions + /// @param agentRegistry AgentRegistry contract address (M8.1) + /// @dev Only owner can set agent wallet bindings. Calls AgentRegistry.registerAgent() + /// which records msg.sender (this account) as the human owner of agentWallet. + /// Reverts if the registry call fails (e.g. already registered). function setAgentWallet( uint256 agentId, address agentWallet, - address erc8004Registry + address agentRegistry ) external onlyOwner { - if (agentWallet == address(0) || erc8004Registry == address(0)) revert InvalidGuardian(); - // Register the agent wallet with the ERC-8004 registry - // setAgentWallet(agentId, wallet) — best-effort, non-blocking - (bool ok,) = erc8004Registry.call( - abi.encodeWithSignature("setAgentWallet(uint256,address)", agentId, agentWallet) + if (agentWallet == address(0) || agentRegistry == address(0)) revert InvalidGuardian(); + // Require agentRegistry to be a deployed contract (extcodesize > 0). + // Low-level calls to EOAs succeed silently — we must reject them explicitly. + uint256 codeSize; + assembly { codeSize := extcodesize(agentRegistry) } + if (codeSize == 0) revert AgentRegistrationFailed(); + (bool ok,) = agentRegistry.call( + abi.encodeWithSignature("registerAgent(address)", agentWallet) ); - (ok); // silence unused variable warning + if (!ok) revert AgentRegistrationFailed(); emit AgentWalletSet(agentId, agentWallet); } diff --git a/src/registries/AgentRegistry.sol b/src/registries/AgentRegistry.sol new file mode 100644 index 0000000..b8ab6bb --- /dev/null +++ b/src/registries/AgentRegistry.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.33; + +/// @title AgentRegistry — maps agent execution wallets to their human AirAccount owners +/// @notice Any AirAccount owner can register their agent's wallet address. +/// Provides the reverse lookup needed by SuperPaymaster to verify sponsorship eligibility. +contract AgentRegistry { + /// @dev agentWallet → humanOwner + mapping(address => address) public agentWalletOwner; + /// @dev humanOwner → agentWallet[] (for enumeration) + mapping(address => address[]) public ownerAgents; + + event AgentRegistered(address indexed humanOwner, address indexed agentWallet); + event AgentDeregistered(address indexed humanOwner, address indexed agentWallet); + + error NotAgentOwner(); + error AgentAlreadyRegistered(); + error InvalidAddress(); + + /// @notice Register msg.sender as the owner of agentWallet. + /// Called by AirAccount.setAgentWallet() on behalf of the account owner. + function registerAgent(address agentWallet) external { + if (agentWallet == address(0)) revert InvalidAddress(); + if (agentWalletOwner[agentWallet] != address(0)) revert AgentAlreadyRegistered(); + agentWalletOwner[agentWallet] = msg.sender; + ownerAgents[msg.sender].push(agentWallet); + emit AgentRegistered(msg.sender, agentWallet); + } + + /// @notice Deregister an agent wallet. Only the original registrant can deregister. + function deregisterAgent(address agentWallet) external { + if (agentWalletOwner[agentWallet] != msg.sender) revert NotAgentOwner(); + agentWalletOwner[agentWallet] = address(0); + // Remove from ownerAgents array (swap-and-pop) + address[] storage agents = ownerAgents[msg.sender]; + uint256 len = agents.length; + for (uint256 i = 0; i < len; i++) { + if (agents[i] == agentWallet) { + agents[i] = agents[len - 1]; + agents.pop(); + break; + } + } + emit AgentDeregistered(msg.sender, agentWallet); + } + + /// @notice Returns true if agentWallet is registered (has any owner). + function isRegisteredAgent(address agentWallet) external view returns (bool) { + return agentWalletOwner[agentWallet] != address(0); + } + + /// @notice Returns the human owner count — for SuperPaymaster compatibility with balanceOf(address). + /// Returns 1 if the address has registered any agents, 0 otherwise. + function balanceOf(address humanOwner) external view returns (uint256) { + return ownerAgents[humanOwner].length > 0 ? 1 : 0; + } + + /// @notice Returns agentWallets[index] for a given owner (for enumeration). + function getAgentByIndex(address owner, uint256 index) external view returns (address) { + return ownerAgents[owner][index]; + } + + /// @notice Returns count of agent wallets registered by this owner. + function getAgentCount(address owner) external view returns (uint256) { + return ownerAgents[owner].length; + } +} diff --git a/test/AAStarAirAccountV7_M7.t.sol b/test/AAStarAirAccountV7_M7.t.sol index 758eb7d..a37acdc 100644 --- a/test/AAStarAirAccountV7_M7.t.sol +++ b/test/AAStarAirAccountV7_M7.t.sol @@ -72,12 +72,12 @@ contract MockTarget { receive() external payable {} } -// ─── Mock ERC-8004 registry ─────────────────────────────────────────────────── +// ─── Mock AgentRegistry (M8.1) ─────────────────────────────────────────────── contract MockRegistry { - mapping(uint256 => address) public agentWallets; - function setAgentWallet(uint256 agentId, address wallet) external { - agentWallets[agentId] = wallet; + mapping(address => address) public agentWalletOwner; + function registerAgent(address agentWallet) external { + agentWalletOwner[agentWallet] = msg.sender; } } @@ -778,8 +778,8 @@ contract AAStarAirAccountV7_M7Test is Test { vm.prank(ownerWallet.addr); account.setAgentWallet(7, agentWallet, address(mockRegistry)); - // Registry should have recorded the agent wallet - assertEq(mockRegistry.agentWallets(7), agentWallet); + // Registry should have recorded agentWallet → account as owner + assertEq(mockRegistry.agentWalletOwner(agentWallet), address(account)); } function test_setAgentWallet_notOwner_reverts() public { @@ -800,16 +800,13 @@ contract AAStarAirAccountV7_M7Test is Test { account.setAgentWallet(1, makeAddr("agent"), address(0)); } - function test_setAgentWallet_failingRegistry_doesNotRevert() public { - // setAgentWallet uses best-effort (ok is silenced) — a failing registry should not revert + function test_setAgentWallet_failingRegistry_reverts() public { + // setAgentWallet now hard-fails if the registry call fails (M8.1: AgentRegistrationFailed) address agentWallet = makeAddr("agentWallet"); - address brokenRegistry = makeAddr("brokenRegistry"); // no code → call fails silently + address brokenRegistry = makeAddr("brokenRegistry"); // no code → call returns false - // Give it some bytecode-like status — actually makeAddr returns EOA with no code - // The (bool ok,) call will fail silently. The emit should still happen. vm.prank(ownerWallet.addr); - vm.expectEmit(true, true, false, false); - emit AAStarAirAccountBase.AgentWalletSet(99, agentWallet); + vm.expectRevert(AAStarAirAccountBase.AgentRegistrationFailed.selector); account.setAgentWallet(99, agentWallet, brokenRegistry); } diff --git a/test/AgentRegistry.t.sol b/test/AgentRegistry.t.sol new file mode 100644 index 0000000..6e06fd0 --- /dev/null +++ b/test/AgentRegistry.t.sol @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.33; + +import {Test} from "forge-std/Test.sol"; +import {AgentRegistry} from "../src/registries/AgentRegistry.sol"; +import {AAStarAirAccountV7} from "../src/core/AAStarAirAccountV7.sol"; +import {AAStarAirAccountBase} from "../src/core/AAStarAirAccountBase.sol"; +import {AAStarGlobalGuard} from "../src/core/AAStarGlobalGuard.sol"; + +// ─── Minimal mock EntryPoint ────────────────────────────────────────────────── + +contract MockEntryPoint { + function depositTo(address) external payable {} + function balanceOf(address) external pure returns (uint256) { return 0; } + function withdrawTo(address payable, uint256) external {} + receive() external payable {} +} + +/// @title AgentRegistryTest — Unit + integration tests for AgentRegistry (M8.1) +contract AgentRegistryTest is Test { + AgentRegistry public registry; + + address public alice; + address public bob; + address public agentA; + address public agentB; + + function setUp() public { + registry = new AgentRegistry(); + + alice = makeAddr("alice"); + bob = makeAddr("bob"); + agentA = makeAddr("agentA"); + agentB = makeAddr("agentB"); + } + + // ─── registerAgent ──────────────────────────────────────────────────────── + + function test_RegisterAgent_success() public { + vm.prank(alice); + vm.expectEmit(true, true, false, false); + emit AgentRegistry.AgentRegistered(alice, agentA); + registry.registerAgent(agentA); + + assertEq(registry.agentWalletOwner(agentA), alice); + assertEq(registry.getAgentCount(alice), 1); + assertEq(registry.getAgentByIndex(alice, 0), agentA); + } + + function test_RegisterAgent_zeroAddress_reverts() public { + vm.prank(alice); + vm.expectRevert(AgentRegistry.InvalidAddress.selector); + registry.registerAgent(address(0)); + } + + function test_RegisterAgent_alreadyRegistered_reverts() public { + vm.prank(alice); + registry.registerAgent(agentA); + + // Same caller, same agent — should revert + vm.prank(alice); + vm.expectRevert(AgentRegistry.AgentAlreadyRegistered.selector); + registry.registerAgent(agentA); + } + + function test_RegisterAgent_alreadyRegistered_differentCaller_reverts() public { + vm.prank(alice); + registry.registerAgent(agentA); + + // Different caller but same agentWallet — should still revert (agent already has an owner) + vm.prank(bob); + vm.expectRevert(AgentRegistry.AgentAlreadyRegistered.selector); + registry.registerAgent(agentA); + } + + function test_RegisterAgent_multipleAgents_success() public { + vm.prank(alice); + registry.registerAgent(agentA); + + vm.prank(alice); + registry.registerAgent(agentB); + + assertEq(registry.getAgentCount(alice), 2); + assertEq(registry.agentWalletOwner(agentA), alice); + assertEq(registry.agentWalletOwner(agentB), alice); + } + + // ─── deregisterAgent ───────────────────────────────────────────────────── + + function test_DeregisterAgent_success() public { + vm.prank(alice); + registry.registerAgent(agentA); + + vm.prank(alice); + vm.expectEmit(true, true, false, false); + emit AgentRegistry.AgentDeregistered(alice, agentA); + registry.deregisterAgent(agentA); + + assertEq(registry.agentWalletOwner(agentA), address(0)); + assertEq(registry.getAgentCount(alice), 0); + } + + function test_DeregisterAgent_notOwner_reverts() public { + vm.prank(alice); + registry.registerAgent(agentA); + + // Bob tries to deregister Alice's agent — should revert + vm.prank(bob); + vm.expectRevert(AgentRegistry.NotAgentOwner.selector); + registry.deregisterAgent(agentA); + } + + function test_DeregisterAgent_unregistered_reverts() public { + // agentA was never registered — owner is address(0), so msg.sender != address(0) + vm.prank(alice); + vm.expectRevert(AgentRegistry.NotAgentOwner.selector); + registry.deregisterAgent(agentA); + } + + function test_DeregisterAgent_swapAndPop_preservesOtherAgents() public { + address agentC = makeAddr("agentC"); + + vm.startPrank(alice); + registry.registerAgent(agentA); + registry.registerAgent(agentB); + registry.registerAgent(agentC); + vm.stopPrank(); + + assertEq(registry.getAgentCount(alice), 3); + + // Deregister the middle agent (agentB) + vm.prank(alice); + registry.deregisterAgent(agentB); + + assertEq(registry.getAgentCount(alice), 2); + assertEq(registry.agentWalletOwner(agentB), address(0)); + // agentA and agentC should still be registered + assertEq(registry.agentWalletOwner(agentA), alice); + assertEq(registry.agentWalletOwner(agentC), alice); + } + + // ─── isRegisteredAgent ──────────────────────────────────────────────────── + + function test_IsRegisteredAgent() public { + assertFalse(registry.isRegisteredAgent(agentA)); + + vm.prank(alice); + registry.registerAgent(agentA); + + assertTrue(registry.isRegisteredAgent(agentA)); + + vm.prank(alice); + registry.deregisterAgent(agentA); + + assertFalse(registry.isRegisteredAgent(agentA)); + } + + // ─── balanceOf ──────────────────────────────────────────────────────────── + + function test_BalanceOf() public { + // Before registration: 0 + assertEq(registry.balanceOf(alice), 0); + + vm.prank(alice); + registry.registerAgent(agentA); + + // After first registration: 1 + assertEq(registry.balanceOf(alice), 1); + + vm.prank(alice); + registry.registerAgent(agentB); + + // Still 1 (balanceOf returns 1 if ANY agents registered, not the count) + assertEq(registry.balanceOf(alice), 1); + + vm.prank(alice); + registry.deregisterAgent(agentA); + + vm.prank(alice); + registry.deregisterAgent(agentB); + + // After all deregistered: 0 + assertEq(registry.balanceOf(alice), 0); + } + + // ─── setAgentWallet integration (account → registry) ───────────────────── + + function test_SetAgentWalletCallsRegistry() public { + MockEntryPoint ep = new MockEntryPoint(); + + // Deploy a fresh AirAccount + AAStarAirAccountV7 account = new AAStarAirAccountV7(); + address ownerAddr = makeAddr("accountOwner"); + + uint8[] memory algs = new uint8[](0); + account.initialize( + address(ep), + ownerAddr, + AAStarAirAccountBase.InitConfig({ + guardians: [makeAddr("g0"), makeAddr("g1"), makeAddr("g2")], + dailyLimit: 0, + approvedAlgIds: algs, + minDailyLimit: 0, + initialTokens: new address[](0), + initialTokenConfigs: new AAStarGlobalGuard.TokenConfig[](0) + }) + ); + + address agentWallet = makeAddr("agentWallet"); + + vm.prank(ownerAddr); + vm.expectEmit(true, true, false, false); + emit AAStarAirAccountBase.AgentWalletSet(1, agentWallet); + account.setAgentWallet(1, agentWallet, address(registry)); + + // Verify registry recorded account as owner of agentWallet + assertEq(registry.agentWalletOwner(agentWallet), address(account)); + assertTrue(registry.isRegisteredAgent(agentWallet)); + } + + function test_SetAgentWalletCallsRegistry_notOwner_reverts() public { + MockEntryPoint ep = new MockEntryPoint(); + + AAStarAirAccountV7 account = new AAStarAirAccountV7(); + address ownerAddr = makeAddr("accountOwner"); + + uint8[] memory algs = new uint8[](0); + account.initialize( + address(ep), + ownerAddr, + AAStarAirAccountBase.InitConfig({ + guardians: [makeAddr("g0"), makeAddr("g1"), makeAddr("g2")], + dailyLimit: 0, + approvedAlgIds: algs, + minDailyLimit: 0, + initialTokens: new address[](0), + initialTokenConfigs: new AAStarGlobalGuard.TokenConfig[](0) + }) + ); + + vm.prank(makeAddr("notOwner")); + vm.expectRevert(AAStarAirAccountBase.NotOwner.selector); + account.setAgentWallet(1, makeAddr("agentWallet"), address(registry)); + } + + function test_SetAgentWalletCallsRegistry_failingRegistry_reverts() public { + MockEntryPoint ep = new MockEntryPoint(); + + AAStarAirAccountV7 account = new AAStarAirAccountV7(); + address ownerAddr = makeAddr("accountOwner"); + + uint8[] memory algs = new uint8[](0); + account.initialize( + address(ep), + ownerAddr, + AAStarAirAccountBase.InitConfig({ + guardians: [makeAddr("g0"), makeAddr("g1"), makeAddr("g2")], + dailyLimit: 0, + approvedAlgIds: algs, + minDailyLimit: 0, + initialTokens: new address[](0), + initialTokenConfigs: new AAStarGlobalGuard.TokenConfig[](0) + }) + ); + + address agentWallet = makeAddr("agentWallet"); + address noCodeAddr = makeAddr("noCodeAddr"); // EOA with no code + + vm.prank(ownerAddr); + vm.expectRevert(AAStarAirAccountBase.AgentRegistrationFailed.selector); + account.setAgentWallet(1, agentWallet, noCodeAddr); + } + + function test_SetAgentWalletCallsRegistry_duplicateRegistration_reverts() public { + MockEntryPoint ep = new MockEntryPoint(); + + AAStarAirAccountV7 account = new AAStarAirAccountV7(); + address ownerAddr = makeAddr("accountOwner"); + + uint8[] memory algs = new uint8[](0); + account.initialize( + address(ep), + ownerAddr, + AAStarAirAccountBase.InitConfig({ + guardians: [makeAddr("g0"), makeAddr("g1"), makeAddr("g2")], + dailyLimit: 0, + approvedAlgIds: algs, + minDailyLimit: 0, + initialTokens: new address[](0), + initialTokenConfigs: new AAStarGlobalGuard.TokenConfig[](0) + }) + ); + + address agentWallet = makeAddr("agentWallet"); + + vm.prank(ownerAddr); + account.setAgentWallet(1, agentWallet, address(registry)); + + // Second call with same agentWallet should revert (AgentAlreadyRegistered propagated as AgentRegistrationFailed) + vm.prank(ownerAddr); + vm.expectRevert(AAStarAirAccountBase.AgentRegistrationFailed.selector); + account.setAgentWallet(2, agentWallet, address(registry)); + } +}