Skip to content

Commit 23cfd73

Browse files
Redchar1992claude
andcommitted
feat(pavilion): v2 — claim windows and regret; the boundary second belongs to the heir
- depositGift(tokenId, heir, claimBy): strictly-future deadlines, heirless gifts rejected (v1 trap: heir=address(0) collided with the empty-slot sentinel and stranded the card in custody forever) - gapless, overlap-free windows: heir claims while now <= claimBy (boundary inclusive), giver reclaims only after — no early yank-back, the deadline is a promise; expired gifts wait in custody with no reclaim deadline - Gift{giver,heir,claimBy} struct mapping replaces the parallel mappings - tests 111 → 120, coverage stays 100/100/100/100: boundary-second matrix via evm_setNextBlockTimestamp (claim at claimBy succeeds, reclaim at claimBy still blocked, +1s flips both), double-claim/reclaim, re-gift after reclaim, 365-days-later reclaim - READMEs / architecture / SECURITY / CONTRIBUTING synced Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d1f0fb4 commit 23cfd73

7 files changed

Lines changed: 264 additions & 109 deletions

File tree

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ help keep it that way.
77

88
```bash
99
npm install # node >= 20
10-
npm test # 111 specs, ~3s
10+
npm test # 120 specs, ~3s
1111
npm run coverage # must stay at 100% — CI enforces it
1212
```
1313

‎README.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Three Kingdoms general cards: four factions (WEI / SHU / WU / QUN), five raritie
1818
- **Deliberately spiky Solidity.** File-level types and free functions, a `global` using-for, aliased named imports, a same-file-two-paths import trap, a public-state-var overriding an interface function, `unchecked` blocks, an assembly guard, reverting `receive`/`fallback` — each one placed on purpose to stress compilers, flatteners, UML generators, linters and analyzers, and annotated with *why*. See [docs/architecture.md](docs/architecture.md).
1919
- **Signature-driven lazy minting (虎符 TigerTally).** A tally contract takes the suzerainty — the two-step handover exists precisely so a *contract* can hold the throne safely — and redeems EIP-712 `MintOrder`s: nested-struct hashing (the order wraps a full `Card`), bearer and bound-with-relayer voucher modes, voidable nonces, malleability-guarded `ecrecover`, and a marshal-gated passthrough for every suzerain power including the escape hatch back. The on-chain digest is differentially anchored to ethers' `TypedDataEncoder` in tests.
2020
- **Real value handled the boring way (市集 CardBazaar).** A fixed-price, TRX-settled stall market with escrowed listings and **pull-payment proceeds** — paying sellers inline would let any seller with a reverting `receive()` brick their own listing's purchase. `withdraw()` is checks-effects-interactions, proven by a hostile seller that attempts a reentrant drain mid-payout and finds an already-zeroed ledger. No owner, no fees, no sweep: the bazaar's admin column in the trust model reads *nobody*.
21-
- **111 tests, 100% coverage, gated in CI.** Statements, branches, functions and lines all at 100% (mocks excluded); the CI fails if any of it regresses. Includes differential tests of the on-chain Base64/decimal/JSON-escape/XML-escape helpers against reference implementations, XML well-formedness checks on every rendered SVG, a receiver-behavior matrix, and a seeded random transfer storm checked against a model.
21+
- **120 tests, 100% coverage, gated in CI.** Statements, branches, functions and lines all at 100% (mocks excluded); the CI fails if any of it regresses. Includes differential tests of the on-chain Base64/decimal/JSON-escape/XML-escape helpers against reference implementations, XML well-formedness checks on every rendered SVG, a receiver-behavior matrix, and a seeded random transfer storm checked against a model.
2222
- **A real dogfooding campaign, honestly kept.** The entire project — scaffold, edit, lint, compile, VM deploy, debug, record/replay, TronBox export, git push, TronLink mainnet-style deploys, flatten & verification — was executed **inside TronIDE**, driving 23 IDE features and filing 13 findings, of which 6 were fixed upstream with regression gates and **3 were retracted after strict re-verification** (the ledger counts our own misreads too). See [docs/case-study.md](docs/case-study.md).
2323

2424
> **Audit status:** engineered to audit-grade practice, **not yet externally audited**. Read [SECURITY.md](SECURITY.md) before depositing value you care about.
@@ -40,8 +40,8 @@ contracts/
4040
│ ├── libs/CardCodec.sol Card → data:application/json;base64 (JSON-escaped)
4141
│ ├── libs/Base64.sol assembly encoder (public-node CPU caps forced it — see docs)
4242
│ └── utils/StrUtils.sol toString / equal / escapeJson / escapeXml
43-
├── PeachPavilion.sol gift escrow: deposit a card for an heir to claim;
44-
│ rejects naked safeTransferFrom deliveries
43+
├── PeachPavilion.sol gift escrow with claim windows: the heir claims until
44+
│ the deadline (boundary inclusive), the giver reclaims after
4545
├── TigerTally.sol 虎符 — EIP-712 signed mint orders (lazy minting);
4646
│ holds the suzerainty while in service
4747
├── CardBazaar.sol 市集 — fixed-price stalls, TRX-settled, escrowed
@@ -65,7 +65,7 @@ Transactions, deployer, energy numbers, the honest incident notes and verificati
6565

6666
```bash
6767
npm install
68-
npm test # 111 specs, ~3s, no local TRON node needed
68+
npm test # 120 specs, ~3s, no local TRON node needed
6969
npm run coverage # istanbul report; CI enforces 100%
7070
npx hardhat compile # solc 0.8.20, evm target paris (no PUSH0 ahead of the TVM)
7171
```

‎README.zh-CN.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
- **故意刁钻的 Solidity**:文件级类型与自由函数、`global` using-for、别名命名导入、同文件双路径导入陷阱、public 状态变量覆写接口函数、`unchecked`、assembly 守卫、reverting `receive`/`fallback`——每一处都是为了压测编译器、拍平器、UML、linter 与分析器而放置,并注明了 *为什么*。详见 [docs/architecture.md](docs/architecture.md)。
1919
- **签名驱动的 lazy mint(虎符 TigerTally)**:虎符合约受让 suzerainty 出任"在任铸造官"——两步移交的存在意义正是让**合约**能安全持有王座——并兑现 EIP-712 `MintOrder`:嵌套结构体哈希(订单内嵌完整 `Card`)、不记名/定向 + 代付两种券模式、可作废 nonce、防延展 `ecrecover`、全部 suzerain 权能的元帅直通道与王座归还逃生门。链上 digest 在测试中与 ethers `TypedDataEncoder` 差分锚定。
2020
- **用最无聊的方式处理真金白银(市集 CardBazaar)**:固定价、TRX 结算的摊位市场,挂单入柜托管 + **pull-payment 提款**——若采用内联打款,任何 `receive()` 回滚的卖家都能卡死自己挂单的成交。`withdraw()` 走 checks-effects-interactions,由一个在收款瞬间尝试重入抽干的恶意卖家实证:它只看到已清零的账本。无 owner、无手续费、无 sweep:信任模型里市集的管理员一栏写着 *nobody*。
21-
- **111 个测试、100% 覆盖率、CI 闸门**:语句 / 分支 / 函数 / 行全部 100%(mocks 除外),回退即 CI 失败。含链上 Base64 / 十进制 / JSON 转义 / XML 转义对参考实现的差分测试、每张 SVG 的 XML 良构校验、safe transfer 受体行为矩阵、种子随机转账风暴对账。
21+
- **120 个测试、100% 覆盖率、CI 闸门**:语句 / 分支 / 函数 / 行全部 100%(mocks 除外),回退即 CI 失败。含链上 Base64 / 十进制 / JSON 转义 / XML 转义对参考实现的差分测试、每张 SVG 的 XML 良构校验、safe transfer 受体行为矩阵、种子随机转账风暴对账。
2222
- **真实的 dogfooding 战役、诚实的账本**:从脚手架、编辑、lint、编译,到 VM 部署、调试、录制回放、TronBox 导出、git 推送、TronLink 实链部署、拍平与验证包——全程在 TronIDE 内完成,走遍 23 项 IDE 功能,提交 13 条发现:6 条修复入库(带回归门禁)、**3 条经严格复验后诚实撤回**(自己的误判也记账)。详见 [docs/case-study.md](docs/case-study.md)。
2323

2424
> **审计状态**:按审计级实践工程化,**尚未经外部审计**。托管真实价值前请先读 [SECURITY.md](SECURITY.md)。
@@ -40,8 +40,8 @@ contracts/
4040
│ ├── libs/CardCodec.sol Card → data:application/json;base64(含 JSON 转义)
4141
│ ├── libs/Base64.sol assembly 编码器(公共节点 CPU 时限所迫——见文档)
4242
│ └── utils/StrUtils.sol toString / equal / escapeJson / escapeXml
43-
├── PeachPavilion.sol 桃园馆礼物托管:存卡指定继承人、继承人领取;
44-
│ 拒收绕过托管的裸 safeTransferFrom
43+
├── PeachPavilion.sol 桃园馆礼物托管(v2 期限版):继承人限期内领取
44+
│ (含边界秒),逾期后赠予人可悔赠
4545
├── TigerTally.sol 虎符 —— EIP-712 签名铸卡券(lazy mint);
4646
│ 在任期间持有 suzerainty
4747
├── CardBazaar.sol 市集 —— 固定价摊位、TRX 结算、托管挂单、
@@ -65,7 +65,7 @@ contracts/
6565

6666
```bash
6767
npm install
68-
npm test # 111 个用例,约 3 秒,无需本地 TRON 节点
68+
npm test # 120 个用例,约 3 秒,无需本地 TRON 节点
6969
npm run coverage # istanbul 报告;CI 强制 100%
7070
npx hardhat compile # solc 0.8.20,evm target paris(不让 PUSH0 跑在 TVM 前面)
7171
```

‎SECURITY.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
## Engineering posture
1919

20-
- 111 Hardhat specs; **100% statement / branch / function / line coverage**
20+
- 120 Hardhat specs; **100% statement / branch / function / line coverage**
2121
over every deployable contract (mocks excluded), enforced by a CI gate
2222
that fails below 100%.
2323
- Differential tests: the on-chain Base64 and decimal encoders are compared

‎contracts/PeachPavilion.sol‎

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,45 @@ import { ITRC721Receiver } from "./interfaces/ITRC721Receiver.sol";
88
// the import resolver and the flattener's dedup both get tested here.
99
import { Card } from "../contracts/types/CardTypes.sol";
1010

11-
/// @title 桃园馆 — gift escrow: deposit a card for a designated heir, the heir
12-
/// claims it. try/catch over an external interface call, reverting receive AND
13-
/// fallback, immutable interface reference, custom errors.
11+
/// @title 桃园馆 · PeachPavilion v2 — gift escrow with deadlines and regret.
12+
/// @notice Deposit a card for a designated heir with a claim window. The
13+
/// boundary rule is crisp and gapless: the heir may claim while
14+
/// `block.timestamp <= claimBy` (the boundary second is theirs); once
15+
/// `block.timestamp > claimBy` the giver — and only the giver — may reclaim.
16+
/// The giver cannot reclaim inside the window: a gift you can yank back at
17+
/// will is no gift, the deadline is a promise. try/catch over an external
18+
/// interface call, reverting receive AND fallback, immutable interface
19+
/// reference, custom errors.
20+
/// @dev v2 also closes a v1 trap: a gift to address(0) used to collide with
21+
/// the "nothing deposited" sentinel and strand the card in custody forever —
22+
/// heirless gifts are now rejected outright. Deadlines are header timestamps
23+
/// (~3s block cadence on TRON): do not cut windows finer than that.
1424
contract PeachPavilion is ITRC721Receiver {
1525
error NotCardHolder(address caller, uint256 tokenId);
1626
error NothingDeposited(uint256 tokenId);
1727
error NotDesignatedHeir(address caller, uint256 tokenId);
28+
error NotGiftGiver(address caller, uint256 tokenId);
29+
error HeirlessGift();
30+
error DeadlineInThePast(uint64 claimBy);
31+
error GiftExpired(uint256 tokenId, uint64 claimBy);
32+
error GiftStillClaimable(uint256 tokenId, uint64 claimBy);
1833
error PavilionTakesNoTribute();
1934
error CardContractRejected(uint256 tokenId, string reason);
2035
error GiftsOnlyViaDeposit();
2136

22-
event GiftDeposited(uint256 indexed tokenId, address indexed from, address indexed heir);
37+
event GiftDeposited(uint256 indexed tokenId, address indexed from, address indexed heir, uint64 claimBy);
2338
event GiftClaimed(uint256 indexed tokenId, address indexed heir);
39+
event GiftReclaimed(uint256 indexed tokenId, address indexed giver);
40+
41+
/// @notice One escrowed gift: who gave it, who may claim it, until when.
42+
struct Gift {
43+
address giver;
44+
address heir;
45+
uint64 claimBy;
46+
}
2447

2548
ITRC721 public immutable cards;
26-
mapping(uint256 => address) public heirOf;
27-
mapping(uint256 => address) public giverOf;
49+
mapping(uint256 => Gift) public giftOf;
2850

2951
constructor(ITRC721 cardContract) {
3052
cards = cardContract;
@@ -40,9 +62,12 @@ contract PeachPavilion is ITRC721Receiver {
4062
revert GiftsOnlyViaDeposit();
4163
}
4264

43-
/// @notice Escrow `tokenId` for `heir` to claim later. The caller must
44-
/// hold the card and have approved the pavilion beforehand.
45-
function depositGift(uint256 tokenId, address heir) external {
65+
/// @notice Escrow `tokenId` for `heir`, claimable until `claimBy`
66+
/// (inclusive). The caller must hold the card and have approved the
67+
/// pavilion beforehand; the deadline must be strictly in the future.
68+
function depositGift(uint256 tokenId, address heir, uint64 claimBy) external {
69+
if (heir == address(0)) revert HeirlessGift();
70+
if (claimBy <= block.timestamp) revert DeadlineInThePast(claimBy);
4671
address holder;
4772
try cards.ownerOf(tokenId) returns (address h) {
4873
holder = h;
@@ -54,19 +79,32 @@ contract PeachPavilion is ITRC721Receiver {
5479
}
5580
if (holder != msg.sender) revert NotCardHolder(msg.sender, tokenId);
5681
cards.transferFrom(msg.sender, address(this), tokenId);
57-
heirOf[tokenId] = heir;
58-
giverOf[tokenId] = msg.sender;
59-
emit GiftDeposited(tokenId, msg.sender, heir);
82+
giftOf[tokenId] = Gift(msg.sender, heir, claimBy);
83+
emit GiftDeposited(tokenId, msg.sender, heir, claimBy);
6084
}
6185

62-
/// @notice The designated heir collects the escrowed card.
86+
/// @notice The designated heir collects the gift — the boundary second
87+
/// still counts as inside the window.
6388
function claimGift(uint256 tokenId) external {
64-
address heir = heirOf[tokenId];
65-
if (heir == address(0)) revert NothingDeposited(tokenId);
66-
if (msg.sender != heir) revert NotDesignatedHeir(msg.sender, tokenId);
67-
delete heirOf[tokenId];
68-
delete giverOf[tokenId];
89+
Gift memory gift = giftOf[tokenId];
90+
if (gift.heir == address(0)) revert NothingDeposited(tokenId);
91+
if (msg.sender != gift.heir) revert NotDesignatedHeir(msg.sender, tokenId);
92+
if (block.timestamp > gift.claimBy) revert GiftExpired(tokenId, gift.claimBy);
93+
delete giftOf[tokenId];
6994
cards.transferFrom(address(this), msg.sender, tokenId);
7095
emit GiftClaimed(tokenId, msg.sender);
7196
}
97+
98+
/// @notice After the window closes unclaimed, the giver takes the card
99+
/// back. There is no reclaim deadline — an expired gift waits safely in
100+
/// custody for its giver, forever.
101+
function reclaimGift(uint256 tokenId) external {
102+
Gift memory gift = giftOf[tokenId];
103+
if (gift.heir == address(0)) revert NothingDeposited(tokenId);
104+
if (msg.sender != gift.giver) revert NotGiftGiver(msg.sender, tokenId);
105+
if (block.timestamp <= gift.claimBy) revert GiftStillClaimable(tokenId, gift.claimBy);
106+
delete giftOf[tokenId];
107+
cards.transferFrom(address(this), msg.sender, tokenId);
108+
emit GiftReclaimed(tokenId, gift.giver);
109+
}
72110
}

‎docs/architecture.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ stress fixture for Solidity toolchains (see [the spiky inventory](#the-spiky-con
4545
| `libs/CardCodec.sol` | `Card → data:application/json;base64` | escapes user strings; aliased import `Str as S` |
4646
| `libs/Base64.sol` | assembly Base64 encoder | was a byte loop until P10: the 4.07M-gas double-encoded tokenURI hit public nodes' constant-call CPU cap (`OutOfTimeException`); now 1.46M. Differential-tested vs Node across the swap |
4747
| `utils/StrUtils.sol` | `toString`, `equal`, `escapeJson` | escape: `"` `\` → backslashed, `< 0x20` → `\u00XX`, UTF-8 passthrough |
48-
| `PeachPavilion.sol` | escrow: deposit card for heir, heir claims | rejects naked `safeTransferFrom` deliveries |
48+
| `PeachPavilion.sol` | gift escrow with claim windows (v2): heir claims while `now <= claimBy`, giver reclaims after — gapless, overlap-free boundary | rejects naked deliveries; rejects heirless gifts (the v1 stuck-card trap) |
4949
| `TigerTally.sol` | 虎符 — EIP-712 signed mint orders (lazy minting); holds the suzerainty while in service | zero-dep nested-struct 712, low-s `ecrecover`, marshal passthroughs for the full suzerain surface |
5050
| `CardBazaar.sol` | 市集 — fixed-price stalls, TRX-settled, escrowed listings | pull-payment proceeds (CEI, reentrancy-tested), `call{value:}` over `.transfer`, zero governance surface |
5151
| `mocks/TestMocks.sol` | receiver mocks, lib harness, abstract-base shims | **never deploy** |

0 commit comments

Comments
 (0)