Hi @JJ_FX,
You have made an excellent effort with this alternative solution, and the main thing is …
- your code compiles;
- your inheritance structure works;
- only the contract owner can destroy the contract; and
- you have successfully achieved your objective: on destruction of the contract, the remaining contract balance is transferred out of the contract and distributed to the external addresses of the individual account holders, each receiving an amount of ether equivalent to the amount they had deposited in the contract at the time of destruction.
We can also streamline the inheritance structure by having Bank explicitly inherit Destroyable only, because it will inherit Ownable implicitly via Destroyable. This will give you a multi-level inheritance structure.
Unfortuantely, there is a flaw in your code which could potentially result in a user losing ether. If a customer calls the deposit function and sends an ether value, but without having previously registered by calling the registerAccount function, then this ether will still be deducted from their external address balance and deposited in the contract. However, as they haven’t previously registered, the amount deposited in the contract will not be recorded in the mapping as belonging to them. The problem arises, because there is no require statement in the deposit function to revert the transaction if the caller has not yet registered. I explain how you can correct this further below. The withdraw function should also revert if the caller has not yet registered, as at the moment the function still executes successfully, although the consequences are less serious than with the deposit function, because the amount requested is not withdrawn from the contract.
Even though Solidity v0.8 requires msg.sender to be explicitly converted to a payable address when used in the following line of code …
… this doesn’t mean that we have to make the withdraw function itself payable. Only functions that receive ether into the contract (adding it to the contract balance) should be marked payable. Functions that only send ether out of the contract (deducting it from the contract balance) should remain non-payable.
Marking the withdraw function as payable doesn’t prevent your code from compiling, or the function from executing, but it is bad practice and poor risk management not to restrict a function to non-payable if it doesn’t need to receive ether. This is because if someone sends ether by mistake, this amount will still be transferred to the contract. As we are programming money, we need our code to be as risk-averse as possible.
In terms of your adaptations for the additional registerAccount function, and your solution for distributing the remaining contract balance amongst the individual account holders, here are some suggestions for improvements …
To avoid having to use a for-loop in nearly every function, you can include a bool
property (e.g. bool registered;
) in the Customer struct, which is assigned true
whenever a new customer registers by calling the registerAccount function. Including this additional property set to true
in each new account that is registered, has the following advantages:
- You can replace several lines of code in registerAccount() with a single require statement that checks whether the caller has an instance in the mapping with a
registered
property value oftrue
. If it does, then the caller is already registered, and the require statement should fail and trigger revert. If the caller doesn’t have an instance in the mapping yet, then the require statement should evaluate to true and allow the function to execute and create a new customer instance. - You don’t need to use the mapping like an array, iterating over the
uint
mapping keys as if they were array indices. You can make the customer’s address the mapping key again, and remove theaddress account
property from the Customer struct. - In the functions where you now don’t need for-loops, you also don’t need to reference the
customersCount
variable. In fact, for another reason explained below, you can remove thecustomersCount
variable altogether. This also means you can remove theid
property from the Customer struct, which you don’t actually use anyway: it’s only there, because for each new customer you are using the assignment of theid
property to incrementcustomersCount
+1 (this would be better done separately). This will leave the Customer struct with just two properties:uint balance
andbool registered
- In the registerAccount() function, you don’t need to place the final statement (which creates the new customer instance) within an if-statement, because this condition is now evaluated in the require statement.
- In the deposit(), withdraw() and getBalance() functions, instead of using a for-loop to check that the caller is already registered, you can define a separate modifier which checks the
registered
property of msg.sender in the mapping to see if it istrue
, and then include this modifier in each of the three function headers. If you include an error message within the modifier’s require statement, there is also no need for the getBalance() function to return 0 if the caller hasn’t registered an account yet — in this particular case, triggering revert and generating an error message would be more appropriate than returning a balance of 0. The deposit and withdraw functions would also then revert and throw error messages if called by an unregistered address, instead of executing successfully and consuming gas unnecessarily. The risk that I mentioned above of an unregistered customer calling deposit() and losing ether would also be prevented.
So, the only place where you need a for-loop is in the closeBank function. If you define an address array (e.g. address[] customerLog
) as well as the customers
mapping, you can use the array to iterate over in the closeBank function. In the registerAccount function, you can push the caller’s address to the array, as well as creating the Customer instance and assigning it to the mapping. Having an array of customer addresses means that you don’t need the customersCount
variable, nor do you need to use uint
values for the mapping keys, because the array indices can perform both of these functions e.g.
mapping(address => Customer) customers;
address[] customerLog;
function closeBank() public onlyOwner {
for (uint i = 0; i < customerLog.length; i++) {
address toPay = customerLog[i];
payable(toPay).transfer(customers[toPay].balance);
}
permanentlyCloseBank();
}
In fact, as you are …
- no longer using the in-built functionality of selfdestruct() to transfer the remaining contract balance to its single address argument; and
- calling the permanentlyCloseBank() function internally from Bank …
… there really isn’t any point in including Destroyable within the inheritance structure. As destroying the contract is now the only operation performed by calling permanentlyCloseBank(), it would be more straightforward just to call selfdestruct() directly at the end of the closeBank function body.
Try implementing these modifications yourself. Just let me know if you need me to clarify anything, or if you have any questions.
You’re making great progress… keep it up!