MultiSigWallet - Pembuatan Smart Contract
Dokumentasi lengkap untuk implementasi smart contract MultiSigWallet dengan sistem weighted voting menggunakan Solidity 0.8.26. Tutorial ini akan memandu Anda membangun sistem wallet multi-signature yang sophisticated.
Daftar Isi
- Arsitektur Smart Contract
- Implementasi WalletGovToken
- Implementasi WeightedMultiSigWallet
- Security Features
- Gas Optimization
- Kompilasi dan Validasi
1. Arsitektur Smart Contract
Overview Sistem
MultiSigWallet menggunakan sistem dual-contract architecture yang terdiri dari:
- WalletGovToken: ERC20 governance token yang menentukan voting weight
- WeightedMultiSigWallet: Core wallet contract dengan weighted voting system
Flow Interaksi
User Action → Signature Collection → Weight Validation → Transaction Execution
↓ ↓ ↓ ↓
Propose Sign with Private Check Token Execute if
Transaction Key + Message Balance ≥ Quorum Validation Pass
Konsep Voting Weight
Voting Weight ditentukan oleh jumlah governance token yang dimiliki:
- 1 token = 1 voting weight
- Total supply: 1,000,000 tokens
- Quorum: Minimum voting weight yang diperlukan (contoh: 600,000 = 60%)
Security Model
Security Layer 1: Executor Validation (hanya executor yang bisa execute)
Security Layer 2: Signature Verification (ECDSA signature validation)
Security Layer 3: Weight Calculation (token-based voting weight)
Security Layer 4: Quorum Check (minimum threshold validation)
Security Layer 5: Reentrancy Protection (ReentrancyGuard)
2. Implementasi WalletGovToken
Tujuan dan Fungsi
WalletGovToken adalah ERC20 token yang berfungsi sebagai:
- Voting Rights Representation: Menentukan voting power setiap address
- Access Control: Hanya pemilik token yang bisa participate dalam voting
- Transparency: Token balance dapat diaudit secara public
- Protection: Mencegah token transfer yang tidak diinginkan
Membuat File WalletGovToken.sol
Buat file src/WalletGovToken.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
/**
* @title WalletGovToken
* @dev ERC20 Governance Token untuk MultiSig Wallet dengan Weighted Voting
*
* Token ini digunakan untuk menentukan voting weight dalam multi-signature wallet.
* Setiap token mewakili 1 unit voting power.
*
* Fitur Khusus:
* - Proteksi transfer ke wallet contract (mencegah token "hilang")
* - Proteksi transfer ke zero address
* - Event tracking untuk semua transfers
* - Helper functions untuk voting calculations
*
* @author MultiSigWallet Team
* @notice Token ini HANYA untuk governance, bukan untuk trading
*/
contract WalletGovToken is ERC20 {
// ==================== STATE VARIABLES ====================
/**
* @dev Address dari wallet contract yang memiliki token ini
* Menggunakan immutable karena tidak akan berubah setelah deployment
*/
address public immutable walletContract;
// ==================== EVENTS ====================
/**
* @dev Event ketika tokens berhasil ditransfer
* @param from Address pengirim
* @param to Address penerima
* @param amount Jumlah tokens yang ditransfer
*/
event TokensTransferred(
address indexed from,
address indexed to,
uint256 amount
);
/**
* @dev Event ketika wallet contract address di-set
* @param wallet Address wallet contract
*/
event WalletContractSet(address indexed wallet);
// ==================== CUSTOM ERRORS ====================
/**
* @dev Error ketika mencoba transfer ke wallet contract
* Gas-efficient alternative untuk require statements
*/
error CannotTransferToWallet();
/**
* @dev Error ketika mencoba transfer ke zero address
*/
error CannotTransferToZero();
// ==================== MODIFIERS ====================
/**
* @dev Modifier untuk memastikan transfer tidak ke wallet contract
* @param to Address tujuan transfer
*
* Mencegah token "hilang" karena di-transfer ke wallet contract
*/
modifier notToWallet(address to) {
if (to == walletContract) revert CannotTransferToWallet();
_;
}
/**
* @dev Modifier untuk memastikan transfer tidak ke zero address
* @param to Address tujuan transfer
*/
modifier notToZero(address to) {
if (to == address(0)) revert CannotTransferToZero();
_;
}
// ==================== CONSTRUCTOR ====================
/**
* @dev Constructor untuk deploy governance token
* @param _totalSupply Total supply token yang akan di-mint
* @param toMint Address yang akan menerima initial tokens (biasanya deployer)
*
* Proses:
* 1. Set nama token "WalletGovToken" dan symbol "WGT"
* 2. Set msg.sender (wallet contract) sebagai walletContract
* 3. Mint semua tokens ke toMint address
* 4. Emit event untuk tracking
*/
constructor(
uint256 _totalSupply,
address toMint
) ERC20("WalletGovToken", "WGT") {
// Set wallet contract address (msg.sender adalah wallet yang deploy token ini)
walletContract = msg.sender;
// Mint semua tokens ke address yang ditentukan
_mint(toMint, _totalSupply);
// Emit event untuk tracking deployment
emit WalletContractSet(msg.sender);
}
// ==================== OVERRIDE FUNCTIONS ====================
/**
* @dev Override transfer function untuk menambah proteksi
* @param to Address tujuan transfer
* @param amount Jumlah token yang akan ditransfer
* @return bool Status success transfer
*
* Proteksi yang ditambahkan:
* - Tidak bisa transfer ke wallet contract (mencegah token hilang)
* - Tidak bisa transfer ke zero address
* - Emit custom event untuk tracking
*/
function transfer(
address to,
uint256 amount
) public virtual override notToWallet(to) notToZero(to) returns (bool) {
// Panggil parent transfer function
bool success = super.transfer(to, amount);
// Jika transfer berhasil, emit custom event
if (success) {
emit TokensTransferred(msg.sender, to, amount);
}
return success;
}
/**
* @dev Override transferFrom function dengan proteksi yang sama
* @param from Address pengirim
* @param to Address tujuan
* @param amount Jumlah token
* @return bool Status success
*
* Digunakan untuk approved transfers (allowance mechanism)
*/
function transferFrom(
address from,
address to,
uint256 amount
) public virtual override notToWallet(to) notToZero(to) returns (bool) {
// Panggil parent transferFrom function
bool success = super.transferFrom(from, to, amount);
// Emit custom event jika berhasil
if (success) {
emit TokensTransferred(from, to, amount);
}
return success;
}
// ==================== VOTING HELPER FUNCTIONS ====================
/**
* @dev Get voting weight dari sebuah address
* @param account Address yang akan dicek voting weight-nya
* @return uint256 Voting weight (sama dengan token balance)
*
* Helper function untuk memudahkan frontend dan contract lain
* mendapatkan voting weight tanpa perlu call balanceOf
*/
function getVotingWeight(address account) external view returns (uint256) {
return balanceOf(account);
}
/**
* @dev Get total voting weight dalam sistem (total supply)
* @return uint256 Total voting weight
*
* Berguna untuk menghitung percentage voting power:
* percentage = (userBalance / totalSupply) * 100
*/
function getTotalVotingWeight() external view returns (uint256) {
return totalSupply();
}
/**
* @dev Check apakah address memiliki voting rights
* @param account Address yang akan dicek
* @return bool True jika memiliki voting rights (balance > 0)
*
* Simple check untuk validation di frontend atau contract lain
*/
function hasVotingRights(address account) external view returns (bool) {
return balanceOf(account) > 0;
}
/**
* @dev Get percentage voting power dari sebuah address
* @param account Address yang akan dicek
* @return uint256 Percentage dalam basis points (10000 = 100.00%)
*
* Contoh output:
* - 2500 = 25.00%
* - 10000 = 100.00%
* - 0 = 0.00%
*/
function getVotingPercentage(address account) external view returns (uint256) {
uint256 total = totalSupply();
if (total == 0) return 0;
uint256 balance = balanceOf(account);
// Menggunakan basis points untuk precision (10000 = 100%)
return (balance * 10000) / total;
}
/**
* @dev Bulk check voting weights untuk multiple addresses
* @param accounts Array of addresses to check
* @return weights Array of voting weights corresponding to accounts
*
* Gas-efficient way untuk get multiple voting weights dalam single call
*/
function getVotingWeights(address[] calldata accounts)
external
view
returns (uint256[] memory weights)
{
weights = new uint256[](accounts.length);
for (uint256 i = 0; i < accounts.length; i++) {
weights[i] = balanceOf(accounts[i]);
}
}
/**
* @dev Calculate total voting weight dari array of addresses
* @param accounts Array of addresses
* @return totalWeight Sum of voting weights from all accounts
*
* Berguna untuk calculate apakah sekelompok addresses
* memiliki cukup voting weight untuk meet quorum
*/
function calculateTotalWeight(address[] calldata accounts)
external
view
returns (uint256 totalWeight)
{
for (uint256 i = 0; i < accounts.length; i++) {
totalWeight += balanceOf(accounts[i]);
}
}
// ==================== INFORMATION FUNCTIONS ====================
/**
* @dev Get comprehensive token information
* @return tokenName Name of the token
* @return tokenSymbol Symbol of the token
* @return tokenDecimals Decimals of the token
* @return tokenTotalSupply Total supply of the token
* @return walletAddr Address of the wallet contract
*/
function getTokenInfo() external view returns (
string memory tokenName,
string memory tokenSymbol,
uint8 tokenDecimals,
uint256 tokenTotalSupply,
address walletAddr
) {
return (
name(),
symbol(),
decimals(),
totalSupply(),
walletContract
);
}
/**
* @dev Get user-specific token information
* @param user Address of the user
* @return balance User's token balance
* @return votingWeight User's voting weight (same as balance)
* @return votingPercentage User's voting percentage in basis points
* @return hasRights Whether user has voting rights
*/
function getUserInfo(address user) external view returns (
uint256 balance,
uint256 votingWeight,
uint256 votingPercentage,
bool hasRights
) {
balance = balanceOf(user);
votingWeight = balance;
votingPercentage = totalSupply() > 0 ? (balance * 10000) / totalSupply() : 0;
hasRights = balance > 0;
}
}
Penjelasan Key Features
1. Transfer Protection
modifier notToWallet(address to) {
if (to == walletContract) revert CannotTransferToWallet();
_;
}
Tujuan: Mencegah token di-transfer ke wallet contract itu sendiri, yang akan membuat token "hilang" dan tidak bisa digunakan untuk voting.
2. Custom Events
event TokensTransferred(address indexed from, address indexed to, uint256 amount);
Tujuan: Memberikan tracking tambahan untuk semua token transfers, memudahkan monitoring dan analytics.
3. Voting Helper Functions
function getVotingWeight(address account) external view returns (uint256)
function getVotingPercentage(address account) external view returns (uint256)
Tujuan: Menyediakan interface yang clean untuk voting weight calculations tanpa perlu understand ERC20 internals.
3. Implementasi WeightedMultiSigWallet
Tujuan dan Fungsi
WeightedMultiSigWallet adalah core contract yang:
- Menyimpan Dana: Dapat menerima dan menyimpan ETH dan tokens
- Multi-Signature Security: Memerlukan multiple signatures untuk transaksi
- Weighted Voting: Voting power berdasarkan governance token ownership
- Access Control: Sistem executor dan governance yang sophisticated
- Transaction Management: Nonce system dan replay protection
Membuat File WeightedMultiSigWallet.sol
Buat file src/WeightedMultiSigWallet.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "./WalletGovToken.sol";
/**
* @title WeightedMultiSigWallet
* @dev Multi-signature wallet dengan weighted voting system berbasis governance token
*
* Wallet ini memungkinkan multiple owners untuk mengontrol dana dengan sistem
* voting berdasarkan governance token ownership. Semakin banyak token yang dimiliki,
* semakin besar voting power dalam decision making.
*
* Key Features:
* - Weighted voting berdasarkan governance token balance
* - Configurable quorum requirements
* - Multiple executors dengan role separation
* - ECDSA signature verification
* - Replay attack protection dengan nonce system
* - Emergency controls melalui governance
*
* @author MultiSigWallet Team
* @notice Contract ini untuk production treasury management
*/
contract WeightedMultiSigWallet is ReentrancyGuard {
using ECDSA for bytes32;
// ==================== EVENTS ====================
/**
* @dev Event ketika status executor berubah
* @param executor Address yang status executornya berubah
* @param status True jika ditambahkan, false jika dihapus
*/
event Executor(address indexed executor, bool status);
/**
* @dev Event ketika transaksi berhasil dieksekusi
* @param executor Address executor yang menjalankan transaksi
* @param to Address tujuan transaksi
* @param value Amount ETH yang dikirim
* @param data Transaction data
* @param nonce Nonce transaksi untuk prevent replay
* @param hash Hash transaksi yang di-sign
* @param result Return data dari transaction call
*/
event ExecuteTransaction(
address indexed executor,
address payable indexed to,
uint256 value,
bytes data,
uint256 nonce,
bytes32 hash,
bytes result
);
/**
* @dev Event ketika quorum requirement diupdate
* @param oldQuorum Quorum lama
* @param newQuorum Quorum baru
*/
event QuorumUpdated(uint256 oldQuorum, uint256 newQuorum);
/**
* @dev Event ketika wallet menerima ETH
* @param sender Address yang mengirim ETH
* @param amount Amount ETH yang diterima
* @param balance Balance total wallet setelah deposit
*/
event Deposit(address indexed sender, uint256 amount, uint256 balance);
/**
* @dev Event ketika governance tokens didistribusikan
* @param to Address penerima tokens
* @param amount Amount tokens yang didistribusikan
*/
event TokensDistributed(address indexed to, uint256 amount);
// ==================== STATE VARIABLES ====================
/**
* @dev Minimum voting weight yang diperlukan untuk approve transaksi
* Dalam satuan per million untuk precision (600000 = 60%)
*/
uint256 public quorumPerMillion;
/**
* @dev Nonce untuk mencegah replay attacks
* Increment setiap kali transaksi berhasil dieksekusi
*/
uint256 public nonce;
/**
* @dev Chain ID untuk signature verification
* Mencegah cross-chain replay attacks
*/
uint256 public chainId;
/**
* @dev Mapping untuk track executor addresses
* Executor adalah address yang bisa mengeksekusi transaksi
* (tapi tetap memerlukan signatures dari token holders)
*/
mapping(address => bool) public executors;
/**
* @dev Jumlah total executors
* Digunakan untuk validation dan access control
*/
uint256 public executorCount;
/**
* @dev Governance token contract
* Immutable karena tidak akan berubah setelah deployment
*/
WalletGovToken public immutable govToken;
// ==================== CONSTANTS ====================
/**
* @dev Total supply governance token (1 million)
*/
uint256 public constant GOV_TOKEN_SUPPLY = 1_000_000;
/**
* @dev Base unit untuk percentage calculations (1 million = 100%)
*/
uint256 public constant MILLION = 1_000_000;
// ==================== CUSTOM ERRORS ====================
error NotSelf(); // Hanya wallet sendiri yang bisa call
error NotExecutor(); // Hanya executor yang bisa call
error InsufficientWeight(); // Caller tidak punya voting weight
error InvalidQuorum(); // Quorum tidak valid (0 atau >100%)
error InvalidSignatures(); // Signatures tidak valid
error DuplicateSignature(); // Signature duplikat atau tidak terurut
error InsufficientSignatures(); // Tidak cukup signatures untuk quorum
error TransactionFailed(); // Transaction execution gagal
error CannotRemoveLastExecutor(); // Tidak bisa hapus executor terakhir
error ExecutorAlreadyExists(); // Executor sudah ada
error ExecutorNotFound(); // Executor tidak ditemukan
error InvalidAddress(); // Address tidak valid (zero address)
// ==================== MODIFIERS ====================
/**
* @dev Hanya contract itu sendiri yang bisa call function ini
* Digunakan untuk governance functions yang harus melalui voting process
*/
modifier onlySelf() {
if (msg.sender != address(this)) revert NotSelf();
_;
}
/**
* @dev Hanya executors yang bisa call function ini
* Executors adalah addresses yang diberi wewenang untuk execute transactions
*/
modifier onlyExecutors() {
if (!executors[msg.sender]) revert NotExecutor();
_;
}
/**
* @dev Caller harus memiliki voting weight (token balance > 0)
* Mencegah addresses tanpa tokens melakukan operations
*/
modifier hasVotingWeight() {
if (!hasWeight(msg.sender)) revert InsufficientWeight();
_;
}
// ==================== CONSTRUCTOR ====================
/**
* @dev Constructor untuk deploy weighted multi-signature wallet
* @param _chainId Chain ID untuk signature verification (10143 untuk Monad Testnet)
* @param _quorumPerMillion Quorum requirement dalam per million (600000 = 60%)
*
* Proses deployment:
* 1. Validate quorum parameter
* 2. Set chain ID dan quorum
* 3. Deploy governance token contract
* 4. Mint semua tokens ke deployer
* 5. Set deployer sebagai executor pertama
* 6. Emit events untuk tracking
*/
constructor(uint256 _chainId, uint256 _quorumPerMillion) {
// Validate quorum (harus 0 < quorum <= 100%)
if (_quorumPerMillion == 0 || _quorumPerMillion > MILLION) {
revert InvalidQuorum();
}
// Set basic parameters
quorumPerMillion = _quorumPerMillion;
chainId = _chainId;
// Deploy governance token dan mint ke deployer
// msg.sender akan menjadi initial token holder
govToken = new WalletGovToken(GOV_TOKEN_SUPPLY, msg.sender);
// Set deployer sebagai executor pertama
executors[msg.sender] = true;
executorCount = 1;
// Emit events untuk tracking
emit Executor(msg.sender, true);
emit QuorumUpdated(0, _quorumPerMillion);
}
// ==================== RECEIVE/FALLBACK FUNCTIONS ====================
/**
* @dev Receive function untuk terima ETH
* Automatically called ketika contract menerima ETH transfer
*/
receive() external payable {
emit Deposit(msg.sender, msg.value, address(this).balance);
}
/**
* @dev Fallback function untuk terima ETH dengan data
* Called ketika function call tidak match dengan existing functions
*/
fallback() external payable {
emit Deposit(msg.sender, msg.value, address(this).balance);
}
// ==================== CORE TRANSACTION FUNCTIONS ====================
/**
* @dev Execute transaksi dengan multiple signatures
* @param _receiver Address tujuan transaksi
* @param _value Jumlah ETH yang akan dikirim (dalam wei)
* @param _calldata Data transaksi (untuk contract calls)
* @param signatures Array signatures dari token holders
* @return bytes Result dari transaction execution
*
* Process Flow:
* 1. Validate executor dan voting weight
* 2. Generate transaction hash dengan current nonce
* 3. Increment nonce untuk prevent replay attacks
* 4. Validate semua signatures dan check quorum
* 5. Execute transaction
* 6. Emit event dan return result
*/
function executeTransaction(
address payable _receiver,
uint256 _value,
bytes memory _calldata,
bytes[] memory signatures
) external onlyExecutors hasVotingWeight nonReentrant returns (bytes memory) {
// Generate hash untuk transaksi ini dengan current nonce
bytes32 _hash = getTransactionHash(nonce, _receiver, _value, _calldata);
// Increment nonce untuk prevent replay attacks
nonce++;
// Validate signatures dan check quorum
_validateSignatures(_hash, signatures);
// Execute transaction
(bool success, bytes memory result) = _receiver.call{value: _value}(_calldata);
if (!success) revert TransactionFailed();
// Emit event untuk tracking
emit ExecuteTransaction(
msg.sender,
_receiver,
_value,
_calldata,
nonce - 1, // nonce yang digunakan untuk transaction ini
_hash,
result
);
return result;
}
/**
* @dev Internal function untuk validate signatures dan check quorum
* @param _hash Transaction hash yang di-sign oleh token holders
* @param signatures Array signatures untuk divalidasi
*
* Validation Process:
* 1. Check minimal ada 1 signature
* 2. Loop semua signatures
* 3. Recover signer address dari setiap signature
* 4. Check signatures terurut untuk prevent duplicates
* 5. Accumulate voting weight dari setiap signer
* 6. Early exit jika sudah mencapai quorum
* 7. Final check apakah total weight >= quorum
*/
function _validateSignatures(bytes32 _hash, bytes[] memory signatures) internal view {
if (signatures.length == 0) revert InsufficientSignatures();
uint256 totalWeight;
address lastSigner = address(0);
for (uint256 i = 0; i < signatures.length; i++) {
// Recover signer address dari signature
address recovered = recover(_hash, signatures[i]);
// Signatures harus terurut untuk prevent duplikasi
// recovered > lastSigner memastikan tidak ada duplicate dan urutan ascending
if (recovered <= lastSigner) revert DuplicateSignature();
lastSigner = recovered;
// Ambil voting weight dari governance token
uint256 weight = govToken.balanceOf(recovered);
totalWeight += weight;
// Early exit untuk save gas jika sudah cukup quorum
if (totalWeight >= quorumPerMillion) break;
}
// Final check apakah total weight cukup untuk quorum
if (totalWeight < quorumPerMillion) revert InsufficientSignatures();
}
/**
* @dev Generate hash untuk transaction yang akan di-sign
* @param _nonce Transaction nonce
* @param to Address tujuan
* @param value Jumlah ETH
* @param _calldata Transaction data
* @return bytes32 Transaction hash
*
* Hash ini yang akan di-sign oleh token holders menggunakan private key mereka.
* Include semua parameter penting untuk prevent manipulation:
* - Contract address (this) - prevent cross-contract attacks
* - Chain ID - prevent cross-chain replay attacks
* - Nonce - prevent replay attacks
* - Transaction parameters - prevent parameter manipulation
*/
function getTransactionHash(
uint256 _nonce,
address to,
uint256 value,
bytes memory _calldata
) public view returns (bytes32) {
return keccak256(
abi.encodePacked(
address(this), // Wallet contract address
chainId, // Chain ID untuk prevent cross-chain attacks
_nonce, // Nonce untuk prevent replay attacks
to, // Transaction recipient
value, // ETH amount
_calldata // Transaction data
)
);
}
/**
* @dev Recover signer address dari signature
* @param _hash Message hash yang di-sign
* @param _signature Signature bytes
* @return address Recovered signer address
*
* Menggunakan Ethereum signed message format untuk keamanan:
* "\x19Ethereum Signed Message:\n32" + hash
*
* Format ini standard untuk wallet signatures dan mencegah
* signature yang dibuat untuk message lain digunakan di sini
*/
function recover(
bytes32 _hash,
bytes memory _signature
) public pure returns (address) {
return keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", _hash)
).recover(_signature);
}
/**
* @dev Check apakah address memiliki voting weight
* @param account Address yang akan dicek
* @return bool True jika memiliki voting weight (token balance > 0)
*/
function hasWeight(address account) public view returns (bool) {
return govToken.balanceOf(account) > 0;
}
// ==================== GOVERNANCE FUNCTIONS ====================
/**
* @dev Update quorum requirement (hanya bisa dipanggil oleh wallet sendiri)
* @param newQuorumPerMillion New quorum dalam per million format
*
* Function ini hanya bisa dipanggil melalui executeTransaction,
* yang berarti butuh voting dari token holders untuk mengubah quorum.
*
* Use case: Adjust quorum berdasarkan perubahan governance needs
*/
function updateQuorumPerMillion(uint256 newQuorumPerMillion) external onlySelf {
if (newQuorumPerMillion == 0 || newQuorumPerMillion > MILLION) {
revert InvalidQuorum();
}
uint256 oldQuorum = quorumPerMillion;
quorumPerMillion = newQuorumPerMillion;
emit QuorumUpdated(oldQuorum, newQuorumPerMillion);
}
/**
* @dev Tambah executor baru (hanya bisa dipanggil oleh wallet sendiri)
* @param newExecutor Address yang akan dijadikan executor
*
* Executor adalah address yang bisa mengeksekusi transaksi yang sudah
* mendapat signatures. Memisahkan role "execution" dari "approval".
*
* Use case: Add team members yang bisa execute transactions
*/
function addExecutor(address newExecutor) external onlySelf {
if (newExecutor == address(0)) revert InvalidAddress();
if (executors[newExecutor]) revert ExecutorAlreadyExists();
executors[newExecutor] = true;
executorCount++;
emit Executor(newExecutor, true);
}
/**
* @dev Remove executor (hanya bisa dipanggil oleh wallet sendiri)
* @param oldExecutor Address executor yang akan dihapus
*
* Tidak bisa remove executor terakhir untuk mencegah wallet "mati".
* Minimal harus ada 1 executor yang bisa mengeksekusi transaksi.
*/
function removeExecutor(address oldExecutor) external onlySelf {
if (executorCount <= 1) revert CannotRemoveLastExecutor();
if (!executors[oldExecutor]) revert ExecutorNotFound();
executors[oldExecutor] = false;
executorCount--;
emit Executor(oldExecutor, false);
}
/**
* @dev Distribute governance tokens (hanya bisa dipanggil oleh wallet sendiri)
* @param to Address tujuan distribusi
* @param amount Jumlah token yang akan didistribusikan
*
* Wallet contract bisa distribute tokens yang dimilikinya untuk
* mengubah voting power distribution dalam sistem governance.
*
* Use case:
* - Initial distribution ke team members
* - Reward untuk contributors
* - Rebalancing voting power
*/
function distributeTokens(address to, uint256 amount) external onlySelf {
if (to == address(0)) revert InvalidAddress();
bool success = govToken.transfer(to, amount);
if (!success) revert TransactionFailed();
emit TokensDistributed(to, amount);
}
// ==================== VIEW FUNCTIONS ====================
/**
* @dev Get informasi wallet secara keseluruhan
* @return balance ETH balance wallet
* @return tokenBalance Governance token balance wallet
* @return currentNonce Nonce saat ini
* @return currentQuorum Quorum requirement saat ini
* @return executorCnt Jumlah executors
*
* One-stop function untuk get semua informasi penting wallet
*/
function getWalletInfo() external view returns (
uint256 balance,
uint256 tokenBalance,
uint256 currentNonce,
uint256 currentQuorum,
uint256 executorCnt
) {
return (
address(this).balance,
govToken.balanceOf(address(this)),
nonce,
quorumPerMillion,
executorCount
);
}
/**
* @dev Get informasi governance token
* @return tokenAddress Address governance token contract
* @return totalSupply Total supply governance token
* @return symbol Token symbol (WGT)
* @return name Token name (WalletGovToken)
*/
function getTokenInfo() external view returns (
address tokenAddress,
uint256 totalSupply,
string memory symbol,
string memory name
) {
return (
address(govToken),
govToken.totalSupply(),
govToken.symbol(),
govToken.name()
);
}
/**
* @dev Get informasi voting untuk user tertentu
* @param user Address user yang akan dicek
* @return votingWeight Voting weight user (token balance)
* @return votingPercentage Persentase voting power user
* @return isExecutor Apakah user adalah executor
* @return hasVotingRights Apakah user memiliki voting rights
*/
function getUserVotingInfo(address user) external view returns (
uint256 votingWeight,
uint256 votingPercentage,
bool isExecutor,
bool hasVotingRights
) {
uint256 weight = govToken.balanceOf(user);
uint256 totalSupply = govToken.totalSupply();
return (
weight,
totalSupply > 0 ? (weight * 100) / totalSupply : 0,
executors[user],
weight > 0
);
}
/**
* @dev Kalkulasi apakah signatures akan memenuhi quorum
* @param signers Array address yang akan sign
* @return meetsQuorum True jika signers akan memenuhi quorum
* @return totalWeight Total weight dari signers
*
* Helper function untuk frontend memvalidasi sebelum submit transaction
*/
function calculateQuorum(address[] memory signers) external view returns (bool meetsQuorum, uint256 totalWeight) {
totalWeight = 0;
for (uint256 i = 0; i < signers.length; i++) {
totalWeight += govToken.balanceOf(signers[i]);
}
meetsQuorum = totalWeight >= quorumPerMillion;
}
/**
* @dev Get required weight untuk memenuhi quorum
* @return uint256 Required weight
*/
function getRequiredWeight() external view returns (uint256) {
return quorumPerMillion;
}
/**
* @dev Validasi apakah transaksi akan berhasil
* @param _receiver Address tujuan transaksi
* @param _value Jumlah ETH
* @param _calldata Transaction data
* @param signatures Proposed signatures
* @return bool True jika transaksi akan berhasil
*
* Comprehensive validation termasuk:
* - Signature validation
* - Quorum check
* - Balance sufficiency
* - Signature ordering
*/
function validateTransaction(
address payable _receiver,
uint256 _value,
bytes memory _calldata,
bytes[] memory signatures
) external view returns (bool) {
// Basic validation
if (signatures.length == 0) return false;
if (_value > address(this).balance) return false;
// Generate transaction hash
bytes32 _hash = getTransactionHash(nonce, _receiver, _value, _calldata);
// Validate signatures dan calculate weight
uint256 totalWeight = 0;
address lastSigner = address(0);
for (uint256 i = 0; i < signatures.length; i++) {
address recovered = recover(_hash, signatures[i]);
// Check signature ordering
if (recovered <= lastSigner) return false;
lastSigner = recovered;
totalWeight += govToken.balanceOf(recovered);
// Early exit jika sudah cukup quorum
if (totalWeight >= quorumPerMillion) return true;
}
return false;
}
/**
* @dev Get daftar addresses dengan voting power tinggi
* @param minWeight Minimum voting weight untuk diinclude
* @return highWeightHolders Array addresses dengan voting weight >= minWeight
* @return weights Array voting weights corresponding ke addresses
*
* Note: Function ini tidak gas-efficient untuk on-chain calls
* karena perlu iterate semua possible addresses.
* Hanya untuk off-chain queries dan analytics.
*/
function getHighWeightHolders(uint256 minWeight) external view returns (
address[] memory highWeightHolders,
uint256[] memory weights
) {
// Simplified implementation - return empty arrays
// Dalam production, implementation ini perlu off-chain indexing
// atau event-based tracking untuk efficiency
return (new address[](0), new uint256[](0));
}
/**
* @dev Emergency function untuk check contract health
* @return isHealthy True jika contract dalam kondisi sehat
* @return healthReport String description kondisi contract
*/
function checkHealth() external view returns (bool isHealthy, string memory healthReport) {
// Check various health indicators
bool hasExecutors = executorCount > 0;
bool validQuorum = quorumPerMillion > 0 && quorumPerMillion <= MILLION;
bool tokenExists = address(govToken) != address(0);
bool tokenSupplyValid = govToken.totalSupply() == GOV_TOKEN_SUPPLY;
isHealthy = hasExecutors && validQuorum && tokenExists && tokenSupplyValid;
if (!isHealthy) {
if (!hasExecutors) return (false, "No executors available");
if (!validQuorum) return (false, "Invalid quorum configuration");
if (!tokenExists) return (false, "Governance token not found");
if (!tokenSupplyValid) return (false, "Invalid token supply");
}
healthReport = "Contract is healthy";
}
// ==================== EMERGENCY FUNCTIONS ====================
/**
* @dev Emergency pause function (hanya melalui governance)
* Function ini bisa ditambahkan untuk emergency situations
*
* Note: Tidak diimplement dalam versi basic ini,
* tapi bisa ditambahkan sebagai extension
*/
/**
* @dev Get contract version
* @return version String version identifier
*/
function getVersion() external pure returns (string memory version) {
return "WeightedMultiSigWallet-v1.0.0";
}
}
Penjelasan Key Features
1. Weighted Voting System
function _validateSignatures(bytes32 _hash, bytes[] memory signatures) internal view {
uint256 totalWeight;
for (uint256 i = 0; i < signatures.length; i++) {
address recovered = recover(_hash, signatures[i]);
uint256 weight = govToken.balanceOf(recovered);
totalWeight += weight;
}
require(totalWeight >= quorumPerMillion);
}
Tujuan: Setiap signature memiliki weight berdasarkan token balance. Total weight harus mencapai quorum untuk approve transaksi.
2. Signature Ordering Protection
if (recovered <= lastSigner) revert DuplicateSignature();
lastSigner = recovered;
Tujuan: Mencegah duplicate signatures dan memastikan signatures dalam urutan ascending untuk security.
3. Replay Attack Protection
bytes32 _hash = getTransactionHash(nonce, _receiver, _value, _calldata);
nonce++;
Tujuan: Setiap transaksi menggunakan nonce yang unique, mencegah replay attacks.
4. Cross-Chain Protection
return keccak256(abi.encodePacked(address(this), chainId, _nonce, to, value, _calldata));
Tujuan: Include chain ID dalam hash untuk mencegah signatures digunakan di chain lain.
4. Security Features
Comprehensive Security Model
1. Access Control Layers
// Layer 1: Executor validation
modifier onlyExecutors() {
if (!executors[msg.sender]) revert NotExecutor();
_;
}
// Layer 2: Voting weight validation
modifier hasVotingWeight() {
if (!hasWeight(msg.sender)) revert InsufficientWeight();
_;
}
// Layer 3: Self-call validation untuk governance
modifier onlySelf() {
if (msg.sender != address(this)) revert NotSelf();
_;
}
2. Reentrancy Protection
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract WeightedMultiSigWallet is ReentrancyGuard {
function executeTransaction(...) external nonReentrant {
// Protected from reentrancy attacks
}
}
3. Signature Security
function recover(bytes32 _hash, bytes memory _signature) public pure returns (address) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _hash)).recover(_signature);
}
Features:
- Ethereum signed message format
- ECDSA signature verification
- Address recovery dari signatures
4. Input Validation
// Validate quorum range
if (_quorumPerMillion == 0 || _quorumPerMillion > MILLION) {
revert InvalidQuorum();
}
// Validate addresses
if (newExecutor == address(0)) revert InvalidAddress();
// Validate array lengths
if (signatures.length == 0) revert InsufficientSignatures();
Security Best Practices
1. Custom Errors untuk Gas Efficiency
error NotExecutor();
error InsufficientWeight();
error DuplicateSignature();
Benefits:
- More gas-efficient than require strings
- Clear error identification
- Better debugging experience
2. Immutable Variables
WalletGovToken public immutable govToken;
address public immutable walletContract;
Benefits:
- Gas savings (no SLOAD operations)
- Security guarantee (cannot be changed)
- Clear contract design
3. Event Emission untuk Transparency
event ExecuteTransaction(address indexed executor, address payable indexed to, uint256 value, bytes data, uint256 nonce, bytes32 hash, bytes result);
Benefits:
- Complete audit trail
- Off-chain monitoring capabilities
- Transparency untuk stakeholders
5. Gas Optimization
Optimization Techniques
1. Packed Structs (untuk future extensions)
struct PackedTransactionData {
uint128 value; // 16 bytes - enough untuk most ETH amounts
uint64 nonce; // 8 bytes - enough untuk many transactions
uint32 timestamp; // 4 bytes - enough untuk timestamps
uint32 gasLimit; // 4 bytes - enough untuk gas limits
// Total: 32 bytes = 1 storage slot
}
2. Efficient Looping
// Early exit untuk save gas
for (uint256 i = 0; i < signatures.length; i++) {
totalWeight += govToken.balanceOf(recovered);
if (totalWeight >= quorumPerMillion) break; // Early exit
}
3. Batch Operations
function getVotingWeights(address[] calldata accounts) external view returns (uint256[] memory weights) {
weights = new uint256[](accounts.length);
for (uint256 i = 0; i < accounts.length; i++) {
weights[i] = govToken.balanceOf(accounts[i]);
}
}
4. Memory vs Storage Optimization
// Use memory untuk temporary data
bytes[] memory signatures = ...;
// Use storage untuk persistent data
mapping(address => bool) public executors;
Gas Usage Analysis
Expected Gas Costs
Contract Deployment: ~2,500,000 gas
Token Distribution: ~50,000 gas
Add Executor: ~45,000 gas
Execute Transaction (2 signatures): ~120,000 gas
Execute Transaction (5 signatures): ~180,000 gas
6. Kompilasi dan Validasi
Compile Contracts
# Compile semua contracts
forge build
# Compile dengan optimization
forge build --optimize --optimizer-runs 1000
# Output yang diharapkan:
# [⠒] Compiling...
# [⠢] Compiling 3 files with 0.8.26
# [⠆] Solc 0.8.26 finished in 2.34s
# Compiler run successful!
Check Compilation Output
# Check compiled contracts
ls out/
# Expected output:
# WalletGovToken.sol/
# WeightedMultiSigWallet.sol/
# Check specific contract artifacts
ls out/WeightedMultiSigWallet.sol/
# WeightedMultiSigWallet.json
# Check contract size
forge build --sizes
Validate Contract Interfaces
Buat file src/interfaces/IWeightedMultiSigWallet.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
interface IWeightedMultiSigWallet {
// Core functions
function executeTransaction(
address payable _receiver,
uint256 _value,
bytes memory _calldata,
bytes[] memory signatures
) external returns (bytes memory);
function getTransactionHash(
uint256 _nonce,
address to,
uint256 value,
bytes memory _calldata
) external view returns (bytes32);
function recover(bytes32 _hash, bytes memory _signature) external pure returns (address);
// View functions
function getWalletInfo() external view returns (uint256, uint256, uint256, uint256, uint256);
function getUserVotingInfo(address user) external view returns (uint256, uint256, bool, bool);
function calculateQuorum(address[] memory signers) external view returns (bool, uint256);
// Governance functions
function updateQuorumPerMillion(uint256 newQuorumPerMillion) external;
function addExecutor(address newExecutor) external;
function removeExecutor(address oldExecutor) external;
function distributeTokens(address to, uint256 amount) external;
}
Create Deployment Helper
Buat file src/utils/DeploymentHelper.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "../WeightedMultiSigWallet.sol";
import "../WalletGovToken.sol";
/**
* @title DeploymentHelper
* @dev Helper contract untuk validate deployment dan setup
*/
contract DeploymentHelper {
/**
* @dev Validate wallet deployment
* @param wallet Address wallet yang akan divalidate
* @return isValid True jika deployment valid
* @return report Error report jika ada issues
*/
function validateWalletDeployment(address wallet) external view returns (bool isValid, string memory report) {
if (wallet == address(0)) {
return (false, "Wallet address is zero");
}
try WeightedMultiSigWallet(payable(wallet)).getWalletInfo() returns (uint256, uint256, uint256, uint256, uint256) {
return (true, "Wallet deployment valid");
} catch {
return (false, "Wallet contract not responsive");
}
}
/**
* @dev Calculate optimal quorum untuk given token distribution
* @param tokenBalances Array of token balances
* @return optimalQuorum Recommended quorum value
*/
function calculateOptimalQuorum(uint256[] memory tokenBalances) external pure returns (uint256 optimalQuorum) {
uint256 totalSupply = 0;
for (uint256 i = 0; i < tokenBalances.length; i++) {
totalSupply += tokenBalances[i];
}
// Recommend 60% untuk most cases
optimalQuorum = (totalSupply * 60) / 100;
}
}
Final Validation Script
Buat file scripts/validate-contracts.sh
:
#!/bin/bash
echo "=== Contract Validation ==="
echo ""
# Check compilation
echo "1. Checking compilation..."
forge build > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo "✓ Contracts compile successfully"
else
echo "✗ Compilation failed"
exit 1
fi
# Check contract sizes
echo ""
echo "2. Checking contract sizes..."
forge build --sizes | grep -E "(WeightedMultiSigWallet|WalletGovToken)"
# Check for common issues
echo ""
echo "3. Checking for common issues..."
# Check untuk floating pragma
if grep -r "pragma solidity \^" src/; then
echo "⚠ Warning: Floating pragma found. Consider using fixed version."
fi
# Check untuk missing SPDX
if ! grep -r "SPDX-License-Identifier" src/; then
echo "✗ Error: Missing SPDX license identifier"
fi
# Check untuk proper imports
if grep -r "import \"" src/; then
echo "✓ Imports using proper syntax"
fi
echo ""
echo "=== Validation Complete ==="
Contract Documentation
Tambahkan comprehensive NatSpec documentation:
/**
* @title WeightedMultiSigWallet
* @author MultiSigWallet Team
* @notice Multi-signature wallet dengan weighted voting untuk treasury management
* @dev Implements EIP-191 signatures dengan governance token-based weighting
*
* @custom:security-contact security@multisigwallet.com
* @custom:version 1.0.0
* @custom:audit-status Pending
*/
Contracts sudah siap! Selanjutnya kita akan membuat comprehensive testing dan deployment procedures.