In my understanding, smart contracts on Ethereum are immutable once they are pushed onto chain. Then how does uniswap keeps upgrading itself from v1 to v2 to v3? How can they modify their smart contract code? "They" here apparently refer to Uniswap Labs. Additionally, since it is decentralized and nothing is special about Uniswap Labs, can anyone else modify the Uniswap contract as well?
Each version of Uniswap is a different set of contracts, deployed on different addresses. So there is no upgrading of existing contracts. See the links for the list of addresses:
Besides this approach of deploying each version to a new address, there's also the proxy pattern. Similarly to networking proxies, you can redirect the request to a target contract, and possibly modify it along the way. If the target address is stored in a variable, you can change the value without changing the actual contract bytecode.
pragma solidity ^0.8;
contract Proxy {
address target;
function setTarget(address _target) external {
// you can change the value of `target` variable in storage
// without chanding the `Proxy` contract bytecode
target = _target;
}
fallback(bytes calldata) external returns (bytes memory) {
(bool success, bytes memory returnedData) = target.delegatecall(msg.data);
require(success);
return returnedData;
}
}
You can read more about the upgradable proxy pattern here: https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies
Note that my code above is simplified to showcase just the basic proxy functionality. It is vulnerable to storage collision mentioned in the article, as well as to Delegatecall to Untrusted Callee.