GitHub 合約代碼庫 (https://github.com/safe-global/safe-contracts)
官網(wǎng) https://gnosis-safe.io/
GnosisSafe 是一個多簽錢包。(Gnosis Safe is the most trusted platform to manage digital assets.)
代碼結(jié)構(gòu)

主要合約是 GnosisSafe,GnosisSafeL2 繼承至GnosisSafe,與之比較就是加了兩個日志
event SafeMultiSigTransaction(
address to,
uint256 value,
bytes data,
Enum.Operation operation,
uint256 safeTxGas,
uint256 baseGas,
uint256 gasPrice,
address gasToken,
address payable refundReceiver,
bytes signatures,
// We combine nonce, sender and threshold into one to avoid stack too deep
// Dev note: additionalInfo should not contain `bytes`, as this complicates decoding
bytes additionalInfo
);
event SafeModuleTransaction(address module, address to, uint256 value, bytes data, Enum.Operation operation);
分別在方法 execTransaction 和 execTransactionFromModule 彈出,應(yīng)該是handle鏈上數(shù)據(jù)的需要添加的 對合約功能沒有影響。
目錄 accessor 訪問器
此目錄下只有一份合約 SimulateTxAccessor 繼承至 Executor 模擬訪問器
contract SimulateTxAccessor is Executor {
address private immutable accessorSingleton;
constructor() {
accessorSingleton = address(this);
}
//只能代理調(diào)用 執(zhí)行的合約不能是此合約
modifier onlyDelegateCall() {
require(address(this) != accessorSingleton, "SimulateTxAccessor should only be called via delegatecall");
_;
}
//模擬執(zhí)行
function simulate(
address to,
uint256 value,
bytes calldata data,
Enum.Operation operation
)
external
onlyDelegateCall()
returns (
uint256 estimate,
bool success,
bytes memory returnData
)
{
uint256 startGas = gasleft();
success = execute(to, value, data, operation, gasleft());
estimate = startGas - gasleft();
// 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
}
}
}
此合約應(yīng)該是測試用的與合約主線沒有關(guān)聯(lián)。
external目錄下面也只有一份合約 GnosisSafeMath
interface目錄 ERC721TokenReceiver,ERC777TokensRecipient,ERC1155TokenReceiver,IERC165,ISignatureValidator,ViewStorageAccessible。
libraries目錄 創(chuàng)建,執(zhí)行GnosisSafe的輔助功能性合約 CreateCall 利用creationCode創(chuàng)建合約,GnosisSafeStorage GnosisSafe的數(shù)據(jù)存儲布局,由于GnosisSafe繼承至多份合約所以數(shù)據(jù)存儲布局看起來沒有那么清晰,GnosisSafeStorage相當(dāng)于做一個清晰的展示。MultiSend,MultiSendCallOnly,多筆交易同時調(diào)用,SignMessageLib。
handler目錄回調(diào)的處理,這部分是可以自定義的。 HandlerContext 提取調(diào)用上下文,DefaultCallbackHandler,CompatibilityFallbackHandler 三個文件。
examples里面又分為兩個目錄一個是libraries -> Migration 一個升級的工具或者示例。另一個是gards(衛(wèi)士)里面有三個合約 DebugTransactionGuard,DelegateCallTransactionGuard,ReentrancyTransactionGuard 不同功能的gards示例 ,這部分也可以自己定義。
proxies
-> 1.GnosisSafeProxy GnosisSafe的代理合約
->2.IProxyCreationCallback 一個創(chuàng)建多簽錢包后的回調(diào)接口,具體實(shí)現(xiàn)有用戶在創(chuàng)建時自己填入
-> 3.GnosisSafeProxyFactory 代理工廠 創(chuàng)建GnosisSafeProxy ,創(chuàng)建多簽錢包。
GnosisSafe 是多簽錢包的具體實(shí)現(xiàn)(邏輯) GnosisSafeProxy 是多簽錢包的代理(存儲)
剩余目錄合約基本上和GnosisSafe 都存在繼承關(guān)系

方法調(diào)用如

contract GnosisSafe is
EtherPaymentFallback,
Singleton,
ModuleManager,
OwnerManager,
SignatureDecoder,
SecuredTokenTransfer,
ISignatureValidatorConstants,
FallbackManager,
StorageAccessible,
GuardManager
EtherPaymentFallback -> EtherPaymentFallback 收到 eth時彈出 SafeReceived 日志
Singleton -> 一個私有變量singleton 對應(yīng) GnosisSafeProxy 的singleton 相當(dāng)于一個占位符
SelfAuthorized -> 主要限制一些函數(shù)只能自己調(diào)用目的是限制只能多簽驗(yàn)證后才能調(diào)用。
Executor -> 一個執(zhí)行方法 execute
ModuleManager -> ModuleManager is SelfAuthorized, Executor,模塊管理器可以添加和刪除模塊 模塊以鏈表形式保存。模塊的作用是被添加的模塊合約可以直接調(diào)用GnosisSafe去執(zhí)行一些操作,而無需驗(yàn)證多簽
function execTransactionFromModule(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation
) public virtual returns (bool success) {
// Only whitelisted modules are allowed 檢驗(yàn)調(diào)用者是被添加的模塊合約
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
}
}
OwnerManager -> OwnerManager is SelfAuthorized 管理多簽成員與前面閾值。成員的保存依然使用的鏈表。
SignatureDecoder -> signatureSplit 簽名解碼
SecuredTokenTransfer -> SecuredTokenTransfer 安全轉(zhuǎn)賬
ISignatureValidatorConstants -> 簽名驗(yàn)證通過后返回驗(yàn)證函數(shù)的簽名
ISignatureValidator -> ISignatureValidator is ISignatureValidatorConstants -> isValidSignature 驗(yàn)證簽名的合法性實(shí)現(xiàn)在 GnosisSafe
ISignatureValidator
FallbackManager -> FallbackManager is SelfAuthorized 管理fallback()函數(shù)執(zhí)行時具體的實(shí)現(xiàn)合約 -> setFallbackHandler
StorageAccessible
1 -> getStorageAt 獲取合約的指定卡槽的指定長度的內(nèi)容。
2 -> simulateAndRevert 模擬執(zhí)行調(diào)用一個合約獲取返回值。但是鏈狀態(tài)回退(不發(fā)生改變)。返回值放入revert里面。有點(diǎn)像uniswap v3的預(yù)兌換,獲取輸入對應(yīng)的輸出。
Guard - > Guard is IERC165 兩個方法 checkTransaction 多簽后方法執(zhí)行前調(diào)用 checkAfterExecution 多簽后方法執(zhí)行后調(diào)用
GuardManager -> GuardManager is SelfAuthorized 安全衛(wèi)士管理器,多簽可以自己設(shè)定這個衛(wèi)士合約(setGuard)。上面有提過示例。
最后就是 GnosisSafe
首先 簽名是使用 EIP712 簽名標(biāo)志
DOMAIN_SEPARATOR_TYPEHASH = keccak256( "EIP712Domain(uint256 chainId,address verifyingContract)" );
SAFE_TX_TYPEHASH = keccak256("SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,addressrefundReceiver,uint256 nonce)" );
這是實(shí)現(xiàn)合約,創(chuàng)建的多簽錢包是代理合約 也就是意味著創(chuàng)建多簽錢包不會執(zhí)行。
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;
}
初始化
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);
// 設(shè)置fallback的實(shí)現(xiàn)合約
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);
}
執(zhí)行多簽事務(wù)
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);
}
//執(zhí)行自定義的安全衛(wèi)士執(zhí)行前的驗(yàn)證
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
//執(zhí)行事務(wù)
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);
}
{
//執(zhí)行安全衛(wèi)士執(zhí)行后的驗(yàn)證
if (guard != address(0)) {
Guard(guard).checkAfterExecution(txHash, success);
}
}
}
檢查簽名
先判斷簽名閾值是否設(shè)置
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);
}
//檢查簽名
function checkNSignatures(
bytes32 dataHash,
bytes memory data,
bytes memory signatures,
uint256 requiredSignatures
) public view {
// Check that the provided signature data is not too short
//簽名數(shù)量是否達(dá)到閾值
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) { //鏈上手動approve
// 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 { //正常的eip712簽名
// 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;
}
}
預(yù)先計(jì)算執(zhí)行事務(wù)需要的gas 也是執(zhí)行后revert 把結(jié)果放在revert里面。
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)));
}
根據(jù)他的測試用例他解析revert的內(nèi)容的步驟
1.部署一個 decoder 合約
contract Decoder {
function decode(address to, bytes memory data) public returns (bytes memory) {
(bool success, bytes memory data) = to.call(data);
require(!success, "Shit happens");
return data;
}
}
2 ether.js解析
const { safe, decoder } = await setupTests()
const data = safe.interface.encodeFunctionData("requiredTxGas", [safe.address, 0, "0x", 0])
const result = await decoder.callStatic.decode(safe.address, data)
BigNumber.from("0x" + result.slice(result.length - 32)).toNumber()
在GnosisSafeProxyFactory的calculateCreateProxyWithNonceAddress中也使用了把結(jié)果放在revert里面 前端調(diào)用時用staiccall。
在create2里面creationCode + args,后面的args好像只能是 uint256類型的。