GnosisSafe是以太坊区块链上最流行的多签钱包!它的最初版本叫 MultiSigWallet,现在新的钱包叫Gnosis Safe,意味着它不仅仅是钱包了。它自己的介绍为:以太坊上的最可信的数字资产管理平台(The most trusted platform to manage digital assets on Ethereum)。
学习完代理合约GnosisSafeProxy
之后我们来学习实现合约,先从GnosisSafe.sol
学习起。
第一件事上源码:
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
import "./base/ModuleManager.sol";
import "./base/OwnerManager.sol";
import "./base/FallbackManager.sol";
import "./base/GuardManager.sol";
import "./common/EtherPaymentFallback.sol";
import "./common/Singleton.sol";
import "./common/SignatureDecoder.sol";
import "./common/SecuredTokenTransfer.sol";
import "./common/StorageAccessible.sol";
import "./interfaces/ISignatureValidator.sol";
import "./external/GnosisSafeMath.sol";
/// @title Gnosis Safe - A multisignature wallet with support for confirmations using signed messages based on ERC191.
/// @author Stefan George -
/// @author Richard Meissner -
contract GnosisSafe is
EtherPaymentFallback,
Singleton,
ModuleManager,
OwnerManager,
SignatureDecoder,
SecuredTokenTransfer,
ISignatureValidatorConstants,
FallbackManager,
StorageAccessible,
GuardManager
{
using GnosisSafeMath for uint256;
string public constant VERSION = "1.3.0";
// keccak256(
// "EIP712Domain(uint256 chainId,address verifyingContract)"
// );
bytes32 private constant DOMAIN_SEPARATOR_TYPEHASH = 0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218;
// keccak256(
// "SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)"
// );
bytes32 private constant SAFE_TX_TYPEHASH = 0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8;
event SafeSetup(address indexed initiator, address[] owners, uint256 threshold, address initializer, address fallbackHandler);
event ApproveHash(bytes32 indexed approvedHash, address indexed owner);
event SignMsg(bytes32 indexed msgHash);
event ExecutionFailure(bytes32 txHash, uint256 payment);
event ExecutionSuccess(bytes32 txHash, uint256 payment);
uint256 public nonce;
bytes32 private _deprecatedDomainSeparator;
// Mapping to keep track of all message hashes that have been approve by ALL REQUIRED owners
mapping(bytes32 => uint256) public signedMessages;
// Mapping to keep track of all hashes (message or transaction) that have been approve by ANY owners
mapping(address => mapping(bytes32 => uint256)) public approvedHashes;
// This constructor ensures that this contract can only be used as a master copy for Proxy contracts
constructor() {
// By setting the threshold it is not possible to call setup anymore,
// so we create a Safe with 0 owners and threshold 1.
// This is an unusable Safe, perfect for the singleton
threshold = 1;
}
/// @dev Setup function sets initial storage of contract.
/// @param _owners List of Safe owners.
/// @param _threshold Number of required confirmations for a Safe transaction.
/// @param to Contract address for optional delegate call.
/// @param data Data payload for optional delegate call.
/// @param fallbackHandler Handler for fallback calls to this contract
/// @param paymentToken Token that should be used for the payment (0 is ETH)
/// @param payment Value that should be paid
/// @param paymentReceiver Adddress that should receive the payment (or 0 if tx.origin)
function setup(
address[] calldata _owners,
uint256 _threshold,
address to,
bytes calldata data,
address fallbackHandler,
address paymentToken,
uint256 payment,
address payable paymentReceiver
) external {
// setupOwners checks if the Threshold is already set, therefore preventing that this method is called twice
setupOwners(_owners, _threshold);
if (fallbackHandler != address(0)) internalSetFallbackHandler(fallbackHandler);
// As setupOwners can only be called if the contract has not been initialized we don't need a check for setupModules
setupModules(to, data);
if (payment > 0) {
// To avoid running into issues with EIP-170 we reuse the handlePayment function (to avoid adjusting code of that has been verified we do not adjust the method itself)
// baseGas = 0, gasPrice = 1 and gas = payment => amount = (payment + 0) * 1 = payment
handlePayment(payment, 0, 1, paymentToken, paymentReceiver);
}
emit SafeSetup(msg.sender, _owners, _threshold, to, fallbackHandler);
}
/// @dev Allows to execute a Safe transaction confirmed by required number of owners and then pays the account that submitted the transaction.
/// Note: The fees are always transferred, even if the user transaction fails.
/// @param to Destination address of Safe transaction.
/// @param value Ether value of Safe transaction.
/// @param data Data payload of Safe transaction.
/// @param operation Operation type of Safe transaction.
/// @param safeTxGas Gas that should be used for the Safe transaction.
/// @param baseGas Gas costs that are independent of the transaction execution(e.g. base transaction fee, signature check, payment of the refund)
/// @param gasPrice Gas price that should be used for the payment calculation.
/// @param gasToken Token address (or 0 if ETH) that is used for the payment.
/// @param refundReceiver Address of receiver of gas payment (or 0 if tx.origin).
/// @param signatures Packed signature data ({bytes32 r}{bytes32 s}{uint8 v})
function execTransaction(
address to,
uint256 value,
bytes calldata data,
Enum.Operation operation,
uint256 safeTxGas,
uint256 baseGas,
uint256 gasPrice,
address gasToken,
address payable refundReceiver,
bytes memory signatures
) public payable virtual returns (bool success) {
bytes32 txHash;
// Use scope here to limit variable lifetime and prevent `stack too deep` errors
{
bytes memory txHashData =
encodeTransactionData(
// Transaction info
to,
value,
data,
operation,
safeTxGas,
// Payment info
baseGas,
gasPrice,
gasToken,
refundReceiver,
// Signature info
nonce
);
// Increase nonce and execute transaction.
nonce++;
txHash = keccak256(txHashData);
checkSignatures(txHash, txHashData, signatures);
}
address guard = getGuard();
{
if (guard != address(0)) {
Guard(guard).checkTransaction(
// Transaction info
to,
value,
data,
operation,
safeTxGas,
// Payment info
baseGas,
gasPrice,
gasToken,
refundReceiver,
// Signature info
signatures,
msg.sender
);
}
}
// We require some gas to emit the events (at least 2500) after the execution and some to perform code until the execution (500)
// We also include the 1/64 in the check that is not send along with a call to counteract potential shortings because of EIP-150
require(gasleft() >= ((safeTxGas * 64) / 63).max(safeTxGas + 2500) + 500, "GS010");
// Use scope here to limit variable lifetime and prevent `stack too deep` errors
{
uint256 gasUsed = gasleft();
// If the gasPrice is 0 we assume that nearly all available gas can be used (it is always more than safeTxGas)
// We only substract 2500 (compared to the 3000 before) to ensure that the amount passed is still higher than safeTxGas
success = execute(to, value, data, operation, gasPrice == 0 ? (gasleft() - 2500) : safeTxGas);
gasUsed = gasUsed.sub(gasleft());
// If no safeTxGas and no gasPrice was set (e.g. both are 0), then the internal tx is required to be successful
// This makes it possible to use `estimateGas` without issues, as it searches for the minimum gas where the tx doesn't revert
require(success || safeTxGas != 0 || gasPrice != 0, "GS013");
// We transfer the calculated tx costs to the tx.origin to avoid sending it to intermediate contracts that have made calls
uint256 payment = 0;
if (gasPrice > 0) {
payment = handlePayment(gasUsed, baseGas, gasPrice, gasToken, refundReceiver);
}
if (success) emit ExecutionSuccess(txHash, payment);
else emit ExecutionFailure(txHash, payment);
}
{
if (guard != address(0)) {
Guard(guard).checkAfterExecution(txHash, success);
}
}
}
function handlePayment(
uint256 gasUsed,
uint256 baseGas,
uint256 gasPrice,
address gasToken,
address payable refundReceiver
) private returns (uint256 payment) {
// solhint-disable-next-line avoid-tx-origin
address payable receiver = refundReceiver == address(0) ? payable(tx.origin) : refundReceiver;
if (gasToken == address(0)) {
// For ETH we will only adjust the gas price to not be higher than the actual used gas price
payment = gasUsed.add(baseGas).mul(gasPrice < tx.gasprice ? gasPrice : tx.gasprice);
require(receiver.send(payment), "GS011");
} else {
payment = gasUsed.add(baseGas).mul(gasPrice);
require(transferToken(gasToken, receiver, payment), "GS012");
}
}
/**
* @dev Checks whether the signature provided is valid for the provided data, hash. Will revert otherwise.
* @param dataHash Hash of the data (could be either a message hash or transaction hash)
* @param data That should be signed (this is passed to an external validator contract)
* @param signatures Signature data that should be verified. Can be ECDSA signature, contract signature (EIP-1271) or approved hash.
*/
function checkSignatures(
bytes32 dataHash,
bytes memory data,
bytes memory signatures
) public view {
// Load threshold to avoid multiple storage loads
uint256 _threshold = threshold;
// Check that a threshold is set
require(_threshold > 0, "GS001");
checkNSignatures(dataHash, data, signatures, _threshold);
}
/**
* @dev Checks whether the signature provided is valid for the provided data, hash. Will revert otherwise.
* @param dataHash Hash of the data (could be either a message hash or transaction hash)
* @param data That should be signed (this is passed to an external validator contract)
* @param signatures Signature data that should be verified. Can be ECDSA signature, contract signature (EIP-1271) or approved hash.
* @param requiredSignatures Amount of required valid signatures.
*/
function checkNSignatures(
bytes32 dataHash,
bytes memory data,
bytes memory signatures,
uint256 requiredSignatures
) public view {
// Check that the provided signature data is not too short
require(signatures.length >= requiredSignatures.mul(65), "GS020");
// There cannot be an owner with address 0.
address lastOwner = address(0);
address currentOwner;
uint8 v;
bytes32 r;
bytes32 s;
uint256 i;
for (i = 0; i < requiredSignatures; i++) {
(v, r, s) = signatureSplit(signatures, i);
if (v == 0) {
// If v is 0 then it is a contract signature
// When handling contract signatures the address of the contract is encoded into r
currentOwner = address(uint160(uint256(r)));
// Check that signature data pointer (s) is not pointing inside the static part of the signatures bytes
// This check is not completely accurate, since it is possible that more signatures than the threshold are send.
// Here we only check that the pointer is not pointing inside the part that is being processed
require(uint256(s) >= requiredSignatures.mul(65), "GS021");
// Check that signature data pointer (s) is in bounds (points to the length of data -> 32 bytes)
require(uint256(s).add(32) <= signatures.length, "GS022");
// Check if the contract signature is in bounds: start of data is s + 32 and end is start + signature length
uint256 contractSignatureLen;
// solhint-disable-next-line no-inline-assembly
assembly {
contractSignatureLen := mload(add(add(signatures, s), 0x20))
}
require(uint256(s).add(32).add(contractSignatureLen) <= signatures.length, "GS023");
// Check signature
bytes memory contractSignature;
// solhint-disable-next-line no-inline-assembly
assembly {
// The signature data for contract signatures is appended to the concatenated signatures and the offset is stored in s
contractSignature := add(add(signatures, s), 0x20)
}
require(ISignatureValidator(currentOwner).isValidSignature(data, contractSignature) == EIP1271_MAGIC_VALUE, "GS024");
} else if (v == 1) {
// If v is 1 then it is an approved hash
// When handling approved hashes the address of the approver is encoded into r
currentOwner = address(uint160(uint256(r)));
// Hashes are automatically approved by the sender of the message or when they have been pre-approved via a separate transaction
require(msg.sender == currentOwner || approvedHashes[currentOwner][dataHash] != 0, "GS025");
} else if (v > 30) {
// If v > 30 then default va (27,28) has been adjusted for eth_sign flow
// To support eth_sign and similar we adjust v and hash the messageHash with the Ethereum message prefix before applying ecrecover
currentOwner = ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v - 4, r, s);
} else {
// Default is the ecrecover flow with the provided data hash
// Use ecrecover with the messageHash for EOA signatures
currentOwner = ecrecover(dataHash, v, r, s);
}
require(currentOwner > lastOwner && owners[currentOwner] != address(0) && currentOwner != SENTINEL_OWNERS, "GS026");
lastOwner = currentOwner;
}
}
/// @dev Allows to estimate a Safe transaction.
/// This method is only meant for estimation purpose, therefore the call will always revert and encode the result in the revert data.
/// Since the `estimateGas` function includes refunds, call this method to get an estimated of the costs that are deducted from the safe with `execTransaction`
/// @param to Destination address of Safe transaction.
/// @param value Ether value of Safe transaction.
/// @param data Data payload of Safe transaction.
/// @param operation Operation type of Safe transaction.
/// @return Estimate without refunds and overhead fees (base transaction and payload data gas costs).
/// @notice Deprecated in favor of common/StorageAccessible.sol and will be removed in next version.
function requiredTxGas(
address to,
uint256 value,
bytes calldata data,
Enum.Operation operation
) external returns (uint256) {
uint256 startGas = gasleft();
// We don't provide an error message here, as we use it to return the estimate
require(execute(to, value, data, operation, gasleft()));
uint256 requiredGas = startGas - gasleft();
// Convert response to string and return via error message
revert(string(abi.encodePacked(requiredGas)));
}
/**
* @dev Marks a hash as approved. This can be used to validate a hash that is used by a signature.
* @param hashToApprove The hash that should be marked as approved for signatures that are verified by this contract.
*/
function approveHash(bytes32 hashToApprove) external {
require(owners[msg.sender] != address(0), "GS030");
approvedHashes[msg.sender][hashToApprove] = 1;
emit ApproveHash(hashToApprove, msg.sender);
}
/// @dev Returns the chain id used by this contract.
function getChainId() public view returns (uint256) {
uint256 id;
// solhint-disable-next-line no-inline-assembly
assembly {
id := chainid()
}
return id;
}
function domainSeparator() public view returns (bytes32) {
return keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, getChainId(), this));
}
/// @dev Returns the bytes that are hashed to be signed by owners.
/// @param to Destination address.
/// @param value Ether value.
/// @param data Data payload.
/// @param operation Operation type.
/// @param safeTxGas Gas that should be used for the safe transaction.
/// @param baseGas Gas costs for that are independent of the transaction execution(e.g. base transaction fee, signature check, payment of the refund)
/// @param gasPrice Maximum gas price that should be used for this transaction.
/// @param gasToken Token address (or 0 if ETH) that is used for the payment.
/// @param refundReceiver Address of receiver of gas payment (or 0 if tx.origin).
/// @param _nonce Transaction nonce.
/// @return Transaction hash bytes.
function encodeTransactionData(
address to,
uint256 value,
bytes calldata data,
Enum.Operation operation,
uint256 safeTxGas,
uint256 baseGas,
uint256 gasPrice,
address gasToken,
address refundReceiver,
uint256 _nonce
) public view returns (bytes memory) {
bytes32 safeTxHash =
keccak256(
abi.encode(
SAFE_TX_TYPEHASH,
to,
value,
keccak256(data),
operation,
safeTxGas,
baseGas,
gasPrice,
gasToken,
refundReceiver,
_nonce
)
);
return abi.encodePacked(bytes1(0x19), bytes1(0x01), domainSeparator(), safeTxHash);
}
/// @dev Returns hash to be signed by owners.
/// @param to Destination address.
/// @param value Ether value.
/// @param data Data payload.
/// @param operation Operation type.
/// @param safeTxGas Fas that should be used for the safe transaction.
/// @param baseGas Gas costs for data used to trigger the safe transaction.
/// @param gasPrice Maximum gas price that should be used for this transaction.
/// @param gasToken Token address (or 0 if ETH) that is used for the payment.
/// @param refundReceiver Address of receiver of gas payment (or 0 if tx.origin).
/// @param _nonce Transaction nonce.
/// @return Transaction hash.
function getTransactionHash(
address to,
uint256 value,
bytes calldata data,
Enum.Operation operation,
uint256 safeTxGas,
uint256 baseGas,
uint256 gasPrice,
address gasToken,
address refundReceiver,
uint256 _nonce
) public view returns (bytes32) {
return keccak256(encodeTransactionData(to, value, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, _nonce));
}
}
从源码头部可以看到,它导入并继承了很多合约,因此我们分开来学习。今天先学习
EtherPaymentFallback
,Singleton
与ModuleManager
。
源码为:
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
/// @title EtherPaymentFallback - A contract that has a fallback to accept ether payments
/// @author Richard Meissner -
contract EtherPaymentFallback {
event SafeReceived(address indexed sender, uint256 value);
/// @dev Fallback function accepts Ether transactions.
receive() external payable {
emit SafeReceived(msg.sender, msg.value);
}
}
源码很简单,在接收ETH时触发一个SafeReceived
事件方便客户端进行追踪。
吐槽一下,这个CSDN的在线编辑器不支持Solidity的代码块,但Typora支持。所以这里只能使用Javascript
代码块,源代码看上去比较丑。大家可以自行和下图使用Solidity代码块的效果对比一下
希望CSDN能早一点支持Solidity.
源码为:
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
/// @title Singleton - Base for singleton contracts (should always be first super contract)
/// This contract is tightly coupled to our proxy contract (see `proxies/GnosisSafeProxy.sol`)
/// @author Richard Meissner -
contract Singleton {
// singleton always needs to be first declared variable, to ensure that it is at the same location as in the Proxy contract.
// It should also always be ensured that the address is stored alone (uses a full word)
address private singleton;
}
这个合约也很简单,只有一个私有的状态变量singleton
.我们前面提到过,代理合约的第一个状态变量是singleton,地址类型,为了保持插槽一致,所以实现合约的第一个状态变量也必须是相同数据类型的singleton
,虽然实现合约本身用不到它,但是逻辑上需要它。
通常,Singleton
是第一个应该继承的合约,否则 singleton
便有可能排不了第一个,但是由于它前面的EtherPaymentFallback
并没有定义状态变量,因此它还是排在第一个。
顾名思义,模块管理器,虽然现在还不知道模块是什么。源代码如下:
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
import "../common/Enum.sol";
import "../common/SelfAuthorized.sol";
import "./Executor.sol";
/// @title Module Manager - A contract that manages modules that can execute transactions via this contract
/// @author Stefan George -
/// @author Richard Meissner -
contract ModuleManager is SelfAuthorized, Executor {
event EnabledModule(address module);
event DisabledModule(address module);
event ExecutionFromModuleSuccess(address indexed module);
event ExecutionFromModuleFailure(address indexed module);
address internal constant SENTINEL_MODULES = address(0x1);
mapping(address => address) internal modules;
function setupModules(address to, bytes memory data) internal {
require(modules[SENTINEL_MODULES] == address(0), "GS100");
modules[SENTINEL_MODULES] = SENTINEL_MODULES;
if (to != address(0))
// Setup has to complete successfully or transaction fails.
require(execute(to, 0, data, Enum.Operation.DelegateCall, gasleft()), "GS000");
}
/// @dev Allows to add a module to the whitelist.
/// This can only be done via a Safe transaction.
/// @notice Enables the module `module` for the Safe.
/// @param module Module to be whitelisted.
function enableModule(address module) public authorized {
// Module address cannot be null or sentinel.
require(module != address(0) && module != SENTINEL_MODULES, "GS101");
// Module cannot be added twice.
require(modules[module] == address(0), "GS102");
modules[module] = modules[SENTINEL_MODULES];
modules[SENTINEL_MODULES] = module;
emit EnabledModule(module);
}
/// @dev Allows to remove a module from the whitelist.
/// This can only be done via a Safe transaction.
/// @notice Disables the module `module` for the Safe.
/// @param prevModule Module that pointed to the module to be removed in the linked list
/// @param module Module to be removed.
function disableModule(address prevModule, address module) public authorized {
// Validate module address and check that it corresponds to module index.
require(module != address(0) && module != SENTINEL_MODULES, "GS101");
require(modules[prevModule] == module, "GS103");
modules[prevModule] = modules[module];
modules[module] = address(0);
emit DisabledModule(module);
}
/// @dev Allows a Module to execute a Safe transaction without any further confirmations.
/// @param to Destination address of module transaction.
/// @param value Ether value of module transaction.
/// @param data Data payload of module transaction.
/// @param operation Operation type of module transaction.
function execTransactionFromModule(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation
) public virtual returns (bool success) {
// Only whitelisted modules are allowed.
require(msg.sender != SENTINEL_MODULES && modules[msg.sender] != address(0), "GS104");
// Execute transaction without further confirmations.
success = execute(to, value, data, operation, gasleft());
if (success) emit ExecutionFromModuleSuccess(msg.sender);
else emit ExecutionFromModuleFailure(msg.sender);
}
/// @dev Allows a Module to execute a Safe transaction without any further confirmations and return data
/// @param to Destination address of module transaction.
/// @param value Ether value of module transaction.
/// @param data Data payload of module transaction.
/// @param operation Operation type of module transaction.
function execTransactionFromModuleReturnData(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation
) public returns (bool success, bytes memory returnData) {
success = execTransactionFromModule(to, value, data, operation);
// solhint-disable-next-line no-inline-assembly
assembly {
// Load free memory location
let ptr := mload(0x40)
// We allocate memory for the return data by setting the free memory location to
// current free memory location + data size + 32 bytes for data size value
mstore(0x40, add(ptr, add(returndatasize(), 0x20)))
// Store the size
mstore(ptr, returndatasize())
// Store the data
returndatacopy(add(ptr, 0x20), 0, returndatasize())
// Point the return data to the correct memory location
returnData := ptr
}
}
/// @dev Returns if an module is enabled
/// @return True if the module is enabled
function isModuleEnabled(address module) public view returns (bool) {
return SENTINEL_MODULES != module && modules[module] != address(0);
}
/// @dev Returns array of modules.
/// @param start Start of the page.
/// @param pageSize Maximum number of modules that should be returned.
/// @return array Array of modules.
/// @return next Start of the next page.
function getModulesPaginated(address start, uint256 pageSize) external view returns (address[] memory array, address next) {
// Init array with max page size
array = new address[](pageSize);
// Populate return array
uint256 moduleCount = 0;
address currentModule = modules[start];
while (currentModule != address(0x0) && currentModule != SENTINEL_MODULES && moduleCount < pageSize) {
array[moduleCount] = currentModule;
currentModule = modules[currentModule];
moduleCount++;
}
next = currentModule;
// Set correct size of returned array
// solhint-disable-next-line no-inline-assembly
assembly {
mstore(array, moduleCount)
}
}
}
它也导入了三个合约并继承了两个,我们先学习导入的三个合约。
代码为:
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
/// @title Enum - Collection of enums
/// @author Richard Meissner -
contract Enum {
enum Operation {Call, DelegateCall}
}
代码很简单,定义了一个枚举Operation
,它有两个值,Call
与DelegateCall
。这样我们就可以使用这个枚举了。注意:枚举定义没有可见性,是默认可见的。
顾名思义,自认证(其实是自调用验证),代码为:
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
/// @title SelfAuthorized - authorizes current contract to perform actions
/// @author Richard Meissner -
contract SelfAuthorized {
function requireSelfCall() private view {
require(msg.sender == address(this), "GS031");
}
modifier authorized() {
// This is a function call as it minimized the bytecode size
requireSelfCall();
_;
}
}
这个合约主要内容是定义了一个修饰符:authorized
它内部调用了一个函数用来验证是不是自己调用自己。
自己调用自己的场景在日常开发中并不多见,主要用于多签钱包和DAO自管理,因为多签钱包所有操作都最终由多签钱包发出,所以msg.sender
是多签钱包,又因为是自管理,是操作多签钱包内部的状态,所以to
也是多签钱包,此时就是自调用。
我们平常在开发智能合约时,比如合约内部调用本合约的一个public
函数f()
,此时如果然我们将f()
修改为this.f()
,那么它就变成了一个合约自调用了。当然其msg.sender
变成合约自己了,所以大家就不要修改了。this.f()
和正常的contract.function()
调用是一样的(例如erc20.decimals()),this
代表一个合约,f()
代表调用函数。
代码为:
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
import "../common/Enum.sol";
/// @title Executor - A contract that can execute transactions
/// @author Richard Meissner -
contract Executor {
function execute(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation,
uint256 txGas
) internal returns (bool success) {
if (operation == Enum.Operation.DelegateCall) {
// solhint-disable-next-line no-inline-assembly
assembly {
success := delegatecall(txGas, to, add(data, 0x20), mload(data), 0, 0)
}
} else
// solhint-disable-next-line no-inline-assembly
assembly {
success := call(txGas, to, value, add(data, 0x20), mload(data), 0, 0)
}
}
}
}
这个合约也很简单,定义了一个执行器,注意它执行两种操作,call
和delegatecall
。这两种操作的含义相信大家都很清楚,在内嵌汇编中使用它们的方法也是类似的,只是delegatecall
并没有value
参数。
注意,这里并没有对返回值做处理,因此不知道是外部调用成功还是失败。它只是一个内部函数,结果由调用它的函数处理。
需要提一下的是如果上面的execute
函数的参数to
为本合约地址的话,那么便是自调用的情况了,刚好和SelfAuthorized
提到的自调用检查相吻合。
话说枚举可以定义在合约外部供多个合约共同使用的,只是此时可能所属权不够清晰。本例中定义在Enum合约中,使用时需要使用全称:Enum.Operation
.
我们回过头来再来看ModuleManager合约。它继承了SelfAuth及orized
及Executor
,因此有一个自调用检查和执行器。
我们跳过事件定义,直接来到常量SENTINEL_MODULES
(哨兵模块)。
当然这个值address(1)
是自定义的,我们后面可以看到,这个代表一个模块链的开始,所以使用1是很合适的。当然你也可以使用address(666)
,没有任何问题的。
mapping(address => address) internal modules;
这里定义了一个map,注意它的key和value都是地址类型,这样可以前一个的value当成后一个的key,形成一个链条。
setupModules
函数用来初始化,为什么这样说呢? 显然下面的代码只能执行一次(初始化)
require(modules[SENTINEL_MODULES] == address(0), "GS100");
modules[SENTINEL_MODULES] = SENTINEL_MODULES;
然后当to
不为零地址时,执行的是委托调用Enum.Operation.DelegateCall,
,那么委托调用改变的是自己的状态变量,所以整体看来,这个函数是一个初始化函数(仅能执行一次)。
注意中写的比较清楚,将一个模块加入白名单,我们查看这个函数的逻辑可以发现,它将原哨兵模块(SENTINEL_MODULES) 对应的地址赋值给最新的模块,然后将哨兵模块的值设置为最新添加的模块。我们模拟一下连续调用本函数的结果
enableModule 0x3
enableModule 0x4
enableModule 0x5
得到的结果为:
modules[0x03] = 0x01
modules[0x04] = 0x03;
modules[0x05] = 0x04;
modules[0x01] = 0x05;
可以看到哨兵模块指向了最新的模块0x05,然后0x05指向0x04,0x04指向0x03,0x03指向了哨兵模块。这样形成了一条模块链,最新的模块在最后面。
注意:本函数有 authorized
限定符,看来是内部管理函数。
这个和 enableModule 刚好相反,移除相关模块。就是把模块链断开去掉一个节点,然后再把断开的两边连接上。
我们模拟一下调用本函数的结果:
disableModule 0x1 0x5
最后得到的模块链为
modules[0x03] = 0x01
modules[0x04] = 0x03;
modules[0x01] = 0x04;
可以看到,最后添加的0x5
被移除了。
注意,同样对应的,它也有authorized
修饰符。
看注释,它允许添加的模块调用它来执行外部调用/自定义操作,因此,上面添加的模块是在这里使用的。
这个函数很简单,注意的是它执行调用后的返回值处理
success = execute(to, value, data, operation, gasleft());
if (success) emit ExecutionFromModuleSuccess(msg.sender);
else emit ExecutionFromModuleFailure(msg.sender);
注意,它执行失败后只是触发了一个事件,并没有重置交易,是因为这个合约本身并没有改变任何状态,所以无需重置整个交易。
它将对外调用执行的结果bool success
并进一步返回,因此调用模块需要处理这个调用失败(success为false)的情况。
同execTransactionFromModule 函数,但是多返回了一个返回值。
外部调用很简单,同execTransactionFromModule,只是没有触发事件。
我们看一下接下来的内嵌汇编:
assembly {
// Load free memory location
let ptr := mload(0x40)
// We allocate memory for the return data by setting the free memory location to
// current free memory location + data size + 32 bytes for data size value
mstore(0x40, add(ptr, add(returndatasize(), 0x20)))
// Store the size
mstore(ptr, returndatasize())
// Store the data
returndatacopy(add(ptr, 0x20), 0, returndatasize())
// Point the return data to the correct memory location
returnData := ptr
}
returnData
execTransactionFromModule
函数,虽然execTransactionFromModule
函数是个public
函数,但是我们这里却只是内部跳转,并没有涉及到消息调用,因此你可以认为是execTransactionFromModule
的代码直接复制了过来。而execTransactionFromModule
又调用了execute
,这同样是一个内部调用,因此返回值来源于execute
的执行结果。 注意,这里只有消息调用(合约之间或者EOA与合约之间)才会有returndata
,它并不是普通函数之间相互调用的返回值(函数返回值是Solidity语言)。returndatacopy
操作码我们已经多次见到,为什么从add(ptr, 0x20)
开始呢,因为ptr
开始的32字节我们在上一步存入了长度前缀。returnData
的内存地址,从prt开始分别为它的长度前缀和实际数据很简单,判断模块是否白名单
分页获取白名单模块,返回一个白名单数组。这里为什么要采用分页呢?因为理论上,可以注册个无数模块,因此返回的数据可以无限大。然而却是有gasLimit
的,所以数组过大会导致调用失败,因此采用了分页模式,可以调整返回的数组大小和起始位置。
这里view
类型的函数同样也会受到gas
限制,同样也会gas
超限。
本函数的逻辑使用了一个while
从后向前循环模块链,取出分页大小的模块。如果不足(没有注册或者为哨兵模块),则立刻终止。
一个比较实用的技巧是最后的内嵌汇编,用来改变返回数组的大小。
// Set correct size of returned array
// solhint-disable-next-line no-inline-assembly
assembly {
mstore(array, moduleCount)
}
我们知道,数组在汇编中的内存layout也是值为内存地址,开始32字节存的是数组长度,后面再接数据内容。
由于我们分页获取的数组可能未填充满,比如取10个,我们只有4个。因此后面6个元素为空的。此时我们返回空元素的话会浪费空间。因此,可以直接修改返回的数据大小为4,这里就示例了一种直接修改方法。
直接将数组地址开始的32字节(存储数组大小)赋值为实际数组大小。
这是一个很实用的技巧,我们平常在遇到数组不能填充满时也可以使用此技巧。
修改大小后本来返回10个元素的数组变成了返回4个元素的数组,而有效内容是相同的