-
Notifications
You must be signed in to change notification settings - Fork 0
fix(registry): align AgentRegistry with IAgentIdentityRegistry + close #37 #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
d527013
8fdc446
3e016f5
a63417d
665b8de
ac4d621
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<typeof createPublicClient>, | ||
| 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<typeof privateKeyToAccount>) { | ||
| 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); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(); | ||
|
Comment on lines
+1578
to
+1581
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| emit AgentWalletSet(agentId, agentWallet); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| // 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 count of agent wallets registered by this owner. | ||
| /// Implements IAgentIdentityRegistry.balanceOf(address) — returns actual count. | ||
| function balanceOf(address humanOwner) external view returns (uint256) { | ||
| return ownerAgents[humanOwner].length; | ||
| } | ||
|
|
||
| /// @notice ERC-721-compatible stub required by IAgentIdentityRegistry. | ||
| /// Always returns address(0) — AgentRegistry does not use token IDs. | ||
| function ownerOf(uint256 /* agentId */) external pure returns (address) { | ||
| return address(0); | ||
| } | ||
|
|
||
| /// @notice Alias for deregisterAgent — matches IAgentIdentityRegistry.revokeAgent(address). | ||
| function revokeAgent(address agentWallet) external { | ||
| if (agentWalletOwner[agentWallet] != msg.sender) revert NotAgentOwner(); | ||
| agentWalletOwner[agentWallet] = address(0); | ||
| 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 Convenience lookup: returns the human AirAccount that registered agentWallet. | ||
| /// Returns address(0) if agentWallet is not registered. | ||
| function getHumanOwner(address agentWallet) external view returns (address) { | ||
| return agentWalletOwner[agentWallet]; | ||
| } | ||
|
|
||
| /// @notice Returns all agent wallets registered by a human owner. | ||
| function getAgents(address humanOwner) external view returns (address[] memory) { | ||
| return ownerAgents[humanOwner]; | ||
| } | ||
|
|
||
| /// @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; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The script claims multi-RPC resiliency, but it performs an unconditional
getBalanceagainstRPC_URLS[0]before entering the retry loop. If the first endpoint is down or rate-limited, deployment aborts before tryingSEPOLIA_RPC_URL2/3, so the fallback mechanism never runs in exactly the failure mode it is meant to handle.Useful? React with 👍 / 👎.