SIP-64: Flexible Contract Storage
Author | |
---|---|
Discussions-To | <https://discordapp.com/invite/AEdUHzt> |
Status | Implemented |
Created | 2020-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
- 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.
How to handle future refactoring of contracts? If we were to store data for
Issuer
say, and then we split out burning fromIssuer
intoBurner
, how could we reuse storage fromIssuer
inBurner
?
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.
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 saysStorage.addContractMapping("Issuer", "Burner")
that would allowBurner
to pass throughIssuer
as a key and still have write access to that space. This isn't great because thenBurner
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:
- It will support
uint
,int
,bool
,address
andbytes32
only.bytes
andstring
are of dynamic sizes and if needed should be a separate contract instance. - All getters and setters take an additional first parameter, the
contractName
asbytes32
- All getters and setters will additionally take a memory array of records and values (for setters) to reduce external calls where possible
- 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 byFlexibleStorage
and removed from Synthetix- All SCCP configurable settings, managed by a new contract
SystemSettings
. This contract will be owned specifically by theprotocolDAO
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 byIssuer
(previously inSynthetix
until SIP-48). FeePoolEternalStorage
can be replaced by this by additionally storing the data from fee periods into this as well asFeePoolEternalStorage
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
Copyright and related rights waived via CC0.