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 合约的 numsendervalue 变量会被更新,而 Delegate 合约的相应变量不会受影响。

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

组合(Composition)