Hi folks -- I am curious about understanding the risks of a state variable change across upgrades. I realize the short answer is "don't do this", but I wanted to dig a bit deeper to understand if this is true in all cases, as well as note a case where I was able to do this inadvertently without being warned.
The quick version is that I realized my choice of string
for a couple of variables within a struct was a poor choice and that I really want bytes32
. The variables are in a struct and they are in an abstract base contract (I roughly mirrored how OZ extensions are modeled).
After some testing both on Hardhat and Ropsten, I found that I was able to upgrade from the initial string
version to bytes32
without any warnings/force flags. On Ropsten I could see that any previous data stored in the struct was zeroed out, but could successfully store subsequent data in the now modified struct.
So here is the specific question: if a struct as noted above (with strings, in an abstract base contract) has never been used to store anything, is it still absolutely a bad idea to continue with the upgrade and store bytes32
instead of string
in the struct going forward?
I presume the answer is still yes, bad idea, but wanted to dig a bit since I wasn't flagged on this upgrade. Also curiosity around what's happening under the hood with respect to storage during an upgrade.
My gapping is probably wrong below, trying to get something abbreviated; the contract calling this base would of course be modified when calling string memory
for each variable it wants to modify in the struct tobytes32
.
baseV1.sol
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
abstract contract ERC721SomethingUpgradeable is Initializable, ERC721Upgradeable {
function __ERC721Something_init() internal initializer {
__Context_init_unchained();
__ERC165_init_unchained();
__ERC721Something_init_unchained();
}
function __ERC721Something_init_unchained() internal initializer {
}
using StringsUpgradeable for uint256;
struct SomeThing {
string randomHash;
string anotherHash;
}
address public _miscAddress;
mapping(uint256 => SomeThing) private _things;
function _setThing(uint256 tokenId, string memory randomHash, string memory anotherHash) internal virtual {
require(_exists(tokenId), "Thing set for nonexistant token");
_things[tokenId].randomHash = randomHash;
_things[tokenId].anotherHash = anotherHash;
}
uint256[47] private __gap;
}
baseV2.sol
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
abstract contract ERC721SomethingUpgradeable is Initializable, ERC721Upgradeable {
function __ERC721Something_init() internal initializer {
__Context_init_unchained();
__ERC165_init_unchained();
__ERC721Something_init_unchained();
}
function __ERC721Something_init_unchained() internal initializer {
}
using StringsUpgradeable for uint256;
struct SomeThing {
bytes32 randomHash;
bytes32 anotherHash;
}
address public _miscAddress;
mapping(uint256 => SomeThing) private _things;
function _setThing(uint256 tokenId, bytes32 randomHash, bytes32 anotherHash) internal virtual {
require(_exists(tokenId), "Thing set for nonexistant token");
_things[tokenId].randomHash = randomHash;
_things[tokenId].anotherHash = anotherHash;
}
uint256[47] private __gap;
}
Environment
Hardhat, 0.8.4, OZ 4.3.1