Skip to content

Commit 1cb8e17

Browse files
ekpyroncameel
andcommitted
Add documentation section about dangling references.
Co-authored-by: Kamil Śliwak <[email protected]>
1 parent 9542e46 commit 1cb8e17

File tree

1 file changed

+113
-0
lines changed

1 file changed

+113
-0
lines changed

docs/types/reference-types.rst

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,119 @@ Array Members
468468
}
469469
}
470470
471+
.. index:: ! array;dangling storage references
472+
473+
Dangling References to Storage Array Elements
474+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
475+
476+
When working with storage arrays, you need to take care to avoid dangling references.
477+
A dangling reference is a reference that points to something that no longer exists or has been
478+
moved without updating the reference. A dangling reference can for example occur, if you store a
479+
reference to an array element in a local variable and then ``.pop()`` from the containing array:
480+
481+
.. code-block:: solidity
482+
483+
// SPDX-License-Identifier: GPL-3.0
484+
pragma solidity >=0.8.0 <0.9.0;
485+
486+
contract C {
487+
uint[][] s;
488+
489+
function f() public {
490+
// Stores a pointer to the last array element of s.
491+
uint[] storage ptr = s[s.length - 1];
492+
// Removes the last array element of s.
493+
s.pop();
494+
// Writes to the array element that is no longer within the array.
495+
ptr.push(0x42);
496+
// Adding a new element to ``s`` now will not add an empty array, but
497+
// will result in an array of length 1 with ``0x42`` as element.
498+
s.push();
499+
assert(s[s.length - 1][0] == 0x42);
500+
}
501+
}
502+
503+
The write in ``ptr.push(0x42)`` will **not** revert, despite the fact that ``ptr`` no
504+
longer refers to a valid element of ``s``. Since the compiler assumes that unused storage
505+
is always zeroed, a subsequent ``s.push()`` will not explicitly write zeroes to storage,
506+
so the last element of ``s`` after that ``push()`` will have length ``1`` and contain
507+
``0x42`` as its first element.
508+
509+
Note that Solidity does not allow to declare references to value types in storage. These kinds
510+
of explicit dangling references are restricted to nested reference types. However, dangling references
511+
can also occur temporarily when using complex expressions in tuple assignments:
512+
513+
.. code-block:: solidity
514+
515+
// SPDX-License-Identifier: GPL-3.0
516+
pragma solidity >=0.8.0 <0.9.0;
517+
518+
contract C {
519+
uint[] s;
520+
uint[] t;
521+
constructor() {
522+
// Push some initial values to the storage arrays.
523+
s.push(0x07);
524+
t.push(0x03);
525+
}
526+
527+
function g() internal returns (uint[] storage) {
528+
s.pop();
529+
return t;
530+
}
531+
532+
function f() public returns (uint[] memory) {
533+
// The following will first evaluate ``s.push()`` to a reference to a new element
534+
// at index 1. Afterwards, the call to ``g`` pops this new element, resulting in
535+
// the left-most tuple element to become a dangling reference. The assignment still
536+
// takes place and will write outside the data area of ``s``.
537+
(s.push(), g()[0]) = (0x42, 0x17);
538+
// A subsequent push to ``s`` will reveal the value written by the previous
539+
// statement, i.e. the last element of ``s`` at the end of this function will have
540+
// the value ``0x42``.
541+
s.push();
542+
return s;
543+
}
544+
}
545+
546+
It is always safer to only assign to storage once per statement and to avoid
547+
complex expressions on the left-hand-side of an assignment.
548+
549+
You need to take particular care when dealing with references to elements of
550+
``bytes`` arrays, since a ``.push()`` on a bytes array may switch :ref:`from short
551+
to long layout in storage<bytes-and-string>`.
552+
553+
.. code-block:: solidity
554+
555+
// SPDX-License-Identifier: GPL-3.0
556+
pragma solidity >=0.8.0 <0.9.0;
557+
558+
contract C {
559+
bytes x = "012345678901234567890123456789";
560+
561+
function test() external returns(uint) {
562+
(x.push(), x.push()) = (0x01, 0x02);
563+
return x.length;
564+
}
565+
}
566+
567+
Here, when the first ``x.push()`` is evaluated, ``x`` is still stored in short
568+
layout, thereby ``x.push()`` returns a reference to an element in the first storage slot of
569+
``x``. However, the second ``x.push()`` switches the bytes array to large layout.
570+
Now the element that ``x.push()`` referred to is in the data area of the array while
571+
the reference still points at its original location, which is now a part of the length field
572+
and the assignment will effectively garble the length of ``x``.
573+
To be safe, only enlarge bytes arrays by at most one element during a single
574+
assignment and do not simultaneously index-access the array in the same statement.
575+
576+
While the above describes the behaviour of dangling storage references in the
577+
current version of the compiler, any code with dangling references should be
578+
considered to have *undefined behaviour*. In particular, this means that
579+
any future version of the compiler may change the behaviour of code that
580+
involves dangling references.
581+
582+
Be sure to avoid dangling references in your code!
583+
471584
.. index:: ! array;slice
472585

473586
.. _array-slices:

0 commit comments

Comments
 (0)