पाठ 14 / 25
Calling Other Contracts and Upgradeability
Make external calls, understand delegatecall and use proxy upgrade patterns.
Composability and its sharp edges
Contracts call each other through interfaces (IERC20(token).balanceOf(user)), which the compiler checks and which revert if the callee reverts. Low-level calls (address.call(data)) return a success flag and raw bytes instead of reverting, so you must check them. staticcall forbids state changes. delegatecall runs another contract's code in the context of the caller: same storage, msg.sender and balance. That is the basis of proxy upgradeability: users interact with a proxy that stores the state and delegates every call to an implementation contract, which can later be replaced. Common patterns are Transparent proxies and UUPS (where the upgrade function lives in the implementation), standardised storage slots (ERC-1967), and beacon proxies for many instances. Upgradeable contracts use initializers instead of constructors and must never reorder or remove existing storage variables, or storage collides. Upgradeability trades immutability for flexibility: whoever controls upgrades controls the contract, so protect it with a multisig and timelock, or make contracts immutable when possible. Note that selfdestruct has been effectively neutered (EIP-6780) and should not be used.
A UUPS upgradeable contract (OpenZeppelin)
Initializer instead of constructor; upgrades restricted to the owner.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract RewardsV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
mapping(address => uint256) public points; // storage layout must be preserved in V2
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers(); // lock the implementation contract itself
}
function initialize(address owner_) external initializer {
__Ownable_init(owner_);
__UUPSUpgradeable_init();
}
function award(address user, uint256 amount) external onlyOwner {
points[user] += amount;
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
// RewardsV2 must keep `points` first and only append new variables:
// contract RewardsV2 is RewardsV1 {
// mapping(address => uint256) public redeemed; // appended: OK
// function version() external pure returns (string memory) { return "2"; }
// }A shop front with a replaceable back office
A proxy is a shop front that customers always visit at the same address, with all the stock (storage) kept there. Behind it, the back office (implementation) that decides what to do can be swapped, but the new staff must use the same shelving layout, or everything ends up in the wrong place.
त्वरित जाँच: What does delegatecall do?
- Calls another contract with its own storage
- Sends ether only
- Deploys a new contract
- Runs another contract's code using the calling contract's storage, msg.sender and balance
Answer
Runs another contract's code using the calling contract's storage, msg.sender and balance — delegatecall borrows code while keeping the caller's context; proxies rely on it.