引言
在数字创作生态中,知名生成艺术家 Jack Butcher(Checks VV 创作者)带来 Credits 这场极具颠覆性的社会学艺术实验,口号为 “What belief looks like”。这场实验,给执着于代码自动执行逻辑的创作者圈子带来全新思考。 本文深度拆解 Credits 背后的实验设计思路,解析其第一阶段不依赖可编程代码的设计思路,并基于 Solidity 0.8.28 编译器与 OpenZeppelin V5 标准库,还原第二阶段作品重组逻辑,搭配 Viem 自动化测试完成代码验证。
一、核心母题:打破边界的 “Web 2.5” 跨界艺术实验
传统数字作品分发,高度依赖预设好的可编程交互,用户需要数字账户,通过签名交互完成作品生成。Jack Butcher 的 Credits 却采用完全相反的思路:
- 无代码参与的法币参与阶段(Phase 1) 开放参与窗口期间,参与者不需要加密账户,仅通过 X 平台支付通道,直接支付 8 美元,并在转账备注填写个人数字账户地址。整个过程运行在 Web2 体系,没有任何自动化代码参与。
- 支付流水号作为艺术生成种子 Jack 团队后端脚本捕获支付成功产生的唯一交易流水 ID。这串唯一标识作为随机种子输入生成艺术代码,决定 8×8 CMYK 四色像素网格作品的图层、样式与特征。渲染完成后,由脚本批量将作品分发至用户的数字账户。
- 用 “人的信任” 反思 “自动化代码” 这场实验最核心的社会学创新:参与者需要完全信任创作者团队。用户支付 8 美元,依靠的不是代码自动执行,而是对创作者本身的认可,这也是 Credits 名称的含义。
- 80 合 1 的作品重组机制(Phase 2) 第一阶段一共生成 122154 份基础 Credits 作品。分发完成后,第二阶段 Statement 重组通道开启。用户可以主动提交自己账户内 80 份基础作品,通过可编程模块原子化重组,生成 1 份限量总数 1526 份的高阶 Statement 作品。这套重组机制,构成整个实验最核心的博弈设计。
二、现代化重组代码设计(Solidity 0.8.28 + OZ V5)
进入第二阶段 80 合 1 重组流程,可编程模块介入,保障操作原子性。为降低网络循环计算开销,Solidity 0.8.28 + OpenZeppelin V5 是当前优选方案:
- 自定义错误(Custom Errors) 使用
error CustomError()搭配 revert,Solidity 0.8.28 原生支持 PUSH0 操作码优化,相比传统 require 字符串报错,显著节省计算资源。 - 规避零地址限制的安全归集方案 OpenZeppelin V5 的 ERC721 底层
_update函数,禁止将作品转移至零地址,防止数据丢失。因此采用更安全方案:将 80 份基础作品通过transferFrom转入模块自身内部永久归集。模块没有任何转出接口,在逻辑上等同于永久保存。
📄 代码文件说明 第二阶段核心代码StatementNFT基于 Solidity 0.8.28、OpenZeppelin V5 开发,实现 80 份基础作品归集、原子生成 1 份高阶 Statement 作品。配套MockCreditsNFT用来模拟第一阶段作品分发。 为保持文章简洁,完整源码、MockCreditsNFT,以及基于 Hardhat Viem 的集成测试脚本(初始化校验、批量归集、边界参数拦截、权限保护等用例),可以参考源码仓库查看完整实现。
2.1. 智能合约
- 2.1.1.MockCreditsNFT.sol(测试模拟作品合约)
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract MockCreditsNFT is ERC721 {
constructor() ERC721("Mock Credits", "CREDITS") {}
// 模拟第一阶段批量分发作品到用户地址
function mockAirdrop(address to, uint256 startId, uint256 count) external {
for (uint256 i = 0; i < count; i++) {
_safeMint(to, startId + i);
}
}
}
- 2.1.2 StatementNFT.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
interface ICreditsNFT {
function ownerOf(uint256 tokenId) external view returns (address);
function transferFrom(address from, address to, uint256 tokenId) external;
}
contract StatementNFT is ERC721, Ownable, ReentrancyGuard {
error InvalidBatchSize();
error NotNFTOwner();
error MaxStatementsReached();
ICreditsNFT public immutable creditsContract;
uint256 public constant BURN_REQUIRED = 80;
uint256 public constant MAX_STATEMENTS = 1526;
uint256 public currentStatementSupply;
event StatementAssembled(address indexed creator, uint256 indexed statementId, uint256[] burnedTokenIds);
constructor(address _creditsAddress) ERC721("Credits Statement", "STATEMENT") Ownable(msg.sender) {
creditsContract = ICreditsNFT(_creditsAddress);
}
function assembleStatement(uint256[] calldata tokenIdsToBurn) external nonReentrant {
if (tokenIdsToBurn.length != BURN_REQUIRED) revert InvalidBatchSize();
if (currentStatementSupply >= MAX_STATEMENTS) revert MaxStatementsReached();
for (uint256 i = 0; i < BURN_REQUIRED; i++) {
uint256 tokenId = tokenIdsToBurn[i];
if (creditsContract.ownerOf(tokenId) != msg.sender) revert NotNFTOwner();
// 将基础作品转入合约地址永久锁定,绕过OZ零地址转移限制
creditsContract.transferFrom(msg.sender, address(this), tokenId);
}
uint256 newStatementId = currentStatementSupply;
currentStatementSupply++;
_safeMint(msg.sender, newStatementId);
emit StatementAssembled(msg.sender, newStatementId, tokenIdsToBurn);
}
}
2.2. 完备测试脚本
-
测试用例:Jack Butcher Credits Phase 2 作品重组集成测试
- 初始化验证:重组模块正确绑定基础作品数据源地址
- 核心实验逻辑:用户授权模块,提交 80 份基础作品归集,原子生成 1 份 Statement 作品
- 边界校验:传入作品数量不等于 80,操作直接拦截
- 权限安全校验:尝试使用他人名下作品发起重组,模块拒绝执行
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { getAddress } from "viem";
import { network } from "hardhat";
describe("Jack Butcher Credits Phase 2 Synthesis Integration", function () {
async function deployFixture() {
const { viem } = await (network as any).connect();
const [owner, collector1, attacker] = await viem.getWalletClients();
const publicClient = await viem.getPublicClient();
// 1. 部署模拟基础作品合约
const mockCredits = await viem.deployContract("MockCreditsNFT");
// 2. 部署Statement重组合约
const statementNFT = await viem.deployContract("StatementNFT", [mockCredits.address]);
return {
mockCredits,
statementNFT,
owner,
collector1,
attacker,
publicClient,
};
}
it("初始化验证:Statement合约应正确绑定基础作品源地址", async function () {
const { mockCredits, statementNFT } = await deployFixture();
const boundCreditsAddress = await statementNFT.read.creditsContract();
const burnRequired = await statementNFT.read.BURN_REQUIRED();
const maxStatements = await statementNFT.read.MAX_STATEMENTS();
assert.equal(getAddress(boundCreditsAddress), getAddress(mockCredits.address), "绑定的基础作品地址不匹配");
assert.equal(burnRequired, 80n, "单次重组需要提交80份作品");
assert.equal(maxStatements, 1526n, "高阶作品总量上限1526份");
});
it("核心实验机制:用户授权合约,提交80份基础作品封存,原子生成Statement作品", async function () {
const { mockCredits, statementNFT, collector1 } = await deployFixture();
// 1. 模拟分发80份基础Credits作品 (TokenID: 0 到 79)
await mockCredits.write.mockAirdrop([collector1.account.address, 0n, 80n]);
assert.equal(getAddress(await mockCredits.read.ownerOf([0n])), getAddress(collector1.account.address));
assert.equal(getAddress(await mockCredits.read.ownerOf([79n])), getAddress(collector1.account.address));
// 2. 授权重组合约访问作品
await mockCredits.write.setApprovalForAll([statementNFT.address, true], {
account: collector1.account,
});
const tokenIdsToBurn = Array.from({ length: 80 }, (_, i) => BigInt(i));
// 3. 调用重组接口
const txHash = await statementNFT.write.assembleStatement([tokenIdsToBurn], {
account: collector1.account,
});
assert.ok(txHash, "重组交易正常执行");
// 4. 验证基础作品已经锁定在合约地址内
const lockedNftOwner = await mockCredits.read.ownerOf([0n]);
assert.equal(getAddress(lockedNftOwner), getAddress(statementNFT.address), "基础作品未成功锁定");
// 5. 验证Statement作品成功铸造给用户
const statementOwner = await statementNFT.read.ownerOf([0n]);
assert.equal(getAddress(statementOwner), getAddress(collector1.account.address), "用户未收到高阶作品");
const currentSupply = await statementNFT.read.currentStatementSupply();
assert.equal(currentSupply, 1n, "高阶作品计数器正常递增");
});
it("数量边界拦截:作品数量不足或超过80,合约拒绝交易", async function () {
const { mockCredits, statementNFT, collector1 } = await deployFixture();
await mockCredits.write.mockAirdrop([collector1.account.address, 0n, 100n]);
await mockCredits.write.setApprovalForAll([statementNFT.address, true], {
account: collector1.account,
});
const invalidShortList = Array.from({ length: 79 }, (_, i) => BigInt(i));
await assert.rejects(
async () => {
await statementNFT.write.assembleStatement([invalidShortList], {
account: collector1.account,
});
},
/InvalidBatchSize/,
"作品数量不等于80,合约抛出自定义错误拦截"
);
});
it("越权安全保护:尝试使用他人作品重组,合约拦截", async function () {
const { mockCredits, statementNFT, collector1, attacker } = await deployFixture();
await mockCredits.write.mockAirdrop([collector1.account.address, 0n, 1n]);
await mockCredits.write.mockAirdrop([attacker.account.address, 1n, 79n]);
const maliciousList = Array.from({ length: 80 }, (_, i) => BigInt(i));
await mockCredits.write.setApprovalForAll([statementNFT.address, true], {
account: attacker.account,
});
await assert.rejects(
async () => {
await statementNFT.write.assembleStatement([maliciousList], {
account: attacker.account,
});
},
/NotNFTOwner/,
"无法使用不属于自己的作品执行重组"
);
assert.equal(getAddress(await mockCredits.read.ownerOf([0n])), getAddress(collector1.account.address), "非法操作回滚,原有作品不受影响");
});
});
三、独一无二的生命周期:Credits 从生成到重组完整流水线
Credits 被称为一场反向数字艺术实验,因为它的全流程,都在挑战行业固有认知。
1. 传统数字作品分发 vs Credits 实验
| 阶段 / 属性 | 传统数字作品分发 | Jack Butcher 的 Credits 艺术实验 |
|---|---|---|
| 参与入口 | 网页前端,需要连接数字账户。 | 无代码前端,在 X 平台支付入口参与。 |
| 支付媒介 | 平台原生代币。 | 法币(8 美元) ,信用卡支付。 |
| 随机数种子 | 链上预言机或区块哈希。 | 支付交易流水 ID。 |
| 作品交付 | 用户调用代码,即时生成。 | 后端离线处理,团队批量分发作品。 |
| 后续机制 | 总量固定,依靠创作者持续运营。 | 80 合 1 重组机制,基础作品被归集,生成限量高阶作品。 |
2. 全景流水线架构
【 第一阶段:法币参与 + 作品分发 】
[ 用户 ] --(支付 8 美元 + 填写数字账户地址)--> [ X 平台支付系统 ]
│
(生成唯一支付流水ID)
▼
[ 作品分发至用户数字账户 ] <--(脚本批量生成分发)-- [ Jack团队后端生成艺术脚本 (CMYK) ]
【 第二阶段:作品重组 】
[ 用户持有基础作品 ]
│
(用户集齐80份基础作品发起重组)
▼
[ StatementAssembler 重组模块(Solidity 0.8.28) ]
│
┌───────────────┴───────────────┐
▼ ▼
[ 80 份基础Credits作品 ] [ 1 份高阶Statement作品 ]
(永久归集在模块内) (原子生成,总量上限1526份)
3. 核心节点深度解析
❖ 节点一:支付流水号作为艺术生成源
Credits 里,支付行为本身就是艺术创作的一部分。当完成 8 美元支付,系统生成一串唯一流水号。后端将流水号做 SHA-256 哈希,映射到 8×8 网格的 64 个坐标与 CMYK 色彩浓度。你得到的不是提前做好的图片,而是由这笔支付行为生成的独一无二数字图像。
❖ 节点二:离线异步分发,反思即时代码交互
很多开发者习惯追求代码自动执行,Credits 反其道而行,采用离线异步交付。用户完成支付后,等待团队对账、脚本渲染,再统一分发作品。这种人为制造的延迟,放大了艺术实验的叙事感。
❖ 节点三:作品重组带来的实验博弈
基础作品全部分发完成后,80 合 1 重组通道开启。用户主动提交自己的基础作品归集,换取限量高阶作品。这套设计是整个艺术实验的核心博弈机制。
结语
Jack Butcher 的 Credits 证明:优秀的数字创作创新,不一定依赖复杂自动化代码,更多在于打破 Web2、创作者信用、可编程代码之间的边界。 从第一阶段纯信任交付,到第二阶段 Solidity 0.8.28 代码实现原子化作品重组。Credits 既是创作者分发模式的先锋实验,也是程序员学习代码安全、机制设计的优质工程案例。