About the contract account, there are 3 subtasks that make up the bad account series. This article is the first task of the series, Stealing Souls.
After downloading the task, there are 2 contract accounts, tombkeeper_1.cairo and tombkeeper_2.cairo. When deployed, 100 $SOUL will be automatically minted to the contract. Each transaction will deduct 0.1 $SOUL, and the task requires stealing all $SOUL.
Contract 1#
In the validate_calls function, there is a comment stating that it will verify that the interacting contract address is not blacklisted (i.e., the Soul ERC20 contract address), meaning that direct Transfer Token transactions cannot be sent.
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)
}
However, the __execute__
function of this contract lacks validation for the caller, allowing it to bypass __validate__
and directly call __execute__
.
In the snforge test, construct a call to transfer soul tokens without fees. After passing the local test, serialize the test calls and send them to __execute__
using sncast or a browser to complete the task.
Contract 2#
Contract 2 has a bug fix:
fn __execute__
addsassert(get_caller_address().is_zero(), 'INVALID_CALLER');
, preventing direct function calls from other contract wallets.- If the recipient is the $SOUL contract, an extremely large value of IMPOSSIBLE_SOUL_FEE is required as gas. Since the type is u256, overflow is not possible.
if call.to == blacklisted {
// Trying to steal some soul? Nice try...
total_fee + IMPOSSIBLE_SOUL_FEE
} else {
total_fee + SOUL_FEE
}
First, I tried passing 1000 calldata calls to __validate__
, intending to increase the total_fee to 100 and then create a random call to execute and transfer the tokens.
After modifying the total_fee, it was found that the execute function initially set that it cannot be called by a contract, as it is to prevent attacks from other contracts. This blocked the method of directly calling using starkli and a browser.
After consulting with the mentor, it was suggested to sign with a local private key and use Tombkeeper as an account to call other contracts to complete the task. This requires the use of the SDK.
I redeployed the sand_devils contract from the Introduction to CTF and set the count to 1000, allowing any number to be subtracted from 1000 each time.
The SDK has versions in different languages, and I used starknet.js. The important steps are:
- Use
getClassAt
to get the ABI and create an instance of the sand_devil contract. - Create an Account instance using the Tombkeeper2 account address and the private key used to deploy the contract.
- Use
await devilContract.invoke("slay", [1],{ parseRequest: false}
to send the transaction.
It was found that only 0.1 SOUL was deducted as a fee, and the previous 1000 calls to __validate__
did not increase the fee to 100 SOUL. It seems that there is a check at the lower level that if __execute__
is not called, a separate __validate__
will not cause a change in state.
So I tried to directly send a transaction with 1000 calls using starknet.js, which should complete both __validate__
and __execute__
at the same time. The account contract supports transaction bundling operations.
By sending a bundled transaction in starknet.js using contract.populate
to convert the address, variables, and parameters into the built-in Call type, and using account.execute
to send Call[]
, even with 1000 transactions, it can be confirmed quickly.
By checking the hash in the browser, it was successful in embedding 1000 slay calls and one transfer in a single transaction, depleting all the SOUL in the Tombkeeper account.
Summary#
Account abstraction is an important part of StarkNet, and the two tasks operate on custom contracts from both the contract side and the SDK side, allowing users to deepen their understanding of account abstraction. Additionally, there is an introduction to using the SDK, learning how to connect to contracts and send bundled transactions.