普通视图

发现新文章,点击刷新页面。
昨天 — 2026年3月16日首页

深度解析 AgentFi:基于 ERC-6551 与 AI 驱动的 DeFi 进化论

作者 木西
2026年3月16日 16:54

前言

在传统的 DeFi 生态中,用户往往需要手动操作或依赖中心化的机器人(Bot)来管理资产。AgentFi(代理金融)的出现,标志着资产管理从“自动化”向“智能化”的范式转移。它通过 ERC-6551 (Token Bound Accounts)  标准,赋予 NFT 独立的链上账户能力,使其进化为可以自主决策、自动捕获收益的 AI 智能体 (AI Agents)

补充说明:如果对ERC6551标准不了解,可以配合往期作品《拒绝NFT只好看!用代码给NFT装【独立账号】:ERC6551开发实战》

一、 业务逻辑:NFT 即智能体

AgentFi 的核心逻辑可以拆解为四个关键维度:

  1. 身份与账户绑定 (TBA) :通过 ERC-6551,每一个 NFT 不再仅仅是小图片,而是一个功能完备的智能合约钱包。
  2. 资产托管与策略封装:资产不存放在协议池,而是存放在 NFT 绑定的账户中。策略(如循环贷、流动性管理)直接在账户内执行。
  3. 原生收益捕获 (以 Blast 为例) :利用 Blast 等网络的 Rebase 机制,智能体账户能自动赚取 ETH 和稳定币的基础利息。
  4. 可交易的策略仓位:用户可以在二级市场交易这些 NFT。买家买到的不仅是所有权,还有该智能体持有的资产及其正在运行的获利策略。

二、 核心架构实现

AgentFi 的系统架构由 Registry(注册表)Proxy(代理层)  和 Implementation(逻辑层)  组成。

1. AgentNFT.sol (NFT 身份合约)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract AgentNFT is ERC721, Ownable {
    uint256 private _nextTokenId;

    constructor() ERC721("AgentFi Bot", "AFIB") Ownable(msg.sender) {}

    // 用户铸造一个 Agent 智能体
    function mintAgent() public returns (uint256) {
        uint256 tokenId = _nextTokenId++;
        _safeMint(msg.sender, tokenId);
        return tokenId;
    }
}

2. AgentAccount.sol (TBA 策略执行合约)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; // 必须导入以使用 ownerOf

// 1. 将接口移出合约外部
interface IERC6551Account {
    function owner() external view returns (address);
    function token() external view returns (uint256 chainId, address tokenContract, uint256 tokenId);
}

// 模拟 DeFi 协议接口
interface ILendingPool {
    function deposit(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external;
}

contract AgentAccount is IERC6551Account {
    
    // 执行策略:自动将账户内的余额存入借贷协议获取收益
    function executeStrategy(address pool, address asset) external {
        // 只有 NFT 的持有者可以触发策略
        require(msg.sender == owner(), "Not authorized");

        uint256 balance = IERC20(asset).balanceOf(address(this));
        require(balance > 0, "No funds to deploy");

        // 授权并存入
        IERC20(asset).approve(pool, balance);
        ILendingPool(pool).deposit(asset, balance, address(this), 0);
    }

    // ERC-6551 标准必需函数:查询谁拥有这个智能体
    function owner() public view override returns (address) {
        (uint256 chainId, address tokenContract, uint256 tokenId) = token();
        if (chainId != block.chainid) return address(0);
        // 使用 OpenZeppelin 的 IERC721 接口
        return IERC721(tokenContract).ownerOf(tokenId);
    }

    function token() public view override returns (uint256, address, uint256) {
        // 简化演示:此处硬编码。实际开发中建议通过构造函数或 Immutable Args 传入。
        return (block.chainid, 0x5FbDB2315678afecb367f032d93F642f64180aa3, 1); 
    }

    // 允许接收 ETH
    receive() external payable {}
}

3.ERC-6551 Registry 合约实现

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/utils/Create2.sol";

// --- 1. 定义代理合约 (必须存在,否则 Registry 无法获取其 creationCode) ---
contract AgentProxy {
    // 代理合约通常非常简单:存储逻辑地址并转发调用
    address public immutable implementation;

    constructor(address _implementation, uint256, uint256, address, uint256) {
        implementation = _implementation;
    }

    // 转发所有调用到逻辑合约
    fallback() external payable {
        address _impl = implementation;
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), _impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }

    receive() external payable {}
}

// --- 2. 注册表接口 ---
interface IERC6551Registry {
    event ERC6551AccountCreated(
        address account,
        address indexed implementation,
        uint256 salt,
        uint256 chainId,
        address indexed tokenContract,
        uint256 indexed tokenId
    );

    function createAccount(
        address implementation,
        uint256 salt,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId
    ) external returns (address);

    function account(
        address implementation,
        uint256 salt,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId
    ) external view returns (address);
}

// --- 3. 注册表实现 ---
contract AgentRegistry is IERC6551Registry {
    function createAccount(
        address implementation,
        uint256 salt,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId
    ) external returns (address) {
        bytes memory code = _creationCode(implementation, salt, chainId, tokenContract, tokenId);
        
        address _account = Create2.computeAddress(bytes32(salt), keccak256(code));
        
        // 如果已部署则直接返回
        if (_account.code.length > 0) return _account;

        // 部署代理合约
        _account = Create2.deploy(0, bytes32(salt), code);

        emit ERC6551AccountCreated(_account, implementation, salt, chainId, tokenContract, tokenId);
        return _account;
    }

    function account(
        address implementation,
        uint256 salt,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId
    ) external view returns (address) {
        bytes memory code = _creationCode(implementation, salt, chainId, tokenContract, tokenId);
        return Create2.computeAddress(bytes32(salt), keccak256(code));
    }

    function _creationCode(
        address implementation,
        uint256 salt,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId
    ) internal pure returns (bytes memory) {
        // 使用 abi.encodePacked 将 Proxy 的字节码与构造函数参数拼接
        return abi.encodePacked(
            type(AgentProxy).creationCode,
            abi.encode(implementation, salt, chainId, tokenContract, tokenId)
        );
    }
}

4.Blast 收益处理逻辑合约 (AgentImplementation.sol)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24; // Blast 建议使用 0.8.24+ 以支持某些操作码

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";

// Blast 官方收益接口
interface IBlast {
    // 配置收益模式:0 = 被动, 1 = 自动复利(Automatic), 2 = 认领模式(Claimable)
    function configureClaimableYield() external;
    function configureAutomaticYield() external;
    function configureGovernor(address _governor) external;
}

interface IERC721 {
    function ownerOf(uint256 tokenId) external view returns (address);
}

contract AgentImplementation is Initializable {
    // Blast 系统合约固定地址
    IBlast public constant BLAST = IBlast(0x4300000000000000000000000000000000000002);
    
    // ERC-6551 账户元数据(通常在代理合约中,这里模拟读取)
    address public tokenContract;
    uint256 public tokenId;

    // 只有 NFT 持有者可以调用
    modifier onlyTokenOwner() {
        require(msg.sender == IERC721(tokenContract).ownerOf(tokenId), "Not Agent Owner");
        _;
    }

    /// @notice 初始化函数,由 Registry 部署代理后立即调用
    function initialize(address _tokenContract, uint256 _tokenId) public initializer {
        tokenContract = _tokenContract;
        tokenId = _tokenId;

        // 核心步骤:激活 Blast 原生收益
        // 这样存入这个 Agent 账户的 ETH 就会自动产生 4%+ 的利息
        if (block.chainid == 81457 || block.chainid == 168587773) { // Blast 主网或测试网
            BLAST.configureAutomaticYield(); 
            BLAST.configureGovernor(msg.sender); // 让 NFT 持有者管理收益设置
        }
    }

    /// @notice 业务逻辑:将账户内的资金投入外部协议(如去中心化交易所)
    /// @param target 外部协议地址
    /// @param data 交互的编码数据
    function executeStrategy(address target, bytes calldata data) 
        external 
        onlyTokenOwner 
        returns (bytes memory) 
    {
        // 这里的逻辑代表 Agent 正在根据“策略”操作资金
        (bool success, bytes memory result) = target.call(data);
        require(success, "Strategy execution failed");
        return result;
    }

    /// @notice 提取收益或本金
    function withdraw(address asset, uint256 amount) external onlyTokenOwner {
        if (asset == address(0)) {
            payable(msg.sender).transfer(amount);
        } else {
            IERC20(asset).transfer(msg.sender, amount);
        }
    }

    // 必须能够接收 ETH 才能获取 Blast 的原生收益
    receive() external payable {}
}

5.ERC20代币

// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.5.0
pragma solidity ^0.8.24;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract BoykaYuriToken is ERC20, ERC20Burnable, Ownable, ERC20Permit {
    constructor(address recipient, address initialOwner)
        ERC20("MyToken", "MTK")
        Ownable(initialOwner)
        ERC20Permit("MyToken")
    {
        _mint(recipient, 1000000 * 10 ** decimals());
    }
    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}

三、 集成测试:验证智能体的自主性

测试用例

  • 初始化验证:Agent 账户应正确绑定 NFT 身份并开启 Blast 配置
  • 资产流动性:Agent 账户应能接收 ETH 并在持有者授权下转出
  • 策略执行:Agent 应能操作 ERC20 代币(模拟 USDB 收益)
  • 权限拦截:非 NFT 持有者无法调用 executeStrategy 或 withdraw
  • 所有权动态转移:NFT 卖出后,新持有者自动获得 Agent 资金控制权
import assert from "node:assert/strict";
import { describe, it, beforeEach } from "node:test";
import { parseEther, encodeFunctionData, zeroAddress, getAddress } from "viem";
import { network } from "hardhat";

describe("AgentFi Protocol Full Integration", function () {
  async function deployFixture() {
    const { viem } = await (network as any).connect();
    const [owner, otherAccount] = await viem.getWalletClients();
    const publicClient = await viem.getPublicClient();

    // 1. 部署基础合约
    const nft = await viem.deployContract("AgentNFT");
    const registry = await viem.deployContract("AgentRegistry");
    const implementation = await viem.deployContract("AgentImplementation");
    // 模拟一个 ERC20 代币用于策略测试
    const mockUSDB = await viem.deployContract("BoykaYuriToken", [owner.account.address, owner.account.address]);

    const salt = 0n;
    const chainId = BigInt(await publicClient.getChainId());
    const tokenId = 0n; // 第一个 mint 的 ID

    // 2. 铸造 Agent NFT
    await nft.write.mintAgent({ account: owner.account });

    // 3. 计算并预部署 TBA (Token Bound Account)
    const tbaAddress = await registry.read.account([
        implementation.address, 
        salt, 
        chainId, 
        nft.address, 
        tokenId
    ]);

    // 4. 部署账户合约
    await registry.write.createAccount([
        implementation.address, 
        salt, 
        chainId, 
        nft.address, 
        tokenId
    ]);

    const tbaContract = await viem.getContractAt("AgentImplementation", tbaAddress);

    // 5. 初始化 Agent (配置 Blast 收益模式等)
    await tbaContract.write.initialize([nft.address, tokenId], { account: owner.account });

    return { 
        nft, registry, implementation, mockUSDB, 
        owner, otherAccount, publicClient, 
        tbaAddress, tbaContract, tokenId, chainId, salt 
    };
  }

  it("初始化验证:Agent 账户应正确绑定 NFT 身份并开启 Blast 配置", async function () {
    const { tbaContract, nft, tokenId } = await deployFixture();
    
    const boundNFT = await tbaContract.read.tokenContract();
    const boundID = await tbaContract.read.tokenId();

    assert.equal(getAddress(boundNFT), getAddress(nft.address), "绑定的 NFT 地址不匹配");
    assert.equal(boundID, tokenId, "绑定的 TokenID 不匹配");
  });

  it("资产流动性:Agent 账户应能接收 ETH 并在持有者授权下转出", async function () {
    const { tbaAddress, tbaContract, otherAccount, publicClient } = await deployFixture();
    const amount = parseEther("1");

    // 外部用户向 Agent 转账
    await otherAccount.sendTransaction({ to: tbaAddress, value: amount });
    assert.equal(await publicClient.getBalance({ address: tbaAddress }), amount);

    // NFT 持有者指挥 Agent 将 ETH 转回
    const beforeBalance = await publicClient.getBalance({ address: otherAccount.account.address });
    await tbaContract.write.withdraw([zeroAddress, amount], { account: (await deployFixture()).owner.account });

    assert.equal(await publicClient.getBalance({ address: tbaAddress }), 0n);
  });

  it("策略执行:Agent 应能操作 ERC20 代币(模拟 USDB 收益)", async function () {
    const { tbaAddress, tbaContract, mockUSDB, owner } = await deployFixture();
    const amount = parseEther("100");

    // 给 Agent 发放 USDB
    await mockUSDB.write.transfer([tbaAddress, amount]);

    // 模拟策略:通过 executeStrategy 接口调用 ERC20 transfer
    const transferData = encodeFunctionData({
      abi: mockUSDB.abi,
      functionName: "transfer",
      args: [owner.account.address, amount],
    });

    await tbaContract.write.executeStrategy([mockUSDB.address, transferData], {
      account: owner.account,
    });

    assert.equal(await mockUSDB.read.balanceOf([tbaAddress]), 0n, "策略执行后代币应已转出");
  });

  it("权限拦截:非 NFT 持有者无法调用 executeStrategy 或 withdraw", async function () {
    const { tbaContract, otherAccount } = await deployFixture();

    // 尝试越权提取
    await assert.rejects(
      async () => {
        await tbaContract.write.withdraw([zeroAddress, 1n], {
          account: otherAccount.account,
        });
      },
      /Not Agent Owner/,
      "非所有者不应被允许提取资金"
    );
  });

  it("所有权动态转移:NFT 卖出后,新持有者自动获得 Agent 资金控制权", async function () {
    const { nft, tbaContract, owner, otherAccount, tokenId } = await deployFixture();

    // 1. 转移 NFT
    await nft.write.transferFrom([owner.account.address, otherAccount.account.address, tokenId]);

    // 2. 旧持有者(owner)尝试操作,应该失败
    await assert.rejects(
      async () => {
        await tbaContract.write.withdraw([zeroAddress, 0n], { account: owner.account });
      },
      /Not Agent Owner/
    );

    // 3. 新持有者(otherAccount)尝试操作,应该成功
    const tx = await tbaContract.write.withdraw([zeroAddress, 0n], {
      account: otherAccount.account,
    });
    assert.ok(tx, "新持有者应能成功发起交易");
  });
});

四、 部署脚本

// scripts/deploy.js
import { network, artifacts } from "hardhat";
async function main() {
  // 连接网络
  const { viem } = await network.connect({ network: network.name });//指定网络进行链接
  
  // 获取客户端
  const [deployer] = await viem.getWalletClients();
  const publicClient = await viem.getPublicClient();
 
  const deployerAddress = deployer.account.address;
   console.log("部署者的地址:", deployerAddress);
  // 加载合约
  const BoykaYuriTokenArtifact = await artifacts.readArtifact("BoykaYuriToken");
  const AgentNFTArtifact = await artifacts.readArtifact("AgentNFT");
  const AgentRegistryArtifact = await artifacts.readArtifact("AgentRegistry");
  const AgentImplementationArtifact = await artifacts.readArtifact("AgentImplementation");
  // 部署(构造函数参数:recipient, initialOwner)
  const BoykaYuriTokenHash = await deployer.deployContract({
    abi: BoykaYuriTokenArtifact.abi,//获取abi
    bytecode: BoykaYuriTokenArtifact.bytecode,//硬编码
    args: [deployerAddress,deployerAddress],//process.env.RECIPIENT, process.env.OWNER
  });

  // 等待确认并打印地址
  const BoykaYuriTokenReceipt = await publicClient.waitForTransactionReceipt({ hash: BoykaYuriTokenHash });
  console.log("BoykaYuriToken合约地址:", BoykaYuriTokenReceipt.contractAddress);
  const AgentNFTHash = await deployer.deployContract({
    abi: AgentNFTArtifact.abi,//获取abi
    bytecode: AgentNFTArtifact.bytecode,//硬编码
    args: [],//process.env.RECIPIENT, process.env.OWNER
  });
    const AgentNFTReceipt = await publicClient.waitForTransactionReceipt({ hash: AgentNFTHash });
    console.log("AgentNFT合约地址:", AgentNFTReceipt.contractAddress);
    const AgentRegistryHash = await deployer.deployContract({   
      abi: AgentRegistryArtifact.abi,//获取abi
      bytecode: AgentRegistryArtifact.bytecode,//硬编码
      args: [],//process.env.RECIPIENT, process.env.OWNER
    });
    const AgentRegistryReceipt = await publicClient.waitForTransactionReceipt({ hash: AgentRegistryHash });
    console.log("AgentRegistry合约地址:", AgentRegistryReceipt.contractAddress);
    const AgentImplementationHash = await deployer.deployContract({ 
        abi: AgentImplementationArtifact.abi,//获取abi
        bytecode: AgentImplementationArtifact.bytecode,//硬编码
        args: [],//process.env.RECIPIENT, process.env.OWNER
    });
    const AgentImplementationReceipt = await publicClient.waitForTransactionReceipt({ hash: AgentImplementationHash });
    console.log("AgentImplementation合约地址:", AgentImplementationReceipt.contractAddress);
}

main().catch(console.error);

五、 总结与展望

AgentFi 不仅仅是 DeFi 的一个分支,它是  “意图中心(Intent-centric)”  架构的终极形态。通过这种架构:

  • 对于用户:无需关注底层的 Swap 或 LP 路径,只需持有 NFT 即可享受 AI 优化的收益。
  • 对于开发者:可以编写更复杂的链上逻辑,将 AI 模型预测的结果直接推送到 TBA 账户执行。

AgentFi 的未来在于 跨链互操作性 与 链上 AI 推理 (On-chain Inference)  的结合。当智能体能够跨越链的限制寻找最佳利息,并实时调整风险参数时,真正的去中心化资产管理时代才算真正开启。

昨天以前首页

深度拆解 Web3 预测市场:基于 Solidity 0.8.24 与 UMA 乐观预言机的核心实现

作者 木西
2026年3月9日 15:52

前言

在 Web3 领域,Polymarket 的成功证明了“链上对冲+现实预测”模式的巨大潜力。不同于传统的博弈平台,Polymarket 的精髓在于利用乐观预言机(Optimistic Oracle) 将现实世界的非对称信息转化为链上可结算的资产。本文将从架构设计、核心代码到自动化测试,完整拆解一个去中心化预测市场的技术实现。

一、 核心架构:为何选择 UMA?

传统的预言机(如 Chainlink Price Feeds)擅长处理高频、标准化的数据(如币价)。但对于“2026年比特币是否突破20万美金”这类离散的、需人工核实的事件,UMA 乐观预言机提供了更优的方案:

  1. 断言机制(Assertion) :任何人都可以提交一个结果断言,并缴纳保证金。
  2. 挑战期(Liveness Period) :如果在特定时间内(如 2 小时)没人挑战,系统默认该断言为真。
  3. 博弈均衡:通过经济激励确保提交者不敢造假,因为挑战者可以推翻错误断言并赢得其保证金。

二、 核心智能合约实现

我们使用 Solidity 0.8.24 和 OpenZeppelin V5 编写了核心逻辑。合约实现了资产托管、双向对冲池和预言机异步结算。

注释:奖励瓜分算法

采用 Pari-mutuel(等额奖池)  机制:胜方根据其投入的份额,等比例瓜分败方的资金池。

𝑅𝑒𝑤𝑎𝑟𝑑=𝑈𝑠𝑒𝑟𝐵𝑒𝑡𝑇𝑜𝑡𝑎𝑙𝑊𝑖𝑛𝑛𝑖𝑛𝑔𝑃𝑜𝑜𝑙×𝑇𝑜𝑡𝑎𝑙𝑃𝑜𝑡𝑅𝑒𝑤𝑎𝑟𝑑=\frac{𝑈𝑠𝑒𝑟𝐵𝑒𝑡}{𝑇𝑜𝑡𝑎𝑙𝑊𝑖𝑛𝑛𝑖𝑛𝑔𝑃𝑜𝑜𝑙}×𝑇𝑜𝑡𝑎𝑙𝑃𝑜𝑡

1. SimplePolymarketWithUMA合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

interface IOptimisticOracleV3 {
    function assertTruthWithDefaults(bytes calldata claim, address callbackRecipient) external returns (bytes32);
    function getAssertion(bytes32 assertionId) external view returns (bool settled, bool domainResolved, address caller, uint256 expirationTime, bool truthValue);
}

contract SimplePolymarketWithUMA is ReentrancyGuard {
    IERC20 public immutable usdc;
    IOptimisticOracleV3 public immutable umaOO;

    struct Market {
        string description;
        uint256 totalYes;
        uint256 totalNo;
        uint8 outcome; // 0: Open, 1: Yes, 2: No
        bool resolved;
        bytes32 assertionId;
    }

    uint256 public marketCount;
    mapping(uint256 => Market) public markets;
    mapping(uint256 => mapping(address => uint256)) public yesBets;
    mapping(uint256 => mapping(address => uint256)) public noBets;

    constructor(address _usdc, address _umaOO) {
        usdc = IERC20(_usdc);
        umaOO = IOptimisticOracleV3(_umaOO);
    }

    function createMarket(string calldata _description) external {
        uint256 marketId = marketCount++;
        markets[marketId].description = _description;
    }

    function placeBet(uint256 _marketId, bool _isYes, uint256 _amount) external nonReentrant {
        Market storage m = markets[_marketId];
        require(!m.resolved, "Market resolved");
        require(usdc.transferFrom(msg.sender, address(this), _amount), "Transfer failed");

        if (_isYes) {
            yesBets[_marketId][msg.sender] += _amount;
            m.totalYes += _amount;
        } else {
            noBets[_marketId][msg.sender] += _amount;
            m.totalNo += _amount;
        }
    }

    // 向 UMA 提出断言:结果为 YES (1) 或 NO (2)
    function proposeOutcome(uint256 _marketId, uint8 _proposedOutcome) external {
        Market storage m = markets[_marketId];
        require(m.assertionId == bytes32(0), "Outcome already proposed");
        
        string memory claim = string(abi.encodePacked("Market:", m.description, " Result:", _proposedOutcome == 1 ? "YES" : "NO"));
        bytes32 aid = umaOO.assertTruthWithDefaults(bytes(claim), address(this));
        
        m.assertionId = aid;
    }

    // 挑战期结束后,根据 UMA 判定结果结算
    function settleMarket(uint256 _marketId) external {
        Market storage m = markets[_marketId];
        require(!m.resolved, "Already resolved");
        
        (bool settled, , , , bool truthValue) = umaOO.getAssertion(m.assertionId);
        require(settled, "UMA assertion not settled");

        // 若 UMA 判定断言为真,则接受提议的结果
        // 注意:这里简化逻辑,假设提议结果总是 1 (YES)
        m.outcome = truthValue ? 1 : 2;
        m.resolved = true;
    }

    function claimReward(uint256 _marketId) external nonReentrant {
        Market storage m = markets[_marketId];
        require(m.resolved, "Not resolved");

        uint256 reward;
        uint256 totalPool = m.totalYes + m.totalNo;

        if (m.outcome == 1) {
            uint256 userBet = yesBets[_marketId][msg.sender];
            require(userBet > 0, "No winning bet or already claimed"); // 增加此行效果更佳
            reward = (userBet * totalPool) / m.totalYes;
            yesBets[_marketId][msg.sender] = 0;
        } else {
            uint256 userBet = noBets[_marketId][msg.sender];
            reward = (userBet * totalPool) / m.totalNo;
            noBets[_marketId][msg.sender] = 0;
        }
        require(usdc.transfer(msg.sender, reward), "Reward failed");
    }
}

2. TestUSDT代币

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

/**
 * @dev 测试网专用 USDT,任意人都能 mint
 */
contract TestUSDT is ERC20 {
    uint8 private _decimals;

    constructor(
        string memory name,
        string memory symbol,
        uint8 decimals_
    ) ERC20(name, symbol) {
        _decimals = decimals_;
    }

    function decimals() public view override returns (uint8) {
        return _decimals;
    }

    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }
}

3. MockOptimisticOracleV3合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract MockOptimisticOracleV3 {
    struct Assertion {
        bool settled;
        bool truthValue;
        uint256 expirationTime;
    }

    mapping(bytes32 => Assertion) public assertions;
    uint256 public constant LIVENESS = 7200; // 2小时挑战期

    function assertTruthWithDefaults(bytes calldata claim, address) external returns (bytes32) {
        bytes32 aid = keccak256(abi.encodePacked(claim, block.timestamp));
        assertions[aid] = Assertion(false, false, block.timestamp + LIVENESS);
        return aid;
    }

    // 模拟挑战期结束并手动结算
    function mockSettle(bytes32 aid, bool _truth) external {
        require(block.timestamp >= assertions[aid].expirationTime, "Liveness not met");
        assertions[aid].settled = true;
        assertions[aid].truthValue = _truth;
    }

    function getAssertion(bytes32 aid) external view returns (bool, bool, address, uint256, bool) {
        Assertion memory a = assertions[aid];
        return (a.settled, true, address(0), a.expirationTime, a.truthValue);
    }
}

四、 自动化测试:Viem + Hardhat

测试用例:

  • ✅ 经 UMA 判定后,胜方成功瓜分奖池
  • Polymarket + UMA 自动化集成测试
    • ✔ 完整流程:下注 -> UMA断言 -> 时间推进 -> 结算 -> 领奖
  • ✅ 第一次合法领取完成
  • ✅ 重复领取拦截成功
    • ✔ 安全性测试 (重复领取拦截)
import assert from "node:assert/strict";
import { describe, it, beforeEach } from "node:test";
import { network } from "hardhat"; 
import { type Address, parseUnits, decodeEventLog } from "viem";

describe("Polymarket + UMA 自动化集成测试", function () {
    let poly: any, usdc: any, uma: any;
    let admin: any, userYes: any, userNo: any;
    let vClient: any, pClient: any;
    let testClient: any;
    beforeEach(async function () {
        const { viem } = await (network as any).connect();
        vClient = viem;
        [admin, userYes, userNo] = await vClient.getWalletClients();
        pClient = await vClient.getPublicClient();
        testClient = await viem.getTestClient();
        // 1. 部署环境
        usdc = await vClient.deployContract("TestUSDT", ["USDC", "USDC", 6]);
        uma = await vClient.deployContract("MockOptimisticOracleV3");
        poly = await vClient.deployContract("SimplePolymarketWithUMA", [usdc.address, uma.address]);

        // 2. 准备资金
        const amount = parseUnits("1000", 6);
        for (const u of [userYes, userNo]) {
            await usdc.write.mint([u.account.address, amount], { account: admin.account });
            await usdc.write.approve([poly.address, amount], { account: u.account });
        }
    });

    it("完整流程:下注 -> UMA断言 -> 时间推进 -> 结算 -> 领奖", async function () {
        const marketId = 0n;
        await poly.write.createMarket(["Bitcoin > $100k?"], { account: admin.account });

        // 用户下注
        await poly.write.placeBet([marketId, true, parseUnits("100", 6)], { account: userYes.account });
        await poly.write.placeBet([marketId, false, parseUnits("50", 6)], { account: userNo.account });

        // 提出断言 (提议结果为 YES)
        await poly.write.proposeOutcome([marketId, 1], { account: admin.account });
        const mInfo = await poly.read.markets([marketId]);
        const aid = mInfo[5]; // 获取 assertionId

        // 模拟时间推进 (跳过 2 小时挑战期)
        // await network.provider.send("evm_increaseTime", [7201]);
        // await network.provider.send("evm_mine");
        await testClient.increaseTime({ seconds: 7201 });
        await testClient.mine({ blocks: 1 });
        // 模拟 UMA 结算该断言
        await uma.write.mockSettle([aid, true], { account: admin.account });

        // Polymarket 最终结算
        await poly.write.settleMarket([marketId], { account: admin.account });

        // 验证领奖:UserYes 投入 100,UserNo 投入 50,总池 150
        const balBefore = await usdc.read.balanceOf([userYes.account.address]);
        await poly.write.claimReward([marketId], { account: userYes.account });
        const balAfter = await usdc.read.balanceOf([userYes.account.address]);

        assert.strictEqual(balAfter - balBefore, parseUnits("150", 6), "奖池分配错误");
        console.log("✅ 经 UMA 判定后,胜方成功瓜分奖池");
    });
    it("安全性测试 (重复领取拦截)", async function () {
    const marketId = 0n;
    await poly.write.createMarket(["安全性攻击测试"], { account: admin.account });

    // 1. 下注
    await poly.write.placeBet([marketId, true, parseUnits("100", 6)], { account: userYes.account });
    await poly.write.proposeOutcome([marketId, 1], { account: admin.account });
    
    // 2. 推进时间
    await testClient.increaseTime({ seconds: 7201 });
    await testClient.mine({ blocks: 1 });
    
    // 3. 修复后的 aid 获取方式
    const mInfoBefore = await poly.read.markets([marketId]);
    const aid = mInfoBefore[5]; // 获取 bytes32 类型的 assertionId
    
    // 确保 aid 不是 undefined
    assert.ok(aid && aid !== '0x0000000000000000000000000000000000000000000000000000000000000000', "未获取到有效的 Assertion ID");

    await uma.write.mockSettle([aid, true], { account: admin.account });
    await poly.write.settleMarket([marketId], { account: admin.account });

    // 4. 第一次领取
    await poly.write.claimReward([marketId], { account: userYes.account });
    console.log("✅ 第一次合法领取完成");

    // 5. 第二次领取(预期失败)
    try {
        await poly.write.claimReward([marketId], { account: userYes.account });
        assert.fail("不应允许重复领取");
    } catch (err: any) {
        // Viem 的错误通常在 err.message 或 err.shortMessage 中
        assert.ok(err.message.includes("revert"), "应该触发合约 revert");
        console.log("✅ 重复领取拦截成功");
    }
});


});

部署脚本

// scripts/deploy.js
import { network, artifacts } from "hardhat";
import { parseUnits } from "viem";
async function main() {
  // 连接网络
  const { viem } = await network.connect({ network: network.name });//指定网络进行链接
  
  // 获取客户端
  const [deployer, investor] = await viem.getWalletClients();
  const publicClient = await viem.getPublicClient();
 
  const deployerAddress = deployer.account.address;
   console.log("部署者的地址:", deployerAddress);
  
  // 部署TestUSDTReceipt合约
  const TestUSDTArtifact = await artifacts.readArtifact("TestUSDT");
  // 1. 部署合约并获取交易哈希
  const TestUSDTHash = await deployer.deployContract({
    abi: TestUSDTArtifact.abi,
    bytecode: TestUSDTArtifact.bytecode,
    args: ["USDC", "USDC", 6],
  });
  const TestUSDTReceipt = await publicClient.waitForTransactionReceipt({ 
     hash: TestUSDTHash 
   });
   console.log("TestUSDTReceipt合约地址:", TestUSDTReceipt.contractAddress);
   // 部署MockOptimisticOracleV3合约
   const MockOptimisticOracleV3Artifact = await artifacts.readArtifact("MockOptimisticOracleV3");
   // 1. 部署合约并获取交易哈希
   const MockOptimisticOracleV3Hash = await deployer.deployContract({
     abi: MockOptimisticOracleV3Artifact.abi,
     bytecode: MockOptimisticOracleV3Artifact.bytecode,
     args: [],
   });
   const MockOptimisticOracleV3Receipt = await publicClient.waitForTransactionReceipt({ 
      hash: MockOptimisticOracleV3Hash 
    });
    console.log("MockOptimisticOracleV3合约地址:", MockOptimisticOracleV3Receipt.contractAddress);
    // SimplePolymarketWithUMA脚本
    const SimplePolymarketWithUMAArtifact=await artifacts.readArtifact("SimplePolymarketWithUMA");
    const SimplePolymarketWithUMAHash = await deployer.deployContract({
     abi: SimplePolymarketWithUMAArtifact.abi,
     bytecode: SimplePolymarketWithUMAArtifact.bytecode,
     args: [TestUSDTReceipt.contractAddress,MockOptimisticOracleV3Receipt.contractAddress],
   });
   const SimplePolymarketWithUMAReceipt = await publicClient.waitForTransactionReceipt({ 
      hash: SimplePolymarketWithUMAHash 
    });
    console.log("SimplePolymarketWithUMAReceipt合约地址",SimplePolymarketWithUMAReceipt.contractAddress)
}

main().catch(console.error);

总结

至此,简洁版 Polymarket 核心运行机制相关智能合约已完成开发、测试与部署全流程。期间完成了理论梳理、核心架构设计,并明确了 UMA 乐观预言机的选型依据,整体工作圆满落地。

❌
❌