const { expect } = require("chai"); const { ethers } = require("hardhat"); // ERC-6551 token-bound accounts: canonical registry + AereTokenBoundAccount. // Verifies that an NFT can own a wallet that (1) is deployed at the registry's // deterministic address, (2) is controlled by the current NFT holder, (3) holds // and moves native value via execute(), (4) produces EIP-1271 signatures for the // holder, (5) tracks its ERC-6551 state, and (6) rejects non-holders and // non-CALL operations. describe("AereTokenBoundAccount (ERC-6551)", function () { let registry, impl, nft, owner, other; const SALT = ethers.ZeroHash; const TOKEN_ID = 1n; let chainId; before(async function () { [owner, other] = await ethers.getSigners(); chainId = (await ethers.provider.getNetwork()).chainId; registry = await (await ethers.getContractFactory("ERC6551Registry")).deploy(); await registry.waitForDeployment(); impl = await (await ethers.getContractFactory("AereTokenBoundAccount")).deploy(); await impl.waitForDeployment(); nft = await (await ethers.getContractFactory("MockERC721")).deploy(); await nft.waitForDeployment(); await (await nft.mint(owner.address, TOKEN_ID)).wait(); }); it("account() is deterministic and createAccount() deploys there", async function () { const predicted = await registry.account.staticCall( await impl.getAddress(), SALT, chainId, await nft.getAddress(), TOKEN_ID ); // no code yet expect(await ethers.provider.getCode(predicted)).to.equal("0x"); const created = await registry.createAccount.staticCall( await impl.getAddress(), SALT, chainId, await nft.getAddress(), TOKEN_ID ); expect(created).to.equal(predicted); await (await registry.createAccount( await impl.getAddress(), SALT, chainId, await nft.getAddress(), TOKEN_ID )).wait(); expect(await ethers.provider.getCode(predicted)).to.not.equal("0x"); // idempotent: same address on repeat const again = await registry.account.staticCall( await impl.getAddress(), SALT, chainId, await nft.getAddress(), TOKEN_ID ); expect(again).to.equal(predicted); }); async function boundAccount() { const addr = await registry.account.staticCall( await impl.getAddress(), SALT, chainId, await nft.getAddress(), TOKEN_ID ); return ethers.getContractAt("AereTokenBoundAccount", addr); } it("token() returns the exact binding and owner() tracks the NFT holder", async function () { const acct = await boundAccount(); const [cid, tc, tid] = await acct.token(); expect(cid).to.equal(chainId); expect(tc).to.equal(await nft.getAddress()); expect(tid).to.equal(TOKEN_ID); expect(await acct.owner()).to.equal(owner.address); }); it("holds native value and executes a CALL as the holder; non-holder reverts", async function () { const acct = await boundAccount(); const acctAddr = await acct.getAddress(); // fund the account await (await owner.sendTransaction({ to: acctAddr, value: ethers.parseEther("1") })).wait(); expect(await ethers.provider.getBalance(acctAddr)).to.equal(ethers.parseEther("1")); expect(await acct.state()).to.equal(0n); // holder moves 0.4 AERE out to `other` const before = await ethers.provider.getBalance(other.address); await (await acct.connect(owner).execute(other.address, ethers.parseEther("0.4"), "0x", 0)).wait(); const after = await ethers.provider.getBalance(other.address); expect(after - before).to.equal(ethers.parseEther("0.4")); expect(await acct.state()).to.equal(1n); // state bumped // a non-holder cannot execute await expect( acct.connect(other).execute(other.address, 0, "0x", 0) ).to.be.revertedWithCustomError(acct, "InvalidSigner"); // only CALL (operation 0) is supported await expect( acct.connect(owner).execute(other.address, 0, "0x", 1) ).to.be.revertedWithCustomError(acct, "UnsupportedOperation"); }); it("isValidSignature (EIP-1271) accepts the holder's signature and rejects others", async function () { const acct = await boundAccount(); const digest = ethers.hexlify(ethers.randomBytes(32)); const goodSig = await owner.signMessage(ethers.getBytes(digest)); const ethDigest = ethers.hashMessage(ethers.getBytes(digest)); expect(await acct.isValidSignature(ethDigest, goodSig)).to.equal("0x1626ba7e"); const badSig = await other.signMessage(ethers.getBytes(digest)); expect(await acct.isValidSignature(ethDigest, badSig)).to.equal("0x00000000"); }); it("control follows the NFT: transferring it re-points owner()", async function () { const acct = await boundAccount(); await (await nft.transferFrom(owner.address, other.address, TOKEN_ID)).wait(); expect(await acct.owner()).to.equal(other.address); // now `other` controls it, `owner` does not await (await acct.connect(other).execute(owner.address, 0, "0x", 0)).wait(); await expect( acct.connect(owner).execute(owner.address, 0, "0x", 0) ).to.be.revertedWithCustomError(acct, "InvalidSigner"); // restore for isolation await (await nft.connect(other).transferFrom(other.address, owner.address, TOKEN_ID)).wait(); }); it("supportsInterface reports ERC-165 / ERC-6551 / ERC-1271", async function () { const acct = await boundAccount(); expect(await acct.supportsInterface("0x01ffc9a7")).to.equal(true); // ERC-165 expect(await acct.supportsInterface("0x6faff5f1")).to.equal(true); // IERC6551Account expect(await acct.supportsInterface("0x51945447")).to.equal(true); // IERC6551Executable expect(await acct.supportsInterface("0x1626ba7e")).to.equal(true); // IERC1271 expect(await acct.supportsInterface("0xffffffff")).to.equal(false); }); });