Below I show the whole process of how I code smart-contract and how I think about the problem.
Remember deploying smart contracts inevitably brings in several vulnerabilities and risks and might be exploited against you and your solution.
tldr; Quick summary of the whole text on another post:
- The flat is too expensive for a single individual to buy. A group of friends or investors can buy, a flat together and later on share income on this investment proportionally to how each of them paid.
- Anyone staying at the apartment pays the rent directly to the apartment smart contract address.
- This rent can be withdrawn by any shareholder any time proportionally to the number of shares.
I will be using
vscode with hardhat installed. I will be writing unit tests in
chai.js delivered with a hardhat out of the box. I assume I will use
openzepellin's erc20 implementation and treat each of 100 shares as a single coin.
Github repository presents a path to achieve end results such that each step (unit test) is committed separately with test case description as the commit message.
Contents (unit test headers):
1. The contract creator should have 100 shares of the apartment.
2. It should be possible to transfer some shares to another user.
3. It should be possible to pay the rent and deposit it in ethers in the apartment contract
4. Owner should be able to withdraw resources paid as rent
5. Shareholder be able to withdraw resources paid as rent
6. Attempt to withdraw by non shareholder should be reverted
7. Apartment shareholder be able to withdraw resources proportional to his share
8. It should not be possible to withdraw more than one has
9. It should be possible to withdraw multiple times provided there were incomes in between
10. Each withdrawal should be calculated against new income, not the total balance
11. Transfer of shares should withdraw current funds of both parties
Test case 1: The contract creator should have 100 shares of the apartment.
Let's start with the test case. We assume that the apartment will only have 100 shares and that all of those will be initially in possession on the contract owner who is also the real-estate initial owner.
ERC20 there come the
_mint the function which is very handy in that case as it requires no effort to fulfil the first test.
Test case 2: It should be possible to transfer some shares to another user
This test case states that apartment shares can simply be transferred to someone else. For instance to the second investor. Like as previously described in the problem context a single person cannot afford the apartment for themselves so they invite others (shareholders or co-investors) to participate in both costs and incomes. There is no implementation required on the smart contract side because again it is all
Test case 3: It should be possible to pay the rent and deposit it in ethers in the apartment contract
The whole point of this problem is to allow investors earn money. They want earn by acquiring the apartment and renting it for money. The beauty of the smart-contract is that is has all the logic implemented inside. If contract got any funds it will manage it according to the programmed logic. There will be no need to got to bank withdraw cash and split it by any of investors. The smart contract will do this making the whole cash flow:
- convenient - no action required, automation introduced
- trustless - nobody has the whole cash even for a moment
This test case ensures that there is a method in the smart contract that will be called whenever a funds transfer is called. This method will be
receive and is decorated with an
external modifier. More about the logic behind this function under link receive keyword
So let's look at the code and the unit test case
As we see in the test case there is another player introduced to the picture
Bob . He is not an investor. He is the apartment guest and he pays for the stay directly to the smart contract address. As it happens the smart contract balance is increased and can be queried. In the next steps, those paid funds will be a matter of proportional distribution among investors. Stay tuned!
Test case 4: Owner should be able to withdraw resources paid as rent
This lesson is kind of the beginning of several commits about withdrawing of funds from smart contract functionality. Generally speaking, the goal is to be able to allow shareholders (and shareholders only) to withdraw an applicable amount of means from smart-contract. It is supposed to be safe in such a way that shareholders can not call this function infinitely draining the contract funds as well as always allow to withdraw the right amount of funds. Calculating the right amount of funds seems easy but will require analysing several cases and will be discussed over couple of test cases for simplicity. One detail at the time.
In this lesson, there is just starting point added. There is no safety mechanism whatsoever and leaving it as is will have severe consequences as losing all funds since anyone can call withdraw all funds with no math involved.
Let's take a look at the withdrawing method that is for now just a starting point.
Test case 5: Shareholder be able to withdraw resources paid as rent
No big deal in this increment. The only thing is that we kind of add some protections against misuse of the withdrawal function. In the previous lesson literally, everyone was allowed to call this method and take over all funds. Pretty scary, huh?. Right now we limit possibilities to call it only to those having at least one share (more than zero to be more accurate). Let's remember that it still gives no safety as right now any shareholder can drain a smart contract anytime. It is just a little bit better than allowing to do the same to literally anyone. But not much :)
Test case 6: Attempt to withdraw by non shareholder should be reverted
Little to discuss here. The only new thing is a meaningful message when an
unauthorized person calls this method. The most important here is the unit test that takes care of securing this condition. Only shareholders can withdraw, and any attempt to withdraw when you are not one is supposed to be rejected. Cool that hardhat allows for such unit test that explicitly testes that some specific call is rejected. I have already written about testing transaction rejections here.
Test case 7: Apartment shareholder be able to withdraw resources proportional to his share
Alright, now here goes some math. We cannot allow anyone to withdraw all funds but only funds proportionally to the shares in the apartment. Let's have a look at the function.
As we see the math is simple but it allows to calculate exact funds to be withdrawn. The math here is simple as it bases on some integer math. It would be much more complicated if those numbers require rounding. As always there is a designated unit test ensuring this thing work as expected. Now and in the future.
The unit test just makes sure that there is the right increase (estimated) on user balance and also the right amount of funds left on the contract after the transaction. The missing piece here is that although we accurately calculate the funds' amount we do not limit the number of consecutive calls to this function. Let's see this problem in next lesson.
Test case 8: It should not be possible to withdraw more than one has
Even though Alice (from the unit test above) cannot withdraw more than the value proportional to her shares, she can easily cheat and call the function multiple times. Almost draining the smart contract to zero. An example what can happen below:
- Alice withdraws 20% of 1 Ether (Alice has 0.2 Ether, Contract has 0.8)
- Alice withdraws 20% of 0.8 Ether (Alice: 0.2 + 0.16, Contract 0.64)
- Alice withdraws 20% of 0.64 Ether (Alice: 0.488, Contract 0.512)
So now we know the threat left in the previous lesson let's fix it.
- There is a new mapping was introduced. In this mapping of every shareholder, I save the total income earned so far on the smart contract. It is something like a pointer of what was the smart-contract state last time someone has been withdrawing. It may look like on image below.
- By having the register any time someone tries to withdraw I just make sure that there is anything new earned since the last withdrawal of that user.
- Provided there are new funds earned the user is allowed to move on and withdraw their share of those new funds*. However current contract total income wrote down next to his name and next time on withdrawal this value will be used for verification
their share of those new funds* - in fact, this is not true. The user will get i.e 20% of all funds on the smart-contract not the 20% on the new funds only. This will be taken into account in `lesson 10`
Let's now take a look at the unit test. As stated here there is no way to call withdraw twice so Alice will only be able to call it once per any new income.
Test case 9: It should be possible to withdraw multiple times provided there were incomes in between
There is no new solidity code here just kind of making sure that it is still possible to withdraw multiple times if the new funds are appearing on the smart-contract in between.
Test Case 10: Each withdrawal should be calculated against new income, not the total balance
So as mentioned in lesson 8 each withdrawal should be calculated based on new funds earned on smart contract, not total funds available, what has been the case until now.
As we see there is great use of the
withdrawRegister. Thanks to this we not only limit unfair withdrawing approaches but also calculate withdrawal amount based on new income from the last withdrawal of that user. This makes sure we will always have funds left for those patient shareholders that are more patient and do not withdraw as often as others.
This time there is a massive unit test creating the whole history of various operations like several incomes and withdrawals.
Let's see the history on a timeline. On the image below there is Alice having 15%, not 20% as in the unit test, but is still is helpful to understand it.
Test case 11: transfer of shares should withdraw current funds of both parties
In this lesson, there is too much code to present so it will not be pasted. Take a look at GitHub to see the exact source code of the change
And now goes the most complicated case. At least in terms of the code. From a business logic perspective, it is quite easy to understand. Whenever there is the transfer of a share (an operation that shifts some shares of apartment from one user to another) the two affected parties should have their funds withdrawn just before the action of share transfer. Why? Just to make the whole logic easier as all funds earned and not withdrawn until this point should be treated according to the old share division and all the new funds earned after this point according to the new one. To clearly state the switching point it is best to clear things out and forget about old reality and from now on think only about the new one.
And again yet another problem here might be that one party involved in shares transfer might in fact be unaware of this operation. The recipient will is not needed to make shares transfer and in the current solution, this user will not only get some new shares but also will possibly get unexpected ethers.
Time line explainer below:
This is a simple presentation of the way one may think and work with couple of simple requirements about the contracts. This is far from all the work needs to de done until this code can be on production and serve users in secure way.
The great resources to go deeper into this matter are:
Thank you for getting that far!