深入解析:Solidity中call与delegatecall的核心区别——谁在修改谁的存储?

Solidity中call与delegatecall的核心区别

对比表格

特性 A.call(B) A.delegatecall(B)
执行的代码 合约B的代码 合约B的代码
存储修改 合约B的存储 合约A的存储
address(this) 合约B地址 合约A地址
msg.sender 合约A地址 原始调用者地址
msg.value 可传递ETH 被忽略

详细示例

示例代码

复制代码
// 合约B - 被调用者
contract B {
    address public contractAddress;
    address public sender;
    uint public value;
    
    function setValues() public payable {
        contractAddress = address(this);  // 注意这里!
        sender = msg.sender;
        value = msg.value;
    }
}

// 合约A - 调用者
contract A {
    address public contractAddress;
    address public sender;
    uint public value;
    
    // 方式1:使用 call()
    function callB(address bAddress) public payable {
        (bool success, ) = bAddress.call{value: msg.value}(
            abi.encodeWithSignature("setValues()")
        );
        require(success, "call failed");
    }
    
    // 方式2:使用 delegatecall()
    function delegatecallB(address bAddress) public {
        (bool success, ) = bAddress.delegatecall(
            abi.encodeWithSignature("setValues()")
        );
        require(success, "delegatecall failed");
    }
}

执行结果对比

场景1:A.callB(B地址)

复制代码
// 用户调用:A.callB(B地址) 并发送 1 ETH

合约A的状态

  • contractAddress: ❌ 未改变
  • sender: ❌ 未改变
  • value: ❌ 未改变

合约B的状态

  • contractAddress: B的地址 ✅(因为 address(this) = B)
  • sender: A的地址 ✅(因为 msg.sender = A)
  • value: 1 ETH ✅(因为 msg.value = 1 ETH)

场景2:A.delegatecallB(B地址)

复制代码
// 用户调用:A.delegatecallB(B地址) 并发送 1 ETH

合约A的状态

  • contractAddress: A的地址 ✅(因为 address(this) = A)
  • sender: 用户的地址 ✅(因为 msg.sender = 用户)
  • value: 0 ✅(因为 delegatecall 忽略 msg.value

合约B的状态

  • contractAddress: ❌ 未改变
  • sender: ❌ 未改变
  • value: ❌ 未改变

可视化理解

call() 的情况

复制代码
用户 → 合约A.call() → 合约B.setValues()
          │                 │
          └─ msg.sender     ├─ address(this) = 合约B
          └─ msg.value      └─ msg.sender = 合约A

修改的是合约B的存储

delegatecall() 的情况

复制代码
用户 → 合约A.delegatecall() → 合约B.setValues()的代码
          │                          │
          ├─ 提供存储                │
          ├─ address(this) = 合约A ←─┘
          └─ msg.sender = 用户

修改的是合约A的存储

内存地址分析

复制代码
contract Debug {
    function testCall(address b) public payable {
        // 在合约A中执行
        address aAddress = address(this);  // 合约A的地址
        
        // call:切换到合约B的上下文
        b.call{value: 1 ether}(
            abi.encodeWithSignature("debug()")
        );
    }
    
    function testDelegateCall(address b) public {
        // 在合约A中执行
        address aAddress = address(this);  // 合约A的地址
        
        // delegatecall:保持合约A的上下文
        b.delegatecall(
            abi.encodeWithSignature("debug()")
        );
    }
}

contract Target {
    function debug() public payable {
        // 当被 call 调用时:
        // address(this) = Target 地址
        // msg.sender = Debug 地址
        
        // 当被 delegatecall 调用时:
        // address(this) = Debug 地址  
        // msg.sender = 原始用户地址
    }
}

代理模式的实际应用

复制代码
// 逻辑合约
contract Logic {
    address public implementation;  // 这个变量实际上属于代理合约!
    
    function upgrade(address newImplementation) public {
        // 这个函数修改的是代理合约的存储
        implementation = newImplementation;
    }
}

// 代理合约
contract Proxy {
    address public implementation;  // 与 Logic 中的变量在相同槽位
    
    constructor(address _logic) {
        implementation = _logic;
    }
    
    fallback() external {
        // 关键:使用 delegatecall
        implementation.delegatecall(msg.data);
    }
}

// 使用
Proxy proxy = new Proxy(logicAddress);
// 当调用 proxy.upgrade(newLogic) 时:
// 1. 调用 proxy.fallback()
// 2. delegatecall 到 logic.upgrade()
// 3. logic.upgrade() 的代码执行
// 4. 但修改的是 proxy.implementation
// 5. address(this) 在 upgrade() 函数中是 proxy 地址

核心规则总结

  1. call()
    • address(this) = 被调用合约的地址
    • msg.sender = 调用合约的地址
    • 修改被调用合约的存储
  2. delegatecall()
    • address(this) = 调用合约的地址 ✅
    • msg.sender = 原始用户的地址
    • 修改调用合约的存储

安全提醒

复制代码
// ⚠️ 危险示例
contract Malicious {
    address public owner;
    
    function takeOwnership() public {
        // 如果被 delegatecall 调用,这会修改调用合约的 owner!
        owner = msg.sender;
    }
}

// 安全的代理合约应该:
contract SafeProxy {
    address public implementation;
    address public owner;
    
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }
    
    function upgrade(address newImplementation) public onlyOwner {
        // 1. 验证新合约
        require(newImplementation != address(0), "Invalid address");
        
        // 2. 防止重入
        address oldImplementation = implementation;
        implementation = newImplementation;
        
        // 3. 发出事件
        emit Upgraded(oldImplementation, newImplementation);
    }
}
相关推荐
电报号dapp11911 小时前
DApp定制开发与源码交付:打造专属区块链应用的核心战略
web3·去中心化·区块链·智能合约
电报号dapp11913 小时前
交易所定制化开发:拒绝模板化与源码交付的战略价值
金融·web3·去中心化·区块链·智能合约
TechubNews14 小时前
《每周策略通》1.15
区块链
MQLYES15 小时前
07-BTC-挖矿难度
区块链
电报号dapp11916 小时前
质押挖矿DAPP与Swap交易所开发:构建DeFi新基建的全景指南
web3·去中心化·区块链·智能合约
区块链小八歌16 小时前
从流动性枢纽到衍生品引擎:2026 新年伊始,Kodiak 的进化之路
区块链
草原猫1 天前
DAPP开发应用场景解析:DAPP开发可以用在哪些业务中?
区块链·dapp开发
商业讯网11 天前
国家电投海外项目运营经验丰富
大数据·人工智能·区块链
老蒋每日coding1 天前
Solidity入门(2):Foundry框架开发指南
区块链
DICOM医学影像2 天前
8. go语言从零实现以太坊响应端 - 查询区块链账户余额
golang·区块链·以太坊·web3.0·响应端·从零实现