Secureum Book
  • 🛡️Secureum Bootcamp
    • 🛡️Secureum Bootcamp
    • 🙌Participate
    • 📜History
  • 📚LEARN
    • Introduction
      • 🔷1. Ethereum Basics
        • 1.1 Ethereum: Concept, Infrastructure & Purpose
        • 1.2 Properties of the Ethereum Infrastructure
        • 1.3 Ethereum vs. Bitcoin
        • 1.4 Ethereum Core Components
        • 1.5 Gas Metering: Solving the Halting Problem
        • 1.6 web2 vs. web3: The Paradigm Shift
        • 1.7 Decentralization
        • 1.8 Cryptography, Digital Signature & Keys
        • 1.9 Ethereum State & Account Types
        • 1.10 Transactions: Properties & Components
        • 1.11 Contract Creation
        • 1.12 Transactions, Messages & Blockchain
        • 1.13 EVM (Ethereum Virtual Machine) in Depth
        • 1.14 Transaction Reverts & Data
        • 1.15 Block Explorer
        • 1.16 Mainnet & Testnets
        • 1.17 ERCs & EIPs
        • 1.18 Legal Aspects in web3: Pseudonymity & DAOs
        • 1.19 Security in web3
        • 1.20 web2 Timescales vs. web3 Timescales
        • 1.21 Test-in-Prod. SSLDC vs. Audits
        • Summary: 101 Keypoints
      • 🌀2. Solidity
        • 2.1 Solidity: Influence, Features & Layout
        • 2.2 SPDX & Pragmas
        • 2.3 Imports
        • 2.4 Comments & NatSpec
        • 2.5 Smart Contracts
        • 2.6 State Variables: Definition, Visibility & Mutability
        • 2.7 Data Location
        • 2.8 Functions
        • 2.9 Events
        • 2.10 Solidity Typing
        • 2.11 Solidity Variables
        • 2.12 Address Type
        • 2.13 Conversions
        • 2.14 Keywords & Shorthand Operators
        • 2.15 Solidity Units
        • 2.16 Block & Transaction Properties
        • 2.17 ABI Encoding & Decoding
        • 2.18 Error Handling
        • 2.19 Mathematical & Cryptographic Functions
        • 2.20 Control Structures
        • 2.21 Style & Conventions
        • 2.22 Inheritance
        • 2.23 EVM Storage
        • 2.24 EVM Memory
        • 2.25 Inline Assembly
        • 2.26 Solidity Version Changes
        • 2.27 Security Checks
        • 2.28 OpenZeppelin Libraries
        • 2.29 DAppSys Libraries
        • 2.30 Important Protocols
        • Summary: 201 Keypoints
      • 🔏3. Security Pitfalls & Best Practices
        • 3.1 Solidity Versions
        • 3.2 Access Control
        • 3.3 Modifiers
        • 3.4 Constructor
        • 3.5 Delegatecall
        • 3.6 Reentrancy
        • 3.7 Private Data
        • 3.8 PRNG & Time
        • 3.9 Math & Logic
        • 3.10 Transaction Order Dependence
        • 3.11 ecrecover
        • 3.12 Unexpected Returns
        • 3.13 Ether Accounting
        • 3.14 Transaction Checks
        • 3.15 Delete Mappings
        • 3.16 State Modification
        • 3.17 Shadowing & Pre-declaration
        • 3.18 Gas & Costs
        • 3.19 Events
        • 3.20 Unary Expressions
        • 3.21 Addresses
        • 3.22 Assertions
        • 3.23 Keywords
        • 3.24 Visibility
        • 3.25 Inheritance
        • 3.26 Reference Parameters
        • 3.27 Arbitrary Jumps
        • 3.28 Hash Collisions & Byte Level Issues
        • 3.29 Unicode RTLO
        • 3.30 Variables
        • 3.31 Pointers
        • 3.32 Out-of-range Enum
        • 3.33 Dead Code & Redundant Statements
        • 3.34 Compiler Bugs
        • 3.35 Proxy Pitfalls
        • 3.36 Token Pitfalls
        • 3.37 Special Token Pitfalls
        • 3.38 Guarded Launch Pitfalls
        • 3.39 System Pitfalls
        • 3.40 Access Control Pitfalls
        • 3.41 Testing, Unused & Redundand Code
        • 3.42 Handling Ether
        • 3.43 Application Logic Pitfalls
        • 3.44 Saltzer & Schroeder's Design Principles
        • Summary: 201 Keypoints
      • 🗜️4. Audit Techniques & Tools
        • 4.1 Audit
        • 4.2 Analysis Techniques
        • 4.3 Specification, Documentation & Testing
        • 4.4 False Positives & Negatives
        • 4.5 Security Tools
        • 4.6 Audit Process
        • Summary: 101 Keypoints
      • ☝️5. Audit Findings
        • 5.1 Criticals
        • 5.2 Highs
        • 5.3 Mediums
        • 5.4 Lows
        • 5.5 Informationals
        • Summary: 201 Keypoints
  • 🌱CARE
    • CARE
      • CARE Reports
  • 🚩CTFs
    • A-MAZE-X CTFs
      • Secureum A-MAZE-X
      • Secureum A-MAZE-X Stanford
      • Secureum A-MAZE-X Maison de la Chimie Paris
Powered by GitBook
On this page
  • Initializers
  • State Variables
  • Import Contracts
  • selfdestruct
  • State Variables
  • Function Id
  • Shadowing
  1. LEARN
  2. Introduction
  3. 3. Security Pitfalls & Best Practices

3.35 Proxy Pitfalls

The next set of security pitfalls and best practices that we'll discuss is related to Proxy-based contracts.

Remember that Proxy-based architectures are used for upgradability and other aspects desired in smart contract applications. In this Proxy setup, there is typically a Proxy contract that does a delegatecall to a logic contract, and because of the delegatecall, the logic contract gets to implement logic that executes on the state of the Proxy contract.

Under this specific setup, due to the delegatecall the data, the logic aspects has specific requirements that need to be met by both the Proxy as well as the logic contract. These lead to some security pitfalls and best practices.

Initializers

In this particular pitfall, initializer functions should not be callable multiple times: they should be callable only once, and by the authorized Proxy contract as soon as the logic contract has been deployed.

Remember that under a Proxy setup, the implementation contract can't use a constructor to initialize its state, because it is working with the state of the Proxy contract that does a delegatecall. So instead, implementation contract is expected to declare an initializer function which does all the required initializations for it. Such functions need to have an external or public visibility because they need to be callable from an external contract (which is the Proxy contract).

The deployment is typically done from a deploy script or from a factory contract. Preventing multiple invocations is critical because such invocations could happen from unauthorized contracts (or unauthorized users). In those cases, they could re-initialize the contract with values that let them exploit some of the contract functionality.

The best practice here is to use OpenZeppelin's Initializable library, which provides an initializer modifier that can be applied to the initialize function, preventing it from being called multiple times.

State Variables

This is a pitfall related to the previous one discussed. This specifically applies to initializing state variables in the Proxy-based setup.

Constructors should not be used in the implementation (or logic contracts) to initialize its state, but using an initializer function instead. State variables in the implementation contract, similarly, should not be initialized in their declarations themselves because such initializations will not be reflected when the Proxy contract makes a delegatecall to this implementation.

So instead, the state variables should be initialized within the initializer function because otherwise they would not be set when the delegatecall happens.

Import Contracts

The contracts used in a Proxy setup may also derive from other libraries or other contracts within the project itself, which could be defined in other files. In this case they are imported to be used in the Proxy contract.

These imported contracts that the Proxy contracts derive from, should also adhere to the same rules discussed: the base contracts should also not use a constructor, they should be using an initializer function. In addition, such contacts should also not initialize state variables during declaration.

The best practice is to make sure that the imported contracts also follow those rules, because if not, the state would be uninitialized and using that state could result in undefined behavior or potentially even serious vulnerabilities.

selfdestruct

If a Data Proxy calls a logic implementation contract that has a selfdestruct call in it, then that logic contract would end up getting destroyed and thereafter all calls to that logic contract will end up delegating calls to an address without any code.

Similarly, the use of delegatecall may also cause issues because the logic implementation works with the state of the Data Proxy.

The best practice is to avoid entirely the use of selfdestruct or delegatecalls with Proxy-based contracts.

State Variables

In Proxy-based contracts, the order layout type and mutability of state variables declared in the proxy, the corresponding implementation (or different versions of the implementation), should be preserved exactly while upgrading. This is to prevent storage layout mismatch errors. These ones can lead to very critical errors if they are inherited.

The best practice is to make sure that these aspects of state variables are exactly the same in Proxy-based setups.

Function Id

Remember that Solidity and EVM have the notion of a function selector which is the keccak256() hash of the function signatures.

These selectors are used to determine which contact function is being called, so at runtime the function dispatcher in the contract byte code should determine (by looking at the function selector) if one of the functions in the proxy is being called or if this call needs to be delegated to the implementation contract.

In Proxy-based setups, a malicious Proxy contract may declare a function such that its function Id collides (is the same as) with one of the Proxy Functions. So even though the call was targeting an implementation function, the malicious proxy, hijacks that call and could lead to the execution of a function that can cause an exploit.

The best practice here is to pay attention to the proxy, any trust assumptions related to the proxy and implementation contracts. Also, to check if the proxy has or can declare a function whose Id might collide with one of the implementation contract functions.

Shadowing

Instead of the Proxy contract trying to hijack calls meant for the implementation by declaring functions whose Id collides with the implementation contract functions, they could simply shadow the functions in the implementation contract.

This means that a Proxy contract can declare functions that have the same name, the same parameter numbers and types as functions in the implementation contract. In such a scenario, the function dispatcher would simply call the proxy contact function instead of forwarding it to the implementation contract.

This way, a malicious proxy can intercept (or hijack) calls instead of delegating it to the implementation contract and exploit this aspect to cause malicious behavior.

The best practice here is again to pay attention to the Proxy contract, the implementation contract, look at all the functions declared in both these contracts to see if any of the Proxy Functions are indeed Shadowing those in the implementation contract. If that is the case, recognize that such functions will be executed in the context of the proxy without being forwarded to the implementation contract.

Previous3.34 Compiler BugsNext3.36 Token Pitfalls

Last updated 1 year ago

📚
🔏