SIP-64: Flexible Contract Storage

Author
Discussions-To<https://discordapp.com/invite/AEdUHzt>
StatusImplemented
Created2020-05-28

Simple Summary

Provide a reusable storage location for any Synthetix contract.

Abstract

Take the EternalStorage contract pattern and generalize it for use in any number of various contracts.

Motivation

Currently the EternalStorage contract pattern is useful as storage for a single contract. However, using it means every new section of Synthetix requires its own instance of the storage contract, which continues to expand surface area of the project and requires more maintainence as each is paired to one single contract. Instead, this SIP proposes to create one central storage contract, where any contract can access storage mapped to it by contract name using the AddressResolver.

Specification

Overview

This SIP proposes to create a new version of the EternalStorage contract that instead of being limited to a single associatedContract via the State mixin, it can support any number of contracts that wish to use it in a safe and segregated manner.

All getters and setters from EternalStorage will be used, yet with an extra initial parameter, contractName. This property will be the mapping for the storage entries. For any setter function, the msg.sender must match the address that the Synthetix AddressResolver has for it. As such, this storage contract must manage a reference to the AddressResolver (and this can be done one-time using the ReadProxyAddressResolver).

This contract will be called FlexibleStorage. It is designed to be deployed once and used by many contracts as a flexible storage location.

Additionally, this SIP will create a reusable abstract contract - ContractStorage - which FlexibleStorage will build upon. The abstract contract will have the onlyContract modifier and the ability to migrate.

Resolved Questions

  1. Should the getters be limited to only be read the contract as well? This would prevent other internal Synthetix contracts from reading directly from the storage contract on behalf of another contract and would prevent third party contracts from reading these values on-chain. The cost is that this limitation may have unforeseen consequences for integrations down the line, moreover it is slightly more gas efficint to look up the storage directly, whereas the benefit is that the contracts themselves are the abstraction for viewing storage, and it could be problematic to allow third party contracts to expect the storage to be formatted a certain way, and break those expectations in future releases.
    • Decision: No. This would add unnecessary friction to the process.
  1. How to handle future refactoring of contracts? If we were to store data for Issuer say, and then we split out burning from Issuer into Burner, how could we reuse storage from Issuer in Burner?

    1. Use contract-auth calls to migrate the data over to the new mapping, one key at a time. This is manageable in the case of settings and properties, but much more onerous for address-based keys such as how Issuer uses EternalStorage.

    2. A potentially better solution (though it could get ugly) is having an available contract mapping. Initially the mapping is empty but it could be added to by a contract that allows other named contracts in the AddressResolver to set/get on its behalf. So for the above example, Issuer would have something added to itself that says Storage.addContractMapping("Issuer", "Burner") that would allow Burner to pass through Issuer as a key and still have write access to that space. This isn't great because then Burner needs to keep a reference to the "Issuer" storage key, but is manageable. Any other suggestions?

  • Decision: A version of #2 based off the proposal by @zyzek. The key to the entry can be stored in a mapping and if a migration is allowed then an additional mapping entry is created for the new contract to the old one.

Rationale

The current EternalStorage contract is quite flexible in how it stores state, it makes sense to double down on this useful pattern, hence a new version based off of it (but not extending it as we don't want to use the State mixin here). One trade-off of this approach however is the storage of packed structs. If a struct condenses it's entries down to maximize space: e.g. SystemStatus.Suspension (which uses a bool that is uint8 in Solidity, combined with a uint248 reason code which makes up the remainder of the 32 bytes in the storage slot), then it would be less efficient to store the individual components into two separate storage slots.

Unfortunately, there's no easy solution to generalizing the storage of structs. The usage of this storage contract will come down to whether or not the contract in question can efficiently compress and decompress its required data before going in and out of the storage contract.

Additionally, there will be a slightly higher gas cost when persisting storage now as each contract will need to do a cross-contract call both a) to the new Storage contract and then b) from the new Storage contract to the AddressResolver to ascertain if msg.sender is indeed the address it expects from the AddressResolver. This second step cannot be alleviated by having Storage use MixinResolver as other Synthetix contracts do because Storage is not an upgradable contract, and thus we can't hard-code the names of the contracts it needs to store in its cache.

Technical Specification

The API is nearly identical to EternalStorage with a few exceptions:

  1. It will support uint, int, bool, address and bytes32 only. bytes and string are of dynamic sizes and if needed should be a separate contract instance.
  2. All getters and setters take an additional first parameter, the contractName as bytes32
  3. All getters and setters will additionally take a memory array of records and values (for setters) to reduce external calls where possible
  4. Basic migration functionality to move keys over to a new contractName. This addresses question 2a above. 2b would require more functionality.

// abstract
contract ContractStorage {

    // onlyContract(fromContractName)
    function migrateContractKey(bytes32 fromContractName, bytes32 toContractName, bool removeAccessFromPreviousContract) external onlyContract(fromContractName) {
        // ...
    }

    modifier onlyContract(bytes32 contractName) {
        // ...
    }
}


interface IFlexibleStorage is ContractStorage {

    function getUintValue(bytes32 contractName, bytes32 record) external view returns (uint);

    function getUintValues(bytes32 contractName, bytes32[] calldata records) external view returns (uint[] memory);

    // onlyContract(contractName)
    function setUIntValue(bytes32 contractName, bytes32 record, uint value) external;

    // onlyContract(contractName)
    function setUIntValues(bytes32 contractName, bytes32[] calldata records, uint[] values) external;


    // onlyContract(contractName)
    function deleteUIntValue(bytes32 contractName, bytes32 record) external;

    // (as above for int, bool, address and bytes32 values)
    // ...

}

Additionally, contracts now need to know their own contractName. To solve this, we can add another constructor argument to MixinResolver with the contract's name added to itself as a public property, which it can then use for getting and setting storage. Though this may be superceded if a refactor occurs - see Question 2 above.

Usages

  • IssuanceEternalStorage should be replaced by wholesale by FlexibleStorage and removed from Synthetix
  • All SCCP configurable settings, managed by a new contract SystemSettings. This contract will be owned specifically by the protocolDAO in order to expedite any SCCP change without requiring a migration contract (from SIP-59).
Contract Property Type Notes
Exchanger exchangeFeeRateForSynths mapping(bytes32 => uint) (currently on FeePool)
Exchanger priceDeviationThresholdFactor uint
Exchanger waitingPeriodSecs uint
ExchangeRates rateStalePeriod uint
FeePool feePeriodDuration uint
FeePool targetThreshold uint
Issuer minimumStakeTime uint
Liquidations liqudationDelay uint
Liquidations liqudationPenalty uint
Liquidations liqudationRatio uint
SynthetixState issuanceRatio uint Cannot be modified directly, so all references need to be updated instead

interface ISystemSettings {

    function priceDeviationThresholdFactor() external view returns (uint);

    function waitingPeriodSecs() external view returns (uint);

    function issuanceRatio() external view returns (uint);

    function feePeriodDuration() external view returns (uint);

    function targetThreshold() external view returns (uint);

    function liquidationDelay() external view returns (uint);

    function liquidationRatio() external view returns (uint);

    function liquidationPenalty() external view returns (uint);

    function rateStalePeriod() external view returns (uint);

    function exchangeFeeRate(bytes32 currencyKey) external view returns (uint);

    function minimumStakeTime() external view returns (uint);
}

Potential Other Future Uses

  • The list of synths managed by Issuer (previously in Synthetix until SIP-48).
  • FeePoolEternalStorage can be replaced by this by additionally storing the data from fee periods into this as well as FeePoolEternalStorage during the transition period (two week claim window). The following upgrade can then remove this.

Test Cases

Test cases for an implementation are mandatory for SIPs but can be included with the implementation.

Configurable Values (Via SCCP)

Please list all values configurable via SCCP under this implementation.

Copyright and related rights waived via CC0.