Skip to content

Instantly share code, notes, and snippets.

@IdrisAkintobi
Created September 11, 2024 08:43
Show Gist options
  • Save IdrisAkintobi/59e137b1bca2ac77eccfd43f4f7ef85f to your computer and use it in GitHub Desktop.
Save IdrisAkintobi/59e137b1bca2ac77eccfd43f4f7ef85f to your computer and use it in GitHub Desktop.
A multisig contract example.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract Multisig {
uint8 public quorum;
uint8 public noOfValidSigners;
uint256 public txCount;
uint8 public newQuorum;
uint8 private newQuorumSignerCount;
mapping(address => bool) proposedNewQuorum;
address[] public proposedNewQuorumAddresses;
struct Transaction {
uint256 id;
uint256 amount;
address sender;
address recipient;
bool isCompleted;
uint256 timestamp;
uint256 noOfApproval;
address tokenAddress;
address[] transactionSigners;
}
mapping(address => bool) isValidSigner;
mapping(uint256 => Transaction) transactions; // txId -> Transaction
mapping(address => mapping(uint256 => bool)) hasSigned;
event ProposedNewQuorum(address indexed proposer, uint8 proposedQuorum);
constructor(uint8 _quorum, address[] memory _validSigners) {
require(_validSigners.length > 1, "few valid signers");
require(_quorum > 1, "quorum is too small");
for (uint256 i = 0; i < _validSigners.length; i++) {
require(_validSigners[i] != address(0), "zero address not allowed");
require(!isValidSigner[_validSigners[i]], "signer already exist");
isValidSigner[_validSigners[i]] = true;
}
noOfValidSigners = uint8(_validSigners.length);
if (!isValidSigner[msg.sender]) {
isValidSigner[msg.sender] = true;
noOfValidSigners += 1;
}
require(
_quorum <= noOfValidSigners,
"quorum greater than valid signers"
);
quorum = _quorum;
}
function setNewQuorum(uint8 proposedQuorum) public {
require(msg.sender != address(0), "address zero found");
require(isValidSigner[msg.sender], "invalid signer");
require(proposedQuorum != quorum, "Cannot use existing quorum");
require(proposedQuorum > 1, "Quorum must be more than 1");
require(
proposedQuorum <= noOfValidSigners,
"Quorum cannot exceed signers"
);
require(!proposedNewQuorum[msg.sender], "You already proposed");
if (proposedQuorum != newQuorum) {
// Reset quorum proposals if a new proposal is made
newQuorum = proposedQuorum;
newQuorumSignerCount = 0;
// Clear previous proposals
for (uint8 i = 0; i < proposedNewQuorumAddresses.length; i++) {
delete proposedNewQuorum[proposedNewQuorumAddresses[i]];
}
delete proposedNewQuorumAddresses;
emit ProposedNewQuorum(msg.sender, proposedQuorum);
}
// Record the new proposal
proposedNewQuorum[msg.sender] = true;
newQuorumSignerCount++;
proposedNewQuorumAddresses.push(msg.sender);
// If all signers agree, update the quorum
if (newQuorumSignerCount == noOfValidSigners) {
quorum = proposedQuorum;
newQuorumSignerCount = 0;
}
}
function transfer(
uint256 _amount,
address _recipient,
address _tokenAddress
) external {
require(msg.sender != address(0), "address zero found");
require(isValidSigner[msg.sender], "invalid signer");
require(_amount > 0, "can't send zero amount");
require(_recipient != address(0), "address zero found");
require(_tokenAddress != address(0), "address zero found");
require(
IERC20(_tokenAddress).balanceOf(address(this)) >= _amount,
"insufficient funds"
);
uint256 _txId = txCount + 1;
Transaction storage trx = transactions[_txId];
trx.id = _txId;
trx.amount = _amount;
trx.recipient = _recipient;
trx.sender = msg.sender;
trx.timestamp = block.timestamp;
trx.tokenAddress = _tokenAddress;
trx.noOfApproval += 1;
trx.transactionSigners.push(msg.sender);
hasSigned[msg.sender][_txId] = true;
txCount += 1;
}
function approveTx(uint256 _txId) external {
require(_txId != 0, "invalid tx id");
Transaction storage trx = transactions[_txId];
require(
IERC20(trx.tokenAddress).balanceOf(address(this)) >= trx.amount,
"insufficient funds"
);
require(!trx.isCompleted, "transaction already completed");
require(trx.noOfApproval < quorum, "approvals already reached");
require(isValidSigner[msg.sender], "not a valid signer");
require(!hasSigned[msg.sender][_txId], "can't sign twice");
hasSigned[msg.sender][_txId] = true;
trx.noOfApproval += 1;
trx.transactionSigners.push(msg.sender);
if (trx.noOfApproval == quorum) {
trx.isCompleted = true;
IERC20(trx.tokenAddress).transfer(trx.recipient, trx.amount);
}
}
}
// In most `maker and checker ` systems the maker might not essentially have the permission to approve. I think a separation of concern can also be implemented there. where we don't have to make the sender auto approve the transaction.
// I also think we should not automatically set the sender as a valid signer and stick to the use of the array passed in the constructor parameter.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/Multisig.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// Create a simple ERC20 token for testing
contract TestToken is ERC20 {
constructor() ERC20("TestToken", "TTK") {
_mint(msg.sender, 1000 * 10 ** 18); // mint 1000 TTK to msg.sender
}
}
contract MultisigTest is Test {
Multisig public multisig;
TestToken public token;
address[] public validSigners;
address public signer1;
address public signer2 = address(0x2);
address public signer3 = address(0x3);
address public recipient = address(0x4);
function setUp() public {
signer1 = address(this);
validSigners.push(signer1);
validSigners.push(signer2);
validSigners.push(signer3);
// Initialize multisig contract with quorum 2 and 3 signers
multisig = new Multisig(2, validSigners);
// Deploy a test ERC20 token and transfer 500 tokens to the multisig contract
token = new TestToken();
token.transfer(address(multisig), 500 * 10 ** 18);
}
// Test if the contract initializes with the correct quorum
function testInitialQuorum() public view {
assertEq(multisig.quorum(), 2);
assertEq(multisig.noOfValidSigners(), 3); // 3 signers in total
}
// Test setting a new quorum
function testSetNewQuorum() public {
vm.prank(signer1);
multisig.setNewQuorum(3);
vm.prank(signer2);
multisig.setNewQuorum(3);
vm.prank(signer3);
multisig.setNewQuorum(3);
assertEq(multisig.quorum(), 3); // Quorum should now be updated to 3
}
// Test transferring tokens from the multisig contract
function testTransfer() public {
vm.prank(signer1);
multisig.transfer(100 * 10 ** 18, recipient, address(token));
// Verify transaction details
assertEq(token.balanceOf(address(multisig)), 500 * 10 ** 18); // Before transfer
assertEq(multisig.txCount(), 1); // One transaction created
}
// Test approving and completing a transaction
function testApproveTransaction() public {
// First, create a transaction by signer1
vm.prank(signer1);
multisig.transfer(100 * 10 ** 18, recipient, address(token));
// Approve the transaction by signer2
vm.prank(signer2);
multisig.approveTx(1);
// After reaching quorum (2), the transaction should be completed
assertEq(token.balanceOf(recipient), 100 * 10 ** 18); // Recipient should have received 100 tokens
assertEq(token.balanceOf(address(multisig)), 400 * 10 ** 18); // Multisig balance reduced
}
// Test that a signer cannot sign twice
function testCannotSignTwice() public {
vm.prank(signer1);
multisig.transfer(100 * 10 ** 18, recipient, address(token));
vm.expectRevert("can't sign twice");
vm.prank(signer1);
multisig.approveTx(1); // This should revert because signer1 already approved the transaction on initiation
}
// Test that a quorum higher than the number of signers cannot be set
function testCannotSetQuorumHigherThanSigners() public {
vm.prank(signer1);
vm.expectRevert("Quorum cannot exceed signers");
multisig.setNewQuorum(5); // Trying to set a quorum higher than the number of signers
}
// Test that transferring more tokens than the contract balance should fail
function testCannotTransferMoreThanBalance() public {
vm.prank(signer1);
vm.expectRevert("insufficient funds");
multisig.transfer(600 * 10 ** 18, recipient, address(token)); // Attempting to transfer more than the balance
}
// Test that a signer cannot propose the same quorum twice
function testCannotProposeSameQuorumTwice() public {
vm.prank(signer1);
multisig.setNewQuorum(3);
vm.expectRevert("You already proposed");
vm.prank(signer1);
multisig.setNewQuorum(3); // Trying to propose the same quorum again should fail
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment