How do you create a front end contract that calls a back end with the idea of being able to upgrade or change the back end contracts without having to redeploy the front end contracts?
3 Answers
There are different approaches at Upgradeable smart contracts
One of the simplest is for the frontend contracts to refer to the backend contracts, not as hardcoded addresses, but using ENS (https://ens.domains). The ENS entries are updated when new backend contracts are needed.
Note that a significant benefit of smart contracts is their trustlessness, and upgradability can break that trust significantly, for example: https://medium.com/consensys-diligence/upgradeability-is-a-bug-dba0203152ce
-
As I understand it would be versioning of each front contract where back-end contract's storage would remain untouched. But this will bring high additional gas consumtion to get the back-end contracts' storage from the front-end contract.– alperCommented Nov 16, 2019 at 16:39
In case back-end contract is stateful and ought to preserve its state during upgrade, then usual practice is to deploy two-layer back-end contract. First layer is a proxy contract that keeps storage and delegates serving almost all calls to the second layer contract. The address of the second level back-end contract is stored in proxy's storage and could be changed. The proxy contract may look like this:
contract BackendProxy {
address private backendImplementation;
address private owner;
constructor (address _backendImplementation) public {
backendImplementation = _backendImplementation;
owner = msg.sender;
}
function () external payable {
if (msg.data.length > 0) {
address oldBackendImplementation = backendImplementation;
address oldOwner = owner;
bool status;
bytes memory data;
(status, data) = backendImplementation.delegatecall (msg.data);
require (oldBackendImplementation == backendImplementation);
require (oldOwner == owner);
if (status) {
assembly {
return (add (data, 0x40), mload (data))
}
} else {
assembly {
revert (add (data, 0x40), mload (data))
}
}
}
}
function setBackendImplementation (address _backendImplementation) public {
require (msg.sender == owner);
backendImplementation = _backendImplementation;
}
}
So proxy contract delegates everything to backendImplementation
except for changing address of backendImplementation
(to make sure the owner will not loose contract by pointing proxy to wrong implementation, and except for transfer with no data (which, once delegated, would not fit into 2300 gas stipend). The address of the proxy contract remains stable, while implementation may be changed by the owner at any time.
There are two leading approaches and a variant that works in some cases.
Eternal Storage: Persistent storage contracts that are "owned" by front-ends. The idea is the front end might change from time to time and the stored data should persist. I think this was the first succinct description of the pattern. https://blog.colony.io/writing-upgradeable-contracts-in-solidity-6743f0eecc88/. You could use ENS to address the evolution of the front-end addresses.
Proxy contracts: The Proxy holds the state and uses delegatecall to fetch and execute code that can be changed by adjusting the address it uses to fetch the code. I believe this is more popular of late. It's trickier and more prone to error so be sure to understand what's happening and the security pitfalls before using in production. https://medium.com/@daonomic/upgradeable-ethereum-smart-contracts-d036cb373d6
As Eth (above) and Steve Marx described in https://medium.com/consensys-diligence/upgradeability-is-a-bug-dba0203152ce, upgradeability undermines immutability, so one ought to carefully consider the desirability of enabling such a possibility - how it re-introduces centralized control and defeats assurances that might be important to users.
In some cases, you might consider trustless upgrades. This pattern puts the upgrade decision in the hands of individual users. The centralized authority is the issuance of compatible versions. The users decide, for themselves, if they want to upgrade or stick with the original contract. It works in cases where it is not necessary for all users to work with the exact same implementation and ABI, e.g. profileV1 and profileV2 with extra features. It would probably not work for something heavily-dependent on uniformity like a token, but many of those cases are strong candidates for no upgrades at all. https://medium.com/hackernoon/trustless-upgrades-in-solidity-bf0bd4047d28
Hope it helps.