script (⏱,💰)

script (⏱,💰)

NG#7 - calldata

The content of this task is about the calldata of the StarkNet contract. Calldata refers to the data passed to a function during a function call. In StarkNet, the data passed can be of various types, but it will ultimately be converted into a snapshot consisting of multiple felt252s. For more details, see the call_contract_syscall in the StarkNet contract.

There are two contracts, the "PortalSpell" contract with a "cast" function that accepts an Array<PortalData>, and the "DrunkenMage" contract with a "cast" function that takes two Array<felt252>. You need to pass in calldata consisting of two arrays, and the first two PortalData in the Array<PortalData> should be the same (as verified in the contract code).

struct PortalData {
  location: felt252,
  details: Array<felt252>
}

// Expected function signature
cast(portal_data: Array<PortalData>)

// Mage's function signature
cast(origin: Array<felt252>, destination: Array<felt252>)

The above "cast" will be called across contracts through a dispatcher, so there is no need to verify if the parameters are the same.

Approach#

First, you need to understand how the StarkNet contract parses calldata, which is explained clearly in the task's walkthrough.

The goal is to encode an Array<PortalData>, with the value being [ 0: { location: 'TAVERN', details: [ 'OPEN', 'PORTAL' ] }, 1: { location: 'HOME', details: [ 'CLOSE', 'PORTAL' ] } ].

Since Serde is implemented, the struct can be serialized. When printed in the test, a single struct is encoded as follows:

[DEBUG] TAVERN          (raw: 0x54415645524e
[DEBUG]                 (raw: 0x2
[DEBUG] OPEN            (raw: 0x4f50454e
[DEBUG] PORTAL          (raw: 0x504f5254414c

When the two PortalData are combined and a 2 is added at the beginning, it becomes the final encoding.

The drunk_spell.cast(origin, destination) is passed separately, and we must find the values for origin and destination so that when it is interpreted as Array<PortalData>, it reflects the desired calldata.

Only the first two Portals need to be verified, which are ['TAVERN', 2, 'OPEN', 'PORTAL', 'HOME', 2, 'CLOSE', 'PORTAL'].

If an additional PortalData {location: 'VOID', details: ['ANY']} is added, the encoding becomes [3, 'TAVERN', 2, 'OPEN', 'PORTAL', 'HOME', 2, 'CLOSE', 'PORTAL','VOID', 1 , 'ANY'], which can also be verified.

However, if it is split into two arrays and passed separately, it becomes [3, 'TAVERN', 2, 'OPEN', 'PORTAL'] and ['PORTAL', 'HOME', 2, 'CLOSE', 'PORTAL','VOID', 1 , 'ANY']. The conversion of 'PORTAL' to a felt decimal number will be very large, and adding more details to meet the length requirement or adding more portals will consume a lot of gas, resulting in an error when running the test.

If the number of Portals is increased until the length of the second parameter array is a very small number, it will not consume much gas. Now you should know how to write it.

After passing the test, I am ready to deploy the contract for hacking. It seems a bit complicated to deploy the hack contract using starkli invoke, as it does not support passing arrays. snforge invoke --calldata can pass calldata, but I don't know how to pass additional parameters such as the account. Finally, I found that sending the two calldata directly through the Voyager browser is the simplest method.

Conclusion#

The calldata composition rule of StarkNet is very simple, and the challenging part of this question is adding more Portals to form a specific calldata. It is an interesting CTF question.

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