From b3bb943433842e6ffe2b2df80bf44df754ad9e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20L=C3=B3pez?= Date: Thu, 21 Aug 2025 22:37:10 -0300 Subject: [PATCH 1/4] Remove `estimateGas` documentation This functionality has been removed from Echidna --- .github/workflows/echidna.yml | 7 - SUMMARY.md | 1 - program-analysis/echidna/advanced/README.md | 1 - ...-transactions-with-high-gas-consumption.md | 165 ------------------ .../echidna/example/blacklistpushpop.yaml | 3 - program-analysis/echidna/example/gas.sol | 21 --- program-analysis/echidna/example/gas.yaml | 2 - program-analysis/echidna/example/pushpop.sol | 30 ---- program-analysis/echidna/example/pushpop.yaml | 1 - 9 files changed, 231 deletions(-) delete mode 100644 program-analysis/echidna/advanced/finding-transactions-with-high-gas-consumption.md delete mode 100644 program-analysis/echidna/example/blacklistpushpop.yaml delete mode 100644 program-analysis/echidna/example/gas.sol delete mode 100644 program-analysis/echidna/example/gas.yaml delete mode 100644 program-analysis/echidna/example/pushpop.sol delete mode 100644 program-analysis/echidna/example/pushpop.yaml diff --git a/.github/workflows/echidna.yml b/.github/workflows/echidna.yml index 4b9396ea..dfe7edce 100644 --- a/.github/workflows/echidna.yml +++ b/.github/workflows/echidna.yml @@ -81,13 +81,6 @@ jobs: contract: TestToken outcome: failure expected: 'echidna_balance_under_1000:\s*failed' - - name: Gas estimation - workdir: program-analysis/echidna/example/ - files: gas.sol - config: gas.yaml - outcome: success - expected: "f(42,123," - flaky: true - name: Multi workdir: program-analysis/echidna/example/ files: multi.sol diff --git a/SUMMARY.md b/SUMMARY.md index 651d630e..9327e61f 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -89,7 +89,6 @@ - [Advanced](./program-analysis/echidna/advanced/README.md) - [How to collect a corpus](./program-analysis/echidna/advanced/collecting-a-corpus.md) - [How to use optimization mode](./program-analysis/echidna/advanced/optimization_mode.md) - - [How to detect high gas consumption](./program-analysis/echidna/advanced/finding-transactions-with-high-gas-consumption.md) - [How to perform smart contract fuzzing at a large scale](./program-analysis/echidna/advanced/smart-contract-fuzzing-at-scale.md) - [How to test bytecode-only contracts](./program-analysis/echidna/advanced/testing-bytecode.md) - [How and when to use cheat codes](program-analysis/echidna/advanced/on-using-cheat-codes.md) diff --git a/program-analysis/echidna/advanced/README.md b/program-analysis/echidna/advanced/README.md index 71733565..fbbde385 100644 --- a/program-analysis/echidna/advanced/README.md +++ b/program-analysis/echidna/advanced/README.md @@ -2,7 +2,6 @@ - [How to Collect a Corpus](./collecting-a-corpus.md): Learn how to use Echidna to gather a corpus of transactions. - [How to Use Optimization Mode](./optimization_mode.md): Discover how to optimize a function using Echidna. -- [How to Detect High Gas Consumption](./finding-transactions-with-high-gas-consumption.md): Find out how to identify functions with high gas consumption. - [How to Perform Large-scale Smart Contract Fuzzing](./smart-contract-fuzzing-at-scale.md): Explore how to use Echidna for long fuzzing campaigns on complex smart contracts. - [How to Test a Library](https://blog.trailofbits.com/2020/08/17/using-echidna-to-test-a-smart-contract-library/): Learn about using Echidna to test the Set Protocol library (blog post). - [How to Test Bytecode-only Contracts](./testing-bytecode.md): Learn how to fuzz contracts without source code or perform differential fuzzing between Solidity and Vyper. diff --git a/program-analysis/echidna/advanced/finding-transactions-with-high-gas-consumption.md b/program-analysis/echidna/advanced/finding-transactions-with-high-gas-consumption.md deleted file mode 100644 index 37fbf93f..00000000 --- a/program-analysis/echidna/advanced/finding-transactions-with-high-gas-consumption.md +++ /dev/null @@ -1,165 +0,0 @@ -# Identifying High Gas Consumption Transactions - -**Table of contents:** - -- [Identifying high gas consumption transactions](#identifying-high-gas-consumption-transactions) - - [Introduction](#introduction) - - [Measuring Gas Consumption](#measuring-gas-consumption) -- [Running Echidna](#running-echidna) -- [Excluding Gas-Reducing Calls](#excluding-gas-reducing-calls) - - [Summary: Identifying high gas consumption transactions](#summary-identifying-high-gas-consumption-transactions) - -## Introduction - -This guide demonstrates how to identify transactions with high gas consumption using Echidna. The target is the following smart contract ([gas.sol](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/example/gas.sol)): - -```solidity -contract C { - uint256 state; - - function expensive(uint8 times) internal { - for (uint8 i = 0; i < times; i++) { - state = state + i; - } - } - - function f(uint256 x, uint256 y, uint8 times) public { - if (x == 42 && y == 123) { - expensive(times); - } else { - state = 0; - } - } - - function echidna_test() public returns (bool) { - return true; - } -} -``` - -The `expensive` function can have significant gas consumption. - -Currently, Echidna always requires a property to test - in this case, `echidna_test` always returns `true`. -We can run Echidna to verify this: - -``` -echidna gas.sol -... -echidna_test: passed! 🎉 - -Seed: 2320549945714142710 -``` - -## Measuring Gas Consumption - -To enable Echidna's gas consumption feature, create a configuration file [gas.yaml](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/example/gas.yaml): - -```yaml -estimateGas: true -``` - -In this example, we'll also reduce the size of the transaction sequence for easier interpretation: - -```yaml -seqLen: 2 -estimateGas: true -``` - -# Running Echidna - -With the configuration file created, we can run Echidna as follows: - -``` -echidna gas.sol --config config.yaml -... -echidna_test: passed! 🎉 - -f used a maximum of 1333608 gas - Call sequence: - f(42,123,249) Gas price: 0x10d5733f0a Time delay: 0x495e5 Block delay: 0x88b2 - -Unique instructions: 157 -Unique codehashes: 1 -Seed: -325611019680165325 -``` - -- The displayed gas is an estimation provided by [HEVM](https://github.com/dapphub/dapptools/tree/master/src/hevm#hevm-). - -# Excluding Gas-Reducing Calls - -The tutorial on [filtering functions to call during a fuzzing campaign](../basic/filtering-functions.md) demonstrates how to remove certain functions during testing. -This can be crucial for obtaining accurate gas estimates. -Consider the following example ([example/pushpop.sol](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/example/pushpop.sol)): - -```solidity -contract C { - address[] addrs; - - function push(address a) public { - addrs.push(a); - } - - function pop() public { - addrs.pop(); - } - - function clear() public { - addrs.length = 0; - } - - function check() public { - for (uint256 i = 0; i < addrs.length; i++) - for (uint256 j = i + 1; j < addrs.length; j++) if (addrs[i] == addrs[j]) addrs[j] = address(0); - } - - function echidna_test() public returns (bool) { - return true; - } -} -``` - -With this [`config.yaml`](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/example/pushpop.yaml), Echidna can call all functions but won't easily identify transactions with high gas consumption: - -``` -echidna pushpop.sol --config config.yaml -... -pop used a maximum of 10746 gas -... -check used a maximum of 23730 gas -... -clear used a maximum of 35916 gas -... -push used a maximum of 40839 gas -``` - -This occurs because the cost depends on the size of `addrs`, and random calls tend to leave the array almost empty. -By blacklisting `pop` and `clear`, we obtain better results ([blacklistpushpop.yaml](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/example/blacklistpushpop.yaml)): - -```yaml -estimateGas: true -filterBlacklist: true -filterFunctions: ["C.pop()", "C.clear()"] -``` - -``` -echidna pushpop.sol --config config.yaml -... -push used a maximum of 40839 gas -... -check used a maximum of 1484472 gas -``` - -## Summary: Identifying high gas consumption transactions - -Echidna can identify transactions with high gas consumption using the `estimateGas` configuration option: - -```yaml -estimateGas: true -``` - -```bash -echidna contract.sol --config config.yaml -... -``` - -After completing the fuzzing campaign, Echidna will report a sequence with the maximum gas consumption for each function. diff --git a/program-analysis/echidna/example/blacklistpushpop.yaml b/program-analysis/echidna/example/blacklistpushpop.yaml deleted file mode 100644 index 13e19ec6..00000000 --- a/program-analysis/echidna/example/blacklistpushpop.yaml +++ /dev/null @@ -1,3 +0,0 @@ -estimateGas: true -filterBlacklist: true -filterFunctions: ["C.pop()", "C.clear()"] diff --git a/program-analysis/echidna/example/gas.sol b/program-analysis/echidna/example/gas.sol deleted file mode 100644 index 9ea36c06..00000000 --- a/program-analysis/echidna/example/gas.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.0; - -contract C { - uint256 state; - - function expensive(uint8 times) internal { - for (uint8 i = 0; i < times; i++) { - state = state + i; - } - } - - function f(uint256 x, uint256 y, uint8 times) public { - if (x == 42 && y == 123) expensive(times); - else state = 0; - } - - function echidna_test() public pure returns (bool) { - return true; - } -} diff --git a/program-analysis/echidna/example/gas.yaml b/program-analysis/echidna/example/gas.yaml deleted file mode 100644 index b8781035..00000000 --- a/program-analysis/echidna/example/gas.yaml +++ /dev/null @@ -1,2 +0,0 @@ -seqLen: 2 -estimateGas: true diff --git a/program-analysis/echidna/example/pushpop.sol b/program-analysis/echidna/example/pushpop.sol deleted file mode 100644 index ba4e5c18..00000000 --- a/program-analysis/echidna/example/pushpop.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.0; - -contract C { - address[] addrs; - - function push(address a) public { - addrs.push(a); - } - - function pop() public { - addrs.pop(); - } - - function clear() public { - addrs.length = 0; - } - - function check() public { - for (uint256 i = 0; i < addrs.length; i++) { - for (uint256 j = i + 1; j < addrs.length; j++) { - if (addrs[i] == addrs[j]) addrs[j] = address(0); - } - } - } - - function echidna_test() public pure returns (bool) { - return true; - } -} diff --git a/program-analysis/echidna/example/pushpop.yaml b/program-analysis/echidna/example/pushpop.yaml deleted file mode 100644 index fdcea313..00000000 --- a/program-analysis/echidna/example/pushpop.yaml +++ /dev/null @@ -1 +0,0 @@ -estimateGas: true From 0e13bc4e711ffa034566b41b013b374ea154b91d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20L=C3=B3pez?= Date: Thu, 21 Aug 2025 23:03:50 -0300 Subject: [PATCH 2/4] Update medusa config file, fix multi.sol medusa test --- .github/workflows/medusa.yml | 5 +---- program-analysis/echidna/example/medusa.json | 22 +++++++++++++++----- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/.github/workflows/medusa.yml b/.github/workflows/medusa.yml index b9be3025..23d638fa 100644 --- a/.github/workflows/medusa.yml +++ b/.github/workflows/medusa.yml @@ -84,13 +84,11 @@ jobs: workdir: program-analysis/echidna/example/ files: multi.sol contract: C - config: filter.yaml outcome: failure expected: 'echidna_state4()\" failed after the following call sequence' - name: Assert workdir: program-analysis/echidna/example/ files: assert.sol - config: assert.yaml contract: Incrementor outcome: failure expected: 'inc(uint256)\" resulted in an assertion failure after the following call sequence' @@ -112,7 +110,6 @@ jobs: workdir: program-analysis/echidna/example/ files: TestDepositWithPermit.sol solc-version: 0.8.0 - config: testdeposit.yaml contract: TestDepositWithPermit outcome: success expected: '\[PASSED\] Assertion Test: TestDepositWithPermit.testERC20PermitDeposit(uint256)' @@ -162,7 +159,7 @@ jobs: go build -o medusa -v . go install -v . sudo cp medusa /usr/bin - pip install crytic-compile solc-select + pip install crytic-compile solc-select slither-analyzer - name: Run Medusa continue-on-error: true diff --git a/program-analysis/echidna/example/medusa.json b/program-analysis/echidna/example/medusa.json index b9696b2a..b15e0f8e 100644 --- a/program-analysis/echidna/example/medusa.json +++ b/program-analysis/echidna/example/medusa.json @@ -6,9 +6,11 @@ "testLimit": 0, "shrinkLimit": 5000, "callSequenceLength": 100, + "pruneFrequency": 5, "corpusDirectory": "", "coverageEnabled": true, "coverageFormats": ["html", "lcov"], + "revertReporterEnabled": false, "targetContracts": [], "predeployedContracts": {}, "targetContractsBalances": [], @@ -17,17 +19,16 @@ "senderAddresses": ["0x10000", "0x20000", "0x30000"], "blockNumberDelayMax": 60480, "blockTimestampDelayMax": 604800, - "blockGasLimit": 125000000, "transactionGasLimit": 12500000, "testing": { "stopOnFailedTest": true, "stopOnFailedContractMatching": false, "stopOnNoTests": true, "testAllContracts": false, - "traceAll": false, + "testViewMethods": true, + "verbosity": 1, "assertionTesting": { "enabled": true, - "testViewMethods": false, "panicCodeConfig": { "failOnCompilerInsertedPanic": false, "failOnAssertion": true, @@ -50,7 +51,7 @@ "testPrefixes": ["optimize_"] }, "targetFunctionSignatures": [], - "excludeFunctionSignatures": [] + "excludeFunctionSignatures": ["C.reset1()", "C.reset2()"] }, "chainConfig": { "codeSizeCheckDisabled": true, @@ -58,7 +59,13 @@ "cheatCodesEnabled": true, "enableFFI": false }, - "skipAccountChecks": true + "skipAccountChecks": true, + "forkConfig": { + "forkModeEnabled": false, + "rpcUrl": "", + "rpcBlock": 1, + "poolSize": 20 + } } }, "compilation": { @@ -70,6 +77,11 @@ "args": [] } }, + "slither": { + "useSlither": true, + "cachePath": "slither_results.json", + "args": [] + }, "logging": { "level": "info", "logDirectory": "", From 7c74b0c0bd14b9abc04b0f9c678471b4f2cd1084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20L=C3=B3pez?= Date: Thu, 21 Aug 2025 23:18:39 -0300 Subject: [PATCH 3/4] Update link checking action to supported fork, add config to retry --- .github/workflows/lint_links.yml | 3 +-- mlc_config.json | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 mlc_config.json diff --git a/.github/workflows/lint_links.yml b/.github/workflows/lint_links.yml index 838c7582..a46c159f 100644 --- a/.github/workflows/lint_links.yml +++ b/.github/workflows/lint_links.yml @@ -22,8 +22,7 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive - - uses: gaurav-nelson/github-action-markdown-link-check@v1 + - uses: tcort/github-action-markdown-link-check@v1 with: use-quiet-mode: "yes" check-modified-files-only: ${{ (github.event_name == 'pull_request' && 'yes') || 'no' }} - submodules: true diff --git a/mlc_config.json b/mlc_config.json new file mode 100644 index 00000000..cb666fb6 --- /dev/null +++ b/mlc_config.json @@ -0,0 +1,16 @@ +{ + "httpHeaders": [ + { + "urls": [ + "https://github.com/", + "https://guides.github.com/", + "https://help.github.com/", + "https://docs.github.com/" + ], + "headers": { + "Accept-Encoding": "zstd, br, gzip, deflate" + } + } + ], + "retryOn429": true +} From 7b2a7f3c8cfeefd21e06b09939b51104a0b93727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20L=C3=B3pez?= Date: Thu, 21 Aug 2025 23:48:12 -0300 Subject: [PATCH 4/4] Fix some broken links --- README.md | 2 +- development-guidelines/incident_response.md | 5 ++--- .../cairo/L1_to_L2_address_conversion/README.md | 2 +- not-so-smart-contracts/cosmos/abci_panic/README.md | 2 +- not-so-smart-contracts/substrate/randomness/README.md | 2 +- not-so-smart-contracts/substrate/verify_first/README.md | 2 +- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d6c8ff6f..3364fe3e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Building Secure Smart Contracts -![](https://github.com/crytic/building-secure-contracts/actions/workflows/slither.yml/badge.svg) ![](https://github.com/crytic/building-secure-contracts/actions/workflows/echidna.yml/badge.svg) ![](https://github.com/crytic/building-secure-contracts/actions/workflows/medusa.yml/badge.svg) +![](https://github.com/crytic/building-secure-contracts/actions/workflows/echidna.yml/badge.svg) ![](https://github.com/crytic/building-secure-contracts/actions/workflows/medusa.yml/badge.svg) Brought to you by [Trail of Bits](https://www.trailofbits.com/), this repository offers guidelines and best practices for developing secure smart contracts. Contributions are welcome, you can contribute by following our [contributing guidelines](https://github.com/crytic/building-secure-contracts/blob/master/CONTRIBUTING.md). diff --git a/development-guidelines/incident_response.md b/development-guidelines/incident_response.md index 2e3f23b2..71a0d7ce 100644 --- a/development-guidelines/incident_response.md +++ b/development-guidelines/incident_response.md @@ -49,11 +49,10 @@ Additionally, consider conducting a threat modeling exercise. This exercise will - An approachable guide for incident response. Chapter 4 includes examples for how to approach practicing your process. - [PagerDuty Incident Response](https://response.pagerduty.com/) - A _very_ detailed handbook of how PagerDuty handles incident response themselves. Some useful ideas and resources, but more practical for larger organizations. -- [How to Hack the Yield Protocol](https://docs.yieldprotocol.com/#/operations/how_to_hack) -- [Emergency Procedures for Yearn Finance](https://github.com/yearn/yearn-devdocs/blob/master/docs/developers/v2/EMERGENCY.md) +- [Emergency Procedures for Yearn Finance](https://github.com/yearn/yearn-devdocs/blob/master/docs/developers/security/EMERGENCY.md) - [Rekt pilled: What to do when your dApp gets pwned and how to stay kalm - Heidi Wilder (DSS 2023)](https://www.youtube.com/watch?v=TDlkkg8N0wc) - [Crisis Handbook - Smart Contract Hack (SEAL)](https://docs.google.com/document/d/1DaAiuGFkMEMMiIuvqhePL5aDFGHJ9Ya6D04rdaldqC0/edit) ### Community Incident Retrospectives -- [Yield Protocol](https://medium.com/yield-protocol/post-mortem-of-incident-on-august-5th-2022-7bb70dbb9ada) +- [Yield Protocol](https://web.archive.org/web/20230105183841/https://medium.com/yield-protocol/post-mortem-of-incident-on-august-5th-2022-7bb70dbb9ada) diff --git a/not-so-smart-contracts/cairo/L1_to_L2_address_conversion/README.md b/not-so-smart-contracts/cairo/L1_to_L2_address_conversion/README.md index cc2bd48c..5afc78ed 100644 --- a/not-so-smart-contracts/cairo/L1_to_L2_address_conversion/README.md +++ b/not-so-smart-contracts/cairo/L1_to_L2_address_conversion/README.md @@ -4,7 +4,7 @@ In Starknet, addresses are of the `felt` type, while on L1 addresses are of the # Example -Consider the following code to initiate L2 deposits from L1. The first example has no checks on the `to` parameter, and depending on the user's address, it could transfer tokens to an unexpected address on L2. The second example, however, adds verification to ensure this does not happen. Note that the code is a simplified version of how messages are sent on L1 and processed on L2. For a more comprehensive overview, see here: [https://www.cairo-lang.org/docs/hello_starknet/l1l2.html](https://docs.cairo-lang.org/hello_starknet/l1l2.html). +Consider the following code to initiate L2 deposits from L1. The first example has no checks on the `to` parameter, and depending on the user's address, it could transfer tokens to an unexpected address on L2. The second example, however, adds verification to ensure this does not happen. Note that the code is a simplified version of how messages are sent on L1 and processed on L2. For a more comprehensive overview, see here: [https://www.cairo-lang.org/docs/hello_starknet/l1l2.html](https://web.archive.org/web/20250117175431/https://docs.cairo-lang.org/hello_starknet/l1l2.html). ```solidity contract L1ToL2Bridge { diff --git a/not-so-smart-contracts/cosmos/abci_panic/README.md b/not-so-smart-contracts/cosmos/abci_panic/README.md index 0e9757f3..6d16b0b4 100644 --- a/not-so-smart-contracts/cosmos/abci_panic/README.md +++ b/not-so-smart-contracts/cosmos/abci_panic/README.md @@ -43,6 +43,6 @@ func validateTotalBorrows(ctx sdk.Context, k keeper.Keeper) { ## External examples -- [Gravity Bridge can `panic` in multiple locations in the `EndBlocker` method](https://giters.com/althea-net/cosmos-gravity-bridge/issues/348) +- [Gravity Bridge can `panic` in multiple locations in the `EndBlocker` method](https://github.com/althea-net/cosmos-gravity-bridge/issues/348) - [Agoric `panic`s purposefully if the `PushAction` method returns an error](https://github.com/Agoric/agoric-sdk/blob/9116ede69169ebb252faf069d90022e8e05c6a4e/golang/cosmos/x/vbank/module.go#L166) - [Setting invalid parameters in `x/distribution` module causes `panic` in `BeginBlocker`](https://github.com/cosmos/cosmos-sdk/issues/5808). Valid parameters are [described in the documentation](https://docs.cosmos.network/v0.45/modules/distribution/07_params.html). diff --git a/not-so-smart-contracts/substrate/randomness/README.md b/not-so-smart-contracts/substrate/randomness/README.md index db11afac..5c22cdea 100644 --- a/not-so-smart-contracts/substrate/randomness/README.md +++ b/not-so-smart-contracts/substrate/randomness/README.md @@ -61,4 +61,4 @@ Note that the quality of randomness provided to the `pallet-bad-lottery` pallet - https://docs.substrate.io/reference/how-to-guides/pallet-design/incorporate-randomness/ - https://ethresear.ch/t/rng-exploitability-analysis-assuming-pure-randao-based-main-chain/1825/7 - https://ethresear.ch/t/collective-coin-flipping-csprng/3252/21 -- https://github.com/paritytech/ink/issues/57#issuecomment-486998848 +- https://github.com/use-ink/ink/issues/57#issuecomment-486998848 diff --git a/not-so-smart-contracts/substrate/verify_first/README.md b/not-so-smart-contracts/substrate/verify_first/README.md index f5feb554..223b6d3f 100644 --- a/not-so-smart-contracts/substrate/verify_first/README.md +++ b/not-so-smart-contracts/substrate/verify_first/README.md @@ -1,6 +1,6 @@ # Verify First, Write Last -**NOTE**: As of [Polkadot v0.9.25](https://github.com/substrate-developer-hub/substrate-docs/issues/1215), the **Verify First, Write Last** practice is no longer required. However, since older versions are still vulnerable and because it is still best practice, it is worth discussing the "Verify First, Write Last" idiom. +**NOTE**: As of [Polkadot v0.9.25](https://github.com/polkadot-developers/substrate-docs/issues/1215), the **Verify First, Write Last** practice is no longer required. However, since older versions are still vulnerable and because it is still best practice, it is worth discussing the "Verify First, Write Last" idiom. Substrate does not cache state prior to extrinsic dispatch. Instead, state changes are made as they are invoked. This is in contrast to a transaction in Ethereum where, if the transaction reverts, no state changes will persist. In the case of Substrate, if a state change is made and then the dispatch throws a `DispatchError`, the original state change will persist. This unique behavior has led to the "Verify First, Write Last" practice.