Simple summary
A proposal to allow multicalls where a transaction uses a previous transaction output (from the same multicall) as an input.
Here is the proof of concept repo: GitHub - Th0rgal/better-multicall: An experiment to improve multicall on StarkNet
Why is this important?
Multicall is a great tool on Starknet, it improves the user experience by not having to wait for one transaction to complete and helps reducing fees. For instance, this allows you to change your ETH allowance for a specific contract and call this contract function which spends this allowance in one go. You save two data writes and take advantage of sublinear computation fees. The thing is that for now, this is limited to functions that are not dependent on each other and whose parameters can be determined in advance. If you want to mint an NFT and then change its name, you have to do two transactions because you can’t know your nft id before minting it (it might not be the same if someone mints another nft at the same time).
Proposed solution
I propose to change the execute function so that it can take felts or references to felts as parameters. I used the execute
implementation from open zeppelin repo (cairo 0.10.0) as a basis.
I created a Felt struct, which takes two parameters : a type (it can be a default felt, or a reference), I also added a Type struct to be used as an enum to make the code cleaner. Instead of using a felt in your calldata, you could use Felt(Type.DEFAULT, your_value)
. To put a reference to the first felt from the returned output, you could use Felt(Type.REFERENCE, 0)
. Please note that you can only use a reference to an output given by a previous call. If you merge 2 calls that both return a felt, you can only use the first returned felt as an input for the second call, since you don’t know the rest of the output.
This is just a proposal and I am curious to hear your ideas to make it clearer.
Example
Here is an example contract interface where this improved multicall could be useful. Implementation is available here: better-multicall/contract.cairo at master · Th0rgal/better-multicall · GitHub.
This contract allows you to mint a NFT and returns its id to you. It also allows you to change a NFT name if you own it and know its id. You can read the NFT to find its owner and its name.
Example contract
struct NFT {
owner: felt,
name: felt,
}
@contract_interface
namespace ExampleContract {
func mint_nft() -> (nft_id: felt) {
}
func set_nft_name(id, name) {
}
func read_nft(id) -> (nft: NFT) {
}
}
Using the old fashion (two calls)
// 1st tx: minting the nft
let (callarray: AccountCallArray*) = alloc();
assert callarray[0] = AccountCallArray(example_contract_addr, mint_nft_selector, 0, 0);
let (result_len, result: felt*) = execute(1, callarray, 0, new ());
assert result_len = 1;
local minted_nft = result[0];
// 2nd tx: changing nft name
let (calldata: felt*) = alloc();
assert calldata[0] = minted_nft;
assert calldata[1] = 'aloha';
let (callarray: AccountCallArray*) = alloc();
assert callarray[0] = AccountCallArray(example_contract_addr, set_nft_name_selector, 0, 2);
let (result_len, result: felt*) = execute(1, callarray, 2, calldata);
Using the improved execute (a single transaction)
let (calldata: Felt*) = alloc();
assert calldata[0] = Felt(Type.REFERENCE, 0);
assert calldata[1] = Felt(Type.DEFAULT, 'aloha');
let (callarray: BetterAccountCallArray*) = alloc();
assert callarray[0] = BetterAccountCallArray(example_contract_addr, mint_nft_selector, 0, 0);
assert callarray[1] = BetterAccountCallArray(example_contract_addr, set_nft_name_selector, 0, 2);
let (result_len, result: felt*) = better_execute(2, callarray, 2, calldata);
Full test is available here (written in cairo 0.10.0 using protostar): actually I don’t have the right to post more than two links, it’s in tests folder on the github repo.
Please let me know what you think. I’m not sure if we should replace the account interface, or add a second dedicated execute, or maybe even do this as a plugin? My guess is that it would be nice to have it available on all accounts so that dapps can assume that it’s possible, but if it turns out that it adds a significant additional cost for transactions that don’t require it, maybe having two versions of execute would make sense.