Skip to main content

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​

  1. Arsitektur Smart Contract
  2. Implementasi WalletGovToken
  3. Implementasi WeightedMultiSigWallet
  4. Security Features
  5. Gas Optimization
  6. Kompilasi dan Validasi

1. Arsitektur Smart Contract​

Overview Sistem​

MultiSigWallet menggunakan sistem dual-contract architecture yang terdiri dari:

  1. WalletGovToken: ERC20 governance token yang menentukan voting weight
  2. 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.