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, namely 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 (Soul ERC20 contract address), which means 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 of the caller, so it can skip __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');
, which prevents calling the function directly from other contract wallets.- If the recipient is the $SOUL contract, an extremely large value of IMPOSSIBLE_SOUL_FEE is required as gas. The type is u256 and cannot overflow.
if call.to == blacklisted {
// Trying to steal some soul? Nice try...
total_fee + IMPOSSIBLE_SOUL_FEE
} else {
total_fee + SOUL_FEE
}
First, I tried to pass 1000 calldata calls to __validate__
, intending to increase the total_fee to 100 and then create a random call to execute and transfer the token.
After modifying the total_fee, I found that execute initially set that it cannot be called by a contract, to prevent attacks from other contracts. This blocked the method of directly calling through starkli and the 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:
- Get the ABI through
getClassAt
and create a Contract instance for sand_devil. - 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 the fee, and the previous 1000 calls to __validate__
did not increase the fee to 100 SOUL. It seems that there is a judgment at the bottom that if __execute__
is not called, the separate __validate__
will not cause a change in state.
Then try to directly send a transaction with 1000 calls in starknet.js, which should complete both __validate__
and __execute__
at the same time. The account contract supports transaction packaging operations.
To send a bundled transaction, in starknet.js, contract.populate
can convert the address, variables, and parameters into the built-in Call type, and account.execute
supports sending 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 one 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 the contract side and the SDK side, allowing users to deepen their understanding of account abstraction. They also have a preliminary understanding of using the SDK to connect to contracts and send bundled transactions.