Skip to main content

reuse

合约中实现复用的方式?​

在智能合约开发中,除了继承之外,还有其他几种方式可以实现代码复用和模块化设计:

  1. 库(Libraries): Solidity 中的库是一种特殊类型的合约,它们被设计用来作为可复用的代码块。库函数可以被调用而不需要部署一个库的实例,这可以节省 gas 成本,因为库代码被部署一次后,可以被多个合约共享。

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;

    library SafeMath {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    require(c >= a, "SafeMath: addition overflow");
    return c;
    }
    // Other arithmetic functions...
    }

    contract MyContract {
    using SafeMath for uint256;
    // Now you can use SafeMath's add function on uint256 variables
    }
  2. 接口(Interfaces): 接口定义了一个合约的外部函数,但不包含这些函数的实现。通过接口,你可以定义一个合约必须遵守的规则,而具体的实现可以在不同的合约中进行。这允许不同的合约共享相同的接口,但具有不同的实现。

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;

    interface IMyContract {
    function myFunction(uint256 _myParam) external returns (uint256);
    }

    contract MyContractImplementation is IMyContract {
    function myFunction(uint256 _myParam) external override returns (uint256) {
    // Implementation code
    }
    }
  3. 委托调用(Delegatecall): delegatecall 是一个低级别的函数调用,它允许一个合约以自己的上下文调用另一个合约的函数。这意味着被调用合约的代码在调用合约的存储上下文中执行,可以实现合约逻辑的复用。

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;

    contract LibraryContract {
    function doSomething() public returns (uint256) {
    // Some function logic
    }
    }

    contract MyContract {
    address libraryAddress;

    function delegateDoSomething() public returns (uint256) {
    (bool success, bytes memory data) = libraryAddress.delegatecall(
    abi.encodeWithSignature("doSomething()")
    );
    require(success);
    return abi.decode(data, (uint256));
    }
    }
  4. 组合(Composition): 合约可以通过将其他合约的实例作为其成员变量来实现复用,这种方式称为组合。你可以调用成员合约的函数来复用其逻辑。

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;

    contract UtilityContract {
    function utilityFunction() public pure returns (string memory) {
    return "Utility function called";
    }
    }

    contract MyContract {
    UtilityContract private utilityContract;

    constructor(address _utilityContractAddress) {
    utilityContract = UtilityContract(_utilityContractAddress);
    }

    function useUtilityFunction() public view returns (string memory) {
    return utilityContract.utilityFunction();
    }
    }

每种方法都有其适用场景和优势。继承适用于当你想要扩展基础合约的功能时;库适用于当你想要在多个合约中共享函数逻辑但不存储状态时;接口适用于定义合约之间的通信协议;delegatecall 适用于想要在不同合约之间共享逻辑和存储;组合适用于想要利用其他合约的功能,但又不想与其形成密切耦合的关系时。

库(Libraries)​

手动​

在 Solidity 中,当你编译一个合约并且它依赖于一个库时,编译器会产生一个二进制文件,但这个文件还不包含库的地址。你需要在部署合约之前“链接”库的地址到你的合约代码中。这个链接过程可以通过多种方式完成,包括使用 Solidity 编译器(solc)的链接功能,或者使用开发框架如 Truffle 或 Hardhat 的自动链接功能。

以命令行编译器 solc 为例,链接库的过程通常涉及以下步骤:

  1. 编译合约和库,得到字节码。
  2. 部署库合约到区块链上,得到库的地址。
  3. 使用库的地址替换合约字节码中的占位符。
  4. 部署链接后的合约字节码到区块链上。

这是一个简化的例子,展示如何在命令行中手动链接库:

# 编译库和合约
solc --bin SafeMath.sol
solc --bin MyContract.sol

# 假设部署后的库地址为 0x123...(实际地址会更长)
# 使用库地址替换合约字节码中的占位符
# 占位符通常是库名称的哈希值的形式,例如 __SafeMath____________________
sed 's/__SafeMath____________________/123.../' MyContract.bin > MyContract_linked.bin

# 部署链接后的合约

在实际的开发过程中,开发者通常使用开发框架(如 Truffle 或 Hardhat)来自动处理这个链接过程。这些框架提供了工具和插件来自动检测合约代码中的库依赖,并在部署时自动链接库地址。

例如,在 Truffle 中,你可以在迁移脚本中指定库的地址,Truffle 会自动处理链接:

// migrations/2_deploy_contracts.js
const SafeMath = artifacts.require('SafeMath');
const MyContract = artifacts.require('MyContract');

module.exports = async function (deployer) {
await deployer.deploy(SafeMath);
const library = await SafeMath.deployed();

deployer.link(SafeMath, MyContract);
await deployer.deploy(MyContract);
};

在这个 Truffle 迁移脚本中,deployer.link 函数接受库合约和需要链接的合约作为参数,并自动处理链接过程。这样,开发者就不需要手动替换字节码中的占位符了。

自动化工具​

在 Solidity 中使用库(Library)确实比使用合约(Contract)更为复杂,因为它涉及到额外的步骤,如编译、链接和部署。不过,库提供了一些优势,比如代码复用和减少部署合约的气体成本(因为库代码在区块链上只部署一次,并且可以被多个合约共享)。

为了简化使用库的过程,许多开发者会采用开发框架,如 Truffle、Hardhat 或 Brownie,这些框架提供了自动化的工具来处理库的编译、链接和部署。以下是使用这些框架时的一些简化步骤:

  1. 编写库代码:你首先需要编写库代码,正如编写任何 Solidity 代码一样。

  2. 使用开发框架:使用开发框架可以自动处理编译和链接的过程。

  3. 部署库:当你运行部署脚本时,框架会自动部署库(如果它还没有被部署)并记住其地址。

  4. 链接库:在部署依赖库的合约时,框架会自动将库的地址链接到合约中。

  5. 部署合约:链接后的合约可以像任何其他合约一样被部署。

下面是一个使用 Hardhat 部署库和合约的示例:

// scripts/deploy.js
const hre = require('hardhat');

async function main() {
// 部署库
const SafeMath = await hre.ethers.getContractFactory('SafeMath');
const safeMath = await SafeMath.deploy();
await safeMath.deployed();
console.log('SafeMath deployed to:', safeMath.address);

// 链接库
const MyContract = await hre.ethers.getContractFactory('MyContract', {
libraries: {
SafeMath: safeMath.address,
},
});

// 部署合约
const myContract = await MyContract.deploy();
await myContract.deployed();
console.log('MyContract deployed to:', myContract.address);
}

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

在这个 Hardhat 脚本中,hre.ethers.getContractFactory 方法用于获取合约的工厂实例,并且在获取MyContract的实例时,通过libraries对象传入了SafeMath库的地址。这样,Hardhat 就会自动处理链接过程。

尽管初次接触时可能有一些学习曲线,但一旦你熟悉了这个过程,使用库就会变得相对简单,并且可以为你的项目带来长期的好处。

接口(Interfaces)​

skip

委托调用(Delegatecall)​

委托调用示例 1​

在 Solidity 中进行委托调用时,并不需要在调用者合约中定义被调用合约的接口。相反,你可以直接使用delegatecall低级函数,并传递一个包含函数签名和参数的字节串。以下是一个修正后的例子,其中不需要定义接口,直接进行委托调用:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Caller {
uint256 public number;

// 通过委托调用来调用另一个合约的`setNumber`函数
function delegateCallSetNumber(address target, uint256 _num) public {
(bool success, bytes memory data) = target.delegatecall(
abi.encodeWithSignature("setNumber(uint256)", _num)
);

require(success, "Delegatecall failed.");

// 可选:处理返回值
// 如果`setNumber`函数有返回值,可以这样解码
// (uint256 result) = abi.decode(data, (uint256));
// 但在这个例子中,我们不需要处理返回值
}
}

contract Target {
uint256 public number;

// 注意:这个函数的存储布局必须与Caller合约兼容
function setNumber(uint256 _num) public {
number = _num;
}
}

在这个修正后的例子中,Caller合约有一个delegateCallSetNumber函数,它执行对Target合约的setNumber函数的委托调用。在这种情况下,Target合约的number状态变量不会改变,因为delegatecall是在Caller合约的存储上下文中执行的,所以Caller合约的number状态变量会被设置为传递的参数_num。

请注意,委托调用时要非常小心,因为它可能引起存储冲突。确保调用者合约和被调用合约的存储布局保持一致,特别是它们共享的状态变量。如果存储布局不匹配,委托调用可能会导致数据被错误地修改,从而引起严重的安全问题。

委托调用示例 2​

下面是一个完整的 Solidity 示例,展示了一个简单的委托调用场景。在这个场景中,有两个合约:Delegate合约包含一个可以被委托调用的函数,Caller合约可以通过委托调用Delegate合约的函数来间接执行它。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Delegate 合约包含一个可以被委托调用的函数
contract Delegate {
uint public num;
address public sender;
uint public value;

function setVars(uint _num) public payable {
num = _num;
sender = msg.sender;
value = msg.value;
}
}

// Caller 合约通过委托调用来间接调用 Delegate 合约的函数
contract Caller {
uint public num;
address public sender;
uint public value;

// 通过委托调用来调用 Delegate 合约的 `setVars`
function setVars(address _delegate, uint _num) public payable {
// `_delegate` 是 Delegate 合约的地址
// `abi.encodeWithSignature` 用于编码函数调用
(bool success, bytes memory data) = _delegate.delegatecall(
abi.encodeWithSignature("setVars(uint256)", _num)
);

// 确保委托调用成功
require(success, "Delegatecall failed.");
}
}

在这个例子中:

  • Delegate 合约有一个名为 setVars 的函数,它接受一个 uint 类型的参数,并且可以接收以太币(通过 payable 关键字)。
  • Caller 合约有一个同名的 setVars 函数,它接收 Delegate 合约的地址和一个 uint 参数。这个函数使用 delegatecall 来调用 Delegate 合约的 setVars 函数。
  • Caller 合约的 setVars 函数将 Delegate 合约的 setVars 函数的调用编码为字节数据,并通过 delegatecall 发送给 Delegate 合约。
  • delegatecall 的返回值是一个布尔值(表示成功或失败)和返回的数据(如果有的话)。在这个例子中,我们只关心调用是否成功,如果不成功则抛出异常。

要使用这些合约,你需要:

  1. 部署 Delegate 合约。
  2. 部署 Caller 合约。
  3. 通过 Caller 合约调用 setVars 函数,传入 Delegate 合约的地址和你想设置的数字。

当你通过 Caller 合约调用 setVars 时,Delegate 合约中的 setVars 函数会在 Caller 合约的上下文中执行,这意味着 Caller 合约的 num、sender 和 value 变量会被更新,而 Delegate 合约的相应变量不会受影响。

请记住,在实际的应用中,委托调用通常用于更复杂的逻辑,如代理模式和合约升级。在使用委托调用时需要格外小心,因为如果不正确地管理存储槽位,可能会导致意外的行为和安全问题。

组合(Composition)​