Inheritance Assignment

Contract: ownable.sol

pragma solidity 0.8.7;

contract Ownable {
    address owner;

    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }

    constructor(){
        owner = msg.sender;
    }
}

Contract: destroyable.sol

pragma solidity 0.8.7;

import "./ownable.sol";

contract Destroyable is Ownable {

    event DestroyContract (string _message);

    function destroy() onlyOwner public payable {
        emit DestroyContract ("Self destruct initiated.");
        address payable ownerAddress = payable(owner);
        selfdestruct(ownerAddress);
    }

}

Contract: bank.sol

pragma solidity 0.8.7;

import "./destroyable.sol";

contract Bank is Destroyable {
    mapping(address => uint256) balance;

    event depositDone(uint256 _amount, address _depositedTo);
    event amountWithdrawn(uint256 _amount, address _withdrawnFrom);

    function deposit() public payable returns (uint256) {
        balance[msg.sender] += msg.value;
        emit depositDone(msg.value, msg.sender);
        return balance[msg.sender];
    }

    function withdraw(uint256 amount) public returns (uint256) {
        require(
            balance[msg.sender] >= amount,
            "You cannot withdraw more than what you have!"
        );

        balance[msg.sender] -= amount;
        payable(msg.sender).transfer(amount);

        emit amountWithdrawn(amount, msg.sender);
        return balance[msg.sender];
    }

    function getBalance() public view returns (uint256) {
        return balance[msg.sender];
    }

    function transfer(address recipient, uint256 amount) public {
        require(msg.sender != recipient, "You cannot transfer to yourself!");
        require(balance[msg.sender] >= amount, "Balance insufficient.");

        uint256 previousSenderBalance = balance[msg.sender];

        _transfer(msg.sender, recipient, amount);

        assert(balance[msg.sender] == previousSenderBalance - amount);
    }

    function _transfer(
        address from,
        address to,
        uint256 amount
    ) private {
        balance[from] = balance[from] - amount;
        balance[to] = balance[to] + amount;
    }
}
1 Like

Hi @oioii1999,

Sorry for not replying sooner!

No, the default visibility for state variables is internal

Yes, if by “hiding”, you mean that you want the state variable in the base contract/class to only be accessible from within that same base contract i.e. it won’t be inherited.
If you do not include any visibility keyword in a definition of a state variable in the base contract/class, then it will have internal visibility by default, meaning that it will be accessible both from within that same base contract and from within the derived contract/class i.e. it will be inherited. But unlike a state variable defined with the public visibility keyword, it won’t be accessible from an external service (e.g. a web3 client) or from an external smart contract.

Yes, except that with functions the visibility keyword must always be included in the function header. Functions have no default visibility, and if you omit the visibility keyword, you’ll get a compiler error.

The other difference with functions is that as well as public, private and internal visibility, we can also define them with external visibility. An external function in a base contract/class will still be inherited by the derived contract/class, but when the derived contract is deployed, the function can only be called from an external service (e.g. a web3 client) or from an external smart contract i.e. it cannot be called from within the deployed contract itself. An external function cannot be called from within the same contract/class or from within a derived contract/class.

I hope that’s answered your questions and made things clearer. But do let me know if you have any further questions :slight_smile:

Hi @Daniel_O,

Sorry for the delay in replying!

The balance mapping records the individual user balances. These balances perform an internal accounting role, and record each user’s share of the contract’s total Ether balance i.e. each user’s entitlement to withdraw Ether from the contract.

The Ether is held in the contract address balance. When a user calls the deposit() function to deposit Ether in the contract, the fact that the function has been marked payable enables the Ether value sent to the function (input into the Value field in the Deploy & Run Transactions panel in Remix) to be added automatically to the contract address balance. It might help to think of payable in a function header 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.

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.

A user can retrieve their individual share of, or entitlement to, the total contract balance by calling getBalance(). However, if we want to retrieve the actual amount of Ether held in the contract at a particular point in time (the contract address balance) then we need to add a function which returns this instead e.g.

function getContractBalance() public view returns(uint) {
    return address(this).balance;
}

The withdraw() function does the opposite of what the deposit() function does: it deducts Ether from the contract address balance and sends it to the caller’s external address (msg.sender). It does this using Solidity’s in-built transfer method, which is called on a payable address …

<address payable>.transfer(uint256 amount);

Just as the execution of a function marked with the payable keyword results in the EVM performing low level operations which add any Ether value sent to the function to the contract address balance …
… when Solidity’s in-built transfer method is executed, it results in the EVM performing low level operations which (i) deduct an amount of Ether from the contract address balance which is equivalent to the uint256 argument the transfer method is called with, and (ii) transfer and add this amount of Ether to the external address the transfer method is called on.

In the withdraw() function, the line …

balance[msg.sender] -= amount;

… ensures that the same value deducted from the contract address balance, and transferred out of the smart contract, is deducted from the individual user’s balance in the mapping, in order to keep track of their share of the total funds held in the contract, which is necessary to be able to ensure that they can’t withdraw more Ether than they are entitled to.

Just as the payable keyword and the transfer method are each pre-defined within the Solidity lanaguage and result in the low level operations described above …
… when Solidity’s in-built selfdestruct method is executed, it results in the EVM performing low level operations which (i) transfer an amount of Ether equivalent to  address(this).balance  to the external address argument which the selfdestruct method is called with (in our case the contract owner’s address), and then immediately (ii) destroy the smart contract address(this) by removing it from the Ethereum blockchain.

I hope that clarifies things. But just let me know if anything is still unclear, or if you have any further questions :slight_smile:

@jon_m Thanks very much for the detailed answer. I was actually thinking that there is some low level EVM magic behind the scene, that’s why I asked the question. I mean, it made sense what was happening and I kinda guessed here and there (given my java background and familiarity with the JVM) but just wanted to understand exactly how it works on the Solidity side. Now everything make sense, so thanks again for that (Really appreciate it). I am just about to start ETH 201 course today, really looking forward to that.

1 Like

Really appreciate your patient explanation! @jon_m

I have learned a lot about the inheritance of class variables and functions. :sunflower:

1 Like

Hi @Daniel_O,

Glad the explanation was helpful … I know exactly what you mean when you can draw on your general experience to make an intuitive guess about what’s probably going on behind the scenes, but that it’s also good to get some confirmation and a more detailed explanation just to tie up any loose ends, put your mind at rest, and also just in case you’ve missed something important :sweat_smile:

Enjoy 201 :smiley:

Hi @karendj,

This is a very well-presented solution which works and meets all of the main assignment objectives :ok_hand:

Your implementation of a multi-level inheritance structure is the best approach. Bank only needs to explicitly inherit Destroyable because it will implicitly inherit Ownable via Destroyable.

A few observations and comments about the code in your Destroyable contract …

(1) Only functions that receive Ether from an external address (in order to add it to the contract address balance) should be marked payable (e.g. the deposit function in Bank). So, your destroy() function shouldn’t be marked payable because it doesn’t receive Ether. Instead, it deducts Ether from the contract address balance and sends it to an external address (the contract owner’s). This is also why the withdraw() function isn’t marked payable.

The selfdestruct method within the destroy() function does require a payable address argument. This is so it can transfer the remaining Ether in the contract to this receiving address when the contract is destroyed. You have already ensured that this address argument is payable by converting the non-payable owner address using payable() in the line above…

In fact, a more concise alternative to using this additional ownerAddress variable is to call selfdestruct with owner directly, and convert owner to a payable address within the function call itself…

selfdestruct(payable(owner));

Have a look at this post for further details about the use of the additional local variable receiver in the model solution.


(2) Your DestroyContract event is certainly a good idea. Even though an event’s emit statement should be placed after the code associated with the event itself — in order to minimise the risk of the event being emitted erroneously — you are right that we can’t do this here. Once selfdestruct has executed, the contract will have been destroyed and its code removed from the blockchain; so, if the emit statement is placed after the call to selfdestruct, it can’t be reached and the intended event data will never be logged. The only solution is to do what you’ve done and place the emit statement before the call to selfdestruct.

It’s true that if selfdestruct failed, then the destroy() function would revert, including the emitted event, but we still need to carefully weigh up the benefits of emitting this event against the potential risk of emitting it before the actual event itself (contract destruction) has occurred.

The reason why I say that emitting an event is a good idea (at least in theory) is because of a serious drawback of using selfdestruct, the potential repercussions of which need to be fully understood before choosing to implement it. As you may or may not already have realised, after selfdestruct has been executed, successful calls can still be made to the destroyed contract’s functions! As a result of this, if a smart contract deployed on the mainnet is ever destroyed, all users need to be notified and informed that they must not send any more funds to that contract address. If a user doesn’t know that the contract has been destroyed, and goes ahead and sends funds to it (e.g. by calling the deposit function), then those funds will be lost. You can see this for yourself by calling deposit() with an Ether value. Remix will notify you that the transaction has been successful, and you will see that the Ether amount has been deducted from the caller’s external address balance. However, as the contract no longer exists, the Ether has been lost.

It’s worth bearing in mind that emitting an event doesn’t necessarily mean that users will see that infomation, and so may not actually prevent them from subsequently calling the destroyed contract’s functions, incurring a gas cost, and losing any Ether they might send. There would still need to be some form of communication to all users informing them of the contract’s destruction and what the consequences and implications of this are in terms of what they must and mustn’t do from now on.

Obviously, this is hardly an ideal situation! In practice, other types of security mechanisms, such as pausable or proxy contracts, are implemented to deal with these kinds of emergency situations, instead of selfdestruct. But this is out of the scope of this introductory course. You will learn about more advanced security techniques and practices if you go on to take the Ethereum Smart Contract Security Course.

Let me know if you have any questions :slight_smile:

1 Like

Hi @jon_m, thank you as always for your thorough and patient explanations, I have learnt a lot from reading them.

Though I understand the general idea of how the code works, I think there are still many parts missing in my comprehension, I hope you do not mind me asking some questions and can help clarify them for me.

My questions are as below:

  1. For simple illustration purposes: if I assume the contract Bank is owned by let’s say a bank, and if a customer of that bank deposits a certain amount to their individual account through an ATM, that scenario would be equivalent to:
  • function deposit(): the operation of customer depositing money into their account

  • msg.sender: the customer who deposits, which in Remix IDE is represented by the address that is currently selected and displayed on the left bar, ‘Account’ dropdown

  • so msg.sender is not necessarily the owner of the contract Bank

  • msg.value: the amount s/he deposits into their individual account

  • function transfer(): the operation of one customer transferring to another customer of that bank, with the recepient of the amount represented by address recipient as a parameter in the function transfer()

  • balance[msg.sender]: the total amount currently in that customer’s individual account, who is right now accessing the ATM

  • function withdraw(uint256 amount): is equivalent to that customer withdrawing let’s assume amount is $50, and getting a physical $50 note ejected from the ATM

  1. And let’s assume the CEO of this bank wants to see how much money his/her bank is holding right now (from all the deposits the customers of the bank have made in point no. 1 above), s/he can call the command: address(this).balance ? With this referring to contract Bank, where all the functions: withdraw, deposit, transfer, etc. are held?

  2. And the modifier onlyOwner / the address address owner is the initial msg.sender that was used to create/write the scripts of this contract Bank? The initial address that is shown on the left side of Remix IDE while deploying contract Bank? Let’s say…the CEO of the Bank who has the Super Admin power?

  3. And if someone hacks the bank’s system, withdrawing the money bit by bit, the CEO can call the function destroy(), quickly save the rest of the money left that hasn’t been implicated by the hack, and then destroy the whole bank’s system to stop the hacker once the remaining money is secured? Hence the lines below?

address payable ownerAddress = payable(owner);
selfdestruct(ownerAddress);

Excuse my many questions – I just want to make sure I have the foundation correct before going any further. So in the case my understanding is incorrect and have to rewatch the videos, I don’t have to go way too far back :sweat_smile:

Thanks a lot!

1 Like

When we use the selfdestruct method in an Ownable function (so it ensures owner = msg.sender), would the be any difference having selfdestruct(owner) or selfdestruct(msg.sender) ?

1 Like
  1. I guess both selfdestruct(payable(owner)); and (from the solution):
address payable receiver = msg.sender;
selfdestruct(receiver);

are valid.

  1. Though I don’t see in the solution where the remaining balance in Wei of the smart contract (of Bank) is transferred to the owner so that’s why I have in my solution :

payable(msg.sender).transfer(address(this).balance);
I believe using msg.sender or owner makes no difference as the function is onlyOwner.
I’m not so sure though that “this” refers to the contract Bank or the Destroyable contract.

My solution (before checking solution):

Destroyable

//SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.7;

import "./ownable.sol";

contract Destroyable is Ownable {

    function SelfDestructContract () public onlyOwner {
        payable(msg.sender).transfer(address(this).balance); // or owner. Here we transfer all what's on the contract balance to the owner of that contract
        selfdestruct(payable(owner)); // doesn't work if we don't put payable
    }

}

Ownable

//SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.7;

contract Ownable {
    address public owner ;
    modifier onlyOwner {
            require (msg.sender ==owner);
            _; 
        }
    constructor() {
        owner = msg.sender;
    }
}

Top of Bank Contract

//SPDX-License-Identifier: UNLICENSED

pragma solidity 0.8.7;

import "./ownable.sol";

import "./Destroyable.sol";

contract Bank is Ownable, Destroyable {
1 Like

Not sure if that’s correct but here’s my try:

  • Ownalbe.sol
contract Ownable{
 
    address public owner;

    modifier onlyOwner {

        require(msg.sender == owner);

        _; //run the function

    }

    constructor(){

        owner = msg.sender;

    }

}
  • Destroyable.sol (is Ownable)
import "./Ownable.sol";

contract Destroyable is Ownable{

address addr = address(this);

address payable addr_payable = payable(addr);

    function selfdestruct_contract() internal onlyOwner{

        selfdestruct( addr_payable);

    }

}
  • Bank.sol (is Destroyable)
pragma solidity 0.7.5;

import "./Destroyable.sol";

contract Bank is Destroyable {

    mapping(address => uint) balance;

    event depositDone(uint amount, address depositedTo);

    function init_selfdestruct_with_transfer() public onlyOwner () {

        msg.sender.transfer(address(this).balance);

        selfdestruct_contract();

    }
1 Like

Hi @karendj,

I’m glad to hear you’ve found the feedback and explanations helpful and informative :slight_smile:

Ask as many questions as you need to — that’s what the forum is here for! :raised_hands:

Correct

Correct

Correct … msg.sender is whichever external address calls the function. If this function (fx1) then calls another function (fx2) in either the same contract or a derived contract, then msg.sender remains the same address in fx2. However, if fx1 calls a function (fxE) in an external contract (an external function call), then msg.sender will be the calling smart-contract’s address in fxE. We don’t have any external function calls in this assignment, though. They come in the next section of the course.

When Bank is deployed, the constructor inherited from Ownable (via Destroyable) is called. The address that deploys the contract is the msg.sender in the constructor. The constructor assigns this address to the owner state variable, and so owner holds the address that deployed the contract. We only call this the contract owner’s address, because we are assuming that the contract deployer is the contract owner. In the same way, in your example we might assume that whoever “opens” the bank is the owner of the bank.

The owner address is just another external address like those of all the bank’s customers, and any external address can deposit and withdraw funds, and perform internal transfers. Therefore, as well as being the bank’s owner (and the only one authorised to “close” the bank in an emergency), the owner address is also free to “open” a bank account, deposit funds into it, withdraw funds from it, and transfer funds from it to another customer’s bank account.

Correct

Correct

Calling the transfer() function doesn’t involve any Ether entering or leaving the Bank contract at all. Instead, it performs an internal transfer of funds (effectively, a reallocation of the bank’s total funds) between two individual users (customers) of the Bank contract. The net effect to the total contract 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

The individual balances in the mapping perform an internal accounting role, and record each customer’s share of the Bank contract’s total Ether balance.

Correct … and if the customer puts that $50 note in their wallet, or pocket, or under their mattress, then that wallet, pocket or mattress is equivalent to the Ethereum address which calls:
function withdraw(uint256 amount)

Correct

address(this) references the Ethereum address which “this” contract is deployed at, and  address(this).balance  references that address’s Ether balance.

Let’s say the CEO is the owner address. This address would call a view function such as…

function getContractBalance() public view returns(uint) {
    return address(this).balance;
}

In fact, any address (CEO, or customer, or non-customer) could call such a function to view the total funds held in the bank on behalf of all of its bank account holders. If you only wanted the CEO to be able to see this total amount, then you would need to restrict access to this function with the onlyOwner modifier.

And just to be clear … address(this).balance will be equal to the total deposits LESS the total withdrawals already made. (The total transfers have a net effect of zero, as they are internal bank transfers, as I’ve explained above).

I’ve already covered this in my comments about who msg.sender is (part of your Q1), above…

…it’s the address that deployed the contract i.e. the address that signed and sent the transaction that deployed the compiled bytecode of contract Bank (including its inherited functionality) on the Ethereum blockchain at a specific Ethereum address.

Yes … the address showing in the Account field (near the top of the Deploy & Run Transactions panel in Remix) when the orange Deploy button is clicked.

Yes :muscle::star_struck:

Yes … that could be one of several different disaster scenarios, the negative consequences of which this function and its code (including selfdestruct) may have been designed and implemented to reduce.

Don’t hesitate to let me know if anything is unclear, or if you have any further questions.

1 Like

Hey @seybastiens,

Your solution meets all of the assignment objectives, and your inheritance structure works :ok_hand:

In fact, Bank only needs to explicitly inherit Destroyable, because it will inherit Ownable implicitly via Destroyable. This will give you a multi-level inheritance structure.


This line of code in your SelfDestructContract() function achieves the desired result, and from a technical point of view it’s well coded. However, as well as destroying the contract, selfdestruct also automatically transfers the remaining contract balance  (address(this).balance)  to the payable address argument it is called with (in our case, the contract owner’s address). This is all part of the pre-defined selfdestruct functionality, and is why there is no need to include your extra line of code.

It’s transferred out of the contract to the contract owner’s external address balance (in the dropdown list of addresses in the Account field near the top of the Deploy & Run Transactions panel in Remix). Only this address can call SelfDestructContract() and trigger selfdestruct, so this should already be the address showing in the Account field when this transfer is made, anyway. We obviously can’t assign these funds to the contract owner’s individual user balance in the mapping, because that only records each individual bank account holder’s share of the total contract balance and, after the contract has been destroyed, neither the contract’s funds nor the mapping (which tracks and records the allocation of these funds) will exist anymore.

If you perform your prior transactions (before calling selfdestruct) using whole amounts of Ether, rather than small amounts of Wei, it will be easier to see the movement of funds e.g.

If address A is the contract owner (contract deployer), and addresses B and C are two other users…

  1. Add the following function to Bank, if you don’t already have it …
      function getContractBalance() external view returns(uint) {
          return address(this).balance;
      }

Only one contract has been deployed at a single Ethereum address: Bank.
When Bank is compiled, the result is a single set of bytecode which incorporates the inherited functionality from the parent contract(s).
So, whether address(this) is in Bank, Destroyable or Ownable, if we deploy Bank, it will always reference whichever address Bank’s compiled bytecode (which address(this) will have been included in) is deployed at.

  1. Address A deploys contract (make sure Bank is selected from the 3 contracts available in the Contract field’s dropdown, and is showing in the Contract field, which is just above the orange Deploy button):
    A’s external Ether balance is still approx. 100 (slightly less due to gas cost: 99.99999999)

  2. Address B deposits 8 Ether:
    B’s external Ether balance is now approx. 92 (100 - 8)

  3. B calls getBalance:
    B’s individual share of contract balance is 8 Ether (displayed in Wei, 8 + 18 zeros)

  4. Any address calls getContractBalance:
    Total contract balance is also 8 Ether (displayed in Wei)

  5. B transfers 4000000000000000000 Wei (4 Ether) to C, and then transfers another 100000000000000000 Wei (1 Ether) to A — these are both internal bank transfers and do not affect these users’ external address balances.

  6. All 3 address separately call getBalance:
    A’s individual share of contract balance is 1 Ether
    B’s individual share of contract balance is now 3 Ether (8 - 4 - 1)
    C’s individual share of contract balance is 4 Ether

  7. Any address calls getContractBalance:
    Total contract balance is still 8 Ether (1 + 3 + 4)

  8. C withdraws 2000000000000000000 Wei (2 Ether)
    C’s external Ether balance is now approx. 102 (100 + 2)

  9. C calls getBalance: C’s individual share of contract balance is now 2 Ether (4 - 2)
    Any address calls getContractBalance: total contract balance is now 6 Ether (1 + 3 + 2)

  10. The contract owner (A) calls SelfDestructContract:
    A’s external Ether balance is now approx. 106 (100 + 6)
    Remaining contract balance (6 Ether) has been successfuly transferred out of the smart contract (just before it’s destroyed) to contract owner’s external address.

  11. A, B or C calls getBalance() and getContractBalance() functions:
    Both return zero, because the contract has been destroyed.


We want the selfdestruct method’s payable-address argument to reference the contract owner’s address, because this is the address the Ether remaining in the contract will be sent to, when the contract is destroyed. To achieve this, we can either …

  • reference the owner state variable, which is inherited from Ownable (see B below); or

  • use msg.sender (see A below) — if we add the onlyOwner modifier to the header of the function containing selfdestruct, this restricts access to this function to the contract owner’s address, which in turn means that the only address msg.sender can reference in this function is the contract owner’s address.

So, you are right when you say …

As the selfdestruct method requires a payable address argument …

(A) msg.sender

If we use msg.sender then how we code this depends on whether we are using Solidity v0.7 or Solidity v0.8 …

(A.1) Prior to Solidity v0.8, msg.sender is already a payable address by default, and so doesn’t need to be explicitly converted whenever the syntax requires it to reference a payable address…

selfdestruct(msg.sender);

(A.2) From Solidity v0.8, msg.sender is non-payable by default, which means having to explicity convert it to a payable address when necessary…

selfdestruct(payable(msg.sender));

(B) owner

If we do what you’ve done, and reference the owner state variable as the argument, this needs to be explicity converted to a payable address type, because originally it was defined as non-payable in Ownable …

address owner   // internal visibility by default
//or 
address public owner  // public visibility (what you've used in your code)

We can do this in 2 different ways…

(B.1) If we only need the contract owner’s address to be payable for the purposes of executing selfdestruct, we can do what you’ve done, and leave the owner state variable in Ownable as a non-payable address type, and explicitly convert the address to payable locally, within the function where we actually need to use it as payable. As you’ve done, we can perform this explicit conversion directly within the selfdestruct() function call itself, as follows:

This is the syntax you need whether you use Solidity v0.7 or v0.8

(B.2) Or we can define the owner address state variable as a payable address type by adding the payable keyword …

contract Ownable

address payable owner;   // internal visibility by default
//or
address payable public owner   // public visibility

contract Destroyable

function SelfDestructContract () public onlyOwner {
    selfdestruct(owner);
}

Again, this is the syntax you need whether you use Solidity v.0.7 or v0.8. The difference with v0.8 is that you need to make an additional modification in your Ownable contract. In the constructor msg.sender is the address that deploys the contract (the contract owner) and it needs to be explicity converted to a payable address using payable() so that the constructor can assign it to the owner state variable, where this is defined as a payable address type…

constructor() {
    owner = payable(msg.sender);
}

But with v0.7, msg.sender is already a payable address by default, and so it can be assigned to a payable address variable (e.g. owner ) without having to explicitly convert it to a payable address …

constructor() {
    owner = msg.sender;
}

Yes, but for the reasons I’ve just explained above regarding the constructor in Ownable, because you are using Solidity v0.8, if you wanted to implement the code from the model solution, you’d need to explicity convert msg.sender to a payable address …

address payable receiver = payable(msg.sender);
selfdestruct(receiver);

However, using …

… is a more concise alternative to using the additional receiver variable.

Have a look at this post for further details about the use of this additional local variable (receiver) in the model solution for this assignment.


I hope I’ve managed to answer your questions, but let me know if anything is still unclear, or if you have any further questions after reviewing this feedback :slight_smile:

You’re doing a great job analysing and really thinking about what the code is doing, and this will really help you continue to make good, solid progress, and to make the most of the more advanced Solidity courses which follow this one. Keep up the great work! :muscle:

1 Like

Hi @Thunarson7999,

It’s certainly a good attempt! The main thing is that your solution works :muscle:
It meets all of the assignment objectives and your implementation of a multi-level inheritance structure is also well coded and is the best approach: Bank only needs to explicitly inherit Destroyable, because it will implicitly inherit Ownable via Destroyable :ok_hand:

However, you haven’t taken full advantage of your inheritance structure, and you’ve included a lot of code which isn’t necessary. The same objectives can be met in a much more concise way …

(1) By giving your selfdestruct_contract() function in Destroyable internal visibility, it is inherited and available within Bank, but cannot be called externally by the contract owner. To resolve this you’ve added an additional public function to Bank which, when called externally by the contract owner, then calls selfdestruct_contract(). However, an important reason for using inheritance is to reduce code duplication, and if you change the visibility of selfdestruct_contract() from internal to public, it will be inherited and available to be called both from within Bank and externally from Remix. You could also give it external visibility, which would enable it to be called externally from Remix, but not from within Bank, which for the purposes of this assignment would be enough. Either way, this removes the need for the additional init_selfdestruct_with_transfer() function in Bank.

selfdestruct_contract() will now appear as a function-call button in the Remix panel, instead of init_selfdestruct_with_transfer()

Additionally, by keeping all of the code and functionality related to contract destruction within a single, separate contract (Destroyable), you will improve the modularity and re-usability of your code (other important reasons for using inheritance), and you will also keep your code more organised and compartmentalised (making management and further development easier).

(2)

This line of code achieves the desired result, and from a technical point of view it’s well coded. However, as well as destroying the contract, the selfdestruct method also automatically transfers the remaining contract balance, address(this).balance , to the payable-address argument it is called with. This is all part of the pre-defined selfdestruct functionality. The argument which selfdestruct is called with is not the address of the contract being destroyed. This will automatically be the deployed contract containing the bytecode compiled from the selfdestruct method which has been executed. Instead, the payable-address argument needs to reference the external address which the remaining contract balance will be transferred to, just before the contract is destroyed and its code removed from the blockchain.

So, if the selfdestruct function call references the contract owner’s address as its payable-address argument, there is no need to include the above line of code, or your additional state variables in Destroyable …

… and even if these variables were needed, it would be better to include them as local variables instead of state variables, because this would consume less gas (persistent storage in the contract state being more expensive than saving the data in a temporary location).

In summary, instead of the two functions (init_selfdestruct_with_transfer and selfdestruct_contract) and the two state variables (addr and addr_payable) which you currently have, all you actually need is the following function in Destroyable…

function selfdestruct_contract() public onlyOwner {   // visibility can be public or external
    selfdestruct(msg.sender);
}

Just one additional comment …

  • It is not good practice to omit the pragma statement from any Solidity file, because we should always specify the Solidity compiler version (or range of versions) which our code is written to be compatible with.
    Because the Bank contract inherits functionality from Destroyable and Ownable, even if the pragma statements are omitted from both of these files (as in the code you’ve posted), Remix will still attempt to compile Bank as a single contract together with the code it inherits. Bank will compile and deploy successfully as long as the code in both of the inherited contracts is compatible with the Solidity version declared in the pragma statement at the top of the file containing Bank. However, the compiler still generates orange warnings, highlighting that it’s best practice to include pragma statements in each file.
    In the absence of a pragma statement, it seems that Remix will always try to find a suitable compiler version, although this doesn’t seem to be 100% reliable.
    So, in conslusion, always include a pragma statement at the top of every .sol file.

Just let me know if anything is unclear, or if you have any questions :slight_smile:

@jon_m Thank you VERY much for that high-quality feedback, that’s really great! The selfdestruct() functionality is now much clearer to me, also I wasn’t aware of the auto-transfer functionality of selfdestruct. thank you very much for giving such detailed feedback!

1 Like

Destroyable contract

pragma solidity 0.7.5;

contract Destroyable {

    function close() public {
        selfdestruct (msg.sender);
    }
}

Ownable contract

pragma solidity 0.7.5;

import "./26_Destroyable.sol";

contract Ownable is Destroyable{
    address public owner;

    modifier onlyOwner {
        require(msg.sender == owner); 
        _;
    }

    constructor(){
        owner = msg.sender;
    }
}

First lines of Bank contract

import "./26_Ownable.sol";

contract Bank is Ownable  {
...

Thanks.
I just have one thing I’m a bit confused about in your answer.

How come I can call and execute that function from the owner address (Address A in your example) since it’s an external function?
I thought that since it’s external, only the other addresses (B,C etc) could execute it.
To me it acts as if it was “public”.

You’d want Destroyable to be inheriting from Ownable and thus Bank to be inheriting from Destroyable.

The reason is that one of the assignment’s requirements is that only the owner of the contract should be able to call the selfdestruct functionality in a situation where such a grave step is needed. For example, if your smart contract is being attacked by hackers and you can see them slowly draining Eth from your contract’s address, then in such a case, you might have to take the drastic step of selfdestructing your contract and withdrawing all the amount from it. If you leave your code as is, then anyone can selfdestruct your contract as its open to public.

Thus the close() function will also need the onlyOwner modifier in its header to prevent it from being accessible by anyone using the contract.

Hope this helps!

2 Likes

‘external’ access modifier is basically equivalent to the ‘public’ modifier in the way that anything outside the contract can call it however it can’t be called from any code within the contract itself.

1 Like

Hi @Thunarson7999,

You’re very welcome! Glad you found it so helpful :slight_smile: