1. 跨链资产锁定与解锁基础合约

跨链资产锁定与解锁基础合约 学习卡片

任务说明

本案例实现一个最小可用的跨链资产锁定合约(Solidity),核心能力为:锁定资产(用户将代币存入合约并记录锁定信息)与释放资产(仅合约管理员可解锁并退回代币),同时为每次锁定与释放操作发出链上事件,便于跨链监听与审计。合约不涉及真实跨链消息传递,聚焦于“锁定-释放”这一基础原语。

合约实现

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

import "@openzeppelin/contracts//ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/**
 * @title CrossChainAssetVault
 * @notice 跨链资产锁定与释放基础合约
 * @dev 仅支持ERC20代币的锁定与释放,管理员(owner)负责释放操作
 */
contract CrossChainAssetVault is Ownable {
    IERC20 public immutable ;

    // 锁定记录:lockId => LockRecord
    struct LockRecord {
        address user;        // 锁定资产的用户
        uint256 amount;      // 锁定数量
        uint256 lockedAt;    // 锁定时间戳
        bool released;       // 是否已释放
    }

    mapping(uint256 => LockRecord) public lockRecords;
    uint256 public nextLockId = 1;

    // 事件:锁定与释放(供跨链监听)
    event Locked(uint256 indexed lockId, address indexed user, uint256 amount, uint256 lockedAt);
    event Released(uint256 indexed lockId, address indexed user, uint256 amount, uint256 releasedAt);

    constructor(address _) Ownable(msg.sender) {
        require(_ != address(0), "invalid  address");
         = IERC20(_);
    }

    /**
     * @notice 用户锁定代币到合约
     * @param _amount 锁定数量
     * @return lockId 生成的锁定记录ID
     */
    function lock(uint256 _amount) external returns (uint256 lockId) {
        require(_amount > 0, "amount must be > 0");
        require(.transferFrom(msg.sender, address(this), _amount), "transferFrom failed");

        lockId = nextLockId++;
        lockRecords[lockId] = LockRecord({
            user: msg.sender,
            amount: _amount,
            lockedAt: block.timestamp,
            released: false
        });

        emit Locked(lockId, msg.sender, _amount, block.timestamp);
        return lockId;
    }

    /**
     * @notice 管理员释放指定锁定记录,将代币退回原用户
     * @param _lockId 锁定记录ID
     */
    function release(uint256 _lockId) external onlyOwner {
        LockRecord storage record = lockRecords[_lockId];
        require(record.amount > 0, "lock record not found");
        require(!record.released, "already released");

        record.released = true;
        require(.transfer(record.user, record.amount), "transfer failed");

        emit Released(_lockId, record.user, record.amount, block.timestamp);
    }

    /**
     * @notice 查询锁定记录详情
     */
    function getLockRecord(uint256 _lockId) external view returns (address user, uint256 amount, uint256 lockedAt, bool released) {
        LockRecord storage r = lockRecords[_lockId];
        return (r.user, r.amount, r.lockedAt, r.released);
    }
}

关键设计说明

  • 锁定流程:用户需先对合约执行 ERC20.approve,再调用 lock(amount),合约通过 transferFrom 将代币划转至自身地址,并生成自增 lockId 记录。
  • 释放流程:仅合约部署者(owner)可调用 release(lockId),将锁定代币退回原锁定用户,并标记该记录为已释放,防止重复释放。
  • 事件监听:Locked 与 Released 事件携带 lockId、用户地址、金额与时间戳,可作为跨链中继器或子链监听的数据源。

落地与验证

1. 依赖安装

npm init -y
npm install --save-dev hardhat @openzeppelin/contracts

2. 部署脚本(scripts/deploy.js)

const hre = require("hardhat");

async function main() {
  // 使用一个测试ERC20地址(示例:本地部署的MockToken)
  const mockToken = await hre.ethers.deployContract("MockERC20");
  await mockToken.waitForDeployment();

  const vault = await hre.ethers.deployContract("CrossChainAssetVault", [mockToken.target]);
  await vault.waitForDeployment();

  console.log("Vault deployed to:", vault.target);
  console.log("MockToken deployed to:", mockToken.target);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

3. 测试用例(test/vault.test.js)

const { expect } = require("chai");

describe("CrossChainAssetVault", function () {
  let vault, mockToken, owner, user;

  beforeEach(async function () {
    [owner, user] = await ethers.getSigners();

    const MockERC20 = await ethers.getContractFactory("MockERC20");
    mockToken = await MockERC20.deploy();
    await mockToken.waitForDeployment();

    const Vault = await ethers.getContractFactory("CrossChainAssetVault");
    vault = await Vault.deploy(mockToken.target);
    await vault.waitForDeployment();

    // 给user铸造1000个代币并授权给vault
    await mockToken.mint(user.address, 1000);
    await mockToken.connect(user).approve(vault.target, 1000);
  });

  it("应锁定资产并发出Locked事件", async function () {
    await expect(vault.connect(user).lock(500))
      .to.emit(vault, "Locked")
      .withArgs(1, user.address, 500, anyValue);
    expect(await mockToken.balanceOf(vault.target)).to.equal(500);
  });

  it("仅owner可释放资产", async function () {
    await vault.connect(user).lock(300);
    await expect(vault.connect(user).release(1)).to.be.revertedWith("Ownable: caller is not the owner");
    await expect(vault.release(1))
      .to.emit(vault, "Released")
      .withArgs(1, user.address, 300, anyValue);
    expect(await mockToken.balanceOf(user.address)).to.equal(1000);
  });

  it("不可重复释放", async function () {
    await vault.connect(user).lock(200);
    await vault.release(1);
    await expect(vault.release(1)).to.be.revertedWith("already released");
  });
});

4. 运行验证

npx hardhat test

判定标准:三个测试全部通过,且事件参数(lockId、user、amount)与锁定/释放逻辑一致,即视为本案例实现正确。

跨链资产锁定与释放 — 合约调用时序 用户 (EOA) CrossChainAssetVault ERC20 1. approve(vault, amount) 2. lock(amount) 3. transferFrom(user, vault, amount) 4. 返回 lockId, 发出 Locked 事件 5. release(lockId) [owner] 6. transfer(user, amount) 事件: Locked(lockId, user, amount, ts) Released(lockId, user, amount, ts)