Hi Federico,
Apologies for the delay in giving you some feedback on this assignment.
Your solution is excellent, and I really like how you’ve adapted and been creative with the code, experimenting with your own variations
You’ve solved the problem with the withdraw function (adding your own adaptation — see my comments below), and the following operations are performed in the correct order within the control flow, which reduces security risk …
- Check inputs (require statements)
- Effects (update the contract state)
- External Interactions (e.g.
transfer
method)
Just as you have done, it’s important to modify the contract state for the reduction in the individual user’s balance…
// if there is sufficient balance to withdraw the whole amount requested
balances[recipient] -= amount;
// if the requested amount is greater than the user's individual balance
balances[recipient] = 0;
before actually transferring the funds out of the contract to the external address…
msg.sender.transfer(amount);
This is to prevent what is known as a re-entrancy attack from occuring after the transfer, but before the user’s individual balance (effectively, the share of the total contract balance they are entitled to withdraw) is reduced to reflect this operation. You’ll learn more about this type of attack, and how the above order of operations helps to prevent it, in the courses which follow this one. But it’s great you’re already getting into good habits in terms of smart contract security
Some additional comments …
(1) Still allowing a withdrawal to go ahead if the amount requested by a user is greater than their individual balance, but reducing the amount actually withdrawn to the total balance the user has available, is an interesting variation. You’ve implemented this very well, and the require() statements that you’ve included instead are appropriate.
However, do you think that this would be appropriate in practice? When someone requests the withdrawal of a specific amount, don’t you think they would rather have the transaction stopped, and be notified that they don’t have sufficient funds, rather than finding out afterwards that the amount they’ve received is less than expected? The transaction can be reverted and the error handled in the front-end user interface in such a way that the user is fully aware of the circumstances, and is given the opportunity to repeat the withdrawal but for a lower amount.
(2) Adding the helper functions _addToBalance() and _removeFromBalance() is a nice implementation of modularity: breaking down a computation and separating out specific chunks of code into their own specialised functions, where they can be reused elsewhere. However, you’ve missed the opportunity to re-use both of these functions in the transfer() function to perform the necessary adjustments to the individual balances of the sender and recipient. Instead of adding another helper function (_transfer), you can call both of the helper functions you already have …
//_transfer(msg.sender, recipient, amount);
amount = _removeFromBalance(msg.sender, amount);
_addToBalance(recipient, amount);
As the _removeFromBalance() function would no longer only be used for withdrawals, you would also need to think of a less specific name for the address parameter than recipient
, so that it makes sense for all use cases. However, your if/else logic could still be applied to internal transfers initiated by the transfer() function, allowing them to still go ahead if the transfer amount requested is greater than the caller’s individual balance. Just like with withdrawals, in these cases the amount actually transferred could also be reduced to the total balance available to the caller.
(3) In your deposit() function, you don’t need to assign the value returned from the _addToBalance function call to a local variable, because this value isn’t used and will just be lost anyway when the function finishes executing.
You should have got an orange compiler warning for this, notifying you of an unused local variable.
The _addToBalance() helper function doesn’t need to return any value, and the function call can be made as follows…
function _addToBalance(address recipient, uint amount) private {
balances[recipient] += amount;
}
function deposit() public payable returns(uint) {
_addToBalance(msg.sender, msg.value);
emit depositReceived(msg.sender, msg.value);
return balances[msg.sender];
}
On the other hand, your _removeFromBalance() helper function does need to return a value, because the amount
may need to be changed to the caller’s maximum balance available in the if-statement. Therefore, it’s also correct to assign the return value to the amount
parameter in the main function that calls this helper …
Notice in point 2, above, that this line of code could be used in both the withdraw() and the transfer() function.
(4) All 3 event declarations and their corresponding emit statements are well coded, and the emit statements log appropriate data when their respective functions are successfully executed.
Where possible, an emit statement should be placed at the end of a function, after all of the operations associated with the event itself have occurred, but before a return statement if there is one. Generally speaking, it’s probably also better to place an emit statement after an assert statement if there is one.
So, bearing this in mind, your emit statement in the withdraw() function is in the correct position, but you should reconsider where you place the one in your deposit() function.
Do you think it’s necessary to emit 2 events for each deposit and each withdrawal? Personally, I think it’s an unnecessary duplication of data, and that you could safely remove the balanceChanged event and its corresponding emit statements in the helper functions. You already have sufficient data logged by the depositReceived and the withdrawalProcessed events. We know that the amount associated with a depositReceived event will be an increase to the sender’s individual balance, and that associated with a withdrawalProcessed event will be a reduction in the recipient’s individual balance, without needing the Boolean values to be logged in a separate balanceChanged event.
Removing the balanceChanged event and adding a separate event only for the transfer() function, means that you’ll only need one event emitted at the end of this function as well (which is the most appropriate place for an event, anyway). At the same time, the transfer() function will still be able to make use of the two helper functions to adjust the individual user balances accordingly.
(5) If you need a getter to return a user’s individual balance to anyone, then you no longer need the getter which only returns the caller’s own balance, because the other getter can now be used for both. Remember that the more code a contract contains, the higher the gas cost will be to deploy it. So, we can’t afford to be wasteful by including redundant code.
I hope you find these comments helpful. Let me know if you have any questions, and please do let me know if you disagree with any of my suggestions.
You’re making great progress with Solidity, Federico Keep it up!