How delegatecall Works in Solidity ?


How delegatecall Works and Why It Matters for Rootstock ?
When building smart contracts with Solidity, developers often need to reuse code across multiple contracts without copying and pasting it everywhere. That's where delegatecall comes in. This powerful feature allows one contract to use functions from another contract while maintaining the calling contract's storage. This mechanism makes your contracts more modular and efficient.
What is delegatecall ?
Think of two contracts where one requires the functionality of the other. Instead of duplicating code, delegatecall lets you reuse functions directly. The magic is that the called function runs in the context of the calling contract, accessing and updating its storage instead of the storage of the original contract.
How delegatecall Fits into Rootstock's Security Model ?
Rootstock (RSK) is a Bitcoin sidechain that enables smart contracts using Ethereum’s EVM. delegatecall fits naturally into Rootstock’s architecture because it inherits Ethereum’s security model while benefiting from Bitcoin's proof-of-work security.
Bitcoin-powered security :– Rootstock uses merged mining, allowing Bitcoin miners to secure the Rootstock blockchain.
Gasless transactions :– By integrating delegatecall into Rootstock-based contracts, developers can create gas-efficient systems where gas fees are minimized using rBTC.
Cross-chain compatibility :– Since Rootstock is EVM-compatible, delegatecall allows developers to create upgradable smart contracts similar to Ethereum.
Example 1: Basic Usage of delegatecall
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract B {
address public owner;
constructor() {
owner = msg.sender;
}
function setOwner(address _account) public {
owner = _account;
}
}
contract A {
address public owner;
function setOwner(address _contract, address _owner) public {
(bool success, bytes memory data) = _contract.delegatecall(
abi.encodeWithSignature("setOwner(address)", _owner)
);
}
}
Explanation:
In this example, contract A calls contract B's setOwner function using delegatecall. Notice how the state change occurs in contract A's storage, not contract B's. Even though the function setOwner belongs to contract B, delegatecall ensures that updates are made in contract A's owner state variable.
Understanding Storage Layout Risks
Before diving deeper, it's crucial to understand one of the most critical risks with delegatecall: storage layout mismatch. When contracts have different variable arrangements, it can lead to unintended behavior.
Example 2: Storage Layout Mismatch
contract Library {
uint256 public count;
address public owner;
function increment() external {
count += 1;
}
}
contract Main {
address public admin;
uint256 public balance;
function incrementCounter(address _library) external {
_library.delegatecall(abi.encodeWithSignature("increment()"));
}
}
Explanation:
In this example, both contracts have two state variables but in different orders. When Main calls Library's increment function via delegatecall, it accidentally increments balance instead of count. This happens because Solidity stores variables sequentially in storage slots, and delegatecall uses the caller's storage layout
Rootstock-Specific Example: Proxy Contract with rBTC
One of the most powerful use cases for delegatecall is implementing upgradable contracts. On Rootstock, developers can create proxy contracts that interact with rBTC and other Rootstock-native tokens.
Example 3: rBTC-Compatible Proxy Contract
contract RBTCVault {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
}
contract RBTCProxy {
address public logicContract;
constructor(address _logic) {
logicContract = _logic;
}
fallback() external payable {
(bool success, ) = logicContract.delegatecall(msg.data);
require(success, "Proxy call failed");
}
function upgradeLogic(address _newLogic) external {
logicContract = _newLogic;
}
}
Explanation:
RBTCVault :– Manages user balances and allows deposits of rBTC.
RBTCProxy :– Forwards calls to RBTCVault using delegatecall. This allows the contract to be upgraded while maintaining storage consistency.
Enhancing Rootstock with delegatecall
- Building Modular dApps
contract RBTCVault {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
}
contract RBTCStaking {
address public vault;
constructor(address _vault) {
vault = _vault;
}
function stake(uint256 amount) external {
(bool success, ) = vault.delegatecall(
abi.encodeWithSignature("deposit()")
);
require(success, "Stake failed");
}
}
Explanation:
RBTCVault :- Manages user balances and allows deposits.
RBTCStaking :- Delegates deposit functionality to RBTCVault using delegatecall, while adding its own staking logic.
- Boosting Security with Bitcoin Integration
contract SecureVault {
address public owner;
constructor(address _owner) {
owner = _owner;
}
function execute(address _logic, bytes memory _data) external {
require(msg.sender == owner, "Unauthorized");
(bool success, ) = _logic.delegatecall(_data);
require(success, "Execution failed");
}
}
Explanation:
- SecureVault :- Delegates execution to external logic contracts while maintaining strict access control to ensure only the owner can trigger logic execution.
Facilitating Innovation in Financial Products
contract LendingProtocolStorage { struct Loan { uint256 amount; uint256 interestRate; uint256 deadline; } mapping(address => Loan) public loans; } contract LendingProtocolLogic { function borrow(address _storage, uint256 _amount, uint256 _interestRate) external { LendingProtocolStorage storageContract = LendingProtocolStorage(_storage); storageContract.loans[msg.sender] = LendingProtocolStorage.Loan({ amount: _amount, interestRate: _interestRate, deadline: block.timestamp + 30 days }); } } contract LendingProtocolProxy { address public logicContract; fallback() external { (bool success, ) = logicContract.delegatecall(msg.data); require(success, "Proxy call failed"); } }
Explanation:
LendingProtocolStorage :- Holds information about loans.
LendingProtocolLogic :- Implements the logic for borrowing.
LendingProtocolProxy :- Delegates calls to LendingProtocolLogic while keeping storage intact.
Using delegatecall with Rootstock Developer Tools
Rootstock is compatible with popular Ethereum development tools:
Hardhat
Truffle
Remix
Example Hardhat Configuration for Rootstock:
module.exports = {
networks: {
rootstock: {
url: "https://public-node.rsk.co",
accounts: [PRIVATE_KEY]
}
}
};
Example Truffle Configuration for Rootstock:
module.exports = {
networks: {
rootstock: {
host: "127.0.0.1",
port: 8545,
network_id: "*",
from: "0xYourAddress"
}
}
};
Key Considerations for Using delegatecall in Rootstock
State Variable Layout
Ensure consistent state variable layout between contracts.
Document storage layouts clearly.
Test thoroughly across different Solidity versions.
Gas Management
Be aware of gas cost differences.
Carefully manage gas usage.
Implement gas limits where necessary.
Risk Assessment
Audit target contracts thoroughly.
Implement proper access controls.
Use multisig wallets for critical operations.
Best Practices
Use call instead of delegatecall when modifying target contract state.
Handle return values correctly.
Pass arguments carefully.
Optimization Techniques
Create reusable libraries.
Implement proxy patterns.
Separate storage from logic
Comparison with Ethereum
Faster transaction speeds (up to 300 TPS vs Ethereum's 27 TPS).
Significantly cheaper gas fees.
Enhanced scalability.
Interoperability Features
Seamless integration with Ethereum smart contracts.
Easy migration of existing projects.
Asset transfer between Bitcoin and Rootstock
Security Risks and Best Practices
Although delegatecall is a powerful feature in Solidity, it comes with risks that developers need to be aware of:
Storage Conflicts: If the contract using delegatecall has a different storage layout from the delegate contract, data can be overwritten incorrectly, leading to serious bugs.
Untrusted Delegate Contracts: If a contract makes a delegatecall to an unverified or malicious contract, it could manipulate the storage of the caller contract, leading to security vulnerabilities.
Reentrancy Attacks: Since delegatecall executes code in the caller’s context, it can make the contract vulnerable to reentrancy attacks if not properly safeguarded.
Lack of Access Control: If a contract allows delegatecall to be made to any arbitrary address, attackers could execute unauthorized functions and take control of the contract
How to Use delegatecall Safely ?
Make sure both contracts have the same storage layout to prevent data corruption.
Only allow delegatecall to trusted, verified contract addresses.
Implement reentrancy guards to avoid recursive attacks.
Restrict delegatecall execution to specific, intended functions.
Regularly audit smart contracts using delegatecall to identify and fix vulnerabilities.
By following these guidelines, developers can use delegatecall securely while minimizing potential risks.
Why This Matters for Rootstock Developers ?
Rootstock's vision is to enhance Bitcoin's utility by enabling smart contracts and decentralized applications. delegatecall aligns perfectly with this vision by:
Encouraging Innovation
Enables modular contract designs.
Unlocks creative use cases.
Promotes experimentation.
Improving dApp Efficiency
External modules for specific tasks.
Better scalability.
More adaptable applications.
Ensuring Future-Proof Solutions
Upgradable contracts.
Evolving functionality.
Maintaining relevance.
Conclusion
delegatecall is not just a technical feature; it's a tool that can drive innovation on Rootstock. By leveraging its EVM compatibility and Bitcoin-backed security, developers can build modular, efficient, and secure dApps that elevate the Rootstock ecosystem. Whether creating upgradable contracts, modular financial protocols, or scalable dApps, delegatecall is your key to unlocking Rootstock's full potential.
By: Gaurang Bharadava / HEXXA Protocol
Subscribe to my newsletter
Read articles from Gaurang directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
