Blog

Arbitrary delegatecall: Explanation and Risks

Blog Article: Arbitrary Delegatecall – A Critical Smart Contract Vulnerability

Introduction to Delegatecall in Solidity

In the Ethereum ecosystem, smart contracts frequently employ advanced patterns to achieve upgradability, modularity, and code reuse. One of the most powerful yet dangerous low-level functions available to Solidity developers is delegatecall(). While this function enables sophisticated architectures—such as proxy patterns—it also introduces significant security risks when implemented incorrectly.
This article explores the mechanics of delegatecall, illustrates how arbitrary delegatecall vulnerabilities can arise, and provides concrete guidance on how to protect your contracts from these potentially devastating exploits. For further assistance with secure contract development, check out Bailsec's audit services for a comprehensive review.

What Is Delegatecall and How Does It Work?

The delegatecall function is a special low-level function in Solidity that allows a contract (the caller) to execute code from another contract (the callee) in the caller's context. Unlike regular function calls, delegatecall:
  • Executes the callee's code using the caller's storage.
  • Preserves the original msg.sender and msg.value.
  • Applies any state modifications to the caller's state rather than the callee's.
This mechanism is fundamental to implementing upgradeable smart contracts, where a proxy contract delegates calls to a separate logic contract that can later be updated without losing state.
For example:

The Proxy Pattern: Legitimate Use of Delegatecall

The proxy pattern is a common and legitimate use of delegatecall. In this pattern:
  • A proxy contract stores data and holds funds.
  • A separate logic contract contains the implementation of functions.
  • The proxy uses delegatecall to execute functions from the logic contract.
Consider this example:
When used correctly, this pattern allows for contract logic to be upgraded while preserving all state data and funds in the proxy contract.

The Arbitrary Delegatecall Vulnerability

The real danger arises when a contract allows delegatecall to be made to an arbitrary address provided by the user without proper validation. This essentially gives the caller complete control over the contract's storage and funds.
For example:
In this vulnerable code, the execute function allows the caller to specify:
  • An arbitrary target address for delegatecall.
  • Arbitrary data, including function calls and parameters.
This is extremely dangerous because it enables an attacker to execute any code in the context of the vulnerable contract.

How an Attacker Can Exploit Arbitrary Delegatecall

An attacker can deploy a malicious contract designed to steal funds or manipulate the vulnerable contract's state. For instance:
The attack might unfold as follows:
  1. The attacker deploys the MaliciousLogic contract.
  2. The attacker calls execute(abi.encodeWithSignature("drainFunds()"), maliciousAddress).
  3. The vulnerable contract executes drainFunds() via delegatecall.
  4. Since delegatecall preserves context, address(this).balance refers to the vulnerable contract's balance.
  5. The attacker receives all funds from the vulnerable contract.
For a comprehensive analysis of these attack vectors, consider consulting Bailsec's security review services which specialize in identifying such vulnerabilities.

Beyond Fund Theft: Additional Risks of Arbitrary Delegatecall

Arbitrary delegatecall can lead to several other severe exploits:
  1. Taking Ownership of the Contract:
  2. An attacker may craft a malicious contract to modify the owner variable. For example:
  1. Manipulating Critical State Variables:
  2. Attackers might alter token balances, modify access controls, or corrupt operational parameters.
  3. Self-Destruction of the Contract:
  4. An attacker can force the vulnerable contract to self-destruct:

Storage Collision: A Subtle Danger

Another critical issue with delegatecall is storage layout collision. For delegatecall to function safely, the storage layout of the logic contract must match that of the proxy contract. If they don't align, functions might inadvertently modify unrelated state variables. In the previous example, note the careful alignment:
If these layouts differ, the logic contract could corrupt unintended storage slots in the proxy.

Real-World Impact: Case Studies

Several high-profile security incidents have involved delegatecall vulnerabilities:
  • Parity Multi-Sig Wallet (2017):
  • A vulnerability in a library contract used by Parity’s multi-signature wallets allowed an attacker to trigger a self-destruct, rendering 587 wallets permanently inaccessible and freezing over 513,000 ETH.
  • Lendf.me (2020):
  • A delegatecall vulnerability in the imBTC contract enabled attackers to manipulate token balances, resulting in thefts of around $25 million.
For further case studies and detailed analysis, visit Bailsec's blog for more in-depth discussions of past vulnerabilities.

Secure Delegatecall Implementations: Best Practices

To mitigate the risks of arbitrary delegatecall, follow these best practices:
  1. Never Allow Arbitrary Addresses:
  2. Always restrict delegatecall targets to fixed, pre-approved addresses. For instance:
  1. Implement Proper Access Control:
  2. Use strict access controls if the logic contract needs to be upgradeable:
  1. Use Established Proxy Patterns:
  2. Adopt established proxy patterns such as Transparent Proxy or UUPS (Universal Upgradeable Proxy Standard) to prevent storage collisions and unauthorized upgrades.
  3. Validate Function Selectors:
  4. When delegating different functions, verify the function selectors instead of allowing arbitrary call data:
For detailed workflows and secure development guidelines, refer to Bailsec's security workflow which offers structured approaches to contract security.

Detecting Arbitrary Delegatecall Vulnerabilities

To detect vulnerabilities in your contracts, consider the following methods:
  1. Automated Tools:
  2. Use static analysis tools to flag delegatecalls that accept user input or unchecked function selectors.
  3. Manual Code Review:
  4. Check for variables passed directly to delegatecall, missing validation, or proxy patterns with incompatible storage layouts.
  5. Professional Audits:
  6. For high-value contracts, professional audits are essential. Bailsec's audit services specialize in identifying low-level vulnerabilities like delegatecall issues.

Conclusion: Balancing Power and Security

Delegatecall is a powerful tool in Solidity that enables upgradeable contract architectures. However, when misused—such as allowing arbitrary delegatecalls—it can lead to catastrophic vulnerabilities, including fund theft, ownership hijacking, and even complete contract destruction.
Key takeaways include:
  • Never allow delegatecall to arbitrary addresses.
  • Implement strict access controls on upgradeable contracts.
  • Ensure storage layout compatibility between proxy and logic contracts.
  • Validate all function selectors before delegation.
  • Consider using established and audited proxy patterns.
By understanding the mechanics and risks associated with delegatecall, developers can harness its power while safeguarding their contracts. For ongoing education and professional support, explore Bailsec's security services and read more on Bailsec's blog.