关于合约账户有3个子任务,共同组成了 bad account 系列。本文是系列的第一个任务 Stealing Souls。
下载任务后有2个账户合约,分别是 tombkeeper_1.cairo 和 tombkeeper_2.cairo,部署时会自动 mint 100 个 $SOUL到合约里,每次执行交易会扣 0.1 个$SOUL,任务要求把 $SOUL 全部偷走。
在 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做了bug修复:
fn __exeute__
中添加了assert(get_caller_address().is_zero(), 'INVALID_CALLER');
,没法通过其他合约钱包去直接调用函数了。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 实例。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交易。