Here is my contract. Probably could have made it in a way that is bit more organized or used less lines of code, but it achieves all of the functionality it needs to have, so Iām happy with it.
Please let me know any suggestions, questions or anything I need to fix. Going to go watch Filipās solution now.

pragma solidity 0.7.5;
pragma experimental ABIEncoderV2;
import "./Ownable.sol";
contract multiSigWallet is Ownable {
address[] public owners;
uint public signersRequired;
uint numOfTransfers = 0;
struct Transfer {
address approver;
uint id;
address payable recipient;
uint amountToTransfer;
uint approvalsReceived;
bool approved;
}
Transfer[] public transferRequests;
mapping(address => mapping(uint => bool)) public approvals;
function addOwners(address owner2, address owner3, uint minimumSignersRequired) public onlyOwner returns(address[] memory) {
owners.push(owner);
owners.push(owner2);
owners.push(owner3);
require(minimumSignersRequired > 0 && minimumSignersRequired < 4, "Minimum 1 signer required, can't have more signers than owners");
require(owner2 != owner3, "You cannot put the same address as two different owners");
require(owner != owner2, "You cannot put the same address as two different owners");
require(owner != owner3, "You cannot put the same address as two different owners");
signersRequired = minimumSignersRequired;
return owners;
}
function deposit() public payable returns (uint) {
return address(this).balance;
}
function getBalance() public view returns (uint) {
return address(this).balance;
}
function createTransferRequest(address payable recipient, uint amount) public returns(string memory){
require(address(this).balance >= amount, "Balance not sufficient");
require(recipient != address(this), "You shouldn't transfer money from the multisig wallet to the multisig wallet");
bool isOwner = false;
for (uint i = 0; i < owners.length; i++) {
if (owners[i] == msg.sender) {
isOwner = true;
break;
}
}
require(isOwner, "Only owners can perfrom transfers");
uint previousSenderBalance = address(this).balance;
_logTransferRequest(msg.sender, recipient, amount); //call the internal/private function where the computation takes place.
return "Transfer request successfully created! Please get other owners to approve the transfer and it will be executed.";
}
function _logTransferRequest(address from, address payable to, uint amount) private {
transferRequests.push(Transfer(from, numOfTransfers, to, amount, 1, false));
approvals[from][numOfTransfers] = true;
numOfTransfers++;
}
function approveTransfers(uint id) public payable returns (string memory){
require(id < transferRequests.length, "There is no transfer with that ID number");
require(transferRequests[id].approved == false, "This transfer has already been approved and executed.");
require(transferRequests[id].approver != msg.sender, "You created and approved this transfer, other owners need to approve it.");
require(transferRequests[id].amountToTransfer < address(this).balance, "Available funds not sufficient to make this transfer.");
require(approvals[msg.sender][id] == false, "You have already approved this transfer, the other owners need to approve it for it to be executed");
bool isOwner = false;
for (uint i = 0; i < owners.length; i++) {
if (owners[i] == msg.sender) {
isOwner = true;
break;
}
}
require(isOwner == true, "Only the owners of the wallet can approve transfers");
transferRequests[id].approvalsReceived++;
approvals[msg.sender][id] = true;
if(transferRequests[id].approvalsReceived == signersRequired) {
transferRequests[id].approved = true;
uint previousSenderBalance = address(this).balance;
transferRequests[id].recipient.transfer(transferRequests[id].amountToTransfer);
assert(address(this).balance == previousSenderBalance - transferRequests[id].amountToTransfer);
return "You have approved the transfer. The transfer has recieved enough approvals and has been executed";
}
return "You have approved the transfer, if the transfer has not executed it needs to be approved by other owners.";
}
}