Hi @thecil
Thanks for the reply. Here’s my solution. For simplicity I’ve included all contracts in the same file. I’m interested why embedding a mapping inside a Struct is seen as bad practice as it would be seen as good practice in OOP. The approvers are a member property of a particular transaction so seemed natural to include that in the Struct. I also attempted the exercise without watching the double mapping video and therefore didn’t really think a double mapping was necessary.
Please refer to this stack overflow comment regarding embedding mappings:
https://stackoverflow.com/questions/66072737/use-mappings-inside-structs-in-solidity
pragma solidity 0.8.0;
pragma abicoder v2;
contract Ownership {
//mapping that is checked when an address attempts to approve a transaction.
mapping(address => bool) public owners;
modifier anyOwner() {
require(owners[msg.sender] == true, "you are not an owner of this wallet!");
_;
}
}
contract Wallet {
uint balance;
Tx[] public txs;
struct Tx {
address initiator;
address payable recipient;
uint amount;
mapping(address => bool) approvers;
uint approvals;
}
modifier checkRecipient(address payable recipient) {
require(msg.sender != recipient, "sender and recipient of this transfer are the same!");
_;
}
modifier checkFunds(uint amount) {
require(balance >= amount, "You do not have enough funds to perform this transfer");
_;
}
modifier checkTransaction(uint transactionId) {
require(transactionId < txs.length, "transaction with supplied id does not exist!");
_;
}
event deposited(address indexed sender, uint amount);
event transferred(address payable indexed recipient, uint amount);
function deposit() public payable {
balance += msg.value;
emit deposited(msg.sender, msg.value);
}
function _transfer(Tx storage transaction) internal checkFunds(transaction.amount) returns (uint) {
transaction.recipient.transfer(transaction.amount);
balance -= transaction.amount;
emit transferred(transaction.recipient, transaction.amount);
return balance;
}
}
contract MultiSigWallet is Ownership, Wallet {
uint approvalLimit;
event deployed(address indexed owner);
event transferRequest(address indexed initiator, address payable indexed recipient, uint amount);
event transactionApproved(address indexed approver, uint transaction);
constructor(uint _approvalLimit, address[] memory _owners) {
require(_approvalLimit > 0, "at least one approver is required for multisig");
require(_owners.length >= _approvalLimit, "not enough owners to satisfy approval limit!");
approvalLimit = _approvalLimit;
owners[msg.sender] = true;
emit deployed(msg.sender);
for (uint8 i = 0; i<_owners.length; i++) {
owners[_owners[i]] = true;
emit deployed(_owners[i]);
}
}
function requestTransfer(address payable recipient, uint amount) public anyOwner checkFunds(amount) checkRecipient(recipient) returns (uint) {
Tx storage transaction = txs.push();
transaction.initiator = msg.sender;
transaction.recipient = recipient;
transaction.amount = amount;
transaction.approvals = 0;
emit transferRequest(msg.sender, recipient, amount);
return txs.length;
}
function approveTransfer(uint transactionId) public anyOwner checkTransaction(transactionId) {
require(txs[transactionId].approvals < approvalLimit, "transaction has already been approved and transferred!");
require(txs[transactionId].approvers[msg.sender] == false, "you have already approved this transaction!");
require(txs[transactionId].initiator != msg.sender, "you are the initiator so cannot approve!");
txs[transactionId].approvals += 1;
if (txs[transactionId].approvals == approvalLimit) {
_transfer(txs[transactionId]);
}
}
}