Upgradeability Is a Bug

Steve Marx
Feb 5 · 5 min read

tl;dr

  1. Immutability is a critical feature to achieve trustlessness.
  2. Upgradeability undermines a contract’s immutability.
  3. Therefore, upgradeability is a bug. (But there are mitigations!)

Why do we need smart contracts?

Although there’s some evidence that people will take such an offer, I hope that you will refuse. You simply have no reason to trust me to hold up my end of the bargain.

Enter smart contracts. Smart contracts let us engage in all sorts of transactions without having to trust each other. Below is a simple Ethereum smart contract we could use for this investment opportunity. If it has 1 ether in it, you can send 1 ether of your own and withdraw 2 ether tomorrow:

contract InvestmentOpportunity {
address public investor;
uint256 public payday;

constructor() public payable {}
function invest() external payable {
require(investor == address(0), "Someone beat you to it!");
require(msg.value == address(this).balance / 2,
"You must match the contract balance.");

investor = msg.sender;
payday = now + 24 hours;
}

function withdraw() external {
require(msg.sender == investor,
"Only the investor can withdraw.");
require(now >= payday,
"You must wait until the payday time.");
msg.sender.transfer(address(this).balance);
}
}

If I deployed that contract to the main Ethereum network and put one ether in it, you’d be crazy not to invest. By encoding the rules into a smart contract, we’ve made our financial transaction “trustless”.

(This “investment” scenario is a bit unrealistic, but I’m going to stick with it because the code is simple. If you prefer, imagine the similar scenario of peer-to-peer gambling.)

The importance of immutability

Now that we have a smart contract, you can safely accept the offer because you do know what the smart contract will do. You can read the code yourself, and Ethereum guarantees that this exact code will execute, because smart contract code is immutable.

There’s nothing I can do to affect the outcome, so you don’t need to trust me at all.

Upgradeability = mutability

Many developers have independently come up with ways to swap out code at runtime. Over the past year, standard approaches to upgradeability have become popular. Perhaps the most common is the “proxy pattern”.

Below is a version of our investment opportunity contract that I’ve made upgradeable using the proxy pattern. The Base contract keeps track of state variables, the Implementation provides the business logic, and the Proxy allows the implementation to be replaced at runtime:

contract Base {
// proxy state
address owner;
address implementation;
// implementation state
address public investor;
uint256 public payday;
}
contract Implementation is Base {
function invest() external payable {
require(investor == address(0), "Someone beat you to it!");
require(msg.value == address(this).balance / 2,
"You must match the contract balance.");

investor = msg.sender;
payday = now + 24 hours;
}

function withdraw() external {
require(msg.sender == investor,
"Only the investor can withdraw.");
require(now >= payday,
"You must wait until the payday time.");

msg.sender.transfer(address(this).balance);
}
}
contract Proxy is Base {
constructor(address _implementation) public payable {
owner = msg.sender;
implementation = _implementation;
}
function setImplementation(address _implementation) external {
require(msg.sender == owner);
implementation = _implementation;
}

// adapted from https://github.com/zeppelinos/labs/blob/master/upgradeability_using_eternal_storage/contracts/Proxy.sol
function() external payable {
address impl = implementation;
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize)
let result := delegatecall(gas, impl, ptr,
calldatasize, 0, 0)
let size := returndatasize
returndatacopy(ptr, 0, size)

switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
}

If I deployed that Proxy contract (using that Implementation) to Ethereum and put an ether in it, would you be willing to invest?

Hopefully, you’ll once again refuse. Because the contract code is no longer immutable, you no longer know what the contract will do. Reading the code doesn’t help you, because I can change that code at any time.

Below is an alternate implementation I might “upgrade” to:

contract ExitScam is Base {
function exit() external {
require(msg.sender == owner);
selfdestruct(msg.sender);
}
}

Note that in this particular example, your ether is locked up for 24 hours, so I have a luxurious time window in which to make my changes. But even if the payout were immediate, I could always race against your transaction and try to change the implementation first.

Mitigations

Just say no

Limit mutability

For example, in the contract described in this article, I could require that code could only be changed after emitting an event and a 48-hour delay. Because funds are only locked for 24 hours, this would leave plenty of time for a user to discontinue their use of the contract if they didn’t like the pending code change.

Use parameters

Migration, not mutation

More sophisticated patterns exist for migration. For example, some contracts use a proxy for upgrade but require that users opt-in before they get the new implementation.

Final thoughts

Keep in mind that users of your smart contract don’t trust you, and that’s why you wrote a smart contract in the first place.

ConsenSys Diligence

ConsenSys Diligence has the mission of solving Ethereum smart contract security. Contact us for an audit at diligence@consensys.net.

Thanks to Maurelian, Bernhard Mueller, and Gerhard Wagner.

Steve Marx

Written by

Working on Ethereum smart contract security at @ConsenSys. Co-creator of https://www.site44.com and https://programtheblockchain.com .

ConsenSys Diligence

ConsenSys Diligence has the mission of solving Ethereum smart contract security. Contact us for an audit at diligence@consensys.net.