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 cast
function in the PortalSpell contract accepts an Array<PortalData>
, and the cast
function in the DrunkenMage contract accepts two Array<felt252>
. You need to pass 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 the 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 walkthrough.
The goal is to encode an Array<PortalData>
value, which is [ 0: { location: 'TAVERN', details: [ 'OPEN', 'PORTAL' ] }, 1: { location: 'HOME', details: [ 'CLOSE', 'PORTAL' ] } ]
.
Since Serde is implemented, the struct can be serialized. In the test, when printed, a single struct is encoded as follows:
[DEBUG] TAVERN (raw: 0x54415645524e
[DEBUG] (raw: 0x2
[DEBUG] OPEN (raw: 0x4f50454e
[DEBUG] PORTAL (raw: 0x504f5254414c
When 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.
The only thing that needs to be verified is the first two Portals, which are ['TAVERN', 2, 'OPEN', 'PORTAL', 'HOME', 2, 'CLOSE', 'PORTAL']
.
If an additional PortalData {location: 'VOID', details: ['ANY']}
is added, the encoding will become [3, 'TAVERN', 2, 'OPEN', 'PORTAL', 'HOME', 2, 'CLOSE', 'PORTAL','VOID', 1 , 'ANY']
, which can also be verified.
However, if it is split into two separate arrays to be passed, it will be [3, 'TAVERN', 2, 'OPEN', 'PORTAL']
and ['PORTAL', 'HOME', 2, 'CLOSE', 'PORTAL','VOID', 1 , 'ANY']
, and converting 'PORTAL' to a felt decimal number will be very large. To satisfy the length requirement of details or add more portals, it will consume a lot of gas and will result 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 to crack it. First, using starkli invoke
, I found that arrays cannot be passed. 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 composition rule of StarkNet's calldata is very simple. The challenging part of this question is adding more Portals to form a specific calldata. It is an interesting CTF question.