This assignment had to sink in for a while. First I made my own contract and got horrible stuck, because my set-up wasnât a proper framework to work with.
So I did some research with Google and Filipâs videos. Then made multiple versions (not all at once). Further I changed names of variables, arguments, functions and events a lot to challenge myself and get comfortable with the code (hence the unusual names :)). And I added the function getWalletFriendsBalance() to easily check the results of deposits and transactions and to check that the transaction to be submitted doesnât exceed the balance.
The best part was testing the contract in Remix. It is awesome to see the wallet in action, make mistakes, do some corrections, try again and finally see all code working as expected. The data field was confusing though. But after I filled in the default setting 0x00 (because while testing transactions in this contract there is no need yet to call other contracts and store bytes) all worked well.
If youâre still reading this and looking into my solution, thank you! And if you happen to have questions or comments, please post them and thank you very much again!
pragma solidity 0.8.13;
pragma abicoder v2;
contract WalletFriends {
//store the owners in an array of addresses
address[] public owners;
//Check if msg.sender is owner.
//If an address is an owner of the wallet it should return true
mapping (address => bool) public isOwner;
mapping(address => uint) balance;
//store number of approvals that is required for a transaction to be executed
uint public numApprovalsRequired;
struct Transaction {
address receiver;
uint value;
bytes data;
bool transactionCompleted;
uint numApprovals;
}
//store all transactions in an array
Transaction[] public transactions;
//make sure msg.sender is one of the owners of this contract
modifier onlyOwner() {
require(isOwner[msg.sender], "owner not verified");
_;
}
//check if the transaction really exists
modifier transactionExists(uint _transactionIndex){
require(_transactionIndex < transactions.length, "transaction does not exist yet");
_;
}
//check if the transaction is not yet approved by msg.sender
modifier notApproved(uint _transactionIndex){
require(!isApproved[_transactionIndex][msg.sender], "transaction is already approved");
_;
}
//make sure the transaction is not already finished
modifier notCompleted(uint _transactionIndex){
require(!transactions[_transactionIndex].transactionCompleted,"transaction is already completed");
_;
}
//store the approval of each transaction by each owner in a mapping
//uint is the transaction index, address is address of the owner who's considering approval
//bool is check wether the transaction is approved by this owner
mapping (uint => mapping (address => bool)) public isApproved;
event Deposit(address indexed sender, uint value, uint balance);
event SubmitTransaction(
address indexed owner,
uint indexed transactionIndex,
address indexed receiver,
uint value,
bytes data
);
event ApproveTransaction(address indexed owner, uint indexed transactionIndex);
event Complete(uint indexed transactionIndex);
constructor(address[] memory _owners, uint _numApprovalsRequired) {
//require that this wallet has 3 owners
require(_owners.length == 3, "owners needed");
//require a correct number of approvals treshold
require(
_numApprovalsRequired > 0 &&
_numApprovalsRequired <= _owners.length,
"incorrect number of required approvals"
);
//save owners to a state variable
//make sure that owner is not equal to the address on index 0
//check if the owner is unique
for (uint i = 0; i < _owners.length; i++) {
address owner = _owners[i];
require(owner != address(0), "unfounded owner");
//verify that the new owner is not yet in our mapping isOwner
require(!isOwner[owner], "owner not unique");
//insert the new owner in the isOwner mapping
isOwner[owner] = true;
//add the owner into the owners state variable
owners.push(owner);
}
//set state variable numApprovalsRequired equal to the _numApprovalsRequired from the input
numApprovalsRequired = _numApprovalsRequired;
}
//anyone can deposit
function deposit() public payable returns(uint){
balance[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value, address(this).balance);
return balance [msg.sender];
}
//get total balance of this wallet contract, to keep track of how much Eth you can actually transfer
function getWalletFriendsBalance() public view returns (uint){
return address(this).balance;
}
//only owners can submit a transaction
function submitTransaction(address _receiver, uint _value, bytes memory _data) public onlyOwner {
uint transactionIndex = transactions.length;
//push parameters into the transaction array
//set additional information on transaction status
transactions.push(Transaction({
receiver: _receiver,
value: _value,
data: _data,
transactionCompleted: false,
numApprovals: 0})
);
//log function data with the event SubmitTransaction
emit SubmitTransaction(msg.sender, transactionIndex, _receiver, _value, _data);
}
//after a transaction is submitted other owners can approve the transaction
function approveTransaction(uint _transactionIndex) external onlyOwner {
require(_transactionIndex < transactions.length, "transaction does not exist yet");
require(!transactions[_transactionIndex].transactionCompleted,"transaction is already completed");
require(!isApproved[_transactionIndex][msg.sender], "transaction is already approved");
//store the transaction approval of msg.sender
Transaction storage transaction = transactions[_transactionIndex];
transaction.numApprovals += 1;
isApproved [_transactionIndex][msg.sender] = true;
emit ApproveTransaction(msg.sender, _transactionIndex);
}
//make sure that the number of approvals is equal to or greater than the required numbers of approvals
//count the number of approvals with a function
function _countApprovals(uint _transactionIndex) private view returns(uint count) {
for (uint i; i < owners.length; i++){
if (isApproved[_transactionIndex][owners[i]]){
count +=1;
}
}
}
//execute the transaction
function completeTransaction(uint _transactionIndex) external {
require(_transactionIndex < transactions.length, "transaction does not exist yet");
require(!transactions[_transactionIndex].transactionCompleted,"transaction is already completed");
//check that the count of approvals is >= than required
require(_countApprovals(_transactionIndex)>= numApprovalsRequired,"approvals is less than required");
//get the data stored in the Transaction struct and then update it
Transaction storage transaction = transactions[_transactionIndex];
transaction.transactionCompleted = true;
//execute the transaction
(bool success, ) = transaction.receiver.call{value: transaction.value}(
transaction.data
);
require(success, "transaction error");
emit Complete(_transactionIndex);
}
}