 You should have good specification on what you want to implement. For example, this can be a white pipe, so you know what your project will actually be doing. Next, you're going to model how you want to implement the different parts of your system. Here, the first step of security starts, because what you should actually be doing is you should develop a threat model, listing risk, possible attack vectors, and possible mitigation of different, so how different actors may threat your system. And already at this stage, think about what can go wrong or how could an attacker attack your project. This is similar like in traditional information security, there is stride attack models or many more, which you can use to think. With this, you can start modeling your system and then start implementing it. But please don't just implement it, implementing and documenting what you're implementing should go hand in hand. Ideally, you start documenting what your system does, what your function do early. When you're writing code, when you're writing a function, you're at a very low level, very deep and very concentrated on this very function, making it efficient, making it nice, making it safe, but you risk losing the whole picture. Writing documentation helps you to place this function in the big picture and gives you another angle to look at your project and how this function interacts with multiple parts of your system and where projects should arise. So writing code and documentation should go hand in hand. Unfortunately, we see it often with our clients that they write documentation as one of the last steps. And sometimes, then the documentation simply describes what they see the code is doing, right? And then it's not really helpful for us as an auditor, because reading the code, we can do this ourselves. So I understand writing documentation is really boring and stuff. We won't cover this in the workshop today. If you want to see a perfect example on how good specification and documentation is done, I point you to Uniswap. They have a very nice white paper, which contains the specification of what their system does. And in addition to that, they have very nice documentation on what the implementation of their smart contract does, what the individual function expects as input, what they do and what they have as output. So I recommend checking out this for your project. What we're going to focus on in the workshop today is testing your project. Unfortunately, as auditor, we've seen it often that projects are not really tested. And testing covers lots of things. So sometimes we need to guide our clients and point them to links on how they can efficiently and meaningfully test the project. So we want to give you some pointers and some initial training so you can get started with your own project. This will include unit tests, property-based testing, forked mainnet tests. At the very end, we'll talk about further things you should consider to get your code in an audit-ready state. And even after the audit is completed, once you deploy your codes to mainnet, there are further steps you should take to ensure the continuing security of your smart contracts. Yeah, that's the only slide covering specification and documentation. As I said, you should tackle this early on. Apart from the threat model we discussed, you should think about invariants, things that should always hold, things that should always hold when the system is in this state and new invariants when the system changes into another state. This can help you find bugs early. And the last point, this is not only to have nice documentation for the auditor or for you during your development process. No, the documentation is also critical for the users of your project. So they know what they interact with. For other DeFi protocols that interact with your protocol, they need to rely on your documentation that it's extensive and accurate. When they integrate with your functions, they mostly refer to your documentation. And this is also the reason why you should have good documentation to avoid problems when third parties integrate with your system. So in the next part of the workshop, we are considering a smart contract example. And based on this example, we're going to step through the workshop. The example is a simple fund. It's a smart contract that holds funds in the form of ERC-20 tokens. Users can buy into the fund by depositing the underlying token of this fund and they get PoolShare tokens back. And there's a fund administrator who can use these funds for trading with Uniswap Aave compound. The goal is, of course, that he increases the value of the fund. At a later point, the users of the funds, the holders of the PoolShare, can withdraw their shares and hopefully make a profit. Of course, it's a toy example and the code in the repo is for this workshop only shouldn't be used for production because I'm very sure there are tons of bugs in it. So in our smart contract, we will have the users, the fund owner, as the two parties interacting. With that, let me switch to the code as it's important to get a good understanding of the code before we think about the properties and write the test to avoid confusion. This code is all in the GitHub repo I shared earlier, if you're interested to follow along. So here I opened our code. Sorry, that's already the test. That's the fund. So it has a lot of code to get it going, right? It's our fund. We inherit from ERC-20 so we have our PoolShare's right built in. We have some admin stuff or stuff to attract the tokens our fund supports. We'll take a fee, it's 3%. So as we, it's, when we deploy the fund, we deploy an underlying asset. This is what we will use for the funds value calculation and we'll add the token as supported asset. The owner can add further tokens, the owner can remove tokens. Both functions are not really relevant for our test cases. Each token needs a price feed so that its value can be evaluated. Here we have deposit and withdrawal. These are the two user-facing functions and these are also the two functions our test gonna cover. So let's us quickly go through them. So we have a good understanding. We deposit an amount, it gets transferred from the user. We estimate the total value of the user. We need to handle the special case for the first depositor separately. As the funds doesn't hold any value back then and we would have zero value. So the standard case after the fund is live and holds fund is the else branch. We calculate the total supply, calculate how many PoolShare's to mean for the user and how many PoolShare's to mean for the admin fee. We'll go in more details here shortly with the test. We're drawing PoolShare's basically the opposite. You give up shares you hold and you get a proportional amount of tokens transferred to you. Obviously your PoolShare's are burned. Then there's a manage function which the admin can use to trade with the funds tokens but it's not gonna be used in this workshop. Down here we have functions to estimate the value of the fund. Yep, lots of information in a quick timeframe. I hope you have the code in front of you and should any questions arise during the tests we can always go back to the code. Let me switch back to the slides. Let's get it full screen. Perfect, yep. So most important points of our funds is its interface because that's what we're gonna interact with in the test. Everyone can call deposit and withdraw. Only the owner can manage the fund at price fits and at tokens. So when implementing a smart contract it should adhere to some specification, right? In our example, the fund should have a fee of 3% and this fee should be taken when the user deposits money and gets his PoolShare's. At this time, 3% of the value should go to the owner. And the owner, another property is the owner should always get these fees when a user deposits. No user should be able to deposit without paying fees to the admin. Another important property we want to have in our smart contract is that redemption for user always worked and cannot be blocked. Let's first start with testing. And there are different kinds of tests. In your smart contract you have small internal helper function which may also be public like our estimate function which estimates the value of the fund. You have the buy shares function, the deposit function which is a bit takes more steps, right? And ideally you have tests on every level. You do unit tests for each function. You do integration tests for processes that include multiple steps. But in the end, you should also test your system like it is in production. And with smart contract, the production environment is the blockchain, right? You deploy it into the blockchain and there it interacts with other existing smart contract. So you should properly test it integrating with the smart contracts on chain so you don't get any negative surprises or any incompatibilities or anything. Let's see what we're gonna discover with our contracts. With this, let's go to the first warm-up exercise. And in this workshop, we're gonna use Foundry. There are lots of other great tools available which all work similarly. For example, Brownie or Truffle. For your project or hardhat, you may use whatever you like. The same basic concept we are presenting here applies similarly. So for the warm-up exercise, we should want to write a test case which asserts that the user pays 3% fee to the owner. And if the user withdraws, that he gets 97% of the value. So if the user deposits and withdraws immediately, he should get 97% of the value back because the other 3% is the fee for the owner. Yeah, so let's jump into the code. We are back into the code editor. We move from the source directory into the test directory. So if you wanna follow along, you can open the integration test file. How Foundry works, you write your test files in the test sub folder and you call them integration.t.sol and it's basically solidity. So as a developer, you're familiar with solidity. No need to learn another language or anything else. You can just use what you're used to, solidity and continue. You inherit from test and you have all these fancy stuff available which allows you to impersonate another account, distribute E for another account, move in time or whatever you need in your tests. And with your tests, you cannot simply test your function as is, right? It's like a blockchain, you first need to set up the state, meaning in our case, we first need to deploy our fund but deploying the fund alone is insufficient. We need also tokens, the underlying tokens, our fund interacts with the token, the user deposits. So we first need to build up our environment. So that's what the function setup does in Foundry. We deploy a new wrapped E for token. We mint one token, so one E to the deployer. We impersonate the deployer address which we've just taken a default address up here. We deploy our test fund. We convert our E for into wrapped E as our fund works with ES20 token, the owner deposits, so the fund is already initialized and holds some value. And at this point, we have the fund up and running, ready for testing. Now we also need users interacting with the fund, right? So we give some money to Alice, also in the form of E for and down here also to Bob. We impersonate Alice, deposit the E as wrapped E for and approve the fund, so we can easily call deposit and it can be transferred from. So after setup, we have an environment setup where our fund is live and both addresses of Alice and Bob have wrapped E and have already approved the fund so we can easily use them for testing. And writing a test is really simple. Remember what we wanted to write a test for? We wanted to see if the code of the fund we implemented adheres to our requirement that when depositing 3% of the fees go to the admin and that the user can withdraw at all time. So of course, this is like the high level statement. We need to make this a bit more concrete in order to be able to write a test case. And the test case we want to write is if you deposit and immediately withdraw, you should get 97% back, right? So that's what we wanna implement and it's really easy. We first impersonate Alice, so don't worry, this one is already pre-coded but we'll have two more examples with kind of live coding. So we impersonate Alice, we query her balance we use her balance, we deposit this, no, sorry. We get Alice's balance of reactive but we know that from the setup that it's a unit so we deposit everything. Adjust that everyone know unit stands for one to the power of 18, so one if and which has 18 decimals as well, right? Not that you get confused here. So we have Alice's balance up here, we deposit it, we check how many shares of the fund Alice has, we withdraw it, we check Alice's balance afterwards. So now we did our actions, right? And here we write our condition we want to assert. We want to assert that the Alice's difference in balance is equal to the fee she's supposed to pay for depositing one unit of her shared tokens. And this should hold if we implemented our smart contract correctly. So with this test case, let's run it. So what I'm basically doing here with match I just say which test case to run. It runs the test but it unfortunately doesn't pass. Reason, the assertion failed. Now we can tell Foundry to give us more verbose output to figure out what went wrong. Minus V, you can go up to five V, it defines the level of verbosity the output should have. So now we see the reason is that the assertion failed. We should have paid 3%. So she deposited one, that's 3% of one to the power of 18, but actually what Alice paid in fee is a bit more. So either our test is written incorrectly or the smart contract has a bug. Let's quickly go back to the test. The test itself looks fine, right? We deposit, we withdraw. We are certain that we didn't pay more than the fee. So let's go to the smart contract. What happens up on deposit? We transfer Alice's money first to the smart contract and only afterwards estimate the fund's value. And based on the value with already the funds of Alice transferred to the fund, we mint the shares. And this is incorrect as Alice's token which is transferred to the fund shouldn't be taken into account in total. If we transfer the tokens from Alice first to the fund, the value of the fund already increased but we haven't issued shares for this increase yet, right? So the shares, the existing shares are overvalued and Alice gets less shares and then can only redeem less and doesn't get enough. So the solution is we need to estimate the value of the fund first. Only afterwards transfer Alice's token to the contract and then issue the shares based on the amount before Alice transferred her funds. We did this, we store this and now let's see if what happened to our test. And as we see, writing a test case while developing our smart contract helped us uncovering a stupid bug which otherwise would have remained in the code until maybe the auditor would have found it way later. Of course, this test just shows that it works with these values we passed. It's no guarantee that it works in general with all values. So in the next step, I wanna talk about property-based testing. Is this like random input test? This is similar to fuzzing. I guess it's a terming issue because fuzzing is mostly seen as black box testing while property-based testing, you call it when you do randomized input when you know the code you interact with. So Foundry allows us to test a function call with the random value and see if our assert condition hold for all this random values we check. We still have no guarantee that it works in any case but at least we have a way better feeling that it works with big numbers which may have triggered an overflow somewhere. It works with small numbers where maybe rounding issues leads to failure. It just allows us to cover a much broader space of potential values and we have a much higher chance to actually detect issues on our smart contracts. Yeah, this is basically what describes the difference. We have the unit test case where we just test one fixed value. The example we just did, below we have the property-based testing where we write a set of action depending on a random value where we assert something and we just tell our tool to test it with many value and see if it can break the condition. Yeah, so for this example we wanna try to work on the condition the owner should always get fees when user deposits. So back to the code. Still in integration. We wanna test that the owner gets a non-zero fee. So the rough idea behind our test case is we safe store the owner's share balance we let the user deposit, we query the owner's share balance again and it should have increased, right? After a user deposited because this assigned fees to the, but I just thought that I've already implemented it forgot to delete it, so. Yeah, code-wise we query Alice's balance. Ah, sorry. No, let me delete it and that's as well. Okay, so let's consider this part first. We query the test code for storing the fund owner's balance before then we impersonate Alice's deposits. We query the fund manager's balance afterwards. We check that the difference is bigger than zero. Of course, this is a simple demo test case. You could actually expand on this condition and check that the expected amount has been received. But just for demo purpose. And Foundry will use random values here, but based on the full range of the unsigned integer, which is way too large for actual valid amounts of for token balances, right? And especially in this case, we're also constrained by Alice's token balance. So we can tell Foundry to bound the value. In this case, we bound it up to the balance Alice has because if we exceed Alice's balance and try to deposit it, obviously won't work, right? So let's see how this test works. And what we see here, this one passes now. And actually while developing this workshop, we had a minor bug, which we first didn't realize only called it in testing. We see here for demo purpose, we only did 256 run with 256 random value, but didn't find any breaking value. You can treat these numbers for actual project. So this gives you already a way better feeling that your code works with lots of different values instead of just with one, but still not good enough, right? Sure, no specific reason. I mean, yes, absolutely correct. Yes, you need to get more elaborate here and for an actual project, you need to think more about what is a meaningful test case and treat these values. Good catch. Yeah, quite. Actually, I don't know, but it definitely makes sense to test edge cases. So if Foundry doesn't do it by default, it definitely should be port. Yes, test deposit. Yeah, good question. Yeah, yes, I agree. You probably should use the expected value to avoid errors there. Yes. Yeah. Admittedly, writing good test takes time, right? You need to design them properly. You need to implement them properly. And you also need to see, check that your tests are bug free. I remember a story once we audited the system. After two weeks of audit, we found a broken functionality while manually reviewing the code. But we really puzzled why is this code broken because they have a test case and it seems to work. Only later we realized that the test case was broken and the small contract was actually wrong. So tests help you, but don't guarantee you that it's error free. Yep. And now, the best of all, mainnet test. Your DeFi project won't be stand alone. It will interact with many tokens, many different DeFi systems. Even if you took great care reading their documentation, reading on how the special behavior tokens might have, you should still test as like in production, as close to the production as possible. Lots of tools out there now enable testing against forks of the mainnet. What this basically is, you run tests against the local copy of the real Ethereum network. So you integrate with the real RV, with the real Uniswap, you use the real USDT code. And this helps you detect more corner cases and other unexpected stuff. One big advantage this helps you to uncover is to help detect undocumented and unexpected behavior of protocols which may break your integration. Again, we have the example that redemption should always work. And let's see if it actually works in our code or not. For this, we go to the fork mainnet test file. And what we want to implement is a function test, USD. And I give you 30 seconds to quickly think about how you would implement this. I just deleted it. You probably saw it's three lines of code. Give you 30 seconds to quickly think then we're gonna do it together. We want to test that redrawing always works and we know from the, okay, in the integration test we had the setup function, right? We deployed our own wrapped if token, we deployed our own fund and everything we deployed our own because we had to build the whole environment ourselves. And if you note, the setup function here is different. We don't deploy our own tokens. Instead, we interact with the real wrapped if token on chain and the real USDT token on chain and the real chain link price feeds. Foundry is gonna do a local copy of the current mainnet state of this contracts and their state locally, which our tests will run again. So we're not deploying a default mock token contract but we're running it against them. So we are basically again testing and the setup function still deploys the fund, right? And the fund owner deposits his one wrapped ifo. So at this point where our test starts the fund owner already has shares of the fund and can simply redraw and we simply want to test if redrawing works. So let's go to the implementation. First we need to impersonate the deployer hard to hold the microphone and type. That's how you do it. Next we wanna see, so our redraw function takes an argument to share amount, right? So we need to see how many shares we have. We just carry the deployer's balance of shares and now we actually try to redraw. And do you think it's gonna work? Because you've seen the code of the fund, right? It should work. The redraw function looks good. Have you checked the redraw function? Is it gonna work? You would need to add your own API key here. And I have it as environment variable work. Last chance. Guess the internet is not good. Sorry, I need to do a hotspot here. Yeah, sorry, we need to download mainnet data. We need internet and I forgot to spend up my hotspot in time. Which hopefully will allow us to. Yeah, couldn't get. No, I also like my key, so I'm gonna need to revoke that one. Damn, I especially did it in environment variable in order to avoid this. Thanks, no, I'm not connected to my phone. That should work. Okay, if it doesn't work, I'm gonna have to explore in here. Yeah, fortunately no chance. So actually the surprise is, as we're now running our code against live on-chain code, and we're integrating with wrapped if and USDT, and the wrapped if transfer will work as expected. However, USDT doesn't really adhere to the ESC20 standard in the sense that their transfer from function doesn't have any return value. And our code of the fund actually depends, expects a return value here on ESC20.transfer. The interface specification of ESC20 expects a return value. So if we don't get a return value back, and our transaction will revert. And this is a well-known strange behavior case of the USDT token, which have led to problems in real life. And every developer integrating with this token should be aware of. And this should also serve as example, even if you have expectations that you interact with legit ESC20 tokens, there can always be weird corner cases or weird unexpected behavior. And that's the beauty of forked mainnet test. And that's why forked mainnet tests are absolutely essential to have with your project. Yes, of course, but then it takes a long time to set up your local test environment when you need to do the real stuff always. And the alternative doing forked main test is so easy. So straightforward. Are you okay in testing it? Yes. Yes, yes, true, true. But you need to carefully inspect everything. And it's hard when you don't know the bug in advance, right? It's hard to look at what you need to look out for. Yes, please. Yes, yes. Yes, that's what didn't work now because I don't have internet connection. We would have great alchemy to give us the state data we needed for our tests. Yes, it's really these lines of code you give up, you point them to an address and it all does everything. You don't see how it works. It's easy and simple to use. So you download the state and based on this state you start evaluating your stuff, right? You start progressing the state. And I guess there you do dynamically generate your addresses. You can't, right? But we've specified the addresses of the contracts we interact with. Yes, yes, you need to see the, right? That's what we list up here and then use pointer. Yes, yes, you can specify the block. So if you don't specify one, it just takes the latest one but you can also specify a previous block. Yes. Cool. That was the interactive part. Of course, this only covered multiple tests in case. There's way more you can do. And what we didn't talk about, you should have proper development process, right? This includes code reviews. You do merge requests. Another one member of your team reviews it, gives you feedback, you discuss. You have proper coding standards and guidelines you adhere to. And important to note, not all bugs can be uncovered by tests. If your test pass, it's never a proof of correctness. There can still be bugs because you didn't think about test case that would have triggered the bug. And formal verification of the code can bring you to the next level. But again, doing formal verification, you need to come out with properties and specification. If you don't write the pro specs, you may miss out on other bugs. And what's absolutely necessary is to have an audit of the code, both internal and external, ideally, to get the code into an audit ready in a state after you've done all of this, after you're done with your project. It shouldn't go directly to the auditor. Instead, the internal team should take a step back and audit the project themselves. And this already helps to uncover a lot of bugs. And then in the next step, give your code to an audit company, which is gonna do a total review, give you an intermediate report with potential issues, and then give you time to address these, discuss potential problems and how they can be mitigated. After that, your code should be ready for deployment, but that's not the end of the road, right? Even after deployment, even after you did everything perfectly before, you should monitor your system, especially if it's a DeFi system, right? Depending on finances and stuff, check if it's healthy in a financial way or if unexpected behavior. But this is very project-specific, what you should look out for. A bug bounty is a very valuable addition for your project. So if there is a bug that has been missed, it gives incentive to the whiter to actually report it to you so they can fix it in time. And approaches, we didn't talk about here. What we've seen from clients is you could also implement it as test-driven development. So you don't start with the code, but you write the tests and then you write code which make these tests passed and you approach it from this way. We've also seen this, but very rarely. And then there's, yes, stateful testing, there's way more you can do here. But there I need to refer you to the internet or invariant-based programming. Yeah, that would conclude my workshop. I'm here open for any question, open to discuss your project. Also, if you see us walking around at DevCon, don't hesitate to us. If you have any question. Thank you. Any questions? Yes. The code. Yeah. At a forked minute, one order integration. Okay. So you seem to, you're using the deployer, you're using three addresses, deployer Alice and Bob. Yes. These are two, three and four. Yes. So Foundry provides you with some default addresses which I can just access with two, three and four. Okay. They resolve the proper addresses. Yeah. But these are pre-compiled contracts. No. These are just random addresses you can add funds to. But I mean on the AVM, these are pre-compiled contracts. No. They're just like EOAs. Zero. You are right that zero X one is a pre-compiled contract but this is something completely different. This is Foundry specific. This results to such an address. Okay. Such a long address as you used to. Ah, okay. Okay. I see. Yeah, because otherwise if you... Yeah, you're right. Pre-compiled contract. But that's something different. Okay. Okay. Great. The other thing I wanted to ask is, so you have a test that asserts that in the same, let's say in the same block, if you deposit, you will get like, if you, for instance, if you deposit and withdraw. Yeah. You will get the same amount of shares or less. Yes, because of the fear. Yes. But what if something happens in the middle and you don't get the same, right? How you can, because it seems like everything is, it's one transaction specific. Yeah. And you're not going to assert about things that can happen in the middle. Yeah. No, we are not asserting. So in this test case, we write these test cases are atomic, right? And of course you are right. In the real world, if you deposit first, if another action happens in between and the fund value changes, when you withdraw then, of course then the value doesn't add up anymore. All right. All right. But that's... If you need to test something like that, how you can do it in this context? Because you... Well, you can still do it like this, right? But you need to write these actions in between yourself or somehow come up with how you can simulate them. You don't want to write the actions, right? You want the faster to be able to simulate them without needing to write the actions because you don't know what can happen. It's a very good idea, yeah. But it's probably not in the scope of this testing. You probably need other tools enabling that. I'm not sure if it's possible with Foundry. Because I think Foundry has a invariant testing, but I haven't tested... Yeah, there's way more. There's also great documentation on Foundry which you definitely should check out. Okay, thank you. Yeah, please. So, most of the time, working for Mainnet, you need to do in your setup function, and then it's basically done. Then if the tests don't themselves also fetch new data, it should be fast. Otherwise, I guess it depends on the node you connect to. Yeah. Yes, it really depends on the individual application. What's... Quite clever, yeah. Yes, please. We're running out of time. It's one part of, but it's not the Holy Grail. Right, it's not enough. Okay, so that was like... Yeah. Yeah, yeah. I mean, any audit report of a reportable firm should make it clear in the executive summary or somewhere that it's unsafe, right? Yeah, so you need to read the audit report carefully to figure out if there are doubts. I think there are other companies which are way further in this field, like runtime verification. Surtik. I mean, they have a lot of talk here which I recommend to attend. Talk. Yes. Are there other advantages of testing in Solidity other than just not having to use a new programming language versus testing in like JS or something? It's just simple for the average developer, right? If you need to use the JavaScript Mocha JS test with the Async wait, it's way simpler here for the average developer. Cool, thanks a lot, everyone.