Trust Bitcoin Info
Trust Bitcoin has an edge in DeFi services because of its best-in-class offerings. With the likes of services like mining, smart contracts, digital wallets, governance and creating dApps. Also, miners will have exceptional benefits for adding a block to the Trust Bitcoin blockchain.
TrustNet Score
The TrustNet Score evaluates crypto projects based on audit results, security, KYC verification, and social media presence. This score offers a quick, transparent view of a project's credibility, helping users make informed decisions in the Web3 space.
Real-Time Threat Detection
Real-time threat detection, powered by Cyvers.io,
is currently not
activated
for this project.
This advanced feature provides continuous monitoring and instant alerts to safeguard your assets from potential security threats. Real-time detection enhances your project's security by proactively identifying and mitigating risks.
For more information, click here.
Summary and Final Words
No crucial issues found
The contract does not contain issues of high or medium criticality. This means that no known vulnerabilities were found in the source code.
Ownership is not renounced
The owner retains significant control, which could potentially be used to modify key contract parameters.
Contract is not upgradeable
The contract does not use proxy patterns or other mechanisms to allow future upgrades. Its behavior is locked in its current state.
Scope of Work
This audit encompasses the evaluation of the files listed below, each verified with a SHA-1 Hash. The team referenced above has provided the necessary files for assessment.
The auditing process consists of the following systematic steps:
- Specification Review: Analyze the provided specifications, source code, and instructions to fully understand the smart contract's size, scope, and functionality.
- Manual Code Examination: Conduct a thorough line-by-line review of the source code to identify potential vulnerabilities and areas for improvement.
- Specification Alignment: Ensure that the code accurately implements the provided specifications and intended functionalities.
- Test Coverage Assessment: Evaluate the extent and effectiveness of test cases in covering the codebase, identifying any gaps in testing.
- Symbolic Execution: Analyze the smart contract to determine how various inputs affect execution paths, identifying potential edge cases and vulnerabilities.
- Best Practices Evaluation: Assess the smart contracts against established industry and academic best practices to enhance efficiency, maintainability, and security.
- Actionable Recommendations: Provide detailed, specific, and actionable steps to secure and optimize the smart contracts.
A file with a different Hash has been intentionally or otherwise modified after the security review. A different Hash may indicate a changed condition or potential vulnerability that was not within the scope of this review.
Final Words
The following provides a concise summary of the audit report, accompanied by insightful comments from the auditor. This overview captures the key findings and observations, offering valuable context and clarity.
Smart Contract Analysis Statement
Contract Analysis
The MultiSigFactory contract implements a permissionless factory that deploys multisignature wallets of type MultiSigWalletV2 with CREATE2, at addresses that commit to the creator and the configuration, with two to fifty signers and a threshold of at least two. Each wallet holds native TBC, ERC-20, ERC-721 and ERC-1155 assets and executes arbitrary calls once enough current signers have approved the exact proposal content, which is bound to a hash. Proposals can be cancelled, approvals of removed signers lapse with their signer term, each call receives the gas limit committed in its proposal, and the wallet can confirm messages for ERC-1271.
This re-audit reviewed the revised contracts before deployment; at the time of the re-audit no instance existed on TBC Network or BNB Smart Chain. Neither contract is upgradeable or has an owner, and neither can charge fees, mint, burn, pause or blacklist.
Ownership Privileges
The ownership of the contracts has been designed without any owner or administrator: anyone can create wallets through the factory, and each wallet is governed only by its own signers acting together. The signers of a wallet retain full privileges including:
- Moving any native TBC, token or NFT the wallet holds and calling any contract, including timelocks and bridges the wallet controls, once the threshold of current signers has approved the exact proposal
- Adding, removing and replacing signers, changing the threshold, cancelling pending proposals, maintaining a precompile allow-list and approving messages for ERC-1271, all through the wallet's own approved proposals
- Executing any proposal that has reached the threshold, which any single signer can do at a time of that signer's choosing, with exactly the gas limit the proposal committed to
- Creating wallets through the factory, which is open to anyone with any name and signer set and a threshold of at least two
- No owner, administrator or factory function can access a wallet's assets or change its configuration
- The contracts cannot mint, burn, pause, blacklist or charge fees, and they are not upgradeable
- A failed or replaced proposal stays executable until it is cancelled, and cancelling needs the threshold
- An approved ERC-1271 message hash stays valid until the wallet revokes it, like a signature
Security Features
The contracts implement several positive security features:
- Approvals are bound to a hash of the chain, the wallet and the full proposal content, and count only while the signer's current term lasts
- Pending proposals can be cancelled permanently, and execution is protected against reentrancy, marks the proposal as executed before the call and reverts entirely if the call fails
- Calls with calldata to addresses without code are rejected unless the target is a precompile, the target always receives the committed gas, and copied return data is bounded
- Wallet addresses commit to the creator and the configuration, the wallet rejects its own address as a signer, and the signer sets stayed consistent in every tested and symbolically explored case
Points Requiring Attention
No critical, high, medium or low issues remain. All findings of the first review are resolved except that the compiler and library versions are unchanged. Signers should compute the proposal hash themselves from the proposal they intend to approve, and should revoke their approvals before cancelling a failed or replaced proposal.
Informational observations include older compiler and library versions.
Note - This Audit report consists of a security analysis of the MultiSigFactory and MultiSigWalletV2 smart contracts. This analysis did not include economic analysis of the contract's tokenomics. Moreover, we only audited the main contracts for the Trust Bitcoin team. Other contracts associated with the project were not audited by our team, in particular the timelocks, bridges and tokens that the wallets are meant to hold or govern. We recommend investors do their own research before investing.
Files and details
Findings and Audit result
medium Issues | 1 findings
Resolved
#1 medium Issue
Failed and superseded transactions remain executable indefinitely
Every submitted transaction stays pending until it is executed. There is no way to cancel a proposal, no expiry and no nonce that would retire older proposals. When an execution fails, for example because the wallet does not yet hold enough native balance or the target reverts at that moment, the whole call reverts and the proposal falls back to its pending state with every approval intact. If the signers then settle the matter in another way, the old proposal remains a live, fully approved instruction that any single signer can execute later, including a signer who never approved it, as soon as the conditions change. The only way to neutralise it is for enough approvers to revoke, one transaction each, and any signer can execute before those revocations are mined. Proposals that were left with partial approvals are affected as well: lowering the threshold later makes them executable at once.
low Issues | 3 findings
Resolved
#1 low Issue
Approvals of a removed signer come back when the address is added again
The wallet counts approvals at execution time from the current signer set, which correctly ignores the approvals of removed signers. removeSigner and replaceSigner, however, leave the approvedBy entries of the departing signer in place. If the same address becomes a signer again later, through addSigner or replaceSigner, every earlier approval it gave on a proposal that is still pending counts again immediately, without the signer doing anything in its new term.
Resolved
#2 low Issue
Wallet addresses are predictable and can be taken by anyone
createMultiSigWallet deploys every wallet with CREATE, so its address depends only on the factory's address and nonce, not on the creator or the configuration. Anyone can call the function, so anyone can take the next address. If the team computes the address of a future wallet in advance, for example to queue an ownership transfer behind the twelve hour timelock or to have funds or roles assigned before the wallet is created, a front-runner can create a wallet with its own signers at exactly that address. The same applies across chains: a factory deployed at the same address on TBC Network and BNB Smart Chain hands out the same wallet addresses to different creators, so assets sent to a team wallet address on a chain where the team has not created that wallet can be captured by whoever creates it there first.
Resolved
#3 low Issue
Calls to addresses without code are reported as successful
executeTransaction forwards the stored calldata with a low-level call and treats every call that does not revert as a success. A call to an address without code never reverts, so a proposal that targets a contract that does not exist on this chain, or a mistyped address, is marked as executed and emits TransactionExecuted although nothing happened. This is a realistic mistake for a team that operates on TBC Network and BNB Smart Chain, where the same token has different addresses; the USDT address of BNB Smart Chain, for instance, has no code on TBC Network. Native value attached to such a proposal is sent to that address and is most likely lost.
optimization Issues | 5 findings
Resolved
#1 optimization Issue
Pack the executed flag with the target address
Transaction stores the target address, the value, the calldata and the executed flag in four slots. Marking a proposal as executed therefore initialises a slot that is still zero, and execution reads two separate slots for the flag and the target.
Resolved
#2 optimization Issue
Use custom errors instead of revert strings
Every check uses require with a string message. Custom errors are cheaper to deploy and to revert with, and because the factory embeds the full wallet bytecode, the saving applies to every wallet creation as well as to the factory deployment.
Resolved
#3 optimization Issue
Approval counting reads storage repeatedly
_activeApprovalCount loops over all signers with two storage reads per signer. It runs on every approval and revocation only to fill the event field, and again on execution. With fifty signers each of these calls spends more than 200,000 gas on the count.
Resolved
#4 optimization Issue
The factory stores data its event already records
Each creation stores the name string, the wallet, the creator and the timestamp, appends to the creator's list and writes two more mappings. Most of this information is already part of MultiSigWalletCreated and can be indexed off-chain; on-chain, mainly isFactoryWallet is needed.
Pending
#5 optimization Issue
Approval and execution hash the stored calldata again
approveTransaction and executeTransaction compute the content hash from storage every time, which reads the whole calldata of the proposal word by word. The cost of every approval therefore grows with the size of the calldata: in our measurement an approval cost 41,680 gas for a proposal without calldata, 112,054 gas with 1 KB and 604,544 gas with 8 KB, and execution reads the calldata once more for the hash before it copies it for the call.
informational Issues | 8 findings
Resolved
#1 informational Issue
Approvals refer to a transaction number, not to its content
Proposals receive their number when the submission is mined, and approveTransaction, revokeApproval and executeTransaction take only that number. Nothing binds an approval to the target, value and calldata the signer reviewed. When signers coordinate off-chain, for example by asking each other to approve number 12, or prepare an approval before the submission is mined, whoever submits first decides what that number stands for. A malicious signer can watch for a colleague's submission and front-run it so that the announced number belongs to its own proposal. Whether this matters depends entirely on how signers approve: a signer who reads and decodes the stored proposal before approving is not affected. We rate it as informational on the understanding that signers do so before every approval; if they approve by number alone, for example through the explorer's contract interface or a script, the risk becomes material.
Resolved
#2 informational Issue
Registry entries do not guarantee multi-party control
The factory is permissionless and accepts any name, any set of two to fifty signers and any threshold from one upward. A threshold of one turns a wallet into a single-key account, and a name such as Treasury can be reused by anyone. isFactoryWallet and the stored name therefore only confirm that the wallet runs the genuine code, not who controls it or how many keys are needed. The creator recorded by the factory has no rights in the wallet and does not need to be a signer, and signers are listed without their consent.
Resolved
#3 informational Issue
The wallet's own address is accepted as a signer
Neither the constructor nor addSigner and replaceSigner reject the wallet's own address, or other addresses that cannot sign, such as the factory. A wallet that lists itself counts towards the signer total, but its approval can only be given through an executed proposal, which already needs the threshold. If the other signers fall below the threshold, for example two people plus the wallet with a threshold of three, no proposal can ever be executed again, including the one that would repair the configuration.
Resolved
#4 informational Issue
Initial signers and threshold are not emitted
The wallet constructor sets the signer set and threshold without emitting SignerAdded or ThresholdChanged, and the factory event only carries the number of signers and the threshold. Anyone who rebuilds a wallet's configuration from events, such as monitoring, indexers or a signing interface, does not see the initial signers and has to read storage instead. Later changes are emitted, so the history is incomplete only at its start.
Resolved
#5 informational Issue
The executing signer decides how much gas the target receives
executeTransaction forwards all available gas to the target and copies all returned data into memory. If the target itself fails, the whole execution reverts, so a failed call can never be recorded as executed. A target that catches the failure of an inner call, as some routers, relayers and keeper contracts do, behaves differently: the signer who executes can pick a gas limit at which the inner call runs out of gas while the outer call still succeeds, and the proposal is consumed in that degraded state. Separately, a target that returns a very large amount of data makes execution expensive or impossible, because the wallet copies and returns all of it.
Resolved
#6 informational Issue
No fallback function and no ERC-1271 signature support
The wallet only accepts native coin through receive(), so a transfer that carries calldata, as some routers and bridges send, reverts. The wallet also does not implement isValidSignature from ERC-1271, so it cannot act as a signer for contracts that verify signatures, such as permit-style approvals, off-chain order books or a bridge whose source verifier is expected to be a contract signer.
Resolved
#7 informational Issue
Contract name reused from the deployed wallet
MultiSigFactory.sol declares a contract named MultiSigWallet, which is also the name of the deployed 3-of-4 wallet in contracts/MultiSigWallet.sol. The two contracts have different constructors and behaviour. Within one build project, deployment scripts, explorer verification and artifact lookups that use the short name become ambiguous, and the wrong contract can be deployed or verified.
Pending
#8 informational Issue
Code quality and documentation
The public and external functions only carry one-line notices without @param and @return tags, although the deployed wallet documents them. The inline assembly in _revertWithReason is not marked memory-safe, which keeps the IR optimizer from using its memory mover. In addition, the compiler version 0.8.28 has seven known bugs, none of which applies to this code with its settings, and later releases fix them, and OpenZeppelin 4.9.6 is the last release of a branch that now only receives critical fixes. Revert strings longer than 32 bytes, such as the one for an existing new signer, increase code size, and the factory computes the signer count before it validates the name.