In the context of upgradable smart contracts, when should one use interfaces and when libraries? I read several similar questions and blog posts, but none of them give a straight-to-the-point answer:
- (Sub) contract vs. library vs. struct vs. Interface
- How to improve smart contact design in order to distinguish data and their manipulation functions for the same domain object?
- Writing upgradable contracts in Solidity
- Interfaces make your Solidity contracts upgradeable
- Library Driven Development in Solidity
- Proxy Libraries in Solidity
- Exploring Code Reuse in Solidity
I understand that the main criteria to consider (besides security) when designing for upgradability are:
- modularity - for reusability and easier maintenance
- gas limit - split huge contracts so that they can be deployed in several transactions, so as to not hit the gas limit
- cost of upgrade - how much does each contract upgrade cost. After a (small) change in one contract, which other contracts need to be re-deployed?
- cost of execution - separate contracts may result in gas overhead on each call. Try to keep that overhead low.
This Medium post suggests to use libraries to encapsulate logic (e.g. when interacting with "storage contracts") and to use interfaces to decouple inter-contract communication. Other posts suggest different techniques. As far as I understand, libraries are linked to contracts prior to deployment, so once the contract changes, libraries need to be re-deployed. Why it is not better to use interfaces for interacting with storage contracts?
Below I present the two solutions I have seen so far - one with library and one with an interface. (I'd like to avoid solutions with inline assembly...)
Solution with library
StorageWithLib.sol:
contract StorageWithLib {
uint public data;
function getData() public returns(uint) {
return data;
}
}
StorageLib.sol:
import './StorageWithLib.sol';
library StorageLib {
function getData(address _storageContract) public view returns(uint) {
return StorageWithLib(_storageContract).getData();
}
}
ActionWithLib.sol:
import './StorageLib.sol';
contract ActionWithLib {
using StorageLib for address;
address public storageContract;
function ActionWithLib(address _storageContract) public {
storageContract = _storageContract;
}
function doSomething() public {
uint data = storageContract.getData();
// do something with data ...
}
}
Solution with interface
IStorage.sol:
contract IStorage {
function getData() public returns(uint);
}
StorageWithInterface.sol:
import './IStorage.sol';
contract StorageWithInterface is IStorage {
uint public data;
function getData() public returns(uint) {
return data;
}
}
ActionWithInterface.sol:
import './IStorage.sol';
contract ActionWithInterface {
IStorage public storageContract;
function ActionWithInterface(address _storageContract) public {
storageContract = IStorage(_storageContract);
}
function doSomething() public {
uint data = storageContract.getData();
// do something with data ...
}
}
Considering the above criteria, which solution is preferred for separating storage and logic, and why? In which other cases is the other solution better?