Hi @Tomaage,
You now have all of the lines of code needed to solve the problem with the withdraw function, and your statements are also in the correct order within the function body to reduce 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…
balance[msg.sender] -= amount;
before actually transferring the funds out of the contract to the external address…
payable(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.
Answers to your questions, and additional comments
Yes, that’s correct. The getBalance() function would return what has been updated in the mapping. This would be the cumulative effect of …
- balance increases for deposits, updated with …
balance[msg.sender] += msg.value;
and
- balance increases (recipients) and balance decreases (senders) for internal transfers executed by the transfer() function, and updated with the _transfer() helper function …
function _transfer(address from, address to, uint amount) private {
balance[from] -= amount;
balance[to] += amount;
}
but not any
- balance decreases for withdrawals
Basically, the internal accounting function of the balance mapping would no longer work, would no longer provide a true record of each user’s share of the total contract balance, and would no longer serve as a means to prevent users from withdrawing more than they are entitled to.
As time went by and more and more transactions took place, including withdrawals, the difference between (i) the sum of all of the individual user balances in the mapping, and (ii) the total contract balance, would become greater and greater. A situation could quickly arise, where any ether deposited in the contract could be withdrawn by anyone who could “get there first”. As the ether held in the contract is finite, and the contract balance is always correct and accurate, where there were “winners” (“thieves”), there would be equal and opposite “losers” (“victims”).
Yes … but it only performs an internal transfer of funds (effectively, a reallocation of funds) between two individual users of the Bank contract. Because no ether is entering or leaving the contract, the net effect to the contract address balance is zero, and the only change is to the individual user balances in the mapping, in order to adjust the amount each of the parties involved in the internal transfer is entitled to withdraw from the contract (their share of the total pooled funds) i.e. the recipient’s entitlement (balance) is increased by the same amount the sender’s entitlement is reduced.
Correct … but we could easily add an additional function which allows users to transfer ether (part of their individual share as recorded in the mapping) out of the contract to an external address that is different to their own. I can see you’ve already tried this …
No… a function only needs to be marked payable
when it needs to receive ether from an external address and add it to the contract address balance (e.g. our deposit function). It might help to think of payable
as the code that enables msg.value
to be added to the contract address balance, without us having to add any further code for this. In the deposit function this happens automatically because we have marked it payable
. The line balance[msg.sender] += msg.value;
then adds the same value to the individual user’s balance in the mapping, in order to keep track of their share of the total funds held in the contract.
Instead, to create a function that allows users to transfer ether out of the contract to an external address that is different to their own, we would just need to make a minor adjustment to the withdraw() function e.g.
function externalTransfer(uint amount, address recipient) public {
require(balance[msg.sender] >= amount);
balance[msg.sender] -= amount;
payable(recipient).transfer(amount);
}
/* Notice, we don't actually need to return a value in either the
withdraw() function, this new function, or the deposit() function */
If we make a function payable
when it doesn’t need to be, and the caller sends an ether value to the function by mistake (as well as calling it with the arguments that it does require), then this ether would be added to the contract balance and potentially lost to the user. So, if a function doesn’t need to receive any ether, it is much securer if we leave it as non-payable, because then, if the caller makes the same mistake, the function will revert, and the ether won’t be sent.
That’s what events are for. We would have a dapp, with a front-end and a user interface. In other courses you can learn how the Web3 client can use a Web3.js library to listen out for events and retrieve the data. It would then be for the front-end code to handle how the end user is notified of the captured event data that is relevant to them.
Unfortunately, that’s correct … and why smart contract security is critically important!
As always, just let me know if anything is unclear, or if you have any further questions