An excellent assignment solution and, overall, a very well-coded contract @xela 
You have included all of the additional functionality needed to solve the problem with the withdraw function.
The additional withdrawal event and corresponding emit statement you’ve added are appropriate and well coded, as are all of your events and their emit statements. The only thing I would say, is that the emit statements are probably better positioned after the assert statements, rather than before.
The additional assert statements you’ve added to both the withdraw function and the deposit function are also well coded.
You have the other code in your withdraw function in the correct order to reduce security risk:
- check inputs (require statements)
- effects (update the contract state for reduction in balance)
- interactions (perform the transfer of funds from smart contract address to wallet address).
It’s important to modify the contract state:
balances[msg.sender] -= amount;
before actually transferring the funds out of the contract:
payable(msg.sender).transfer(amount);
… just in case there is an attack after the transfer, but before the state is modified to reflect this operation. You’ll learn about the type of attack this prevents, and how it does it, in later courses. We wouldn’t expect you to know this at this early stage, though, so I’m just telling you for extra information, in case you haven’t already seen this explanation in some of the other posts in this discussion topic. But it’s great that you’re already getting into good habits in terms of smart contract security 
… when you need to use msg.sender as a payable address, then, yes. That’s because from v0.8 msg.sender is by default a non-payable address, whereas in prior versions it is payable by default. Apart from msg.sender, payable()
has been used to convert non-payable addresses to payable ones since Solidity v0.6.
Your additional modifiers with parameters, each used to provide the same require statement in more than one function, are skillfully implemented, appropriate, and show a good understanding of how modifiers help us avoid code duplication, and keep our code concise 
Just a couple of observations, though…
-
checkFunds
would be better placed in the “main” transfer function, instead of the helper _transfer() function. If an input value needs to be checked or validated, this should happen at its first “point of entry” into the smart contract. In addition, if the transfer amount isn’t checked until it is passed to the helper transfer function, less gas is likely to be refunded if require fails and reverts the transaction, than if the check was performed at the beginning of the “main” transfer function. This is because only the gas cost associated with the unexecuted operations after require() is saved; any gas already consumed executing code for the operations before and including require() is not refunded.
-
Is there a reason why you have used checkValueforZero
to check that amounts deposited into or withdrawn from the contract are > 0, but not amounts transferred between users within the contract?
-
You’re missing the require statement in transfer() which checks that an address isn’t transferring funds to itself.
deposit() has to be a payable function, because it needs to receive ether. However, you don’t need to mark withdraw() as payable, because it only needs to send ether (not receive it).
Let us know if anything is unclear, or if you have any questions about the points I’ve raised.
You are making great progress! 