script (⏱,💰)

script (⏱,💰)

NG#8 - Contract Account

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__ adds assert(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:

  1. Get the ABI through getClassAt and create a Contract instance for sand_devil.
  2. Create an Account instance using the Tombkeeper2 account address and the private key used to deploy the contract.
  3. 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#

ng8 end

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.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.