深入解析: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);
    }
}
相关推荐
LedgerNinja20 小时前
WEEX 真实情况如何?交易平台如何判断是否可靠
大数据·人工智能·区块链
卡牌RWA研究院1 天前
Relique:一张精品卡牌如何从收藏品变成可交易的链上资产?
设计模式·金融·区块链·创业创新
Web3_Daisy2 天前
Pump.fun 与 FOMO 竞争背后的 Meme 市场变局
大数据·人工智能·金融·web3·区块链
磐链科技2 天前
跨链互操作性:打破RWA价值孤岛的终极解决方案
区块链
IvorySQL2 天前
PostgreSQL 日报| PostgreSQL 19 默认 WAL 压缩算法(8 月 8 日)
数据库·postgresql·区块链
北冥you鱼2 天前
DEX 合约接口设计:从核心功能到最佳实践
区块链
大鱼>3 天前
EVM字节码执行引擎深度解析:从Opcodes到Gas计费
区块链
大鱼>3 天前
共识算法:从PBFT到HotStuff的拜占庭容错演进
区块链·php·共识算法
磐链科技4 天前
区块链钱包开发新范式:如何用原生技术打造下一代Web3入口
web3·区块链
水无痕simon4 天前
15 转账对接文档
java·区块链