关于合约账户有 3 个子任务,共同组成了 bad account 系列。本文是系列的第一个任务 Stealing Souls。
下载任务后有 2 个账户合约,分别是 tombkeeper_1.cairo 和 tombkeeper_2.cairo,部署时会自动 mint 100 个 $SOUL 到合约里,每次执行交易会扣 0.1 个 $SOUL,任务要求把 $SOUL 全部偷走。
合约 1#
在 validate_calls 中有注释,会验证交互的合约地址不是 blacklisted(=Soul ERC20 合约地址),也就是不能直接发送 Transfer Token 的交易。
fn validate_calls(mut calls: Array<Call>, blacklisted: ContractAddress) {
match calls.pop_front() {
Option::Some(call) => {
// Trying to steal some soul? Nice try...
assert(call.to != blacklisted, 'CANNOT_CALL_SOUL_TOKEN');
},
Option::None(_) => { return (); }
}
validate_calls(calls, blacklisted)
}
但这个合约的__execute__
少了对 caller 的验证,可以直接跳过__validate__
,去调用__execute__
。
在 snforge 的测试中,构造一笔 call 转移除去手续费的 soul token,本地测试通过后,把测试的 calls 进行 serialized ,用 sncast 或者浏览器发送 calls: Array<felt252>
给 __execute__
完成任务。
合约 2#
合约 2 做了 bug 修复:
fn __exeute__
中添加了assert(get_caller_address().is_zero(), 'INVALID_CALLER');
,没法通过其他合约钱包去直接调用函数了。- 如果 to 是 $SOUL 的合约,需要一个非常大数值的 IMPOSSIBLE_SOUL_FEE 作为 gas。类型是 u256 也没法 overflow。
if call.to == blacklisted {
// Trying to steal some soul? Nice try...
total_fee + IMPOSSIBLE_SOUL_FEE
} else {
total_fee + SOUL_FEE
}
首先我尝试了方法传一个 1000 个 call 的 calldata 给 __validate__
,打算把 total_fee 刷到 100 后,再随便建一个 call 调用一次 execute 把 token 转走。
修改完 total_fee 后,发现 execute 里一开始就设置了不能由合约调用,原因是防止被其他合约攻击。这就堵死了用 starkli 和浏览器直接调用的方法。
在询问导师后,提示可以在本地私钥签名,把 Tombkeeper 当作 accout 去调用其他合约来完成任务,这需要用到 SDK。
我重新部署了一个入门 CTF中的 sand_devils 合约,并把 count 设置成了 1000,每次可以去从 1000 里面减任意数字。
SDK 有不同语言的版本,我使用的是 starknet.js,重要步骤:
- 通过
getClassAt
拿到 abi,创建 sand_devil 的 Contract 实例。 - 使用 Tombkeeper2 账户地址和部署该合约的私钥创建 Account 实例。
- 用
await devilContract.invoke("slay", [1],{ parseRequest: false}
发送交易。
结果发现手续费只扣了 0.1 SOUL,之前的 1000 个 call 的__validate__
没有把手续费增加到 100 SOUL。看样子底层有判断如果没有调用 __execute__
,单独的 __validate__
是不会引起状态改变的。
那尝试直接在 starknetjs 发送一笔 1000 个 calls 的交易,应该会同时完成 __validate__
和 __execute__
,账户合约是支持交易打包操作的。
想要发送打包交易,在 starknet.js 中 contract.populate
可以把地址、变量和参数转换成内置的 Call 类型, account.execute
支持发送 Call[]
。尽管有 1000 笔交易,也能很快确认。
浏览器查看 hash,成功在一笔交易中内置了 1000 笔 slay 和一笔 transfer,把 TompAccout 中的 SOUL 全部耗完了。
总结#
账户抽象是 StarkNet 中很重要的组成部分,两个任务分别从合约端和 SDK 端去操作自定义合约,让用户加深了抽象账户的理解。并且初步接触了 SDK 的使用,了解该如何使用 SDK 连接合约发送 bundle 交易。