1

A lot of contracts and libraries with functions that use arrays and structs for arguments. Structs can themselves include arrays, structs and mappings. For example:

function doSomething(MyStruct storage s) internal returns(bool success) { ...

function somethingElse(address[] storage a) internal returns(bool success) { ...

function getThing(bytes32 key) internal returns(Thing storage t) { ...

Q1: Is it safe to do that?

Elsewhere, I read dire warnings about running out of gas. I also read about the experimental ABI but some of the contract examples I see don't use it.

Q2: How is it possible to return a struct without using the Experimental ABI?

0

The fact that you can do different things with similar syntax can be confusing.

It's important to understand what's happening internally so you know what to use and when. The key here is the internal visibility and the storage pointer.

Values versus pointers

Suppose we say address a = "0x123...";

The compiler will look at that in the context of the overall storage layout and choose a "slot", which is one 32-byte word in the world state. The "location" of the slot, or key, is a hash function that uses the contract address as one of its arguments (to keep the contract's storage segregated from everyone else's).

This is an over-simplification but you can explore the precise details.

Suppose there are three variables, like address a; uint b; int c; a is in slot 0, b is in slot 1 and c is in slot 2 (setting aside optimization wizardry the compiler might work out.)

If the hash function is hash(address(this), slot); then it will produce unique keys for each variable and store the values in those locations in the key/value store.

That means there are two ways to refer to a variable. You can refer to its name (a) and let the compiler find it for you, or you can refer to "slot 0" and fetch what's there. You will get 0x123... either way.

With arrays and mappings, it's a little more complex but not by much. The hash function needs a "slot", so let's say address[] d is in slot 3. So the key would be hash(address(this), 3, <row>);

Here's where it gets interesting.

You can pass the whole array (or a struct full of all kinds of stuff) efficiently to another function inside the same contract by using the slot, or storage pointer.

function countArray(address[] storage a) internal view returns(uint) {
  return a.length;
}

You would call that by passing an array in:

address[] users;

function getCount() public view returns(uint) {
  countArray(users);
}

Great. The compiler kindly works out the storage location of users and passes it through to the other function which picks it up and plays with it. This is all good because it's all going on inside the contract, and the contract trusts itself.

On the other hand, if we wanted the public, or other contacts to use countArray() it would have to marked public or external, and that generates a compiler error. We would have a severe security problem if external actors could pass in storage slots and the contract just believes them. So, it isn't allowed.

You can change it to memory like this:

function countArray(address[] memory a) public returns(uint) {
  return a.length;
}

Great! Now it compiles and it is useable externally. What about security concerns?

We are not relying on a pointer. It's memory, so the ABI will be laid out to receive the length, and then the contents of the array. All of it. As you might surmise, that is potentially a lot of work. And, that work will go on if the function is called internally, which is allowed because the ABI doesn't change for the circumstance. It would be iterated to pack up the message, then iterated again to unpack and write to the function's transient (receiving) variable.

This is not good. The cost of all this will scale with the size of the array and it will potentially skewer the contract when the storage fills up. You generally don't want to do that.

However, if you understand the implications, you can safely use it with some caution. For example, if you want to do an airdrop with batches of transactions:

function sendBatch(address[] memory dest, uint amount) public ... 

The sender is control of the size of each batch, so you can make the case that the contract is useable in all cases. Ran out of gas? Send smaller batches.

The problem arises when the user is not in control and there is no alternative to a function that doesn't scale.

A classic mistake looks like:

function getUsers() public view returns(address[] memory) ...

where there is no other way to fetch that information. There is a very realistic possibility that this will become unuseable at an inopportune time because the issue will be too many users.

Hope it helps.

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.