Created
September 11, 2024 08:43
-
-
Save IdrisAkintobi/59e137b1bca2ac77eccfd43f4f7ef85f to your computer and use it in GitHub Desktop.
A multisig contract example.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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