Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Upgradeable smart contracts security

Upgradeable smart contracts security

One of the fundamental properties of blockchain is the impossibility of data spoofing (immutability). However, not all smart contracts have immutable code. A common practice is to use the contract logic update template with the help of a proxy. You have to be very careful when updating implementation. Otherwise, even the smallest mistake can lead to vulnerabilities, as happened with Nomad, Wormhole, and Audius, with hundreds of millions of dollars worth of damages. The report studies the principles of the proxy template, the associated vulnerabilities, and explain how to find proxy storage collision.

Arseny Reutov

October 19, 2022
Tweet

More Decks by Arseny Reutov

Other Decks in Programming

Transcript

  1. Agenda • Why proxies? • Upgradeability patterns • Proxy storage

    collision • Cases: OpenZeppelin, Wormhole, Audius • Tools & techniques
  2. Smart contracts are immutable Cons • Requires software quality of

    a Mars rover • No way to fix bugs without redeploying a contract to a new address • A single bug can be a disaster Pros • Can’t rug
  3. Security Ops • Find out normal parameters (minimum amount of

    liquidity, solvency criteria, price within specific range) • Monitor (e.g. with Forta) • React (pause the contract, remove liquidity, emergency exit) • Patch
  4. Patching • Why can’t we just deploy a new contract?

    • Because DeFi is composable • DeFi is not used only via a frontend, but by other contracts too • If contract’s address changes you have to change it everywhere • Some workarounds exist though: registry contracts and ENS resolution • But most common practice: proxies
  5. Upgrading via proxy • Proxy contract is a wrapper •

    Think of a reverse proxy in front of a web server • The main function of a proxy: forward calls to the implementation contract • The main property of a proxy: static address
  6. How is it achieved? 💡delegatecall inside a fallback function fallback()

    external payable { if (gasleft() <= 2300) { revert(); } address target_ = target; bytes memory data = msg.data; assembly { let result := delegatecall(gas(), target_, add(data, 0x20), mload(data), 0, 0) let size := returndatasize() let ptr := mload(0x40) returndatacopy(ptr, 0, size) switch result case 0 { revert(ptr, size) } default { return(ptr, size) } } }
  7. delegatecall In EVM there are three ways of calling a

    function: 1. call - state mutable call, i.e. write 2. staticcall - non mutable call, i.e. read 3. delegatecall - mutable call, but on our own storage
  8. delegatecall vs call EOA Contract A Contract B call call

    msg.sender = EOA msg.value = EOA storage = contract A msg.sender = contract A msg.value = contract A storage = contract B EOA Contract A Contract B call delegatecall msg.sender = EOA msg.value = EOA storage = contract A msg.sender = EOA msg.value = EOA storage = contract A
  9. delegatecall vs call EOA Contract A Contract B call call

    msg.sender = EOA msg.value = EOA storage = contract A msg.sender = contract A msg.value = contract A storage = contract B EOA Contract A Contract B call delegatecall msg.sender = EOA msg.value = EOA storage = contract A msg.sender = EOA msg.value = EOA storage = contract A
  10. Proxy initialization • Constructor is automatically called during contract deployment

    • But this is no longer possible with proxies • Because the constructor will change only the implementation contract’s storage • Solution – change the constructor to a regular function • Usually this function is called initialize() • It has initializer modifier which prevents re-initialization
  11. Proxy patterns 1. Transparent proxy pattern (TPP) 2. Universal upgradeable

    proxy system (UUPS) Difference is that TPP proxy contains upgrade logic, while UUPS off-loads this logic to the implementation contract. Credit: @OpenZeppelin
  12. Storage layouts Proxy has to store at least one variable,

    which is the implementation address. There are two storage layouts: 1. Structured storage - usually achieved by inheriting the same contract by both proxy and implementation 2. Unstructured storage - implementation address is stored in a pseudo-random slot location, such that an overwrite possibility is tiny (EIP-1967)
  13. EVM Storage • EVM storage is a sequence of 32-byte

    slots, max length is 2**256 • There is no allocator, contract can read & write everywhere slot 0 uint256 foo slot 1 uint256 bar slot 2 items.length=2 slot 3 slot keccak256(2) items[0]=12 slot keccak256(2)+1 items[1]=42 uint256 foo; uint256 bar; uint256[] items; function allocate() public { require(0 == items.length); items.length = 2; items[0] = 12; items[1] = 42; } https://mixbytes.io/blog/collisions-solidity-storage-layouts
  14. Unstructured storage Proxy Implementation … address _owner … mapping _balances

    … uint256 _supply … … … … address _implementation …
  15. Unstructured storage Proxy Implementation … address _owner … mapping _balances

    … uint256 _supply … … … … address _implementation … 🔀 random slot
  16. Storage collisions between implementations Implementation_v0 Implementation_v1 address _owner address _lastContributor

    mapping _balances address _owner uint256 _supply mapping _balances … uint256 _supply
  17. Storage collisions between implementations Implementation_v0 Implementation_v1 address _owner address _lastContributor

    mapping _balances address _owner uint256 _supply mapping _balances … uint256 _supply 💥 collision
  18. Storage collisions between implementations Implementation_v0 Implementation_v1 address _owner address _owner

    mapping _balances mapping _balances uint256 _supply uint256 _supply … address _lastContributor
  19. Storage collisions between implementations Implementation_v0 Implementation_v1 address _owner address _owner

    mapping _balances mapping _balances uint256 _supply uint256 _supply … address _lastContributor ⬇ storage extension /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ uint256[49] private __gap;
  20. OpenZeppelin CVE-2021-41264 • OpenZeppelin 4.1.0 < 4.3.2 had a critical

    vuln that allowed to brick the proxy by directly initializing the implementation • It existed in UUPS contract in the function upgradeToAndCall which could be called directly • This function updates the implementation address in the proxy and atomically executes any migration/initialization function using DELEGATECALL • But what if a target contract executes SELFDESTRUCT? https://forum.openzeppelin.com/t/uupsupgradeable-vulnerability-post-mortem/15680
  21. OpenZeppelin CVE-2021-41264 • If this happens, the DELEGATECALL caller will

    be destroyed, i.e. the current active implementation contract • Normally, we should not bother about it since onlyOwner can call upgradeToAndCall • But if implementation contract is initialized directly this check is bypassed modifier onlyProxy() { require(address(this) != __self, "Function must be called through delegatecall"); require(_getImplementation() == __self, "Function must be called through active proxy"); _; }
  22. Wormhole • Cross-chain bridge with >500M $ TVL • Was

    hacked in early February, 325M $ lost (non-proxy issue) • Another critical vuln similar to the OpenZeppelin’s was submitted later in February by a whitehat via Immunefi • Bug bounty – 10,000,000 $ 🤯
  23. Wormhole • Vulnerability in Wormhole was possible due to the

    custom upgrade logic similar to the vulnerable OpenZeppelin < 4.3.2 • Wormhole used UUPS-style proxy • A proxy upgrade was executed only if valid signatures of trusted addresses (called Guardians) were passed • Since upgradeTo could be called directly and implementation was not initialized, it was possible to submit own set of Guardians and brick the proxy via SELFDESTRUCT in the new implementation https://medium.com/immunefi/wormhole-uninitialized-proxy-bugfix-review-90250c41a43a
  24. Audius • Audius - web3 Spotify • Governance contract was

    behind a vulnerable custom proxy that inherited OpenZeppelin’s standard transparent proxy • As a result Audius was hacked for 6,000,000 $ • Fun fact: contract was audited by OpenZeppelin
  25. Audius • Custom proxy defined a state var proxyAdmin which

    occupied the first slot in the storage • It overlapped variables initializing and initialized of OpenZeppelin’s Initializable contract Credit: @danielvf