NG#7 - calldata

本任务内容是关于 StarkNet 合约的 calldata。calldata 是指函数调用时传递给函数的数据，在 StarkNet 中，传入的数据可能是各种类型，但最终都会转换为多个 felt252 组成的 snapshot，具体见call_contract_syscall。

有 2 个合约，PortalSpell 合约中的 cast 接受的是 1 个Array<PortalData>，DrunkenMage 合约中的 cast 是传入 2 个Array<felt252>。需要你传入 2 个 Array 构成的 calldata，和Array<PortalData>前 2 个 PortalData 是相同的（验证见合约代码）。

Copy
struct PortalData {
location: felt252,
details: Array<felt252>
}

// Expected function signature
cast(portal_data: Array<PortalData>)

// Mage's function signature
cast(origin: Array<felt252>, destination: Array<felt252>)

上面的 cast 会通过 dispatcher 跨合约调用，无需验证参数是否一样。

思路

首先，先需要了解 StarkNet 合约是怎么解析 calldata 的，任务的 walkthough 里说得很清楚了。

编码的目标是 Array<PortalData>，值是
[ 0: { location: 'TAVERN', details: [ 'OPEN', 'PORTAL' ] }, 1: { location: 'HOME', details: [ 'CLOSE', 'PORTAL' ] } ]

由于实现了 Serde，可以对 struct 进行 serialize，在测试中打印后，单个 struct 是这样编码的

Copy
[DEBUG] TAVERN (raw: 0x54415645524e
[DEBUG] (raw: 0x2
[DEBUG] OPEN (raw: 0x4f50454e
[DEBUG] PORTAL (raw: 0x504f5254414c

2 个 PortalData 加在一起，前面再加个 2，就是最终编码。

drunk_spell.cast(origin, destination) 传入是分开的，我们必须找到 origin 和 destination 的值，以便当它被解释为 Array<PortalData> 时，它反映了所需的 calldata。

需要验证的只是前 2 个 Portal，是 ['TAVERN', 2, 'OPEN', 'PORTAL', 'HOME', 2, 'CLOSE', 'PORTAL']就行。

如果再加一个PortalData {location: 'VOID', details: ['ANY']}，编码会变成[3, 'TAVERN', 2, 'OPEN', 'PORTAL', 'HOME', 2, 'CLOSE', 'PORTAL','VOID', 1 , 'ANY']，也是可以通过验证的。

但如果要拆分成 2 个 array 分别传入，就是 [3, 'TAVERN', 2, 'OPEN', 'PORTAL'] 和 ['PORTAL', 'HOME', 2, 'CLOSE', 'PORTAL','VOID', 1 , 'ANY']，'PORTAL' 转为 felt 的十进制数字会非常大，想要加长 details 满足长度要求，或添加更多的 portal，会消耗很多 gas，在测试时运行会报错。

那如果继续增加 Portal 的个数，直到第二个参数 array 的长度是个非常小的数字的话，就不用消耗很多 Gas 了。接下来你应该知道怎么写了。

我通过了测试后，准备部署合约去破解。先用 starkli invoke 发现不能传 array，snforge invoke --calldata可以传 calldata，但又不知道怎么传入账户等额外参数。如果部署 hack 合约有点麻烦了。最终发现直接走 voyager 浏览器发送两个 calldata 是最简单的。

总结

StarkNet 的 calldata 组成规则很简单，这题烧脑的地方是需要添加更多 Portal 去组成特定的 calldata，蛮有意思的 CTF 题目。