Building Secure Smart Contracts

This repository, brought to you by Trail of Bits, outlines guidelines and best practices to write secure smart contracts.

We welcome contributions, and you can contribute by following our contributing guidelines.

Table of contents:

License

secure-contracts and building-secure-contracts are licensed and distributed under the AGPLv3 license. Contact us if you're looking for an exception to the terms.

List of smart contract development best practices

Development Guidelines

Follow these high-level recommendations to build more secure smart contracts.

Design guidelines

The design of the contract should be discussed ahead of time, prior writing any line of code.

Documentation and specifications

Documentation can be written at different levels, and should be updated while implementing the contracts:

  • A plain English description of the system, describing what the contracts do and any assumptions on the codebase.
  • Schema and architectural diagrams, including the contract interactions and the state machine of the system. Slither printers can help to generate these schemas.
  • Thorough code documentation, the Natspec format can be used for Solidity.

On-chain vs off-chain computation

  • Keep as much code as you can off-chain. Keep the on-chain layer small. Pre-process data with code off-chain in such a way that verification on-chain is simple. Do you need an ordered list? Sort the list off-chain, then only check its order onchain.

Upgradeability

We discussed the different upgradeability solutions in our blog post. In particular, if you are using delegatecall to achieve upgradability, carefully review all items of the above delegatecall proxy guidance. Make a deliberate choice to support upgradeability or not prior to writing any code. The decision will influence how you structure our code. In general, we recommend:

  • Favoring contract migration over upgradeability. Migration system have many of the same advantages than upgradeable, without their drawbacks.
  • Using the data separation pattern over the delegatecall proxy one. If your project has a clear abstraction separation, upgradeability using data separation will necessitate only a few adjustments. The delegatecall proxy requires EVM expertise and is highly error-prone.
  • Document the migration/upgrade procedure before the deployment. If you have to react under stress without any guidelines, you will make mistakes. Write the procedure to follow ahead of time. It should include:
    • The calls that initiate the new contracts
    • Where are stored the keys and how to access them
    • How to check the deployment! Develop and test a post-deployment script.

Delegatecall Proxy Pattern

The delegatecall opcode is a very sharp tool that must be used carefully. Many high-profile exploits utilize little-known edge cases and counter-intuitive aspects of the delegatecall proxy pattern. This section aims to outline the most important risks to keep in mind while developing such smart contract systems. Trail of Bits developed the slither-check-upgradability tool to aid in the development of secure delegatecall proxies, it performs safety checks relevant to both upgradable and immutable delegatecall proxies.

  • Storage layout: The storage layout of the proxy and implementation must be the same. Do not try to define the same state variables on each contract. Instead, both contracts should inherit all of their state variables from one shared base contract.
  • Inheritance: If the base storage contract is split up, beware that the order of inheritance impacts the final storage layout. For example, contract A is B,C and contract A is C,B will not yield the same storage layout if both B and C define state variables.
  • Initialization: Make sure that the implementation is immediately initialized. Well-known disasters (and near disasters) have featured an uninitialized implementation contract. A factory pattern can help ensure that contracts are deployed & initialized correctly while also mitigating front-running risks that might otherwise open up between contract deployment & initialization.
  • Function shadowing: If the same method is defined on the proxy and the implementation, then the proxy’s function will not be called. Be aware of setOwner and other administration functions that commonly exist on proxies.
  • Direct implementation usage: Consider configuring the implementation’s state variables with values that prevent it from being used directly, such as by setting a flag during construction that disables the implementation and causes all methods to revert. This is particularly important if the implementation also performs delegatecall operations because this opens up the possibility of unintended self destruction of the implementation.
  • Immutable and constant variables: These are embedded into the bytecode and can therefore get out of sync between the proxy and implementation. If the implementation has an incorrect immutable variable, this value may still be used even if the same variables are correctly set in the proxy’s bytecode.
  • Contract Existence Checks: All low-level calls, not just delegatecall, will return true against an address with empty bytecode. This might cause callers to be misled to think that a call performed a meaningful operation when it did not or it might result in important safety checks being silently skipped. Be aware that while a contract’s constructor is running, its bytecode remains empty until the end of constructor execution. We recommend that you rigorously verify that all low-level calls are properly protected against nonexistent contracts, keeping in mind that most proxy libraries (such as the one written by Openzeppelin) do not perform contract existence checks automatically.

For more information regarding delegatecall proxies in general, reference our blog posts and presentations:

  • Contract Upgradability Anti-Patterns: Describes the difference between a downstream data contract and delegatecall proxies which use an upstream data contract and how these patterns impact upgradability.
  • How the Diamond standard falls short: This post dives deep into delegatecall risks which apply to all contracts, not just those that follow the diamond standard.
  • Breaking Aave Upgradeability: A write-up describing a subtle problem that we discovered in Aave AToken contracts that resulted from the interplay between delegatecall proxies, contact existence checks, and unsafe initialization.
  • Contract Upgrade Risks and Recommendations: A talk by Trail of Bits describing best-practices for developing upgradable delegatecall proxies. The section starting at 5:49 describes some general risks that also apply to non-upgradable proxies.

Implementation guidelines

Strive for simplicity. Always use the simplest solution that fits your purpose. Any member of your team should be able to understand your solution.

Function composition

The architecture of your codebase should make your code easy to review. Avoid architectural choices that decrease the ability to reason about its correctness.

  • Split the logic of your system, either through multiple contracts or by grouping similar functions together (for example, authentication, arithmetic, ...).
  • Write small functions, with a clear purpose. This will facilitate easier review and allow the testing of individual components.

Inheritance

  • Keep the inheritance manageable. Inheritance should be used to divide the logic, however, your project should aim to minimize the depth and width of the inheritance tree.
  • Use Slither’s inheritance printer to check the contracts’ hierarchy. The inheritance printer will help you review the size of the hierarchy.

Events

  • Log all crucial operations. Events will help to debug the contract during the development, and monitor it after deployment.

Avoid known pitfalls

Dependencies

  • Use well-tested libraries. Importing code from well-tested libraries will reduce the likelihood that you write buggy code. If you want to write an ERC20 contract, use OpenZeppelin.
  • Use a dependency manager; avoid copy-pasting code. If you rely on an external source, then you must keep it up-to-date with the original source.

Testing and verification

  • Write thorough unit-tests. An extensive test suite is crucial to build high-quality software.
  • Write Slither and Echidna custom checks and properties. Automated tools will help ensure your contract is secure. Review the rest of this guide to learn how to write efficient checks and properties.

Solidity

  • Favor Solidity versions outlined in our Slither Recommendations In our opinion, older Solidity are more secure and have better built-in practices. Newer Solidity versions may be too young to be used in production and require additional time to mature.
  • Use a stable release to compile; use the latest release to check for warnings. Check that your code has no reported issues with the latest compiler version. However, Solidity has a fast release cycle and has a history of compiler bugs, so we do not recommend the latest version for deployment (see Slither’s solc version recommendation).
  • Do not use inline assembly. Assembly requires EVM expertise. Do not write EVM code if you have not mastered the yellow paper.

Deployment guidelines

Once the contract has been developed and deployed:

  • Monitor your contracts. Watch the logs, and be ready to react in case of contract or wallet compromise.
  • Add your contact info to blockchain-security-contacts. This list helps third-parties contact you if a security flaw is discovered.
  • Secure the wallets of privileged users. Follow our best practices if you store keys in hardware wallets.
  • Have a response to incident plan. Consider that your smart contracts can be compromised. Even if your contracts are free of bugs, an attacker may take control of the contract owner's keys.

Token integration checklist

The following checklist provides recommendations for interactions with arbitrary tokens. Every unchecked item should be justified, and its associated risks, understood.

For convenience, all Slither utilities can be run directly on a token address, such as the following:

slither-check-erc 0xdac17f958d2ee523a2206206994597c13d831ec7 TetherToken --erc erc20
slither-check-erc 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d KittyCore --erc erc721

To follow this checklist, use the below output from Slither for the token:

- slither-check-erc [target] [contractName] [optional: --erc ERC_NUMBER]
- slither [target] --print human-summary
- slither [target] --print contract-summary
- slither-prop . --contract ContractName # requires configuration, and use of Echidna and Manticore

General considerations

  • The contract has a security review. Avoid interacting with contracts that lack a security review. Check the length of the assessment (i.e., the level of effort), the reputation of the security firm, and the number and severity of the findings.
  • You have contacted the developers. You may need to alert their team to an incident. Look for appropriate contacts on blockchain-security-contacts.
  • They have a security mailing list for critical announcements. Their team should advise users (like you!) when critical issues are found or when upgrades occur.

Contract composition

  • The contract avoids unneeded complexity. The token should be a simple contract; a token with complex code requires a higher standard of review. Use Slither’s human-summary printer to identify complex code.
  • The contract uses SafeMath. Contracts that do not use SafeMath require a higher standard of review. Inspect the contract by hand for SafeMath usage.
  • The contract has only a few non–token-related functions. Non-token-related functions increase the likelihood of an issue in the contract. Use Slither’s contract-summary printer to broadly review the code used in the contract.
  • The token only has one address. Tokens with multiple entry points for balance updates can break internal bookkeeping based on the address (e.g., balances[token_address][msg.sender] may not reflect the actual balance).

Owner privileges

  • The token is not upgradeable. Upgradeable contracts may change their rules over time. Use Slither’s human-summary printer to determine whether the contract is upgradeable.
  • The owner has limited minting capabilities. Malicious or compromised owners can abuse minting capabilities. Use Slither’s human-summary printer to review minting capabilities, and consider manually reviewing the code.
  • The token is not pausable. Malicious or compromised owners can trap contracts relying on pausable tokens. Identify pausable code by hand.
  • The owner cannot blacklist the contract. Malicious or compromised owners can trap contracts relying on tokens with a blacklist. Identify blacklisting features by hand.
  • The team behind the token is known and can be held responsible for abuse. Contracts with anonymous development teams or teams that reside in legal shelters require a higher standard of review.

ERC20 tokens

ERC20 conformity checks

Slither includes a utility, slither-check-erc, that reviews the conformance of a token to many related ERC standards. Use slither-check-erc to review the following:

  • Transfer and transferFrom return a boolean. Several tokens do not return a boolean on these functions. As a result, their calls in the contract might fail.
  • The name, decimals, and symbol functions are present if used. These functions are optional in the ERC20 standard and may not be present.
  • Decimals returns a uint8. Several tokens incorrectly return a uint256. In such cases, ensure that the value returned is below 255.
  • The token mitigates the known ERC20 race condition. The ERC20 standard has a known ERC20 race condition that must be mitigated to prevent attackers from stealing tokens.

Slither includes a utility, slither-prop, that generates unit tests and security properties that can discover many common ERC flaws. Use slither-prop to review the following:

  • The contract passes all unit tests and security properties from slither-prop. Run the generated unit tests and then check the properties with Echidna and Manticore.

Risks of ERC20 Extensions

The behavior of certain contracts may differ from the original ERC specification. Conduct a manual review of the following conditions:

  • The token is not an ERC777 token and has no external function call in transfer or transferFrom. External calls in the transfer functions can lead to reentrancies.
  • Transfer and transferFrom should not take a fee. Deflationary tokens can lead to unexpected behavior.
  • Potential interest earned from the token is taken into account. Some tokens distribute interest to token holders. This interest may be trapped in the contract if not taken into account.

Token scarcity

Reviews of token scarcity issues must be executed manually. Check for the following conditions:

  • The supply is owned by more than a few users. If a few users own most of the tokens, they can influence operations based on the tokens’ repartition.
  • The total supply is sufficient. Tokens with a low total supply can be easily manipulated.
  • The tokens are located in more than a few exchanges. If all the tokens are in one exchange, a compromise of the exchange could compromise the contract relying on the token.
  • Users understand the risks associated with a large amount of funds or flash loans. Contracts relying on the token balance must account for attackers with a large amount of funds or attacks executed through flash loans.
  • The token does not allow flash minting. Flash minting can lead to substantial swings in the balance and the total supply, which necessitate strict and comprehensive overflow checks in the operation of the token.

ERC721 tokens

ERC721 Conformity Checks

The behavior of certain contracts may differ from the original ERC specification. Conduct a manual review of the following conditions:

  • Transfers of tokens to the 0x0 address revert. Several tokens allow transfers to 0x0 and consider tokens transferred to that address to have been burned; however, the ERC721 standard requires that such transfers revert.
  • safeTransferFrom functions are implemented with the correct signature. Several token contracts do not implement these functions. A transfer of NFTs to one of those contracts can result in a loss of assets.
  • The name, decimals, and symbol functions are present if used. These functions are optional in the ERC721 standard and may not be present.
  • If it is used, decimals returns a uint8(0). Other values are invalid.
  • The name and symbol functions can return an empty string. This behavior is allowed by the standard.
  • The ownerOf function reverts if the tokenId is invalid or is set to a token that has already been burned. The function cannot return 0x0. This behavior is required by the standard, but it is not always properly implemented.
  • A transfer of an NFT clears its approvals. This is required by the standard.
  • The token ID of an NFT cannot be changed during its lifetime. This is required by the standard.

Common Risks of the ERC721 Standard

To mitigate the risks associated with ERC721 contracts, conduct a manual review of the following conditions:

  • The onERC721Received callback is taken into account. External calls in the transfer functions can lead to reentrancies, especially when the callback is not explicit (e.g., in safeMint calls).
  • When an NFT is minted, it is safely transferred to a smart contract. If there is a minting function, it should behave similarly to safeTransferFrom and properly handle the minting of new tokens to a smart contract. This will prevent a loss of assets.
  • The burning of a token clears its approvals. If there is a burning function, it should clear the token’s previous approvals.

Incident Response Recommendations

Here, we provide recommendations around the formulation of an incident response plan.

  • Identify who (either specific people or roles) is responsible for carrying out the mitigations (deploying smart contracts, pausing contracts, upgrading the front end, etc.).
    • Specifying these roles will strengthen the incident response plan and ease the execution of mitigating actions when necessary.
  • Document internal processes for situations in which a deployed remediation does not work or introduces a new bug.
    • Consider adding a fallback scenario that describes an action plan in the event of a failed remediation.
  • Clearly describe the intended process of contract deployment.
  • Consider whether and under what circumstances your company will make affected users whole after certain issues occur.
    • Some scenarios to consider include an individual or aggregate loss, a loss resulting from user error, a contract flaw, and a third-party contract flaw.
  • Document how you plan to keep up to date on new issues, both to inform future development and to secure the deployment toolchain and the external on-chain and off-chain services that the system relies on.
    • For each language and component, describe the noteworthy sources for vulnerability news. Subscribe to updates for each source. Consider creating a special private Discord/Slack channel with a bot that will post the latest vulnerability news; this will help the team keep track of updates all in one place. Also consider assigning specific team members to keep track of the vulnerability news of a specific component of the system.
  • Consider scenarios involving issues that would indirectly affect the system.
  • Determine when and how the team would reach out to and onboard external parties (auditors, affected users, other protocol developers, etc.).
    • Some issues may require collaboration with external parties to efficiently remediate them.
  • Define contract behavior that is considered abnormal for off-chain monitoring.
    • Consider adding more resilient solutions for detection and mitigation, especially in terms of specific alternate endpoints and queries for different data as well as status pages and support contacts for affected services.
  • Combine issues and determine whether new detection and mitigation scenarios are needed.
  • Perform periodic dry runs of specific scenarios in the incident response plan to find gaps and opportunities for improvement and to develop muscle memory.
    • Document the intervals at which the team should perform dry runs of the various scenarios. For scenarios that are more likely to happen, perform dry runs more regularly. Create a template to be filled in after a dry run to describe the improvements that need to be made to the incident response.

Incident Response Plan Resources

Well-handled IR Incidents

Secure development workflow

Here's a high-level process we recommend following while you write your smart contracts.

Check for known security issues:

  • Review your contracts with Slither. It has more than 70 built-in detectors for common vulnerabilities. Run it on every check-in with new code and ensure it gets a clean report (or use triage mode to silence certain issues).

Consider special features of your contract:

  • Are your contracts upgradeable? Review your upgradeability code for flaws with slither-check-upgradeability or Crytic. We've documented 17 ways upgrades can go sideways.
  • Do your contracts purport to conform to ERCs? Check them with slither-check-erc. This tool instantly identifies deviations from six common specs.
  • Do you have unit tests in Truffle? Enrich them with slither-prop. It automatically generates a robust suite of security properties for features of ERC20 based on your specific code.
  • Do you integrate with 3rd party tokens? Review our token integration checklist before relying on external contracts.

Visually inspect critical security features of your code:

  • Review Slither's inheritance-graph printer. Avoid inadvertent shadowing and C3 linearization issues.
  • Review Slither's function-summary printer. It reports function visibility and access controls.
  • Review Slither's vars-and-auth printer. It reports access controls on state variables.

Document critical security properties and use automated test generators to evaluate them:

  • Learn to document security properties for your code. It's tough as first, but it's the single most important activity for achieving a good outcome. It's also a prerequisite for using any of the advanced techniques in this tutorial.
  • Define security properties in Solidity, for use with Echidna and Manticore. Focus on your state machine, access controls, arithmetic operations, external interactions, and standards conformance.
  • Define security properties with Slither's Python API. Focus on inheritance, variable dependencies, access controls, and other structural issues.

Finally, be mindful of issues that automated tools cannot easily find:

  • Lack of privacy: everyone else can see your transactions while they're queued in the pool
  • Front running transactions
  • Cryptographic operations
  • Risky interactions with external DeFi components

Ask for help

Office Hours run every Tuesday afternoon. These 1-hour, 1-on-1 sessions are an opportunity to ask us any questions you have about security, troubleshoot using our tools, and get feedback from experts about your current approach. We will help you work through this guide.

Join our Slack: Empire Hacking. We're always available in the #crytic and #ethereum channels if you have any questions.

Security is about more than just smart contracts

Review our quick tips for general application and corporate security. It's most important that your code on-chain is secure, but lapses in off-chain security may be just as severe, especially where owner keys are concerned.

Learn EVM

List of EVM technical knowledge

  • EVM Opcode Reference: Reference and notes for each of the EVM opcodes
  • Transaction Tracing: Helper scripts and guidance for generating and navigating transaction traces
  • Yellow Paper Guidance: Symbol reference for more easily reading the Ethereum yellow paper
  • Forks <> EIPs: Summarize the EIPs included in each fork
    • Forks <> CIPs: Summarize the CIPs and EIPs included in each Celo fork (EVM-compatible chain)
    • Upgrades <> TIPs: Summarize the TIPs included in each TRON upgrade (EVM-compatible chain)
    • Forks <> BEPs: Summarize the BEPs included in each BSC fork (EVM-compatible chain)

Ethereum VM (EVM) Opcodes and Instruction Reference

This reference consolidates EVM opcode information from the yellow paper, stack exchange, solidity source, parity source, evm-opcode-gas-costs and Manticore.

Notes

The size of a "word" in EVM is 256 bits.

The gas information is a work in progress. If an asterisk is in the Gas column, the base cost is shown but may vary based on the opcode arguments.

Table

OpcodeNameDescriptionExtra InfoGas
0x00STOPHalts execution-0
0x01ADDAddition operation-3
0x02MULMultiplication operation-5
0x03SUBSubtraction operation-3
0x04DIVInteger division operation-5
0x05SDIVSigned integer division operation (truncated)-5
0x06MODModulo remainder operation-5
0x07SMODSigned modulo remainder operation-5
0x08ADDMODModulo addition operation-8
0x09MULMODModulo multiplication operation-8
0x0aEXPExponential operation-10*
0x0bSIGNEXTENDExtend length of two's complement signed integer-5
0x0c - 0x0fUnusedUnused-
0x10LTLess-than comparison-3
0x11GTGreater-than comparison-3
0x12SLTSigned less-than comparison-3
0x13SGTSigned greater-than comparison-3
0x14EQEquality comparison-3
0x15ISZEROSimple not operator-3
0x16ANDBitwise AND operation-3
0x17ORBitwise OR operation-3
0x18XORBitwise XOR operation-3
0x19NOTBitwise NOT operation-3
0x1aBYTERetrieve single byte from word-3
0x1bSHLShift LeftEIP1453
0x1cSHRLogical Shift RightEIP1453
0x1dSARArithmetic Shift RightEIP1453
0x20KECCAK256Compute Keccak-256 hash-30*
0x21 - 0x2fUnusedUnused
0x30ADDRESSGet address of currently executing account-2
0x31BALANCEGet balance of the given account-700
0x32ORIGINGet execution origination address-2
0x33CALLERGet caller address-2
0x34CALLVALUEGet deposited value by the instruction/transaction responsible for this execution-2
0x35CALLDATALOADGet input data of current environment-3
0x36CALLDATASIZEGet size of input data in current environment-2*
0x37CALLDATACOPYCopy input data in current environment to memory-3
0x38CODESIZEGet size of code running in current environment-2
0x39CODECOPYCopy code running in current environment to memory-3*
0x3aGASPRICEGet price of gas in current environment-2
0x3bEXTCODESIZEGet size of an account's code-700
0x3cEXTCODECOPYCopy an account's code to memory-700*
0x3dRETURNDATASIZEPushes the size of the return data buffer onto the stackEIP 2112
0x3eRETURNDATACOPYCopies data from the return data buffer to memoryEIP 2113
0x3fEXTCODEHASHReturns the keccak256 hash of a contract's codeEIP 1052700
0x40BLOCKHASHGet the hash of one of the 256 most recent complete blocks-20
0x41COINBASEGet the block's beneficiary address-2
0x42TIMESTAMPGet the block's timestamp-2
0x43NUMBERGet the block's number-2
0x44DIFFICULTYGet the block's difficulty-2
0x45GASLIMITGet the block's gas limit-2
0x46CHAINIDReturns the current chain’s EIP-155 unique identifierEIP 13442
0x47 - 0x4fUnused-
0x48BASEFEEReturns the value of the base fee of the current block it is executing in.EIP 31982
0x50POPRemove word from stack-2
0x51MLOADLoad word from memory-3*
0x52MSTORESave word to memory-3*
0x53MSTORE8Save byte to memory-3
0x54SLOADLoad word from storage-800
0x55SSTORESave word to storage-20000**
0x56JUMPAlter the program counter-8
0x57JUMPIConditionally alter the program counter-10
0x58PCGet the value of the program counter prior to the increment-2
0x59MSIZEGet the size of active memory in bytes-2
0x5aGASGet the amount of available gas, including the corresponding reduction for the cost of this instruction-2
0x5bJUMPDESTMark a valid destination for jumps-1
0x5c - 0x5fUnused-
0x60PUSH1Place 1 byte item on stack-3
0x61PUSH2Place 2-byte item on stack-3
0x62PUSH3Place 3-byte item on stack-3
0x63PUSH4Place 4-byte item on stack-3
0x64PUSH5Place 5-byte item on stack-3
0x65PUSH6Place 6-byte item on stack-3
0x66PUSH7Place 7-byte item on stack-3
0x67PUSH8Place 8-byte item on stack-3
0x68PUSH9Place 9-byte item on stack-3
0x69PUSH10Place 10-byte item on stack-3
0x6aPUSH11Place 11-byte item on stack-3
0x6bPUSH12Place 12-byte item on stack-3
0x6cPUSH13Place 13-byte item on stack-3
0x6dPUSH14Place 14-byte item on stack-3
0x6ePUSH15Place 15-byte item on stack-3
0x6fPUSH16Place 16-byte item on stack-3
0x70PUSH17Place 17-byte item on stack-3
0x71PUSH18Place 18-byte item on stack-3
0x72PUSH19Place 19-byte item on stack-3
0x73PUSH20Place 20-byte item on stack-3
0x74PUSH21Place 21-byte item on stack-3
0x75PUSH22Place 22-byte item on stack-3
0x76PUSH23Place 23-byte item on stack-3
0x77PUSH24Place 24-byte item on stack-3
0x78PUSH25Place 25-byte item on stack-3
0x79PUSH26Place 26-byte item on stack-3
0x7aPUSH27Place 27-byte item on stack-3
0x7bPUSH28Place 28-byte item on stack-3
0x7cPUSH29Place 29-byte item on stack-3
0x7dPUSH30Place 30-byte item on stack-3
0x7ePUSH31Place 31-byte item on stack-3
0x7fPUSH32Place 32-byte (full word) item on stack-3
0x80DUP1Duplicate 1st stack item-3
0x81DUP2Duplicate 2nd stack item-3
0x82DUP3Duplicate 3rd stack item-3
0x83DUP4Duplicate 4th stack item-3
0x84DUP5Duplicate 5th stack item-3
0x85DUP6Duplicate 6th stack item-3
0x86DUP7Duplicate 7th stack item-3
0x87DUP8Duplicate 8th stack item-3
0x88DUP9Duplicate 9th stack item-3
0x89DUP10Duplicate 10th stack item-3
0x8aDUP11Duplicate 11th stack item-3
0x8bDUP12Duplicate 12th stack item-3
0x8cDUP13Duplicate 13th stack item-3
0x8dDUP14Duplicate 14th stack item-3
0x8eDUP15Duplicate 15th stack item-3
0x8fDUP16Duplicate 16th stack item-3
0x90SWAP1Exchange 1st and 2nd stack items-3
0x91SWAP2Exchange 1st and 3rd stack items-3
0x92SWAP3Exchange 1st and 4th stack items-3
0x93SWAP4Exchange 1st and 5th stack items-3
0x94SWAP5Exchange 1st and 6th stack items-3
0x95SWAP6Exchange 1st and 7th stack items-3
0x96SWAP7Exchange 1st and 8th stack items-3
0x97SWAP8Exchange 1st and 9th stack items-3
0x98SWAP9Exchange 1st and 10th stack items-3
0x99SWAP10Exchange 1st and 11th stack items-3
0x9aSWAP11Exchange 1st and 12th stack items-3
0x9bSWAP12Exchange 1st and 13th stack items-3
0x9cSWAP13Exchange 1st and 14th stack items-3
0x9dSWAP14Exchange 1st and 15th stack items-3
0x9eSWAP15Exchange 1st and 16th stack items-3
0x9fSWAP16Exchange 1st and 17th stack items-3
0xa0LOG0Append log record with no topics-375
0xa1LOG1Append log record with one topic-750
0xa2LOG2Append log record with two topics-1125
0xa3LOG3Append log record with three topics-1500
0xa4LOG4Append log record with four topics-1875
0xa5 - 0xafUnused-
0xb0JUMPTOTentative libevmasm has different numbersEIP 615
0xb1JUMPIFTentativeEIP 615
0xb2JUMPSUBTentativeEIP 615
0xb4JUMPSUBVTentativeEIP 615
0xb5BEGINSUBTentativeEIP 615
0xb6BEGINDATATentativeEIP 615
0xb8RETURNSUBTentativeEIP 615
0xb9PUTLOCALTentativeEIP 615
0xbaGETLOCALTentativeEIP 615
0xbb - 0xe0Unused-
0xe1SLOADBYTESOnly referenced in pyethereum--
0xe2SSTOREBYTESOnly referenced in pyethereum--
0xe3SSIZEOnly referenced in pyethereum--
0xe4 - 0xefUnused-
0xf0CREATECreate a new account with associated code-32000
0xf1CALLMessage-call into an account-Complicated
0xf2CALLCODEMessage-call into this account with alternative account's code-Complicated
0xf3RETURNHalt execution returning output data-0
0xf4DELEGATECALLMessage-call into this account with an alternative account's code, but persisting into this account with an alternative account's code-Complicated
0xf5CREATE2Create a new account and set creation address to sha3(sender + sha3(init code)) % 2**160-
0xf6 - 0xf9Unused--
0xfaSTATICCALLSimilar to CALL, but does not modify state-40
0xfbUnused--
0xfdREVERTStop execution and revert state changes, without consuming all provided gas and providing a reason-0
0xfeINVALIDDesignated invalid instruction-0
0xffSELFDESTRUCTHalt execution and register account for later deletion-5000*

Instruction Details


STOP

0x00

() => ()

halts execution


ADD

0x01

Takes two words from stack, adds them, then pushes the result onto the stack.

(a, b) => (c)

c = a + b


MUL

0x02

(a, b) => (c)

c = a * b


SUB

0x03

(a, b) => (c)

c = a - b


DIV

0x04

(a, b) => (c)

c = a / b


SDIV

0x05

(a: int256, b: int256) => (c: int256)

c = a / b


MOD

0x06

(a, b) => (c)

c = a % b


SMOD

0x07

(a: int256, b: int256) => (c: int256)

c = a % b


ADDMOD

0x08

(a, b, m) => (c)

c = (a + b) % m


MULMOD

0x09

(a, b, m) => (c)

c = (a * b) % m


EXP

0x0a

(a, b, m) => (c)

c = (a * b) % m


SIGNEXTEND

0x0b

(b, x) => (y)

y = SIGNEXTEND(x, b)

sign extends x from (b + 1) * 8 bits to 256 bits.


LT

0x10

(a, b) => (c)

c = a < b

all values interpreted as uint256


GT

0x11

(a, b) => (c)

c = a > b

all values interpreted as uint256


SLT

0x12

(a, b) => (c)

c = a < b

all values interpreted as int256


SGT

0x13

(a, b) => (c)

c = a > b

all values interpreted as int256


EQ

0x14

Pops 2 elements off the stack and pushes the value 1 to the stack in case they're equal, otherwise the value 0.

(a, b) => (c)

c = a == b


ISZERO

0x15

(a) => (c)

c = a == 0


AND

0x16

(a, b) => (c)

c = a & b


OR

0x17

(a, b) => (c)

c = a | b


XOR

0x18

(a, b) => (c)

c = a ^ b


NOT

0x19

(a) => (c)

c = ~a


BYTE

0x1a

(i, x) => (y)

y = (x >> (248 - i * 8) & 0xff


SHL

0x1b

Pops 2 elements from the stack and pushes the second element onto the stack shifted left by the shift amount (first element).

(shift, value) => (res)

res = value << shift


SHR

0x1c

Pops 2 elements from the stack and pushes the second element onto the stack shifted right by the shift amount (first element).

(shift, value) => (res)

res = value >> shift


SAR

0x1d

(shift, value) => (res)

res = value >> shift

value: int256


KECCAK256

0x20

(offset, len) => (hash)

hash = keccak256(memory[offset:offset+len])


ADDRESS

0x30

() => (address(this))


BALANCE

0x31

() => (address(this).balance)


ORIGIN

0x32

() => (tx.origin)


CALLER

0x33

() => (msg.sender)


CALLVALUE

0x34

() => (msg.value)


CALLDATALOAD

0x35

(index) => (msg.data[index:index+32])


CALLDATASIZE

0x36

() => (msg.data.size)


CALLDATACOPY

0x37

(memOffset, offset, length) => ()

memory[memOffset:memOffset+len] = msg.data[offset:offset+len]


CODESIZE

0x38

() => (address(this).code.size)


CODECOPY

0x39

(memOffset, codeOffset, len) => ()

memory[memOffset:memOffset+len] = address(this).code[codeOffset:codeOffset+len]


GASPRICE

0x3a

() => (tx.gasprice)


EXTCODESIZE

0x3b

(addr) => (address(addr).code.size)


EXTCODECOPY

0x3c

(addr, memOffset, offset, length) => ()

memory[memOffset:memOffset+len] = address(addr).code[codeOffset:codeOffset+len]


RETURNDATASIZE

0x3d

() => (size)

size = RETURNDATASIZE()

The number of bytes that were returned from the last ext call


RETURNDATACOPY

0x3e

(memOffset, offset, length) => ()

memory[memOffset:memOffset+len] = RETURNDATA[codeOffset:codeOffset+len]

RETURNDATA is the data returned from the last external call


EXTCODEHASH

0x3f

(addr) => (hash)

hash = address(addr).exists ? keccak256(address(addr).code) : 0


BLOCKHASH

0x40

(number) => (hash)

hash = block.blockHash(number)


COINBASE

0x41

() => (block.coinbase)


TIMESTAMP

0x42

() => (block.timestamp)


NUMBER

0x43

() => (block.number)


DIFFICULTY

0x44

() => (block.difficulty)


GASLIMIT

0x45

() => (block.gaslimit)


CHAINID

0x46

() => (chainid)

where chainid = 1 for mainnet & some other value for other networks


SELFBALANCE

0x47

() => (address(this).balance)


BASEFEE

0x48

() => (block.basefee)

current block's base fee (related to EIP1559)


POP

0x50

(a) => ()

discards the top stack item


MLOAD

0x51

(offset) => (value)

value = memory[offset:offset+32]


MSTORE

0x52

Saves a word to the EVM memory. Pops 2 elements from stack - the first element being the word memory address where the saved value (second element popped from stack) will be stored.

(offset, value) => ()

memory[offset:offset+32] = value


MSTORE8

0x53

(offset, value) => ()

memory[offset:offset+32] = value & 0xff


SLOAD

0x54

Pops 1 element off the stack, that being the key which is the storage slot and returns the read value stored there.

(key) => (value)

value = storage[key]


SSTORE

0x55

Pops 2 elements off the stack, the first element being the key and the second being the value which is then stored at the storage slot represented from the first element (key).

(key, value) => ()

storage[key] = value


JUMP

0x56

(dest) => ()

pc = dest


JUMPI

0x57

Conditional - Pops 2 elements from the stack, the first element being the jump location and the second being the value 0 (false) or 1 (true). If the value’s 1 the PC will be altered and the jump executed. Otherwise, the value will be 0 and the PC will remain the same and execution unaltered.

(dest, cond) => ()

pc = cond ? dest : pc + 1


PC

0x58

() => (pc)


MSIZE

0x59

() => (memory.size)


GAS

0x5a

() => (gasRemaining)

not including the gas required for this opcode


JUMPDEST

0x5b

() => ()

noop, marks a valid jump destination


PUSH1

0x60

The following byte is read from PC, placed into a word, then this word is pushed onto the stack.

() => (address(this).code[pc+1:pc+2])


PUSH2

0x61

() => (address(this).code[pc+2:pc+3])


PUSH3

0x62

() => (address(this).code[pc+3:pc+4])


PUSH4

0x63

() => (address(this).code[pc+4:pc+5])


PUSH5

0x64

() => (address(this).code[pc+5:pc+6])


PUSH6

0x65

() => (address(this).code[pc+6:pc+7])


PUSH7

0x66

() => (address(this).code[pc+7:pc+8])


PUSH8

0x67

() => (address(this).code[pc+8:pc+9])


PUSH9

0x68

() => (address(this).code[pc+9:pc+10])


PUSH10

0x69

() => (address(this).code[pc+10:pc+11])


PUSH11

0x6a

() => (address(this).code[pc+11:pc+12])


PUSH12

0x6b

() => (address(this).code[pc+12:pc+13])


PUSH13

0x6c

() => (address(this).code[pc+13:pc+14])


PUSH14

0x6d

() => (address(this).code[pc+14:pc+15])


PUSH15

0x6e

() => (address(this).code[pc+15:pc+16])


PUSH16

0x6f

() => (address(this).code[pc+16:pc+17])


PUSH17

0x70

() => (address(this).code[pc+17:pc+18])


PUSH18

0x71

() => (address(this).code[pc+18:pc+19])


PUSH19

0x72

() => (address(this).code[pc+19:pc+20])


PUSH20

0x73

() => (address(this).code[pc+20:pc+21])


PUSH21

0x74

() => (address(this).code[pc+21:pc+22])


PUSH22

0x75

() => (address(this).code[pc+22:pc+23])


PUSH23

0x76

() => (address(this).code[pc+23:pc+24])


PUSH24

0x77

() => (address(this).code[pc+24:pc+25])


PUSH25

0x78

() => (address(this).code[pc+25:pc+26])


PUSH26

0x79

() => (address(this).code[pc+26:pc+27])


PUSH27

0x7a

() => (address(this).code[pc+27:pc+28])


PUSH28

0x7b

() => (address(this).code[pc+28:pc+29])


PUSH29

0x7c

() => (address(this).code[pc+29:pc+30])


PUSH30

0x7d

() => (address(this).code[pc+30:pc+31])


PUSH31

0x7e

() => (address(this).code[pc+31:pc+32])


PUSH32

0x7f

() => (address(this).code[pc+32:pc+33])


DUP1

0x80

(1) => (1, 1)


DUP2

0x81

(1, 2) => (2, 1, 2)


DUP3

0x82

(1, 2, 3) => (3, 1, 2, 3)


DUP4

0x83

(1, ..., 4) => (4, 1, ..., 4)


DUP5

0x84

(1, ..., 5) => (5, 1, ..., 5)


DUP6

0x85

(1, ..., 6) => (6, 1, ..., 6)


DUP7

0x86

(1, ..., 7) => (7, 1, ..., 7)


DUP8

0x87

(1, ..., 8) => (8, 1, ..., 8)


DUP9

0x88

(1, ..., 9) => (9, 1, ..., 9)


DUP10

0x89

(1, ..., 10) => (10, 1, ..., 10)


DUP11

0x8a

(1, ..., 11) => (11, 1, ..., 11)


DUP12

0x8b

(1, ..., 12) => (12, 1, ..., 12)


DUP13

0x8c

(1, ..., 13) => (13, 1, ..., 13)


DUP14

0x8d

(1, ..., 14) => (14, 1, ..., 14)


DUP15

0x8e

(1, ..., 15) => (15, 1, ..., 15)


DUP16

0x8f

(1, ..., 16) => (16, 1, ..., 16)


SWAP1

0x90

(1, 2) => (2, 1)


SWAP2

0x91

(1, 2, 3) => (3, 2, 1)


SWAP3

0x92

(1, ..., 4) => (4, ..., 1)


SWAP4

0x93

(1, ..., 5) => (5, ..., 1)


SWAP5

0x94

(1, ..., 6) => (6, ..., 1)


SWAP6

0x95

(1, ..., 7) => (7, ..., 1)


SWAP7

0x96

(1, ..., 8) => (8, ..., 1)


SWAP8

0x97

(1, ..., 9) => (9, ..., 1)


SWAP9

0x98

(1, ..., 10) => (10, ..., 1)


SWAP10

0x99

(1, ..., 11) => (11, ..., 1)


SWAP11

0x9a

(1, ..., 12) => (12, ..., 1)


SWAP12

0x9b

(1, ..., 13) => (13, ..., 1)


SWAP13

0x9c

(1, ..., 14) => (14, ..., 1)


SWAP14

0x9d

(1, ..., 15) => (15, ..., 1)


SWAP15

0x9e

(1, ..., 16) => (16, ..., 1)


SWAP16

0x9f

(1, ..., 17) => (17, ..., 1)


LOG0

0xa0

(offset, length) => ()

emit(memory[offset:offset+length])


LOG1

0xa1

(offset, length, topic0) => ()

emit(memory[offset:offset+length], topic0)


LOG2

0xa2

(offset, length, topic0, topic1) => ()

emit(memory[offset:offset+length], topic0, topic1)


LOG3

0xa3

(offset, length, topic0, topic1, topic2) => ()

emit(memory[offset:offset+length], topic0, topic1, topic2)


LOG4

0xa4

(offset, length, topic0, topic1, topic2, topic3) => ()

emit(memory[offset:offset+length], topic0, topic1, topic2, topic3)


CREATE

0xf0

(value, offset, length) => (addr)

addr = keccak256(rlp([address(this), this.nonce]))[12:] addr.code = exec(memory[offset:offset+length]) addr.balance += value this.balance -= value this.nonce += 1


CALL

0xf1

(gas, addr, value, argsOffset, argsLength, retOffset, retLength) => (success)

memory[retOffset:retOffset+retLength] = address(addr).callcode.gas(gas).value(value)(memory[argsOffset:argsOffset+argsLength]) success = true (unless the prev call reverted)


CALLCODE

0xf2

(gas, addr, value, argsOffset, argsLength, retOffset, retLength) => (success)

memory[retOffset:retOffset+retLength] = address(addr).callcode.gas(gas).value(value)(memory[argsOffset:argsOffset+argsLength]) success = true (unless the prev call reverted)

TODO: what's the difference between this & CALL?


RETURN

0xf3

(offset, length) => ()

return memory[offset:offset+length]


DELEGATECALL

0xf4

(gas, addr, argsOffset, argsLength, retOffset, retLength) => (success)

memory[retOffset:retOffset+retLength] = address(addr).delegatecall.gas(gas)(memory[argsOffset:argsOffset+argsLength]) success = true (unless the prev call reverted)


CREATE2

0xf5

(value, offset, length, salt) => (addr)

initCode = memory[offset:offset+length] addr = keccak256(0xff ++ address(this) ++ salt ++ keccak256(initCode))[12:] address(addr).code = exec(initCode)


STATICCALL

0xfa

(gas, addr, argsOffset, argsLength, retOffset, retLength) => (success)

memory[retOffset:retOffset+retLength] = address(addr).delegatecall.gas(gas)(memory[argsOffset:argsOffset+argsLength]) success = true (unless the prev call reverted)

TODO: what's the difference between this & DELEGATECALL?


REVERT

0xfd

(offset, length) => ()

revert(memory[offset:offset+length])


SELFDESTRUCT

0xff

(addr) => ()

address(addr).send(address(this).balance) this.code = 0

Tracing Utils

Transaction Tracing

One great way to learn more about how the EVM works internally is to trace the execution of a transaction opcode by opcode. This technique can also help you assess the correctness of assembly code and catch problems related to the compiler or it's optimization steps.

The following Javascript snippet uses an ethers provider to connect to an ethereum node with the debug JSON RPC endpoints activated. Although this requires an archive node on mainnet, it can also be run quickly & easily against a local development testnet using hardhat node, ganache, or some other ethprovider that targets developers.

Transaction traces for even simple smart contract interactions are verbose so we recommend you provide a filename to save the trace at for further analysis. Note that the following function depends on the fs module built into node.js so it should be copied into a node console rather than a browser console, however the filesystem interactions could be removed for use in the browser.

const ethers = require("ethers");
const fs = require("fs");

const provider = new ethers.providers.JsonRpcProvider(
  process.env.ETH_PROVIDER || "http://localhost:8545"
);

let traceTx = async (txHash, filename) => {
  await provider.send("debug_traceTransaction", [txHash]).then((res) => {
    console.log(`Got a response with keys: ${Object.keys(res)}`);
    const indexedRes = {
      ...res,
      structLogs: res.structLogs.map((structLog, index) => ({
        index,
        ...structLog,
      })),
    };
    if (filename) {
      fs.writeFileSync(filename, JSON.stringify(indexedRes, null, 2));
    } else {
      log(indexecRes);
    }
  });
};

Note that, by default, transaction traces do not feature a sequential index making it difficult to answer, for example, "Which was the 100th opcode executed?" The above script adds such an index for easier navigation and communication.

The output of the above features a list of opcode executions, a snippet of which might look something like:

{
  "structLogs": [
    ...,  
    {
      "index": 191,
      "pc": 3645,
      "op": "SSTORE",
      "gas": 10125,
      "gasCost": 2900,
      "depth": 1,
      "stack": [
        "0xa9059cbb",
        "0x700",
        "0x7fb610713c8404e21676c01c271bb662df6eb63c",
        "0x1d8b64f4775be40000",
        "0x0",
        "0x1e01",
        "0x68e224065325c640131672779181a2f2d1324c4d",
        "0x7fb610713c8404e21676c01c271bb662df6eb63c",
        "0x1d8b64f4775be40000",
        "0x0",
        "0x14af3e50252dfc40000",
        "0x14af3e50252dfc40000",
        "0x7d7d4dc7c32ad4c905ab39fc25c4323c4a85e4b1b17a396514e6b88ee8b814e8"
      ],
      "memory": [
        "00000000000000000000000068e224065325c640131672779181a2f2d1324c4d",
        "0000000000000000000000000000000000000000000000000000000000000002",
        "0000000000000000000000000000000000000000000000000000000000000080"
      ],
      "storage": {
        "7d7d4dc7c32ad4c905ab39fc25c4323c4a85e4b1b17a396514e6b88ee8b814e8": "00000000000000000000000000000000000000000000014af3e50252dfc40000"
      }
    },
    ...,  
  ],
  "gas": 34718,
  "failed": false,
  "returnValue": "0000000000000000000000000000000000000000000000000000000000000001"
}

An overview of the fields for opcode execution trace:

  • index: The index we added, indicates that the above opcode was the 191st one executed. Helpful for staying oriented as you jump around the trace.
  • pc: program counter eg this opcode exists at index 3645 of the contract bytecode. You'll notice that pc increments by one for many common opcodes, by more than one for PUSH opcodes, and is reset entirely by JUMP/JUMP opcodes.
  • op: name of the opcode, because most of the actual data is hex-encoded, using grep or ctrl-f to search through the trace for opcode names is an effective strategy.
  • gas: remaining gas before the opcode is executed
  • gasCost: cost of this operation, for CALL & similar opcodes, this cost takes into account all gas spent by the child execution frame.
  • depth: each call creates a new child execution frame & this variable tracks how many sub-frames exist. Generally, CALL opcodes increase the depth and RETURN opcodes decrease it.
  • stack: a snapshot of the entire stack before the opcode executes
  • memory: a snapshot of the entire memory before the opcode executes
  • storage: an accumulation of all state changes made during the execution of the transaction being traced

One big challenge of navigating such a transaction trace is matching opcode executions to higher-level solidity code. An effective first step is to identify uncommon opcodes which correspond to easily identified logic of the source code. Generally, expensive operations are relatively uncommon so SLOAD and SSTORE are good ones to scan for first and match against places where state variables are being read or written in solidity. Alternatively, CALL and related opcodes are relatively uncommon and can be matched with calls to other contracts in the source code.

If there's a specific part of the source code that you're interested in tracing, matching uncommon opcodes to the source code will give you bounds on where to search. From here, you'll likely start walking through the trace opcode-by-opcode as you review the source code line by line. Leaving a few ephemeral comments in the source code like # opcode 191 can help you keep track and pick up where you left off if you need to take a break.

Exploring transaction traces is challenging work but the reward is an ultra-high-definition view into how the EVM operates internally and can help you identify problems that might not be apparent from just the source code.

Storage Tracing

Although you can get an overview of all the changes to the contract state by checking the storage field of the last executed opcode in the above trace, the following helper function will extract that for you for quicker and easier analysis. If you're doing a more involved investigation into a contract's state, we recommend you check out the slither-read-storage command for a more powerful tool.

const traceStorage = async (txHash) => {
  await provider.send("debug_traceTransaction", [txHash]).then((res) => {
    log(res.structLogs[res.structLogs.length - 1].storage);
  });
};

Ethereum Yellow Paper

So, you want to read the yellow paper. Before we dive in, keep in mind that the yellow paper is out of date and some in the community might refer to it as being depreciated. Check out the BRANCHES.md file of the yellowpaper repository on github to stay up-to-date on how closely this document tracks the latest version of the Ethereum protocol. At the time of writing, the yellow paper is up to date with the Berlin hardfork which occurred in April 2021. For an overview of all Ethereum forks and which EIPs are included in each of them, see the EIPs Forks page.

For a more up-to-date reference, check out the Ethereum Specification which features a detailed description of each opcode for each hardfork in addition to reference implementations written in python.

That said, the yellow paper is still a rich resource for ramping up on the fundamentals of the Ethereum protocol. This document aims to provide some guidance and assistance in deciphering Ethereum's flagship specification.

Mathematical Symbols

One challenging part of the yellow paper, for those of us who are not so well trained in formal mathematics, is comprehending the mathematical symbols. A cheat-sheet of some of these symbols is provided below

  • : there exists
  • : for all
  • : and
  • : or

And some more Ethereum-specific symbols:

  • N_{H}: 1,150,000 aka block number at which the protocol was upgraded from homestead to frontier.
  • T: a transaction eg T = { n: nonce, p: gasPrice, g: gasLimit, t: to, v: value, i: initBytecode, d: data }
  • S(): returns the sender of a transaction eg S(T) = T.from
  • Λ: (lambda) account creation function
  • KEC: Keccak SHA-3 hash function
  • RLP: Recursive Length Prefix encoding

High-level glossary

The following are symbols and function representations that provide a high-level description of ethereum. Many of these symbols represent a data structure, the details of which are described in subsequent sections.

  • σ: ethereum world state
  • B: block
  • μ: EVM state
  • A: accumulated transaction sub-state
  • I: execution environment
  • o: output of H(μ,I) ie null if we're good to go or a set of data if execution should halt
  • Υ(σ,T) => σ': the transaction-level state transition function
  • Π(σ,B) => σ': the block-level state transition function, processes all transactions then finalizes with Ω
  • Ω(B,σ) => σ: block-finalisation state transition function
  • O(σ,μ,A,I): one iteration of the execution cycle
  • H(μ,I) => o: outputs null while execution should continue or a series if execution should halt.

Ethereum World-State: σ

A mapping between addresses (external or contract) and account states. Saved as a Merkle-Patricia tree whose root is recorded on the blockchain backbone.

σ = [ account1={...}, account2={...},
  account3= {
    n: nonce aka number of transactions sent by account3
    b: balance ie number of wei account3 controls
    s: storage root, hash of the merkle-patricia tree that contains this accounts long-term data store
    c: code, hash of the EVM bytecode that controls this account. If this equals the hash of an empty string, this is a non-contract account.
  }, ...
]

The Block: B

B = Block = {
  H: Header = {
    p: parentHash,
    o: ommersHash,
    c: beneficiary,
    r: stateRoot,
    t: transactionsRoot,
    e: receiptsRoot,
    b: logsBloomFilter,
    d: difficulty,
    i: number,
    l: gasLimit,
    g: gasUsed,
    s: timestamp,
    x: extraData,
    m: mixHash,
    n: nonce,
  },
  T: Transactions = [
    tx1, tx2...
  ],
  U: Uncle block headers = [
    header1, header2..
  ],
  R: Transaction Receipts = [
    receipt_1 = {
      σ: root hash of the ETH state after transaction 1 finishes executing,
      u: cumulative gas used immediately after this tx completes,
      b: bloom filter,
      l: set of logs created while executing this tx
    }
  ]
}

Execution Environment: I

I = Execution Environment = {
  a: address(this) address of the account which owns the executing code
  o: tx.origin original sender of the tx that initialized this execution
  p: tx.gasPrice price of gas
  d: data aka byte array of method id & args
  s: sender of this tx or initiator of this execution
  v: value send along w this execution or transaction
  b: byte array of machine code to be executed
  H: header of the current block
  e: current stack depth
}

EVM state: μ

The state of the EVM during execution. This is the data structure provided by the debug_traceTransaction JSON RPC method, see this page for more details about using this method to investigate transaction execution.

μ = {
  g: gas left
  pc: program counter ie index into which instruction of I.b to execute next
  m: memory contents, lazily initialized to 2^256 zeros
  i: number of words in memory
  s: stack contents
}

Accrued sub-state: A

The data accumulated during tx execution that needs to be available at the end to finalize the transactions state changes.

A = {
  s: suicide set ie the accounts to delete at the end of this tx
  l: logs
  t: touched accounts
  r: refunds eg gas received when storage is freed
}

Contract Creation

If we send a transaction tx to create a contract, tx.to is set to null and we include a tx.init field that contains bytecode. This is NOT the bytecode run by the contract, rather it RETURNS the bytecode run by the contract ie the tx.init code is run ONCE at contract creation and never again.

If T.to == 0 then this is a contract creation transaction and T.init != null, T.data == null

The following lists every EIP associated to an Ethereum fork.

ForkEIPWhat it doesOpcodeGasNotes
Homestead (606)2Homestead Hard-fork ChangesX
Homestead (606)7DelegatecallX
Homestead (606)8Networking layer: devp2p Forward Compatibility Requirements for Homestead
DAO Fork (779)779DAO Fork
Tangerine Whistle (608)150Gas cost changes for IO-heavy operationsXDefine the all but one 64th rule
Spurious Dragon (607)155Simple replay attack protection
Spurious Dragon (607)160EXP cost increaseX
Spurious Dragon (607)161State trie clearing (invariant-preserving alternative)X
Spurious Dragon (607)170Contract code size limitChange the semantics of CREATE
Byzantium (609)100Change difficulty adjustment to target mean block time including uncles
Byzantium (609)140REVERT instructionX
Byzantium (609)196Precompiled contracts for addition and scalar multiplication on the elliptic curve alt_bn128
Byzantium (609)197Precompiled contracts for optimal ate pairing check on the elliptic curve alt_bn128
Byzantium (609)198Precompiled contract for bigint modular exponentiation
Byzantium (609)211RETURNDATASIZE and RETURNDATACOPYX
Byzantium (609)214STATICCALLX
Byzantium (609)649Metropolis Difficulty Bomb Delay and Block Reward Reduction
Byzantium (609)658Embedding transaction status code in receipts
Constantinople (1013)145Bitwise shifting instructions in EVMX
Constantinople (1013)1014Skinny CREATE2X
Constantinople (1013)1234Constantinople Difficulty Bomb Delay and Block Reward Adjustment
Constantinople (1013)1283Net gas metering for SSTORE without dirty mapsXThis EIP leads to reentrancies risks (see EIP-1283 incident report) and was directly removed with EIP-1716
Petersburg (1716)1716Remove EIP-1283XSee EIP-1283 incident report
Istanbul (1679)152Precompiled contract for the BLAKE2 F compression function
Istanbul (1679)1108Reduce alt_bn128 precompile gas costsX
Istanbul (1679)1344ChainID opcodeX
Istanbul (1679)1884Repricing for trie-size-dependent opcodesXXThe EIP changes the gas cost of multiple opcodes, and add SELFBALANCE
Istanbul (1679)2028Transaction data gas cost reductionX
Istanbul (1679)2200Structured Definitions for Net Gas MeteringX
Muir Glacier (2387)2384Istanbul/Berlin Difficulty Bomb Delay
Berlin (2070)2565ModExp Gas CostX
Berlin (2070)2929Gas cost increases for state access opcodesX
Berlin (2718)2718Typed Transaction Envelope
Berlin (2718)2930Typed Transaction Envelope
London1559Fee market change for ETH 1.0 chainXSignificant modifications of Ethereum gas pricing
London3198BASEFEEX
London3529Reduction in refundsXRemove gas tokens benefits
London3554Difficulty Bomb Delay to December 1st 2021
Arrow Glacier4345Difficulty Bomb Delay to June 2022
Gray Glacier5133Difficulty Bomb Delay to mid-September 2022
Paris3675Upgrade consensus to Proof-of-StakeChanges to DIFFICULTY and BLOCKHASH
Paris4399Supplant DIFFICULTY opcode with PREVRANDAOXDIFFICULTY becomes PREVRANDAO

In this table:

  • Opcode: the EIP adds or removes an opcode
  • Gas: the EIP changes the gas rules

The following lists every CIP associated to a Celo fork. Celo is an EVM-compatible chain.

ForkCIP/EIPWhat it does
ChurritoEIP 211Create RETURNDATASIZE and RETURNDATACOPY opcodes
DonutCIP 25Add Ed25519 precompile
DonutCIP 31 - copied from EIP-2539Add precompile for BLS12-381 curve operations
DonutCIP 30 - copied from EIP-2539Add precompile for BLS12-377 curve operations
DonutCIP 20Add extensible hash function precompile
DonutCIP 21Add governable lookback window
DonutCIP 22Upgrade epoch SNARK data
DonutCIP 26Add precompile to return BLS pubkey of given validator
DonutCIP 28Split etherbase into separate addresses
DonutCIP 35Add support for Ethereum-compatible transactions
EspressoEIP 2565Define gas cost of ModExp precompile
EspressoCIP 48 - modified from EIP 2929Gas repricing
EspressoEIP 2718Introduce typed transaction envelope
EspressoEIP 2930Introduce optional access lists
EspressoCIP 42 - modified from EIP 1559Fee market changes
EspressoEIP 3529Reduction in gas refunds
EspressoEIP 3541Reject deployment of contract code starting with the 0xEF byte
EspressoCIP 43Block Context
EspressoCIP 47Modify round change timeout formula
EspressoCIP 45Modify transaction fee check
EspressoCIP 50Make replay protection optional

The following lists every TIP associated to a TRON upgrade. TRON is an EVM-compatible chain.

UpgradeTIPWhat it does
Odyssey-v3.512Event subscription model
Odyssey-v3.516Account multi-signature/different permissions support
Odyssey-v3.517Adapative energy upper limit
Odyssey-v3.5.124RocksDB offered as storage engine
Odyssey-v3.6.026Add create2 instruction to TVM
Odyssey-v3.6.028Built-in Message Queue in Event Subscription Model
Odyssey-v3.6.029Add bitwise shifting instructions to TVM
Odyssey-v3.6.030Add extcodehash instruction to TVM to return keccak256 hash of a contract's code
Odyssey-v3.6.031Add triggerConstantContract API to support contracts without ABI
Odyssey-v3.6.032Add clearContractABI API to clear existing ABI of contract
Odyssey-v3.6.141Optimize transactionHistoryStore occupancy space
Odyssey-v3.6.537Prohibit TransferContract & TransferAssetContract use for contract account
Odyssey-v3.6.543Add precompiled contract function batchvalidatesign to TVM that supports parallel signature verification
Odyssey-v3.6.544Add ISCONTRACT opcode
Odyssey-v3.6.553Optimize current TRON delegation mechanism
Odyssey-v3.6.554Automatic account activation when transferring TRX/TRC10 tokens in contracts
Odyssey-v3.6.560Add validatemultisign instruction to TVM to support multi-signature verification
GreatVoyage-v4.0.0135Introduce shielded TRC-20 contract standards
GreatVoyage-v4.0.0137Add ZKP verification functions to shielded TRC-20 contract - verifyMintProof, verifyTransferProof, and verifyBurnProof
GreatVoyage-v4.0.0138Add Pedersen hash computation pedersenHash function to shielded TRC-20 contract
GreatVoyage-v4.1.0127Add new system contracts to support token exchange (including TRX and TRC-10)
GreatVoyage-v4.1.0128Add new node type: Lite Fullnode
GreatVoyage-v4.1.0174Add CHAINID instruction to TVM
GreatVoyage-v4.1.0175Add SELFBALANCE instruction to TVM
GreatVoyage-v4.1.0176altbn128-related operation energy reduction in TVM
GreatVoyage-v4.1.2196Reward SRs with tx fees
GreatVoyage-v4.1.2204MAX_FEE_LIMIT is configurable
GreatVoyage-v4.1.2209Adapt Solidity compilers to Solidity 0.6.0
GreatVoyage-v4.2.0(Plato)157Add freeze instructions to TVM - FREEZE, UNFREEZE, and FREEZEEXPIRETIME
GreatVoyage-v4.2.0(Plato)207Optimize TRX freezing resource utilization
GreatVoyage-v4.2.2(Lucretius)268ABI optimization - Move ABI out of SmartContract and store it in a new ABI store to reduce execution speeds of certain opcodes
GreatVoyage-v4.2.2(Lucretius)269Optimize block processing speed
GreatVoyage-v4.2.2(Lucretius)281Optimize database query performance
GreatVoyage-v4.3.0(Bacon)271Add vote instructions and precompile contracts to TVM
GreatVoyage-v4.3.0(Bacon)276Optimize block verification logic
GreatVoyage-v4.3.0(Bacon)285Optimize node startup
GreatVoyage-v4.3.0(Bacon)292Adjust account free net limit
GreatVoyage-v4.3.0(Bacon)293Adjust total net limit
GreatVoyage-v4.3.0(Bacon)295Optimize account data structure
GreatVoyage-v4.3.0(Bacon)298Add new plugin to optimize levelDB performance startup
GreatVoyage-v4.3.0(Bacon)306Add Error type in smart contract ABI
GreatVoyage-v4.4.0(Rousseau)289Block broadcasting optimization
GreatVoyage-v4.4.0(Rousseau)290Optimize dynamic database query performance
GreatVoyage-v4.4.0(Rousseau)272TVM compatibility with EVM
GreatVoyage-v4.4.0(Rousseau)318Adapt to Ethereum London Upgrade
GreatVoyage-v4.4.2(Augustinus)343Optimize levelDB read performance
GreatVoyage-v4.4.2(Augustinus)343Optimize TVM instruction execution
GreatVoyage-v4.4.4(Plotinus)362Optimize node broadcast data caching
GreatVoyage-v4.4.4(Plotinus)366Optimize node startup process
GreatVoyage-v4.5.1(Tertullian)369Support prometheus (metrics interface)
GreatVoyage-v4.5.1(Tertullian)370Support node conditionalized stop
GreatVoyage-v4.5.1(Tertullian)382Optimize account assets data structure
GreatVoyage-v4.5.1(Tertullian)383Optimize transaction cache loading
GreatVoyage-v4.5.1(Tertullian)388Optimize light node synchronization logic
GreatVoyage-v4.5.1(Tertullian)391Optimize block process and broadcasting logic
GreatVoyage-v4.5.1(Tertullian)397Raise limit of the 13th network parameter
GreatVoyage-v4.5.2(Aurelius)425Speed up TCP connection establishment.
GreatVoyage-v4.5.2(Aurelius)440Optimize transaction cache
GreatVoyage-v4.5.2(Aurelius)428Optimize lock competition in block processing
GreatVoyage-v4.6.0(Socrates)461Upgrade checkpoint mechanism to V2 in database module
GreatVoyage-v4.6.0(Socrates)476Optimize delegate data structure
GreatVoyage-v4.6.0(Socrates)387Add transaction memo fee
GreatVoyage-v4.6.0(Socrates)465Optimize reward calculation algorithm

The following lists every BEP associated to a Binance Smart Chain fork.

ReleaseBEPWhat it does
v1.0.684Issue/bind BEP2 with existing BEP20 tokens
v1.1.593Introduces new block syncing protocol
v1.1.595Creates real-time burning mechanism
v1.1.11127Creates "Temporary Maintenance" mode for validators
v1.1.11131Increase validator set with "Candidate" validators
v1.1.18153Creates native staking protocol

(Not So) Smart Contracts

This repository contains examples of common smart contract vulnerabilities, including code from real smart contracts. Use Not So Smart Contracts to learn about vulnerabilities, as a reference when performing security reviews, and as a benchmark for security and analysis tools:

(Not So) Smart Contracts

This repository contains examples of common Algorand smart contract vulnerabilities, including code from real smart contracts. Use Not So Smart Contracts to learn about Algorand vulnerabilities, as a reference when performing security reviews, and as a benchmark for security and analysis tools.

Features

Each Not So Smart Contract includes a standard set of information:

  • Description of the vulnerability type
  • Attack scenarios to exploit the vulnerability
  • Recommendations to eliminate or mitigate the vulnerability
  • Real-world contracts that exhibit the flaw
  • References to third-party resources with more information

Vulnerabilities

Not So Smart ContractDescriptionApplicable to smart signaturesApplicable to smart contracts
RekeyingAttacker rekeys an accountyesyes
Unchecked Transaction FeesAttacker sets excessive fees for smart signature transactionsyesno
Closing AccountAttacker closes smart signature accountsyesno
Closing AssetAttacker transfers entire asset balance of a smart signatureyesno
Group Size CheckContract does not check transaction group sizeyesyes
Time-based Replay AttackContract does not use lease for periodic paymentsyesno
Access ControlsContract does not enfore access controls for updating and deleting applicationnoyes
Asset Id CheckContract does not check asset id for asset transfer operationsyesyes
Denial of ServiceAttacker stalls contract execution by opting out of a assetyesyes

Credits

These examples are developed and maintained by Trail of Bits.

If you have questions, problems, or just want to learn more, then join the #ethereum channel on the Empire Hacking Slack or contact us directly.

Rekeying

The lack of check for RekeyTo field in the Teal program allows malicious actors to rekey the associated account and control the account assets directly, bypassing the restrictions imposed by the Teal contract.

Description

Rekeying is an Algorand feature which allows a user to transfer the authorization power of their account to a different account. When an account has been rekeyed, all the future transactions from that account are accepted by the blockchain, if and only if the transaction has been authorized by the rekeyed account.

A user can rekey their account to the selected account by sending a rekey-to transaction with rekey-to field set to the target account address. A rekey-to transaction is atransaction which has the rekey-to field set to a well formed Algorand address. Any algorand account can be rekeyed by sending a rekey-to transaction from that account, this includes the contract accounts.

Contract accounts are accounts which are derived from the Teal code that is in control of that account. Anyone can set the fields and submit a transaction from the contract account as long as it passes the checks enforced in the Teal code. This results in an issue if the Teal code is supposed to approve a transaction that passes specific checks and does not check the rekey-to field. A malicious user can first send a transaction approved by the Teal code with rekey-to set to their account. After rekeying, the attacker can transfer the assets, algos directly by authorizing the transactions with their private key.

Similar issue affects the accounts that created a delegate signature by signing a Teal program. Delegator is only needed to sign the contract and any user with access to delegate signature can construct and submit transactions. Because of this, a malicious user can rekey the sender’s account if the Teal logic accepts a transaction with the rekey-to field set to the user controlled address.

Note: From Teal v6, Applications can also be rekeyed by executing an inner transaction with "RekeyTo" field set to a non-zero address. Rekeying an application allows to bypass the application logic and directly transfer Algos and assets of the application account.

Exploit Scenarios

A user creates a delegate signature for recurring payments. Attacker rekeys the sender’s account by specifying the rekey-to field in a valid payment transaction.

Example

Note: This code contains several other vulnerabilities, Unchecked Transaction Fees, Closing Account, Time-based Replay Attack.

def withdraw(
    duration,
    period,
    amount,
    receiver,
    timeout,
):
    return And(
        Txn.type_enum() == TxnType.Payment,
        Txn.first_valid() % period == Int(0),
        Txn.last_valid() == Txn.first_valid() + duration,
        Txn.receiver() == receiver,
        Txn.amount() == amount,
        Txn.first_valid() < timeout,
    )

Recommendations

  • For the Teal programs written in Teal version 2 or greater, either used as delegate signature or contract account, include a check in the program that verifies rekey-to field to be equal to ZeroAddress or any intended address. Teal contracts written in Teal version 1 are not affected by this issue. Rekeying feature is introduced in version 2 and Algorand rejects transactions that use features introduced in the versions later than the executed Teal program version.

  • Use Tealer to detect this issue.

  • For Applications, verify that user provided value is not used for RekeyTo field of a inner transaction. Additionally, avoid rekeying an application to admin controlled address. This allows for the possibility of "rug pull" by a malicious admin.

Unchecked Transaction Fee

Lack of transaction fee check in smart signatures allows malicious users to drain the contract account or the delegator’s account by specifying excessive fees.

Description

Any user can submit transactions using the smart signatures and decide on the transaction fields. It is the responsibility of the creator to enforce restrictions on all the transaction fields to prevent malicious users from misusing the smart signature.

One of these transaction fields is Fee. Fee field specifies the number of micro-algos paid for processing the transaction. Protocol only verifies that the transaction pays a fee greater than protocol decided minimum fee. If a smart signature doesn’t bound the transaction fee, a user could set an excessive fee and drain the sender funds. Sender will be the signer of the Teal program in case of delegate signature and the contract account otherwise.

Exploit Scenarios

A user creates a delegate signature for recurring payments. Attacker creates a valid transaction and drains the user funds by specifying excessive fee.

Examples

Note: This code contains several other vulnerabilities, see Rekeying, Closing Account, Time-based Replay Attack.

def withdraw(
    duration,
    period,
    amount,
    receiver,
    timeout,
):
    return And(
        Txn.type_enum() == TxnType.Payment,
        Txn.first_valid() % period == Int(0),
        Txn.last_valid() == Txn.first_valid() + duration,
        Txn.receiver() == receiver,
        Txn.amount() == amount,
        Txn.first_valid() < timeout,
    )

Recommendations

  • Force the transaction fee to be 0 and use fee pooling. If the users should be able to call the smart signature outside of a group, force the transaction fee to be minimum transaction fee: global MinTxnFee.

Closing Account

Lack of check for CloseRemainderTo transaction field in smart signatures allows attackers to transfer entire funds of the contract account or the delegator’s account to their account.

Description

Algorand accounts must satisfy minimum balance requirement and protocol rejects transactions whose execution results in account balance lower than the required minimum. In order to transfer the entire balance and close the account, users should use the CloseRemainderTo field of a payment transaction. Setting the CloseRemainderTo field transfers the entire account balance remaining after transaction execution to the specified address.

Any user with access to the smart signature may construct and submit the transactions using the smart signature. The smart signatures approving payment transactions have to ensure that the CloseRemainderTo field is set to the ZeroAddress or any other specific address to avoid unintended transfer of funds.

Exploit Scenarios

A user creates a delegate signature for recurring payments. Attacker creates a valid transaction and sets the CloseRemainderTo field to their address.

Examples

Note: This code contains several other vulnerabilities, see Rekeying, Unchecked Transaction Fees, Time-based Replay Attack.

def withdraw(
    duration,
    period,
    amount,
    receiver,
    timeout,
):
    return And(
        Txn.type_enum() == TxnType.Payment,
        Txn.first_valid() % period == Int(0),
        Txn.last_valid() == Txn.first_valid() + duration,
        Txn.receiver() == receiver,
        Txn.amount() == amount,
        Txn.first_valid() < timeout,
    )

Recommendations

Verify that the CloseRemainderTo field is set to the ZeroAddress or to any intended address before approving the transaction in the Teal contract.

Closing Asset

Lack of check for AssetCloseTo transaction field in smart signatures allows attackers to transfer the entire asset balance of the contract account or the delegator’s account to their account.

Description

Algorand supports Fungible and Non Fungible Tokens using Algorand Standard Assets(ASA). An Algorand account must first opti-in to the asset before that account can receive any tokens. Opting to an asset increases the minimum balance requirement of the account. Users can opt-out of the asset and decrease the minimum balance requirement using the AssetCloseTo field of Asset Transfer transaction. Setting the AssetCloseTo field transfers the account’s entire token balance remaining after transaction execution to the specified address.

Any user with access to the smart signature may construct and submit the transactions using the smart signature. The smart signatures approving asset transfer transactions have to ensure that the AssetCloseTo field is set to the ZeroAddress or any other specific address to avoid unintended transfer of tokens.

Exploit Scenarios

User creates a delegate signature that allows recurring transfers of a certain asset. Attacker creates a valid asset transfer transaction with AssetCloseTo field set to their address.

Examples

Note: This code contains several other vulnerabilities, see Rekeying, Unchecked Transaction Fees, Closing Asset, Time-based Replay Attack, Asset Id Check.

def withdraw_asset(
    duration,
    period,
    amount,
    receiver,
    timeout,
):
    return And(
        Txn.type_enum() == TxnType.AssetTransfer,
        Txn.first_valid() % period == Int(0),
        Txn.last_valid() == Txn.first_valid() + duration,
        Txn.asset_receiver() == receiver,
        Txn.asset_amount() == amount,
        Txn.first_valid() < timeout,
    )

Recommendations

Verify that the AssetCloseTo field is set to the ZeroAddress or to the intended address before approving the transaction in the Teal contract.

Group Size Check

Lack of group size check in contracts that are supposed to be called in an atomic group transaction might allow attackers to misuse the application.

Description

Algorand supports atomic transfers, an atomic transfer is a group of transactions that are submitted and processed as a single transaction. A group can contain upto 16 transactions and the group transaction fails if any of the included transactions fails. Algorand applications make use of group transactions to realize operations that may not be possible using a single transaction model. In such cases, it is necessary to check that the group transaction in itself is valid along with the individual transactions. One of the checks whose absence could be misused is group size check.

Exploit Scenarios

Application only checks that transactions at particular indices are meeting the criteria and performs the operations based on that. Attackers can create the transactions at the checked indices correctly and include equivalent application call transactions at all the remaining indices. Each application call executes successfully as every execution checks the same set of transactions. This results in performing operations multiple times, once for each application call. This could be damaging if those operations include funds or assets transfers among others.

Examples

Note: This code contains several other vulnerabilities, see Rekeying, Unchecked Transaction Fees, Closing Account, Time-based Replay Attack.

def split_and_withdraw(
    amount_1,
    receiver_1,
    amount_2,
    receiver_2,
    lock_expire_round,
):
    return And(
        Gtxn[0].type_enum() == TxnType.Payment,
        Gtxn[0].receiver() == receiver_1,
        Gtxn[0].amount() == amount_1,

        Gtxn[1].type_enum() == TxnType.Payment,
        Gtxn[1].receiver() == receiver_2,
        Gtxn[1].amount() == amount_2,

        Gtxn[0].first_valid == lock_expire_round,
    )

Recommendations

  • Verify that the group size of an atomic transfer is the intended size in the contracts.

  • Use Tealer to detect this issue.

  • Favor using ABI for smart contracts and relative indexes to verify the group transaction.

Time-based Replay Attack

Lack of check for lease field in smart signatures that intend to approve a single transaction in the particular period allows attackers to submit multiple valid transactions in that period.

Description

Algorand stops transaction replay attacks using a validity period. A validity period of a transaction is the sequence of blocks between FirstValid block and LastValid block. The transaction is considered valid only in that period and a transaction with the same hash can be processed only once in that period. Algorand also limits the period to a maximum of 1000 blocks. This allows the transaction creator to select the FirstValid, LastValid fields appropriately and feel assured that the transaction is processed only once in that period.

However, The same does not apply for transactions authorized by smart signatures. Even if the contract developer verifies the FirstValid and LastValid transaction fields to fixed values, an attacker can submit multiple transactions that are valid as per the contract. This is because any user can create and submit transactions authorized by a smart signature. The attacker can create transactions which have equal values for most transaction fields, for fields verified in the contract and slightly different values for the rest. Each one of these transactions will have a different hash and will be accepted by the protocol.

Exploit Scenarios

A user creates a delegate signature for recurring payments. Contract verifies the FirstValid and LastValid to only allow a single transaction each time. Attacker creates and submits multiple valid transactions with different hashes.

Examples

Note: This code contains several other vulnerabilities, see Rekeying, Unchecked Transaction Fees, Closing Account.

def withdraw(
    duration,
    period,
    amount,
    receiver,
    timeout,
):
    return And(
        Txn.type_enum() == TxnType.Payment,
        Txn.first_valid() % period == Int(0),
        Txn.last_valid() == Txn.first_valid() + duration,
        Txn.receiver() == receiver,
        Txn.amount() == amount,
        Txn.first_valid() < timeout,
    )

Recommendations

Verify that the Lease field of the transaction is set to a specific value. Lease enforces mutual exclusion, once a transaction with non-zero lease is confirmed by the protocol, no other transactions with same lease and sender will be accepted till the LastValid block

Access Controls

Lack of appropriate checks for application calls of type UpdateApplication and DeleteApplication allows attackers to update application’s code or delete an application entirely.

Description

When an application call is successful, additional operations are executed based on the OnComplete field. If the OnComplete field is set to UpdateApplication the approval and clear programs of the application are replaced with the programs specified in the transaction. Similarly, if the OnComplete field is set to DeleteApplication, application parameters are deleted. This allows attackers to update or delete the application if proper access controls are not enforced in the application.

Exploit Scenarios

A stateful contract serves as a liquidity pool for a pair of tokens. Users can deposit the tokens to get the liquidity tokens and can get back their funds with rewards through a burn operation. The contract does not enforce restrictions for UpdateApplication type application calls. Attacker updates the approval program with a malicious program that transfers all assets in the pool to the attacker's address.

Recommendations

  • Set proper access controls and apply various checks before approving applications calls of type UpdateApplication and DeleteApplication.

  • Use Tealer to detect this issue.

Asset Id Check

Lack of verification of asset id in the contract allows attackers to transfer a different asset in place of the expected asset and mislead the application.

Description

Contracts accepting and doing operations based on the assets transferred to the contract must verify that the transferred asset is the expected asset by checking the asset Id. Absence of check for expected asset Id could allow attackers to manipulate contract’s logic by transferring a fake, less or more valuable asset instead of the correct asset.

Exploit Scenarios

  • A liquidity pool contract mints liquidity tokens on deposit of two tokens. Contract does not check that the asset Ids in the two asset transfer transactions are correct. Attacker deposits the same less valuable asset in the two transactions and withdraws both tokens by burning the pool tokens.
  • User creates a delegate signature that allows recurring transfers of a certain asset. Attacker creates a valid asset transfer transaction of more valuable assets.

Examples

Note: This code contains several other vulnerabilities, see Rekeying, Unchecked Transaction Fees, Closing Asset, Time-based Replay Attack.

def withdraw_asset(
    duration,
    period,
    amount,
    receiver,
    timeout,
):
    return And(
        Txn.type_enum() == TxnType.AssetTransfer,
        Txn.first_valid() % period == Int(0),
        Txn.last_valid() == Txn.first_valid() + duration,
        Txn.asset_receiver() == receiver,
        Txn.asset_amount() == amount,
        Txn.first_valid() < timeout,
    )

Recommendations

Verify the asset id to be expected asset for all asset related operations in the contract.

Denial of Service

When a contract does not verify whether an account has opted in to an asset and attempts to transfer that asset, an attacker can DoS other users if the contract's operation is to transfer asset to multiple accounts.

Description

A user must explicitly opt-in to receive any particular Algorand Standard Asset(ASAs). A user may also opt out of an ASA. A transaction will fail if it attempts to transfer tokens to an account that didn’t opt in to that asset. This could be leveraged by attackers to DOS a contract if the contract’s operation depends on successful transfer of an asset to the attacker owned address.

Exploit Scenarios

Contract attempts to transfer assets to multiple users. One user is not opted in to the asset. The transfer operation fails for all users.

Examples

Note: This code contains several other vulnerabilities, see Rekeying, Unchecked Transaction Fees, Closing Asset, Group Size Check, Time-based Replay Attack, Asset Id Check

def split_and_withdraw_asset(
    amount_1,
    receiver_1,
    amount_2,
    receiver_2,
    lock_expire_round,
):
    return And(
        Gtxn[0].type_enum() == TxnType.AssetTransfer,
        Gtxn[0].asset_receiver() == receiver_1,
        Gtxn[0].asset_amount() == amount_1,

        Gtxn[1].type_enum() == TxnType.AssetTransfer,
        Gtxn[1].receiver() == receiver_2,
        Gtxn[1].amount() == amount_2,

        Gtxn[0].first_valid == lock_expire_round,
    )

Recommendations

Use pull over push pattern for transferring assets to users.

(Not So) Smart Contracts

This repository contains examples of common Cairo smart contract vulnerabilities, including code from real smart contracts. Use Not So Smart Contracts to learn about Cairo vulnerabilities, as a reference when performing security reviews, and as a benchmark for security and analysis tools.

Features

Each Not So Smart Contract includes a standard set of information:

  • Description of the vulnerability type
  • Attack scenarios to exploit the vulnerability
  • Recommendations to eliminate or mitigate the vulnerability
  • Real-world contracts that exhibit the flaw
  • References to third-party resources with more information

Vulnerabilities

Not So Smart ContractDescription
Improper access controlsBroken access controls due to StarkNet account abstraction
Integer division errorsUnexpected results due to division in a finite field
View state modificationsView functions don't prevent state modifications
Arithmetic overflowArithmetic in Cairo is not safe by default
Signature replaysAccount abstraction requires robust reuse protections
L1 to L2 Address ConversionL1 to L2 messaging requires L2 address checks
Incorrect Felt ComparisonUnexpected results can occur during felt comparison
Namespace Storage Var CollisionStorage variables are not scoped by namespaces
Dangerous Public Imports in LibrariesNonimported external functions can still be called

Credits

These examples are developed and maintained by Trail of Bits.

If you have questions, problems, or just want to learn more, then join the #ethereum channel on the Empire Hacking Slack or contact us directly.

Access controls & account abstraction

NOTE: The following was possible before StarkNet OS enforced the use of an account contract.

The account abstraction model used by StarkNet has some important differences from what Solidity developers might be used to. There are no EOA addresses in StarkNet, only contract addresses. Rather than interact with contracts directly, users will usually deploy a contract that authenticates them and makes further calls on the user's behalf. At its simplest, this contract checks that the transaction is signed by the expected key, but it could also represent a multisig or DAO, or have more complex logic for what kinds of transactions it will allow (e.g. deposits and withdrawals could be handled by separate contracts or it could prevent unprofitable trades).

It is still possible to interact with contracts directly. But from the perspective of the contract, the caller's address will be 0. Since 0 is also the default value for uninitialized storage, it's possible to accidentally construct access control checks that fail open instead of properly restricting access to only the intended users.

Example

Consider the following two functions that both allow a user to claim a small amount of tokens. The first, without any checks, will end up sending tokens to the zero address, effectively burning them by removing them from the circlating supply. The latter will ensure that this cannot happen.

@external
func bad_claim_tokens{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}():
    let (user) = get_caller_address()

    let (user_current_balance) = user_balances.read(sender_address)
    user_balances.write(user_current_balance + 200)

    return ()
end

@external
func better_claim_tokens{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}():
    let (user) = get_caller_address()
    assert_not_equal(user,0)

    let (user_current_balance) = user_balances.read(sender_address)
    user_balances.write(user, user_current_balance + 200)

    return ()
end

Mitigations

  • Add zero address checks. Note that this will prevent users from interacting with the contract directly.

External Examples

  • An issue in the ERC721 implementation included in a pre-0.1.0 version of OpenZeppelin's Cairo contract library would have allowed unapproved users to transfer tokens.

Integer Division

Math in Cairo is done in a finite field, which is why the numeric type is called felt, for field elements. In most cases addition, subtraction and multiplication will behave as they would in standard integer operations when writing Cairo code. However, developers need to pay a little bit extra attention when performing division. Unlike in Solidity, where division is carried out as if the values were real numbers and anything after the decimal place is truncated, in Cairo it's more intuitive to think of division as the inverse of multiplication. When a number divides a whole number of times into another number, the result is what we would expect, for example 30/6=5. But if we try to divide numbers that don't quite match up so perfectly, like 30/9, the result can be a bit surprising, in this case 1206167596222043737899107594365023368541035738443865566657697352045290673497. That's because 120...97 * 9 = 30 (modulo the 252-bit prime used by StarkNet)

Example

Consider the following functions that normalize a user's token balance to a human readable value for a token with 10^18 decimals. In the first function, it will only provide meaningful values if a user has a whole number of tokens and return non-sense values in every other case. The better version stores these values as Uint256s and uses more traditional integer division.

@external
func bad_normalize_tokens{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> (
    normalized_balance: felt
) {
    let (user) = get_caller_address();

    let (user_current_balance) = user_balances.read(user);
    let (normalized_balance) = user_current_balance / 10 ** 18;

    return (normalized_balance,);
}

@external
func better_normalize_tokens{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> (
    normalized_balance: Uint256
) {
    let (user) = get_caller_address();

    let (user_current_balance) = user_balances.read(user);
    let (normalized_balance, _) = uint256_unsigned_div_rem(user_current_balance, 10 ** 18);

    return (normalized_balance,);
}

Mitigations

  • Review which numeric type is most appropriate for your use case. Especially if your programs rely on division, consider using the uint256 module instead of the felt primitive type.

External Examples

State modifications in a view function

StarkNet provides the @view decorator to signal that a function should not make state modifications. However, this is not currently enforced by the compiler. Developers should take care when designing view functions but also when calling functions in other contracts as they may result in unexpected behavior if they do include state modifications accidentally.

Example

Consider the following function that's declared as a @view. It may have originally been intended as an actual view function but was later repurposed to fetch a nonce and also increment it in the process to ensure a nonce is never repeated when building a signature.

@view
func bad_get_nonce{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> (
    nonce: felt
) {
    let (user) = get_caller_address();
    let (nonce) = user_nonces.read(user);
    user_nonces.write(user, nonce + 1);

    return (nonce);
}

Mitigations

  • Carefully review all @view functions, including those called in 3rd party contracts, to ensure they don't modify state unexpectedly.

External Examples

Arithmetic overflow

The default primitive type, the felt or field element, behaves a lot like an integer does in any other language but it has a few important differences to keep in mind. A felt can be interpreted as a signed integer in the range (-P/2,P/2) or as an unsigned integer in the range (0, P]. P here is the prime used by Cairo, which is current a 252-bit number. Arithemtic using felts is unchecked for overflow and can lead to unexpected results if this isn't properly accounted for. And since the range of values spans both negative and positive values, things like multiplying two positive numbers can have a negative value as a result, and vice versa, multiplying a two negative numbers doesn't always have a positive result.

StarkNet also provides the Uint256 module which offers a more typical 256-bit integer to developers. However, the arithmetic provided by this module is also unchecked so overflow is still something to keep in mind. For more robust integer support, consider using SafeUint256 from OpenZeppelin's Contracts for Cairo.

Attack Scenarios

Mitigations

Examples

Signature replay protection

The StarkNet account abstraction model allows a lot of the details of authentication to be offloaded to contracts. This provides a greater amount of flexibility, but that also means signature schemas need to be constructed with great care. Signatures must be resilient to replay attacks and signature malleability. Signatures must include a nonce and should have a domain separator to bind the signature to a particular contract and chain, so for example testnet signatures can't be replayed against mainnet contracts.

Example

Consider the following function that validates a signature for EIP712-style permit functionality. The first version includes neither a nonce, nor a way of identifying the specific chain a signature is for. This signature schema would allow signatures to be replayed both on the same chain, but also across chains, for example between a testnet and mainnet.

    # TODO

Mitigations

External Examples

L1 to L2 Address Conversion

In Starknet, addresses are of type felt while on L1 addresses are of type uint160. Thus, in order to pass around address types during cross layer messaging, the address variable is typically given as a uint256. However, this can create an issue where an address on L1 can map to the zero address (or an unexpected address) on L2. This is because the primitive type in Cairo is the felt, which is in between the range 0 < x < P, where P is the prime order of the curve. Typically we have P = 2^251 + 17 * 2^192 + 1.

Example

Suppose that the following code to initiate L2 deposits from L1. The first example has no checks on the to parameter and thus depending on the users' address, it is possible to transfer tokens to an unexpected address on L2. The second example, however, verifies to make sure this check cannot happen. Note that the code is just a simplification of how messages are sent on L1 and processed on L2. For a more thorough overview, see here: https://www.cairo-lang.org/docs/hello_starknet/l1l2.html.

uint256 public constant STARKNET_FIELD_PRIME; // the prime order P of the elliptic curve used 
IERC20 public constant token; //some token to deposit on L2
event Deposited(uint256 sender, uint256 amount);

function badDepositToL2(uint256 to,  uint256 amount) public returns (bool) {
    token.transferFrom(to, address(this),amount);
    emit Deposited(to,amount); // this message gets processed on L2
    return true;
}

function betterDepositToL2(uint256 to,  uint256 amount) public returns (bool) {
    require(to !=0 && to < STARKNET_FIELD_PRIME, "invalid address"); //verifies 0 < to < P 
    token.transferFrom(to, address(this),amount);
    emit Deposited(to,amount); // this message gets processed on L2
    return true;
}

Mitigations

When sending a message from L1 to L2, remember to verify parameters, especially user supplied params. Remember that the type and range of Cairo's default felt type is less than the uint256 type used by Solidity.

Incorrect Felt Comparison

In Cairo, there are two methods for the less than or equal to comparison operator: assert_le and assert_nn_le:

  • assert_le asserts that a number a is less than or equal to b, regardless of the size of a
  • assert_nn_le additionally asserts that a is non-negative, i.e. not greater than or equal to the RANGE_CHECK_BOUND value of 2^128.

assert_nn_le works to compare unsigned integers but with a value less than 2^128 (e.g. an Uint256 field). To compare felts as unsigned integer over the entire range (0, P], assert_le_felt should be used. Note these functions exist also with the is_ prefix where they return 1 (TRUE) or 0 (FALSE).

Due to the complexity of these assertions, a common mistake is to use assert_le when assert_nn_le should be used.

Example

Suppose that a codebase uses the following checks regarding a hypothetical ERC20 token. In the first function, it may be possible that value is in fact greater than max_supply, yet because the function does not verify value >= 0 the assertion will incorrectly pass. The second function, however, asserts that 0 <= value <= max_supply, which will correctly not let an incorrect value go through the assertion.

@storage_var
func max_supply() -> (res: felt) {
}

@external
func bad_comparison{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() {
    let (value: felt) = ERC20.total_supply();
    assert_le{range_check_ptr=range_check_ptr}(value, max_supply.read());

    // do something...

    return ();
}

@external
func better_comparison{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() {
    let (value: felt) = ERC20.total_supply();
    assert_nn_le{range_check_ptr=range_check_ptr}(value, max_supply.read());

    // do something...

    return ();

    
}

Mitigations

  • Review all felt comparisons closely.
  • Determine what sort of behavior the comparison should have, and if assert_nn_le or assert_le_felt is more appropriate.
  • Use assert_le if you explicitly want to make comparison between signed integers - otherwise explicitely document why it is used over assert_nn_le

Namespace Storage Variable Collsion

NOTE: The following was possible until cairo-lang 0.10.0.

In Cairo, it is possible to use namespaces to scope functions under an identifier. However, storage variables are not scoped by these namespaces. If a developer accidentally uses the same variable name in two different namespaces, it could lead to a storage collision.

Example

The following example has been copied from here. Suppose we have two different namespaces A and B, both with the same balance storage variable. In addition, both namespaces have respective functions increase_balance() and get_balance() to increment the storage variable and retrieve it respectively. When either increase_balance_a or increase_balance_b() is called, the expected behavior would be to have two seperate storage variables have their balance increased respectively. However, because storage variables are not scoped by namespaces, there will be one balance variable updated twice:

%lang starknet

from starkware.cairo.common.cairo_builtins import HashBuiltin

from openzeppelin.a import A
from openzeppelin.b import B

@external
func increase_balance_a{
        syscall_ptr : felt*, pedersen_ptr : HashBuiltin*,
        range_check_ptr}(amount : felt):
    A.increase_balance(amount)
    return ()
end

@external
func increase_balance_b{
        syscall_ptr : felt*, pedersen_ptr : HashBuiltin*,
        range_check_ptr}(amount : felt):
    B.increase_balance(amount)
    return ()
end

@view
func get_balance_a{
        syscall_ptr : felt*, pedersen_ptr : HashBuiltin*,
        range_check_ptr}() -> (res : felt):
    let (res) = A.get_balance()
    return (res)
end

@view
func get_balance_b{
        syscall_ptr : felt*, pedersen_ptr : HashBuiltin*,
        range_check_ptr}() -> (res : felt):
    let (res) = B.get_balance()
    return (res)
end

Mitigations

Make sure to not use the same storage variable name in the namespace (or change the return value's name, see here). Also use Amarna to uncover this issue, since it has a detector for storage variable collisions.

Dangerous Public Imports in Libraries

NOTE: The following was possible until cairo-lang 0.10.0.

When a library is imported in Cairo, all functions can be called even if some of them are not declared in the import statement. As a result, it is possible to call functions that a developer may think is unexposed, leading to unexpected behavior.

Example

Consider the library library.cairo. Even though the example.cairo file imports only the check_owner() and the do_something() function, the bypass_owner_do_something() function is still exposed and can thus be called, making it possible to circumvent the owner check.


# library.cairo

%lang starknet
from starkware.cairo.common.cairo_builtins import HashBuiltin
@storage_var
func owner() -> (res: felt):
end

func check_owner{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr: felt*}():
    let caller = get_caller_address()
    let owner = owner.read()
    assert caller = owner
    return ()
end

func do_something():
    # do something potentially dangerous that only the owner can do
    return ()
end

# for testing purposes only
@external
func bypass_owner_do_something():
    do_something()
    return ()
end



# example.cairo
%lang starknet
%builtins pedersen range_check
from starkware.cairo.common.cairo_builtins import HashBuiltin
from library import check_owner(), do_something()
# Even though we just import check_owner() and do_something(), we can still call bypass_owner_do_something()!
func check_owner_and_do_something{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr: felt*}():
    check_owner()
    do_something()
    return ()
end

Mitigations

Make sure to exercise caution when declaring external functions in a library. Recognize the possible state changes that can be made through the function and verify it is acceptable for anyone to call it. In addition, Amarna has a detector to uncover this issue.

(Not So) Smart Cosmos

This repository contains examples of common Cosmos applications vulnerabilities, including code from real applications. Use Not So Smart Cosmos to learn about Cosmos (Tendermint) vulnerabilities, as a reference when performing security reviews, and as a benchmark for security and analysis tools.

Features

Each Not So Smart Cosmos includes a standard set of information:

  • Description of the vulnerability type
  • Attack scenarios to exploit the vulnerability
  • Recommendations to eliminate or mitigate the vulnerability
  • Real-world contracts that exhibit the flaw
  • References to third-party resources with more information

Vulnerabilities

Not So Smart ContractDescription
Incorrect signersBroken access controls due to incorrect signers validation
Non-determinismConsensus failure because of non-determinism
Not prioritized messagesRisks arising from usage of not prioritized message types
Slow ABCI methodsConsensus failure because of slow ABCI methods
ABCI methods panicChain halt due to panics in ABCI methods
Broken bookkeepingExploit mismatch between different modules' views on balances
Rounding errorsBugs related to imprecision of finite precision arithmetic
Unregistered message handlerBroken functionality because of unregistered msg handler
Missing error handlerMissing error handling leads to successful execution of a transaction that should have failed

Credits

These examples are developed and maintained by Trail of Bits.

If you have questions, problems, or just want to learn more, then join the #ethereum channel on the Empire Hacking Slack or contact us directly.

Incorrect Signers

In Cosmos, transaction's signature(s) are validated against public keys (addresses) taken from the transaction itself, where locations of the keys are specified in GetSigners methods.

In the simplest case there is just one signer required, and its address is simple to use correctly. However, in more complex scenarios like when multiple signatures are required or a delegation schema is implemented, it is possible to make mistakes about what addresses in the transaction (the message) are actually authenticated.

Fortunately, mistakes in GetSigners should make part of application's intended functionality not working, making it easy to spot the bug.

Example

The example application allows an author to create posts. A post can be created with a MsgCreatePost message, which has signer and author fields.

service Msg {
      rpc CreatePost(MsgCreatePost) returns (MsgCreatePostResponse);
}

message MsgCreatePost {
  string signer = 1;
  string author = 2;
  string title = 3;
  string body = 4;
}

message MsgCreatePostResponse {
  uint64 id = 1;
}

message Post {
  string author = 1;
  uint64 id = 2;
  string title = 3;
  string body = 4;
}

The signer field is used for signature verification - as can be seen in GetSigners method below.

func (msg *MsgCreatePost) GetSigners() []sdk.AccAddress {
    signer, err := sdk.AccAddressFromBech32(msg.Signer)
    if err != nil {
        panic(err)
    }
    return []sdk.AccAddress{Signer}
}

func (msg *MsgCreatePost) GetSignBytes() []byte {
    bz := ModuleCdc.MustMarshalJSON(msg)
    return sdk.MustSortJSON(bz)
}

func (msg *MsgCreatePost) ValidateBasic() error {
    _, err := sdk.AccAddressFromBech32(msg.Signer)
    if err != nil {
        return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid creator address (%s)", err)
    }
    return nil
}

The author field is saved along with the post's content:

func (k msgServer) CreatePost(goCtx context.Context, msg *types.MsgCreatePost) (*types.MsgCreatePostResponse, error) {
    ctx := sdk.UnwrapSDKContext(goCtx)

    var post = types.Post{
        Author: msg.Author,
        Title:  msg.Title,
        Body:   msg.Body,
    }

    id := k.AppendPost(ctx, post)

    return &types.MsgCreatePostResponse{Id: id}, nil
}

The bug here - mismatch between the message signer address and the stored address - allows users to impersonate other users by sending an arbitrary author field.

Mitigations

  • Keep signers-related logic simple
  • Implement basic sanity tests for all functionalities

Non-determinism

Non-determinism in conensus-relevant code will cause the blockchain to halt. There are quite a few sources of non-determinism, some of which are specific to the Go language:

Example

Below we can see an iteration over a amounts map. If k.GetPool fails for more than one asset, then different nodes will fail with different errors, causing the chain to halt.

func (k msgServer) CheckAmounts(goCtx context.Context, msg *types.MsgCheckAmounts) (*types.MsgCheckAmountsResponse, error) {
    ctx := sdk.UnwrapSDKContext(goCtx)

    amounts := make(map[Asset]int)
    for asset, coin := range allMoney.Coins {
        amounts[asset] = Compute(coin)
    }

    total int := 0
    for asset, f := range amounts {
        poolSize, err := k.GetPool(ctx, asset, f)
        if err != nil {
            return nil, err
        }
        total += poolSize
    }

    if total == 0 {
        return nil, errors.New("Zero total")
    }

    return &types.MsgCheckAmountsResponse{}, nil
}

Even if we fix the map problem, it is still possible that the total overflows for nodes running on 32-bit architectures earlier than for the rest of the nodes, again causing the chain split.

Mitigations

  • Use static analysis, for example custom CodeQL rules
  • Test your application with nodes running on various architectures or require nodes to run on a specific one
  • Prepare and test procedures for recovering from a blockchain split

External examples

Not prioritized messages

Some message types may be more important than others and should have priority over them. That is, the more significant a message type is, the more quickly it should be included in a block, before other messages are.

Failing to prioritize message types allows attackers to front-run them, possibly gaining unfair advantage. Moreover, during high network congestion, the message may be simply not included in a block for a long period, causing the system to malfunction.

In the Cosmos's mempool, transactions are ordered in first-in-first-out (FIFO) manner by default. Especially, there is no fee-based ordering.

Example

An example application implements a lending platform. It uses a price oracle mechanism, where privileged entities can vote on new assets' prices. The mechanism is implemented as standard messages.

service Msg {
  rpc Lend(MsgLend) returns (MsgLendResponse);

  rpc Borrow(MsgBorrow) returns (MsgBorrowResponse);

  rpc Liquidate(MsgLiquidate) returns (MsgLiquidateResponse);

  rpc OracleCommitPrice(MsgOracleCommitPrice) returns (MsgOracleCommitPriceResponse);

  rpc OracleRevealPrice(MsgOracleRevealPrice) returns (MsgOracleRevealPriceResponse);
}

Prices ought to be updated (committed and revealed) after every voting period. However, an attacker can spam the network with low-cost transactions to completely fill blocks, leaving no space for price updates. He can then profit from the fact that the system uses outdated, stale prices.

Example 2

Lets consider a liquidity pool application that implements the following message types:

service Msg {
  rpc CreatePool(MsgCreatePool) returns (MsgCreatePoolResponse);

  rpc Deposit(MsgDeposit) returns (MsgDepositResponse);

  rpc Withdraw(MsgWithdraw) returns (MsgWithdrawResponse);

  rpc Swap(MsgSwap) returns (MsgSwapResponse);

  rpc Pause(MsgPause) returns (MsgPauseResponse);

  rpc Resume(MsgResume) returns (MsgResumeResponse);
}

There is the Pause message, which allows privileged users to stop the pool.

Once a bug in pool's implementation is discovered, attackers and the pool's operators will compete for whose message is first executed (Swap vs Pause). Prioritizing Pause messages will help pool's operators to prevent exploitation, but in this case it won't stop the attackers completely. They can outrun the Pause message by order of magnitude - so the priority will not matter - or even cooperate with a malicious validator node - who can order his mempool in an arbitrary way.

Mitigations

  • Use CheckTx's priority return value to prioritize messages. Please note that this feature has a transaction (not a message) granularity - users can send multiple messages in a single transaction, and it is the transaction that will have to be prioritized.
  • Perform authorization for prioritized transactions as early as possible. That is, during the CheckTx phase. This will prevent attackers from filling whole blocks with invalid, but prioritized transactions. In other words, implement a mechanism that will prevent validators from accepting not-authorized, prioritized messages into a mempool.
  • Alternatively, charge a high fee for prioritized transactions to disincentivize attackers.

External examples

Slow ABCI methods

ABCI methods (like EndBlocker) are not constrained by gas. Therefore, it is essential to ensure that they always will finish in a reasonable time. Otherwise, the chain will halt.

Example

Below you can find part of a tokens lending application. Before a block is executed, the BeginBlocker method charges an interest for each borrower.

func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
    updatePrices(ctx, k)
    accrueInterest(ctx, k)
}

func accrueInterest(ctx sdk.Context, k keeper.Keeper) {
    for _, pool := range k.GetLendingPools() {
        poolAssets := k.GetPoolAssets(ctx, pool.Id)
        for userId, _ := range k.GetAllUsers() {
            for _, asset := range poolAssets {
                for _, loan := range k.GetUserLoans(ctx, pool, asset, userId) {
                    if err := k.AccrueInterest(ctx, loan); err != nil {
                        k.PunishUser(ctx, userId)
                    }
                }
            }
        }
    }
}

The accrueInterest contains multiple nested for loops and is obviously too complex to be efficient. Mischievous users can take a lot of small loans to slow down computation to a point where the chain is not able to keep up with blocks production and halts.

Mitigations

External examples

ABCI methods panic

A panic inside an ABCI method (e.g., EndBlocker) will stop the chain. There should be no unanticipated panics in these methods.

Some less expected panic sources are:

Example

The application below enforces limits on how much coins can be borrowed globally. If the loan.Borrowed array of Coins can be forced to be not-sorted (by coins' denominations), the Add method will panic.

Moreover, the Mul may panic if some asset's price becomes large.

func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
    if !validateTotalBorrows(ctx, k) {
        k.PauseNewLoans(ctx)
    }
}

func validateTotalBorrows(ctx sdk.Context, k keeper.Keeper) {
    total := sdk.NewCoins()
    for _, loan := range k.GetUsersLoans() {
        total.Add(loan.Borrowed...)
    }

    for _, totalOneAsset := range total {
        if totalOneAsset.Amount.Mul(k.GetASsetPrice(totalOneAsset.Denom)).GTE(k.GetGlobalMaxBorrow()) {
            return false
        }
    }
    return true
}

Mitigations

External examples

Broken Bookkeeping

The x/bank module is the standard way to manage tokens in a cosmos-sdk based applications. The module allows to mint, burn, and transfer coins between both users' and modules' accounts. If an application implements its own, internal bookkeeping, it must carefully use the x/bank's features.

Example

An application enforces the following invariant as a sanity-check: amount of tokens owned by a module equals to the amount of tokens deposited by users via the custom x/hodl module.

func BalanceInvariant(k Keeper) sdk.Invariant {
    return func(ctx sdk.Context) (string, bool) {
        weAreFine := true
        msg := "hodling hard"

        weHold := k.bankKeeper.SpendableCoins(authtypes.NewModuleAddress(types.ModuleName)).AmountOf("BTC")
        usersDeposited := k.GetTotalDeposited("BTC")

        if weHold != usersDeposited {
            msg = fmt.Sprintf("%dBTC missing! Halting chain.\n", usersDeposited - weHold)
            weAreFine = false
        }

        return sdk.FormatInvariant(types.ModuleName, "hodl-balance",), weAreFine
    }
}

A spiteful user can simply transfer a tiny amount of BTC tokens directly to the x/hodl module via a message to the x/bank module. That would bypass accounting of the x/hodl, so the GetTotalDeposited function would report a not-updated amount, smaller than the module's SpendableCoins.

Because an invariant's failure stops the chain, the bug constitutes a simple Denial-of-Service attack vector.

Example 2

An example application implements a lending platform. It allows users to deposit Tokens in exchange for xTokens - similarly to the Compound's cTokens. Token:xToken exchange rate is calculated as (amount of Tokens borrowed + amount of Tokens held by the module account) / (amount of uTokens in circulation).

Implementation of the GetExchangeRate method computing an exchange rate is presented below.

func (k Keeper) GetExchangeRate(tokenDenom string) sdk.Coin {
    uTokenDenom := createUDenom(tokenDenom)

    tokensHeld := k.bankKeeper.SpendableCoins(authtypes.NewModuleAddress(types.ModuleName)).AmountOf(tokenDenom).ToDec()
    tokensBorrowed := k.GetTotalBorrowed(tokenDenom)
    uTokensInCirculation := k.bankKeeper.GetSupply(uTokenDenom).Amount

    return (tokensHeld + tokensBorrowed) / uTokensInCirculation
}

A malicious user can screw an exchange rate in two ways:

  • by force-sending Tokens to the module, changing the tokensHeld value
  • by transferring uTokens to another chain via IBC, chaning uTokensInCirculation value

The first "attack" could be pulled of by sending MsgSend message. However, it would be not profitable (probably), as executing it would irreversibly decrease an attacker's resources.

The second one works because the IBC module burns transferred coins in the source chain and mints corresponding tokens in the destination chain. Therefore, it will decrease the supply reported by the x/bank module, increasing the exchange rate. After the attack the malicious user can just transfer back uTokens.

Mitigations

External examples

Rounding errors

Application developers must take care of correct rounding of numbers, especially if the rounding impacts tokens amounts.

Cosmos-sdk offers two custom types for dealing with numbers:

  • sdk.Int (sdk.UInt) type for integral numbers
  • sdk.Dec type for decimal arithmetic

The sdk.Dec type has problems with precision and does not guarantee associativity, so it must be used carefully. But even if a more robust library for decimal numbers is deployed in the cosmos-sdk, rounding may be unavoidable.

Example

Below we see a simple example demonstrating sdk.Dec type's precision problems.

func TestDec() {
    a := sdk.MustNewDecFromStr("10")
    b := sdk.MustNewDecFromStr("1000000010")
    x := a.Quo(b).Mul(b)
    fmt.Println(x)  // 9.999999999999999000

    q := float32(10)
    w := float32(1000000010)
    y := (q / w) * w
    fmt.Println(y)  // 10
}

Mitigations

  • Ensure that all tokens operations that must round results always benefit the system (application) and not users. In other words, always decide on the correct rounding direction. See Appendix G in the Umee audit report

  • Apply "multiplication before division" pattern. That is, instead of computing (x / y) * z do (x * z) / y

  • Observe issue #11783 for a replacement of the sdk.Dec type

External examples

Unregistered message handler

In the legacy version of the Msg Service, all messages have to be registered in a module keeper's NewHandler method. Failing to do so would prevent users from sending the not-registered message.

In the recent Cosmos version manual registration is no longer needed.

Example

There is one message handler missing.

service Msg {
  rpc ConfirmBatch(MsgConfirmBatch) returns (MsgConfirmBatchResponse) {
      option (google.api.http).post = "/gravity/v1/confirm_batch";
  }
  rpc UpdateCall(MsgUpdateCall) returns (MsgUpdateCallResponse) {
      option (google.api.http).post = "/gravity/v1/update_call";
  }
  rpc CancelCall(MsgCancelCall) returns (MsgCancelCallResponse) {
      option (google.api.http).post = "/gravity/v1/cancel_call";
  }
  rpc SetCall(MsgSetCall) returns (MsgSetCallResponse) {
      option (google.api.http).post = "/gravity/v1/set_call";
  }
  rpc SendCall(MsgSendCall) returns (MsgSendCallResponse) {
      option (google.api.http).post = "/gravity/v1/send_call";
  }
  rpc SetUserAddress(MsgSetUserAddress) returns (MsgSetUserAddressResponse) {
      option (google.api.http).post = "/gravity/v1/set_useraddress";
  }
  rpc SendUserAddress(MsgSendUserAddress) returns (MsgSendUserAddressResponse) {
      option (google.api.http).post = "/gravity/v1/send_useraddress";
  }
  rpc RequestBatch(MsgRequestBatch) returns (MsgRequestBatchResponse) {
      option (google.api.http).post = "/gravity/v1/request_batch";
  }
  rpc RequestCall(MsgRequestCall) returns (MsgRequestCallResponse) {
      option (google.api.http).post = "/gravity/v1/request_call";
  }
  rpc RequestUserAddress(MsgRequestUserAddress) returns (MsgRequestUserAddressResponse) {
      option (google.api.http).post = "/gravity/v1/request_useraddress";
  }
  rpc ConfirmEthClaim(MsgConfirmEthClaim) returns (MsgConfirmEthClaimResponse) {
      option (google.api.http).post = "/gravity/v1/confirm_ethclaim";
  }
  rpc UpdateEthClaim(MsgUpdateEthClaim) returns (MsgUpdateEthClaimResponse) {
      option (google.api.http).post = "/gravity/v1/update_ethclaim";
  }
  rpc SetBatch(MsgSetBatch) returns (MsgSetBatchResponse) {
      option (google.api.http).post = "/gravity/v1/set_batch";
  }
  rpc SendBatch(MsgSendBatch) returns (MsgSendBatchResponse) {
      option (google.api.http).post = "/gravity/v1/send_batch";
  }
  rpc CancelUserAddress(MsgCancelUserAddress) returns (MsgCancelUserAddressResponse) {
      option (google.api.http).post = "/gravity/v1/cancel_useraddress";
  }
  rpc CancelEthClaim(MsgCancelEthClaim) returns (MsgCancelEthClaimResponse) {
      option (google.api.http).post = "/gravity/v1/cancel_ethclaim";
  }
  rpc RequestEthClaim(MsgRequestEthClaim) returns (MsgRequestEthClaimResponse) {
      option (google.api.http).post = "/gravity/v1/request_ethclaim";
  }
  rpc UpdateBatch(MsgUpdateBatch) returns (MsgUpdateBatchResponse) {
      option (google.api.http).post = "/gravity/v1/update_batch";
  }
  rpc SendEthClaim(MsgSendEthClaim) returns (MsgSendEthClaimResponse) {
      option (google.api.http).post = "/gravity/v1/send_ethclaim";
  }
  rpc SetEthClaim(MsgSetEthClaim) returns (MsgSetEthClaimResponse) {
      option (google.api.http).post = "/gravity/v1/set_ethclaim";
  }
  rpc CancelBatch(MsgCancelBatch) returns (MsgCancelBatchResponse) {
      option (google.api.http).post = "/gravity/v1/cancel_batch";
  }
  rpc UpdateUserAddress(MsgUpdateUserAddress) returns (MsgUpdateUserAddressResponse) {
      option (google.api.http).post = "/gravity/v1/update_useraddress";
  }
  rpc ConfirmCall(MsgConfirmCall) returns (MsgConfirmCallResponse) {
      option (google.api.http).post = "/gravity/v1/confirm_call";
  }
  rpc ConfirmUserAddress(MsgConfirmUserAddress) returns (MsgConfirmUserAddressResponse) {
      option (google.api.http).post = "/gravity/v1/confirm_useraddress";
  }
}
func NewHandler(k keeper.Keeper) sdk.Handler {
    msgServer := keeper.NewMsgServerImpl(k)

    return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
        ctx = ctx.WithEventManager(sdk.NewEventManager())
        switch msg := msg.(type) {
        case *types.MsgSetBatch:
            res, err := msgServer.SetBatch(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgUpdateUserAddress:
            res, err := msgServer.UpdateUserAddress(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgUpdateCall:
            res, err := msgServer.UpdateCall(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgSendBatch:
            res, err := msgServer.SendBatch(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgCancelUserAddress:
            res, err := msgServer.CancelUserAddress(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgRequestBatch:
            res, err := msgServer.RequestBatch(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgUpdateEthClaim:
            res, err := msgServer.UpdateEthClaim(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgSendCall:
            res, err := msgServer.SendCall(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgSetCall:
            res, err := msgServer.SetCall(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgCancelEthClaim:
            res, err := msgServer.CancelEthClaim(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgConfirmEthClaim:
            res, err := msgServer.ConfirmEthClaim(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgConfirmCall:
            res, err := msgServer.ConfirmCall(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgRequestCall:
            res, err := msgServer.RequestCall(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgConfirmUserAddress:
            res, err := msgServer.ConfirmUserAddress(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgRequestUserAddress:
            res, err := msgServer.RequestUserAddress(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgSendEthClaim:
            res, err := msgServer.SendEthClaim(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgSetEthClaim:
            res, err := msgServer.SetEthClaim(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgCancelBatch:
            res, err := msgServer.CancelBatch(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgSetUserAddress:
            res, err := msgServer.SetUserAddress(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgRequestEthClaim:
            res, err := msgServer.RequestEthClaim(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgConfirmBatch:
            res, err := msgServer.ConfirmBatch(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgUpdateBatch:
            res, err := msgServer.UpdateBatch(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)
        case *types.MsgSendUserAddress:
            res, err := msgServer.SendUserAddress(sdk.WrapSDKContext(ctx), msg)
            return sdk.WrapServiceResult(ctx, res, err)

        default:
            return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, fmt.Sprintf("Unrecognized Gravity Msg type: %v", msg.Type()))
        }
    }
}

And it is the CancelCall msg.

Mitigations

  • Use the recent Msg Service mechanism
  • Test all functionalities
  • Deploy static-analysis tests in CI pipeline for all manually maintained code that must be repeated in multiple files/methods

External examples

  • The bug occured in the Gravity Bridge. It was impossible to send evidence of malicious behavior, which impacted Gravity Bridge's security model.

Missing error handler

The idiomatic way of handling errors in Go is to compare the returned error to nil. This way of checking for errors gives the programmer a lot of control. However, when error handling is ignored it can also lead to numerous problems. The impact of this is most obvious in method calls in the bankKeeper module, which even causes some accounts with insufficient balances to perform SendCoin operations normally without triggering a transaction failure.

Example

In the following code, k.bankKeeper.SendCoins(ctx, sender, receiver, amount) does not have any return values being used, including err. This results in SendCoin not being able to prevent the transaction from executing even if there is an error due to insufficient balance in SendCoin.

func (k msgServer) Transfer(goCtx context.Context, msg *types.MsgTransfer) (*types.MsgTransferResponse, error) {
	...
	k.bankKeeper.SendCoins(ctx, sender, receiver, amount)
	...
	return &types.MsgTransferResponse{}, nil
}

Mitigations

  • Implement the error handling process instead of missing it

External examples

(Not So) Smart Contracts

This repository contains examples of common Solana smart contract vulnerabilities, including code from real smart contracts. Use Not So Smart Contracts to learn about Solana vulnerabilities, as a reference when performing security reviews, and as a benchmark for security and analysis tools.

Features

Each Not So Smart Contract includes a standard set of information:

  • Description of the vulnerability type
  • Attack scenarios to exploit the vulnerability
  • Recommendations to eliminate or mitigate the vulnerability
  • Real-world contracts that exhibit the flaw
  • References to third-party resources with more information

Vulnerabilities

Not So Smart ContractDescription
Arbitrary CPIArbitrary program account passed in upon invocation
Improper PDA ValidationPDAs are vulnerable to being spoofed via bump seeds
Ownership CheckBroken access control due to missing ownership validation
Signer CheckBroken access control due to missing signer validation
Sysvar Account CheckSysvar accounts are vulnerable to being spoofed

Credits

These examples are developed and maintained by Trail of Bits.

If you have questions, problems, or just want to learn more, then join the #solana channel on the Empire Hacking Slack or contact us directly.

Arbitrary CPI

Solana allows programs to call one another through cross-program invocation (CPI). This can be done via invoke, which is responsible for routing the passed in instruction to the program. Whenever an external contract is invoked via CPI, the program must check and verify the program ID. If the program ID isn't verified, then the contract can call an attacker-controlled program instead of the intended one.

View ToB's lint implementation for the arbitrary CPI issue here.

Exploit Scenario

Consider the following withdraw function. Tokens are able to be withdrawn from the pool to a user account. The program invoked here is user-controlled and there's no check that the program passed in is the intended token_program. This allows a malicious user to pass in their own program with functionality to their discretion - such as draining the pool of the inputted amount tokens.

Example Contract

#![allow(unused)]
fn main() {
   pub fn withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
        let account_info_iter = &mut accounts.iter();
        let token_program = next_account_info(account_info_iter)?;
        let pool = next_account_info(account_info_iter)?;
        let pool_auth = next_account_info(account_info_iter)?;
        let destination = next_account_info(account_info_iter)?;
        invoke(
            &spl_token::instruction::transfer(
                &token_program.key,
                &pool.key,
                &destination.key,
                &pool_auth.key,
                &[],
                amount,
            )?,
            &[
                &pool.clone(),
                &destination.clone(),
                &pool_auth.clone(),
            ],
        )
    }
}

Inspired by Sealevel

Mitigation

#![allow(unused)]
fn main() {
        if INPUTTED_PROGRAM.key != &INTENDED_PROGRAM::id() {
            return Err(ProgramError::InvalidProgramId);
        }
}

Improper PDA bump seed validation

PDAs (Program Derived Addresses) are, by definition, program-controlled accounts and therefore can be used to sign without the need to provide a private key. PDAs are generated through a set of seeds and a program id, which are then collectively hashed to verify that the point doesn't lie on the ed25519 curve (the curve used by Solana to sign transactions).

Values on this elliptic curve have a corresponding private key, which wouldn't make it a PDA. In the case a point lying on the elliptic curve is found, our 32-byte address is modified through the addition of a bump to "bump" it off the curve. A bump, represented by a singular byte iterating through 255 to 0, is added onto our input until a point that doesn’t lie on the elliptic curve is generated, meaning that we’ve found an address without an associated private key.

The issue arises with seeds being able to have multiple bumps, thus allowing varying PDAs that are valid from the same seeds. An attacker can create a PDA with the correct program ID but with a different bump. Without any explicit check against the bump seed itself, the program leaves itself vulnerable to the attacker tricking the program into thinking they’re using the expected PDA when in fact they're interacting with an illegitimate account.

View ToB's lint implementation for the bump seed canonicalization issue here.

Exploit Scenario

In Solana, the create_program_address function creates a 32-byte address based off the set of seeds and program address. On its own, the point may lie on the ed25519 curve. Consider the following without any other validation being referenced within a sensitive function, such as one that handles transfers. That PDA could be spoofed by a passed in user-controlled PDA.

Example Contract

#![allow(unused)]
fn main() {
let program_address = Pubkey::create_program_address(&[key.to_le_bytes().as_ref(), &[reserve_bump]], program_id)?;

...
}

Mitigation

The find_program_address function finds the largest bump seeds for which there exists a corresponding PDA (i.e., a point not on the ed25519 curve), and returns both the address and the bump seed. The function panics in the case that no PDA address can be found.

#![allow(unused)]
fn main() {
        let (address, _system_bump) = Pubkey::find_program_address(&[key.to_le_bytes().as_ref()], program_id);

        if program_address != &account_data.key() {
            return Err(ProgramError::InvalidAddress);
        }
}

Missing Ownership Check

Accounts in Solana include metadata of an owner. These owners are identified by their own program ID. Without sufficient checks that the expected program ID matches that of the passed in account, an attacker can fabricate an account with spoofed data to pass any other preconditions.

This malicious account will inherently have a different program ID as owner, but considering there’s no check that the program ID is the same, as long as the other preconditions are passed, the attacker can trick the program into thinking their malicious account is the expected account.

Exploit Scenario

The following contract allows funds to be dispersed from an escrow account vault, provided the escrow account's state is Complete. Unfortunately, there is no check that the State account is owned by the program. Therefore, a malicious actor can pass in their own fabricated State account with spoofed data, allowing the attacker to send the vault's funds to themselves.

Example Contract

#![allow(unused)]
fn main() {
fn pay_escrow(_program_id: &Pubkey, accounts: &[AccountInfo], _instruction_data: &[u8]) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let state_info = next_account_info(account_info_iter)?;
    let escrow_vault_info = next_account_info(account_info_iter)?;
    let escrow_receiver_info = next_account_info(account_info_iter)?;

    let state = State::deserialize(&mut &**state_info.data.borrow())?;

    if state.escrow_state == EscrowState::Complete {
        **escrow_vault_info.try_borrow_mut_lamports()? -= state.amount;
        **escrow_receiver_info.try_borrow_mut_lamports()? += state.amount;
    }

    Ok(())
}
}

Inspired by SPL Lending Program

Mitigation

#![allow(unused)]
fn main() {
  	if EXPECTED_ACCOUNT.owner != program_id {
    	    return Err(ProgramError::IncorrectProgramId);
	}
}

For further reading on different forms of account verification in Solana and implementation refer to the Solana Cookbook.

Missing Signer Check

In Solana, each public key has an associated private key that can be used to generate signatures. A transaction lists each account public key whose private key was used to generate a signature for the transaction. These signatures are verified using the inputted public keys prior to transaction execution.

In case certain permissions are required to perform a sensitive function of the contract, a missing signer check becomes an issue. Without this check, an attacker would be able to call the respective access controlled functions permissionlessly.

Exploit Scenario

The following contract sets an escrow account's state to Complete. Unfortunately, the contract does not check whether the State account's authority has signed the transaction. Therefore, a malicious actor can set the state to Complete, without needing access to the authority’s private key.

Example Contract

#![allow(unused)]
fn main() {
fn complete_escrow(_program_id: &Pubkey, accounts: &[AccountInfo], _instruction_data: &[u8]) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let state_info = next_account_info(account_info_iter)?;
    let authority = next_account_info(account_info_iter)?;

    let mut state = State::deserialize(&mut &**state_info.data.borrow())?;
    
    if state.authority != *authority.key {
        return Err(ProgramError::IncorrectAuthority);
    }
    
    state.escrow_state = EscrowState::Complete;
    state.serialize(&mut &mut **state_info.data.borrow_mut())?;
    
    Ok(())
    
}
}

Inspired by SPL Lending Program

Mitigation

#![allow(unused)]
fn main() {
  	if !EXPECTED_ACCOUNT.is_signer {
    	return Err(ProgramError::MissingRequiredSignature);
	}
}

For further reading on different forms of account verification in Solana and implementation refer to the Solana Cookbook.

Missing Sysvar Account Check

The sysvar (system account) account is often used while validating access control for restricted functions by confirming that the inputted sysvar account by the user matches up with the expected sysvar account. Without this check in place, any user is capable of passing in their own spoofed sysvar account and in turn bypassing any further authentication associated with it, causing potentially disastrous effects.

Exploit Scenario

secp256k1 is an elliptic curve used by a number of blockchains for signatures. Validating signatures is crucial as by bypassing signature checks, an attacker can gain access to restricted functions that could lead to drainage of funds.

Here, load_current_index and load_instruction_at are functions that don't verify that the inputted sysvar account is authorized, therefore allowing serialized maliciously fabricated data to sucessfully spoof as an authorized secp256k1 signature.

Example Contract

#![allow(unused)]
fn main() {
pub fn verify_signatures(account_info: &AccountInfo) -> ProgramResult {
    let index = solana_program::sysvar::instructions::load_current_index(
        &account_info.try_borrow_mut_data()?,
    );

    let secp_instruction = sysvar::instructions::load_instruction_at(
        (index - 1) as usize,
        &account_info.try_borrow_mut_data()?,
    );
    if secp_instruction.program_id != secp256k1_program::id() {
        return Err(InvalidSecpInstruction.into());
    }
    ...
}
}

Refer to Mitigation to understand what's wrong with these functions and how sysvar account checks were added.

Mitigation

  • Solana libraries should be running on version 1.8.1 and up
  • Use load_instruction_at_checked and load_current_index_checked

Utilizing the latest Solana version and referencing checked functions, especially on sensitive parts of a contract is crucial even if potential attack vectors have been fixed post-audit. Leaving the system exposed to any point of failure compromises the entire system's integrity, especially while the contracts are being constantly updated.

Here is the code showing the sysvar account checks added between unchecked and checked functions:


Example: Wormhole Exploit (February 2022)

Funds lost: ~326,000,000 USD

Note: The following analysis is condensed down to be present this attack vector as clearly as possible, and certain details might’ve been left out for the sake of simplification

The Wormhole hack serves to be one of the most memorable exploits in terms of impact DeFi has ever seen.

This exploit also happens to incorporate a missing sysvar account check that allowed the attacker to:

  1. Spoof Guardian signatures as valid
  2. Use them to create a Validator Action Approval (VAA)
  3. Mint 120,000 ETH via calling complete_wrapped function

(These actions are all chronologically dependent on one another based on permissions and conditions - this analysis will only dive into “Step 1”)

The SignatureSet was able to be faked because the verify_signatures function failed to appropriately verify the sysvar account passed in:

#![allow(unused)]
fn main() {
let secp_ix = solana_program::sysvar::instructions::load_instruction_at(
    secp_ix_index as usize,
    &accs.instruction_acc.try_borrow_mut_data()?,
)
}

load_instruction_at doesn't verify that the inputted data came from the authorized sysvar account.

The fix for this was to upgrade the Solana version and get rid of these unsafe deprecated functions (see Mitigation). Wormhole had caught this issue but didn't update their deployed contracts in time before the exploiter had already managed to drain funds.

Resources:

samczsun's Wormhole exploit breakdown thread

(Not So) Smart Pallets

This repository contains examples of common Substrate pallet vulnerabilities. Use Not So Smart Pallets to learn about Substrate vulnerabilities, as a reference when performing security reviews, and as a benchmark for security and analysis tools.

Features

Each Not So Smart Pallet includes a standard set of information:

  • Description of the vulnerability type
  • Attack scenarios to exploit the vulnerability
  • Recommendations to eliminate or mitigate the vulnerability
  • A mock pallet that exhibits the flaw
  • References to third-party resources with more information

Vulnerabilities

Not So Smart PalletDescription
Arithmetic overflowInteger overflow due to incorrect use of arithmetic operators
Don't panic!System panics create a potential DoS attack vector
Weights and feesIncorrect weight calculations can create a potential DoS attack vector
Verify firstVerify first, write last
Unsigned transaction validationInsufficient validation of unsigned transactions
Bad randomnessUnsafe sources of randomness in Substrate
Bad originIncorrect use of call origin can lead to bypassing access controls

Credits

These examples are developed and maintained by Trail of Bits.

If you have questions, problems, or just want to learn more, then join the #ethereum channel on the Empire Hacking Slack or contact us directly.

Arithmetic overflow

Arithmetic overflow in Substrate occurs when arithmetic operations are performed using primitive operations instead of specialized functions that check for overflow. When a Substrate node is compiled in debug mode, integer overflows will cause the program to panic. However, when the node is compiled in release mode (e.g. cargo build --release), Substrate will perform two's complement wrapping. A production-ready node will be compiled in release mode, which makes it vulnerable to arithmetic overflow.

Example

In the pallet-overflow pallet, notice that the transfer function sets update_sender and update_to using primitive arithmetic operations.

#![allow(unused)]
fn main() {
   /// Allow minting account to transfer a given balance to another account.
   ///
   /// Parameters:
   /// - `to`: The account to receive the transfer.
   /// - `amount`: The amount of balance to transfer.
   ///
   /// Emits `Transferred` event when successful.
   #[pallet::weight(10_000)]
   pub fn transfer(
       origin: OriginFor<T>,
       to: T::AccountId,
       amount: u64,
   ) -> DispatchResultWithPostInfo {
       let sender = ensure_signed(origin)?;
       let sender_balance = Self::get_balance(&sender);
       let receiver_balance = Self::get_balance(&to);

       // Calculate new balances.
       let update_sender = sender_balance - amount;
       let update_to = receiver_balance + amount;
       [...]
   }
}

The sender of the extrinsic can exploit this vulnerability by causing update_sender to underflow, which artificially inflates their balance.

Note: Aside from the stronger mitigations mentioned below, a check to make sure that sender has at least amount balance would have also prevented an underflow.

Mitigations

References

  • https://doc.rust-lang.org/book/ch03-02-data-types.html#integer-overflow
  • https://docs.substrate.io/reference/how-to-guides/basics/use-helper-functions/

Don't Panic!

Panics occur when the node enters a state that it cannot handle and stops the program / process instead of trying to proceed. Panics can occur for a large variety of reasons such as out-of-bounds array access, incorrect data validation, type conversions, and much more. A well-designed Substrate node must NEVER panic! If a node panics, it opens up the possibility for a denial-of-service (DoS) attack.

Example

In the pallet-dont-panic pallet, the find_important_value dispatchable checks to see if useful_amounts[0] is greater than 1_000. If so, it sets the ImportantVal StorageValue to the value held in useful_amounts[0].

#![allow(unused)]
fn main() {
    /// Do some work
    ///
    /// Parameters:
    /// - `useful_amounts`: A vector of u64 values in which there is a important value.
    ///
    /// Emits `FoundVal` event when successful.
    #[pallet::weight(10_000)]
    pub fn find_important_value(
        origin: OriginFor<T>,
        useful_amounts: Vec<u64>,
    ) -> DispatchResultWithPostInfo {
        let sender = ensure_signed(origin)?;

        ensure!(useful_amounts[0] > 1_000, <Error<T>>::NoImportantValueFound);
        
        // Found the important value
        ImportantValue::<T>::put(&useful_amounts[0]);
        [...]
    }
}

However, notice that there is no check before the array indexing to see whether the length of useful_amounts is greater than zero. Thus, if useful_amounts is empty, the indexing will cause an array out-of-bounds error which will make the node panic. Since the find_important_value function is callable by anyone, an attacker can set useful_amounts to an empty array and spam the network with malicious transactions to launch a DoS attack.

Mitigations

  • Write non-throwing Rust code (e.g. prefer returning Result types, use ensure!, etc.).
  • Proper data validation of all input parameters is crucial to ensure that an unexpected panic does not occur.
  • A thorough suite of unit tests should be implemented.
  • Fuzz testing (e.g. using test-fuzz) can aid in exploring more of the input space.

References

  • https://docs.substrate.io/main-docs/build/events-errors/#errors

Weights and Fees

Weights and transaction fees are the two main ways to regulate the consumption of blockchain resources. The overuse of blockchain resources can allow a malicious actor to spam the network to cause a denial-of-service (DoS). Weights are used to manage the time it takes to validate the block. The larger the weight, the more "resources" / time the computation takes. Transaction fees provide an economic incentive to limit the number of resources used to perform operations; the fee for a given transaction is a function of the weight required by the transaction.

Weights can be fixed or a custom "weight annotation / function" can be implemented. A weight function can calculate the weight, for example, based on the number of database read / writes and the size of the input paramaters (e.g. a long Vec<>). To optimize the weight such that users do not pay too little or too much for a transaction, benchmarking can be used to empirically determine the correct weight in worst case scenarios.

Specifying the correct weight function and benchmarking it is crucial to protect the Substrate node from denial-of-service (DoS) attacks. Since fees are a function of weight, a bad weight function implies incorrect fees. For example, if some function performs heavy computation (which takes a lot of time) but specifies a very small weight, it is cheap to call that function. In this way an attacker can perform a low-cost attack while still stealing a large amount of block execution time. This will prevent regular transactions from being fit into those blocks.

Example

In the pallet-bad-weights pallet, a custom weight function, MyWeightFunction, is used to calculate the weight for a call to do_work. The weight required for a call to do_work is 10_000_000 times the length of the useful_amounts vector.

#![allow(unused)]
fn main() {
impl WeighData<(&Vec<u64>,)> for MyWeightFunction {
    fn weigh_data(&self, (amounts,): (&Vec<u64>,)) -> Weight {
        self.0.saturating_mul(amounts.len() as u64).into()
    }
}
}

However, if the length of the useful_amounts vector is zero, the weight to call do_work would be zero. A weight of zero implies that calling this function is financially cheap. This opens the opportunity for an attacker to call do_work a large number of times to saturate the network with malicious transactions without having to pay a large fee and could cause a DoS attack.

One potential fix for this is to set a fixed weight if the length of useful_amounts is zero.

#![allow(unused)]
fn main() {
impl WeighData<(&Vec<u64>,)> for MyWeightFunction {
    fn weigh_data(&self, (amounts,): (&Vec<u64>,)) -> Weight {
        // The weight function is `y = mx + b` where `m` and `b` are both `self.0` (the static fee) and `x` is the length of the `amounts` array.
        // If `amounts.len() == 0` then the weight is simply the static fee (i.e. `y = b`)
        self.0 + self.0.saturating_mul(amounts.len() as u64).into()
    }
}
}

In the example above, if the length of amounts (i.e. useful_amounts) is zero, then the function will return self.0 (i.e. 10_000_000).

On the other hand, if an attacker sends a useful_amounts vector that is incredibly large, the returned Weight can become large enough such that the dispatchable takes up a large amount block execution time and prevents other transactions from being fit into the block. A fix for this would be to bound the maximum allowable length for useful_amounts.

Note: Custom fee functions can also be created. These functions should also be carefully evaluated and tested to ensure that the risk of DoS attacks is mitigated.

Mitigations

  • Use benchmarking to empirically test the computational resources utilized by various dispatchable functions. Additionally, use benchmarking to define a lower and upper weight bound for each dispatchable.
  • Create bounds for input arguments to prevent a transaction from taking up too many computational resources. For example, if a Vec<> is being taken as an input argument to a function, prevent the length of the Vec<> from being too large.
  • Be wary of fixed weight dispatchables (e.g. #[pallet::weight(1_000_000)]). A weight that is completely agnostic to database read / writes or input parameters may open up avenues for DoS attacks.

References

  • https://docs.substrate.io/main-docs/build/tx-weights-fees/
  • https://docs.substrate.io/reference/how-to-guides/weights/add-benchmarks/
  • https://docs.substrate.io/reference/how-to-guides/weights/use-custom-weights/
  • https://docs.substrate.io/reference/how-to-guides/weights/use-conditional-weights/
  • https://www.shawntabrizi.com/substrate/substrate-weight-and-fees/

Verify First, Write Last

NOTE: As of Polkadot v0.9.25, 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.

#![allow(unused)]
fn main() {
{
  // all checks and throwing code go here

  // ** no throwing code below this line **

  // all event emissions & storage writes go here
}
}

Example

In the pallet-verify-first pallet, the init dispatchable is used to set up the TotalSupply of the token and transfer them to the sender. init should be only called once. Thus, the Init boolean is set to true when it is called initially. If init is called more than once, the transaction will throw an error because Init is already true.

#![allow(unused)]
fn main() {
/// Initialize the token
/// Transfers the total_supply amount to the caller
/// If init() has already been called, throw AlreadyInitialized error
#[pallet::weight(10_000)]
pub fn init(
    origin: OriginFor<T>,
    supply: u64
) -> DispatchResultWithPostInfo {
    let sender = ensure_signed(origin)?;
    
    if supply > 0 {
        <TotalSupply<T>>::put(&supply);
    }
    // Set sender's balance to total_supply()
    <BalanceToAccount<T>>::insert(&sender, supply);

    // Revert above changes if init() has already been called
    ensure!(!Self::is_init(), <Error<T>>::AlreadyInitialized);

    // Set Init StorageValue to `true`
    Init::<T>::put(true);
    
    // Emit event
    Self::deposit_event(Event::Initialized(sender));

    Ok(().into())
}
}

However, notice that the setting of TotalSupply and the transfer of funds happens before the check on Init. This violates the "Verify First, Write Last" practice. In an older version of Substrate, this would allow a malicious sender to call init multiple times and change the value of TotalSupply and their balance of the token.

Mitigations

  • Follow the "Verify First, Write Last" practice by doing all the necessary data validation before performing state changes and emitting events.

References

  • https://docs.substrate.io/main-docs/build/runtime-storage/#best-practices

Unsigned Transaction Validation

There are three types of transactions allowed in a Substrate runtime: signed, unsigned, and inherent. An unsigned transaction does not require a signature and does not include information about who sent the transaction. This is naturally problematic because there is no by-default deterrent to spam or replay attacks. Because of this, Substrate allows users to create custom functions to validate unsigned transaction. However, incorrect or improper validation of an unsigned transaction can allow anyone to perform potentially malicious actions. Usually, unsigned transactions are allowed only for select parties (e.g., off-chain workers (OCW)). But, if improper data validation of an unsigned transaction allows a malicious actor to spoof data as if it came from an OCW, this can lead to unexpected behavior. Additionally, improper validation opens up the possibility to replay attacks where the same transaction can be sent to the transaction pool again and again to perform some malicious action repeatedly.

The validation of an unsigned transaction must be provided by the pallet that chooses to accept them. To allow unsigned transactions, a pallet must implement the frame_support::unsigned::ValidateUnsigned trait. The validate_unsigned function, which must be implemented as part of the ValidateUnsigned trait, will provide the logic necessary to ensure that the transaction is valid. An off chain worker (OCW) can be implemented directly in a pallet using the offchain_worker hook. The OCW can send an unsigned transaction by calling SubmitTransaction::submit_unsigned_transaction. Upon submission, the validate_unsigned function will ensure that the transaction is valid and then pass the transaction on towards towards the final dispatchable function.

Example

The pallet-bad-unsigned pallet is an example that showcases improper unsigned transaction validation. The pallet tracks the average, rolling price of some "asset"; this price data is being retrieved by an OCW. The fetch_price function, which is called by the OCW, naively returns 100 as the current price (note that an HTTP request can be made here for true price data). The validate_unsigned function (see below) simply validates that the Call is being made to submit_price_unsigned and nothing else.

#![allow(unused)]
fn main() {
/// By default unsigned transactions are disallowed, but implementing the validator
/// here we make sure that some particular calls (the ones produced by offchain worker)
/// are being whitelisted and marked as valid.
fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
    // If `submit_price_unsigned` is being called, the transaction is valid.
    // Otherwise, it is an InvalidTransaction.
    if let Call::submit_price_unsigned { block_number, price: new_price } = call {
        let avg_price = Self::average_price()
        .map(|price| if &price > new_price { price - new_price } else { new_price - price })
        .unwrap_or(0);

        let valid_tx = | provide | {
            ValidTransaction::with_tag_prefix("ExampleOffchainWorker")
            .priority(T::UnsignedPriority::get().saturating_add(avg_price as _))
            .and_provides([&provide])
            .longevity(5)
            .propagate(true)
            .build()
        };
        valid_tx(b"submit_price_unsigned".to_vec())
    } else {
        InvalidTransaction::Call.into()
    }
}
}

However, notice that there is nothing preventing an attacker from sending malicious price data. Both block_number and price can be set to arbitrary values. For block_number, it would be valuable to ensure that it is not a block number in the future; only price data for the current block can be submitted. Additionally, medianization can be used to ensure that the reported price is not severely affected by outliers. Finally, unsigned submissions can be throttled by enforcing a delay after each submission.

Note that the simplest solution would be to sign the offchain submissions so that the runtime can validate that a known OCW is sending the price submission transactions.

Mitigations

  • Consider whether unsigned transactions is a requirement for the runtime that is being built. OCWs can also submit signed transactions or transactions with signed payloads.
  • Ensure that each parameter provided to validate_unsigned is validated to prevent the runtime from entering a state that is vulnerable or undefined.

References

  • https://docs.substrate.io/main-docs/fundamentals/transaction-types/#unsigned-transactions
  • https://docs.substrate.io/main-docs/fundamentals/offchain-operations/
  • https://github.com/paritytech/substrate/blob/polkadot-v0.9.26/frame/examples/offchain-worker/src/lib.rs
  • https://docs.substrate.io/reference/how-to-guides/offchain-workers/offchain-transactions/
  • https://docs.substrate.io/reference/how-to-guides/offchain-workers/offchain-http-requests/

Bad Randomness

To use randomness in a Substrate pallet, all you need to do is require a source of randomness in the Config trait of a pallet. This source of Randomness must implement the Randomness trait. The trait provides two methods for obtaining randomness.

  1. random_seed: This function takes no arguments and returns back a random value. Calling this value multiple times in a block will result in the same value.
  2. random: Takes in a byte-array (a.k.a "context-identifier") and returns a value that is as independent as possible from other contexts.

Substrate provides the Randomness Collective Flip Pallet and a Verifiable Random Function implementation in the BABE pallet. Developers can also choose to build their own source of randomness.

A bad source of randomness can lead to a variety of exploits such as the theft of funds or undefined system behavior.

Example

The pallet-bad-lottery pallet is a simplified "lottery" system that requires one to guess the next random number. If they guess correctly, they are the winner of the lottery.

#![allow(unused)]
fn main() {
#[pallet::call]
impl<T:Config> Pallet<T> {
/// Guess the random value
/// If you guess correctly, you become the winner
#[pallet::weight(10_000)]
pub fn guess(
    origin: OriginFor<T>,
    guess: T::Hash
) -> DispatchResultWithPostInfo {
    let sender = ensure_signed(origin)?;
    // Random value.
    let nonce = Self::get_and_increment_nonce();
    let (random_value, _) = T::MyRandomness::random(&nonce);
    // Check if guess is correct
    ensure!(guess == random_value, <Error<T>>::IncorrectGuess);
    <Winner<T>>::put(&sender);

    Self::deposit_event(Event::NewWinner(sender));

    Ok(().into())
}
}

impl<T:Config> Pallet<T> {
/// Increment the nonce each time guess() is called
pub fn get_and_increment_nonce() -> Vec<u8> {
    let nonce = Nonce::<T>::get();
    Nonce::<T>::put(nonce.wrapping_add(1));
    nonce.encode()
}
}
}

Note that the quality of randomness provided to the pallet-bad-lottery pallet is related to the randomness source. If the randomness source is the "Randomness Collective Flip Pallet", this lottery system is insecure. This is because the collective flip pallet implements "low-influence randomness". This makes it vulnerable to a collusion attack where a small minority of participants can give the same random number contribution making it highly likely to have the seed be this random number (click here to learn more). Additionally, as mentioned in the Substrate documentation, "low-influence randomness can be useful when defending against relatively weak adversaries. Using this pallet as a randomness source is advisable primarily in low-security situations like testing."

Mitigations

  • Use the randomness implementation provided by the BABE pallet. This pallet provides "production-grade randomness, and is used in Polkadot. Selecting this randomness source dictates that your blockchain use Babe consensus."
  • Defer from creating a custom source of randomness unless specifically necessary for the runtime being developed.
  • Do not use random_seed as the method of choice for randomness unless specifically necessary for the runtime being developed.

References

  • https://docs.substrate.io/main-docs/build/randomness/
  • 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

Origins

The origin of a call tells a dispatchable function where the call has come from. Origins are a way to implement access controls in the system.

There are three types of origins that can used in the runtime:

#![allow(unused)]
fn main() {
pub enum RawOrigin<AccountId> {
	Root,
	Signed(AccountId),
	None,
}
}

Outside of the out-of-box origins, custom origins can also be created that are catered to a specific runtime. The primary use case for custom origins is to configure privileged access to dispatch calls in the runtime, outside of RawOrigin::Root.

Using privileged origins, like RawOrigin::Root or custom origins, can lead to access control violations if not used correctly. It is a common error to use ensure_signed in place of ensure_root which would allow any user to bypass the access control placed by using ensure_root.

Example

In the pallet-bad-origin pallet, there is a set_important_val function that should be only callable by the ForceOrigin custom origin type. This custom origin allows the pallet to specify that only a specific account can call set_important_val.

#![allow(unused)]
fn main() {
#[pallet::call]
impl<T:Config> Pallet<T> {
    /// Set the important val
    /// Should be only callable by ForceOrigin
    #[pallet::weight(10_000)]
    pub fn set_important_val(
        origin: OriginFor<T>,
        new_val: u64
    ) -> DispatchResultWithPostInfo {
        let sender = ensure_signed(origin)?;
        // Change to new value
        <ImportantVal<T>>::put(new_val);

        // Emit event
        Self::deposit_event(Event::ImportantValSet(sender, new_val));

        Ok(().into())
    }
}
}

However, the set_important_val is using ensure_signed; this allows any account to set ImportantVal. To allow only the ForceOrigin to call set_important_val the following change can be made:

#![allow(unused)]
fn main() {
T::ForceOrigin::ensure_origin(origin.clone())?;
let sender = ensure_signed(origin)?;
}

Mitigations

  • Ensure that the correct access controls are placed on privileged functions.
  • Develop user documentation on all risks associated with the system, including those associated with privileged users.
  • A thorough suite of unit tests that validates access controls is crucial.

References

  • https://docs.substrate.io/main-docs/build/origins/
  • https://docs.substrate.io/tutorials/work-with-pallets/specify-the-origin-for-a-call/
  • https://paritytech.github.io/substrate/master/pallet_sudo/index.html#
  • https://paritytech.github.io/substrate/master/pallet_democracy/index.html

Program Analysis

We are going use three distinctive testing and program analysis techniques:

  • Static analysis with Slither. All the paths of the program are approximated and analyzed at the same time, through different program presentations (e.g. control-flow-graph)
  • Fuzzing with Echidna. The code is executed with a pseudo-random generation of transactions. The fuzzer will try to find a sequence of transactions to violate a given property.
  • Symbolic execution with Manticore. A formal verification technique, which translates each execution path to a mathematical formula, on which on top constraints can be checked.

Each technique has advantages and pitfalls, and will be useful in specific cases:

TechniqueToolUsageSpeedBugs missedFalse Alarms
Static AnalysisSlitherCLI & scriptssecondsmoderatelow
FuzzingEchidnaSolidity propertiesminuteslownone
Symbolic ExecutionManticoreSolidity properties & scriptshoursnone*none

* if all the paths are explored without timeout

Slither analyzes contracts within seconds, however, static analysis might lead to false alarms and will be less suitable for complex checks (e.g. arithmetic checks). Run Slither via the API for push-button access to built-in detectors or via the API for user-defined checks.

Echidna needs to run for several minutes and will only produce true positives. Echidna checks user-provided security properties, written in Solidity. It might miss bugs since it is based on random exploration.

Manticore performs the "heaviest weight" analysis. Like Echidna, Manticore verifies user-provided properties. It will need more time to run, but it can prove the validity of a property and will not report false alarms.

Suggested workflow

Start with Slither's built-in detectors to ensure that no simple bugs are present now or will be introduced later. Use Slither to check properties related to inheritance, variable dependencies, and structural issues. As the codebase grows, use Echidna to test more complex properties of the state machine. Revisit Slither to develop custom checks for protections unavailable from Solidity, like protecting against a function being overridden. Finally, use Manticore to perform targeted verification of critical security properties, e.g., arithmetic operations.

  • Use Slither's CLI to catch common issues
  • Use Echidna to test high-level security properties of your contract
  • Use Slither to write custom static checks
  • Use Manticore once you want in-depth assurance of critical security properties

A note on unit tests. Unit tests are necessary to build high-quality software. However, these techniques are not the best suited to find security flaws. They are typically used to test positive behaviors of code (i.e. the code works as expected in the normal context), while security flaws tend to reside in edge cases that the developers did not consider. In our study of dozens of smart contract security reviews, unit test coverage had no effect on the number or severity of security flaws we found in our client's code.

Determining Security Properties

To effectively test and verify your code, you must identify the areas that need attention. As your resources spent on security are limited, scoping the weak or high-value parts of your codebase is important to optimize your effort. Threat modeling can help. Consider reviewing:

Components

Knowing what you want to check will also help you to select the right tool.

The broad areas that are frequently relevant for smart contracts include:

  • State machine. Most contracts can be represented as a state machine. Consider checking that (1) No invalid state can be reached, (2) if a state is valid that it can be reached, and (3) no state traps the contract.

    • Echidna and Manticore are the tools to favor to test state-machine specifications.
  • Access controls. If you system has privileged users (e.g. an owner, controllers, ...) you must ensure that (1) each user can only perform the authorized actions and (2) no user can block actions from a more priviledged user.

    • Slither, Echidna and Manticore can check for correct access controls. For example, Slither can check that only whitelisted functions lack the onlyOwner modifier. Echidna and Manticore are useful for more complex access control, such as a permission given only if the contract reaches a given state.
  • Arithmetic operations. Checking the soundness of the arithmetic operations is critical. Using SafeMath everywhere is a good step to prevent overflow/underflow, however, you must still consider other arithmetic flaws, including rounding issues and flaws that trap the contract.

    • Manticore is the best choice here. Echidna can be used if the arithmetic is out-of-scope of the SMT solver.
  • Inheritance correctness. Solidity contracts rely heavily on multiple inheritance. Mistakes such as a shadowing function missing a super call and misinterpreted c3 linearization order can easily be introduced.

    • Slither is the tool to ensure detection of these issues.
  • External interactions. Contracts interact with each other, and some external contracts should not be trusted. For example, if your contract relies on external oracles, will it remain secure if half the available oracles are compromised?

    • Manticore and Echidna are the best choice for testing external interactions with your contracts. Manticore has an built-in mechanism to stub external contracts.
  • Standard conformance. Ethereum standards (e.g. ERC20) have a history of flaws in their design. Be aware of the limitations of the standard you are building on.

    • Slither, Echidna, and Manticore will help you to detect deviations from a given standard.

Tool selection cheatsheet

ComponentToolsExamples
State machineEchidna, Manticore
Access controlSlither, Echidna, ManticoreSlither exercise 2, Echidna exercise 2
Arithmetic operationsManticore, EchidnaEchidna exercise 1, Manticore exercises 1 - 3
Inheritance correctnessSlitherSlither exercise 1
External interactionsManticore, Echidna
Standard conformanceSlither, Echidna, Manticoreslither-erc

Other areas will need to be checked depending on your goals, but these coarse-grained areas of focus are a good start for any smart contract system.

Our public audits contain examples of verified or tested properties. Consider reading the Automated Testing and Verification sections of the following reports to review real-world security properties:

Echidna Tutorial

The aim of this tutorial is to show how to use Echidna to automatically test smart contracts.

Watch our Fuzzing workshop to learn through live coding sessions.

Table of contents:

Join the team on Slack at: https://empireslacking.herokuapp.com/ #ethereum

Introduction

Introductory material to fuzzing and Echidna

Introduction to property-based fuzzing

Echidna is a property-based fuzzer, which we described in our previous blogposts (1, 2, 3).

Fuzzing

Fuzzing is a well-known technique in the security community. It consists of generating more or less random inputs to find bugs in the program. Fuzzers for traditional software (such as AFL or LibFuzzer) are known to be efficient tools to find bugs.

Beyond the purely random generation of inputs, there are many techniques and strategies to generate good inputs, including:

  • Obtain feedback from each execution and guide generation using it. For example, if a newly generated input leads to the discovery of a new path, it makes sense to generate new inputs closest to it.
  • Generate input with respect to a structural constraint. For example, if your input contains a header with a checksum, it makes sense to let the fuzzer generate input validating the checksum.
  • Use known inputs to generate new inputs. If you have access to a large dataset of valid input, your fuzzer can generate new inputs from them, rather than starting from scratch for each generation. These are usually called seeds.

Property-based fuzzing

Echidna belongs to a specific family of fuzzer: property-based fuzzing heavily inspired by QuickCheck. In contrast to a classic fuzzer that will try to find crashes, Echidna will try to break user-defined invariants.

In smart contracts, invariants are Solidity functions that can represent any incorrect or invalid state that the contract can reach, including:

  • Incorrect access control: the attacker became the owner of the contract.
  • Incorrect state machine: the tokens can be transferred while the contract is paused.
  • Incorrect arithmetic: the user can underflow its balance and get unlimited free tokens.

Testing a Property with Echidna

Table of contents:

Introduction

We will see how to test a smart contract with Echidna. The target is the following smart contract (../example/token.sol):

   contract Token{
      mapping(address => uint) public balances;
      function airdrop() public{
          balances[msg.sender] = 1000;
     }
     function consume() public{
          require(balances[msg.sender]>0);
          balances[msg.sender] -= 1;
     }
     function backdoor() public{
          balances[msg.sender] += 1;
     }
   }
  

We will make the assumption that this token has the following properties:

  • Anyone can have at maximum 1000 tokens

  • The token cannot be transferred (it is not an ERC20 token)

Write a property

Echidna properties are Solidity functions. A property must:

  • Have no argument
  • Return true if it is successful
  • Have its name starting with echidna

Echidna will:

  • Automatically generate arbitrary transactions to test the property.
  • Report any transactions leading a property to return false or throw an error.
  • Discard side-effects when calling a property (i.e. if the property changes a state variable, it is discarded after the test)

The following property checks that the caller can have no more than 1000 tokens:

    function echidna_balance_under_1000() public view returns(bool){
         return balances[msg.sender] <= 1000;
    }

Use inheritance to separate your contract from your properties:

    contract TestToken is Token{
         function echidna_balance_under_1000() public view returns(bool){
               return balances[msg.sender] <= 1000;
          }
     }

../example/testtoken.sol implements the property and inherits from the token.

Initiate a contract

Echidna needs a constructor without input arguments. If your contract needs a specific initialization, you need to do it in the constructor.

There are some specific addresses in Echidna:

  • 0x30000 calls the constructor.

  • 0x10000, 0x20000, and 0x30000 randomly call the other functions.

We do not need any particular initialization in our current example. As a result, our constructor is empty.

Run Echidna

Echidna is launched with:

$ echidna-test contract.sol

If contract.sol contains multiple contracts, you can specify the target:

$ echidna-test contract.sol --contract MyContract

Summary: Testing a property

The following summarizes the run of Echidna on our example:

     contract TestToken is Token{
         constructor() public {}
             function echidna_balance_under_1000() public view returns(bool){
                return balances[msg.sender] <= 1000;
             }
       }
$ echidna-test testtoken.sol --contract TestToken
...

echidna_balance_under_1000: failed!💥  
  Call sequence, shrinking (1205/5000):
    airdrop()
    backdoor()

...

Echidna found that the property is violated if backdoor is called.

Basic

How to select the most suitable testing mode

Since Echidna offers several ways to write properties, often developers or auditors are wondering which testing mode should use. We will review how each mode works, as well as their advantages or disadvantages.

Table of contents:

Boolean properties

By default, the "property" testing mode is used, which reports failures using a special functions called properties:

  • Testing functions should be named with a specific prefix (e.g. echidna_).
  • Testing functions take no parameters, and always return a boolean value.
  • Any side effect will be reverted at the end of the execution of the property.
  • Properties pass if they return true, and fail if they return false or revert. As an alternative, properties that starts with "echidna_revert_" will fail if they return any value (true or false), and pass if they revert. This pseudo-code summarizes how properties work:
function echidna_property() public returns (bool) { // No arguments are required

    // The following statements can trigger a failure if they revert 
    publicFunction(..);
    internalFunction(..);
    contract.function(..);

    // The following statement can trigger a failure depending on the returned value
    return ..;
} // side effects are *not* preserved

function echidna_revert_property() public returns (bool) { // No arguments is required

    // The following statements can *never* trigger a failure
    publicFunction(..);
    internalFunction(..);
    contract.function(..);

    // The following statement will *always* trigger a failure regardless of the value returned
    return ..;
} // side effects are *not* preserved

Advantages:

  • Properties can be easier to write and understand compared to other approaches for testing.
  • No need to worry about side-effects, since these are reverted at the end of the property execution.

Disadvantages:

  • Since the properties take no parameters, any additional input should be added using a state variable.
  • Any revert will be interpreted as a failure, which is not always expected.
  • No coverage is collected during its execution so these properties should be used with simple code. For anything complex (e.g. with a non-trivial amount of branches), other types of tests should be used.

Recommendations

This mode can be used when a property can be easily computed from the use of state variables (either internal or public), and there is no need to use extra parameters.

Assertions

Using the "assertion" testing mode, Echidna will report an assert violation if:

  • The execution reverts during a call to assert. Technically speaking, Echidna will detect an assertion failure if it executes an assert call that fails in the first call frame of the target contract (so this excludes any internal transactions in most of the cases).
  • An AssertionFailed event (with any number of parameters) is emitted by any contract. This pseudo-code summarizes how assertions work:
function checkInvariant(..) public { // Any number of arguments is supported

    // The following statements can trigger a failure using `assert`
    assert(..); 
    publicFunction(..);
    internalFunction(..);

    // The following statement will always trigger a failure even if the execution ends with a revert
    emits AssertionFailed(..);

    // The following statement will *only* trigger a failure using `assert` if using solc 0.8.x or newer
    // To make sure it works in older versions, use the AssertionFailed(..) event
    anotherContract.function(..);
    
} // side effects are preserved

Functions checking assertions do not require any particular name and are executed like any other function, and therefore, their side effects are kept if they do not revert.

Advantages

  • Easy to implement, in particular, if there are any number of parameters required to compute the invariant.
  • Coverage is collected during the execution of these tests, so it can help to reach new failures.
  • If the code base already contains assertions for checking invariants, they can be reused.

Disadvantages

  • If the code to test is already using assertions for data validation, it will not work as expected. For example:
function deposit(uint256 tokens) public {
  assert(tokens > 0); // should be strictly positive
  ..

Developers should avoid doing that and use require instead, but if that is not possible because you are calling some contract that is outside your control, you can use the AssertionFailure event.

Recommendation

You should use assertions if your invariant is more natural to be expressed using arguments or if it can only be checked in the middle of a transaction. Another good use case of assertions is complex code that require to check something as well as changing the state. In the following example, we test staking of some ERC20, given that there is at least MINSTAKE tokens in the sender balance.

function testStake(uint256 toStake) public {
    uint256 balance = balanceOf(msg.sender);
    toStake = toStake % (balance + 1); 
    if (toStake < MINSTAKE)                             // Pre: minimal stake is required
      return;
    stake(msg.sender, toStake);                         // Action: token staking
    assert(staked(msg.sender) == toStake);              // Post: staking amount is toStake
    assert(balanceOf(msg.sender) == balance - toStake); // Post: balance decreased
}

testStake checks some invariants on staking, but also ensures that the state of the contract is properly updated (e.g only allowing a user to stake at least MINSTAKE).

Dapptest

Using the "dapptest" testing mode, Echidna will report violations using certain functions following how dapptool and foundry work:

  • This mode uses any function name with one or more arguments, which will trigger a failure if they revert, except in one special case. This is, if the execution reverts with the special reason “FOUNDRY::ASSUME”, then the test will pass (this emulates how the assume foundry cheat code works). This pseudo-code summarizes how dapptests work:
function checkDappTest(..) public { // One or more arguments are required
    // The following statements can trigger a failure if they revert
    publicFunction(..);
    internalFunction(..);
    anotherContract.function(..);
    // The following statement will never trigger a failure
    require(.., “FOUNDRY::ASSUME”);
}
  • Functions implementing these tests do not require any particular name and are executed like any other function, and therefore, their side effects are kept if they do not revert (but usually, this mode is used only in stateless testing).
  • The function should NOT be payable (but this can change in the future)

Advantages:

  • Easy to implement, in particular, for stateless mode.
  • Coverage is collected during the execution of these tests, so it can help to reach new failures.

Disadvantages:

  • Almost any revert will be interpreted as a failure, which is not always expected. To avoid this, you should use reverts with FOUNDRY::ASSUME or use try/catch.

Recommendation

Use dapptest mode if you are testing stateless invariants and the code will never unexpectedly revert. Avoid using it for stateful testing, since it was not designed for that (however, Echidna supports it).

Stateless vs Stateful

Any of these testing modes can be used, in either stateful (by default) or stateless mode (using --seqLen 1). In stateful mode, Echidna will keep the state between each function call and will try to break the invariants. In stateless, Echidna will discard the state changes during the fuzzing. There are notable differences in these two modes.

  • Stateful is more powerful, and can allow breaking invariants that exist only if the contract reaches a specific state.
  • Stateless tests can be easier to use than stateful and benefits from simpler input generation.
  • Stateless tests can hide issues, since some of them can depend on a sequence of operations that is not reachable in a single transaction.
  • Stateless mode forces resetting the EVM after each transaction or test, which is usually slower than resetting the state once every certain amount of transactions (by default, every 100 transactions).

Recommendations

In terms of usage for beginners, we recommend starting using Echidna with stateless, and to switch to stateful once you have a good understanding of the system's invariants.

Common testing approaches

Testing of smart contracts is not as straightforward as testing normal binaries that you run in your local computer. This is caused by the existence of multiple accounts interacting with one or many entry points. While a fuzzer can simulate the Ethereum Virtual Machine and can potentially use any account with any feature (e.g. a an unlimited amount of ETH), we take care to avoid breaking some important underlying assumptions of transactions that are impossible in Ethereum (e.g. for instance using msg.sender as the zero address). That is why it is important to have a clear view of the system to test, and how transactions are going to be simulated. There are a few classifications for the testing approach. We will start with two of them, internal and external:

Table of contents:

Internal testing

In this testing approach, properties are defined inside the contract to test, with complete access to the internal state of the system.

Contract InternalTest is System { 
    function echidna_state_greater_than_X() public returns (bool) {
       return stateVar > X;
    }
}

In this approach, Echidna will generate transactions from a simulated account to the target contract. This testing approach is particularly useful for simpler contracts that do not require a complex initialization and have a single entrypoint. Additionally, properties can be easier to write, since properties can access the system's internal state.

External testing

In the external testing approach, properties are tested using external calls from a different contract. Properties are only allowed to access external/public variables or functions.

contract ExternalTest {
    constructor() public {
       addr = address(0x...);
    }
    function echidna_state_greater_than_X() public returns (bool) {
       return System(addr).stateVar() > X;
    }
}

This testing approach is useful for dealing with contracts that require external initialization (e.g. using Etheno), however, it should handle correctly how Echidna runs the transactions, since the contract with the properties is no longer the same as the one we want to test. Since ExternalTest defines no additional methods, running Echidna directly on this will not allow any code to be executed from the contract to test (there are no functions in ExternalTest to call besides the actual properties). In this case, there are several alternatives:

Contract wrapper: define specific operations to "wrap" the system to test. For every operation that we want Echidna to execute in the system to test, we add one or more functions that performs external call to it.

contract ExternalTest {
    constructor() public {
       addr = ..;
    }

    function method(...) public returns (...) {
       return System(addr).method(..);
    }

    function echidna_state_greater_than_X() public returns (bool) {
       return System(addr).stateVar() > X;
    }
}

There are two important points to consider in this approach:

  • The sender of each transaction will be the ExternalTest contract, instead of the simulated Echidna senders (e.g 0x10000, ..). This means that the real address interacting with the system will be the External contract one, instead of one of the Echidna senders. Please take particular care, if you need to provide ETH or tokens into this contract.

  • This approach is manual and can be time consuming if there a lot of functions operations, but it can be useful when Echidna needs some help calculating some value which cannot be randomly sampled:

contract ExternalTest {
    ...
    function methodUsingF(..., uint256 x) public returns (...) {
       return System(addr).method(.., f(x));
    }
    ... 
}

Multi ABI: Echidna is capable of performing direct calls to every contract, if the multi-abi mode is enabled. This means that using it wil not require wrapped calls, however since every contract deployed can be called, there could be unintended effects. For instance, if we have a property to ensure that the amount of tokens is limited:

contract ExternalTest {
    constructor() public {
       addr = ..;
       MockERC20(..).mint(..);  
    }

    function echidna_limited_supply() public returns (bool) {
       return System(addr).balanceOf(...) <= X;
    }
    ... 
}

If we used "mock" contracts for tokens (e.g. MockERC20) could be an issue, because Echidna could call functions that are public but are only supposed to be used during the initialization such as mint. This can be easily solved using a blacklist of functions to ignore:

filterBlacklist: true
filterFunctions: [“MockERC20.mint(uint256,address)”]

Finally, there is another benefit for using this approach: it will force the developer or auditor to write properties using public data. If an important property cannot be defined using public data, it could be an indication that users or other contracts will NOT be able to easily interact with the system to either perform some operation or verify that the system is in a valid state.

Partial testing

Ideally, testing a smart contract system uses the complete deployed system, with the same parameters that the developers intend to use. Testing with the real code, it is always preferred, even if it is slower than doing something else (but perhaps not in the case that it is extremely slow). However, there are many cases where even if the complete system is deployed, it cannot be simulated because it depends on off-chain components (e.g. a token bridge). In such cases, we are forced to implement alternative solutions.

In this case, we will do testing of some of the components, ignoring or abstracting uninteresting parts such as standard ERC20 tokens or oracles. There are a few ways to do this.

Isolated testing: If a component is properly abstracted from the rest of the system, testing it can be easy. This is particularly useful for testing stateless properties that you can find in components that compute mathematical operations, such as mathematical libraries.

Function override: Solidity allows to override functions, in order to change the functionality of a piece of code, without affecting the rest of the code base. We can use this to disable certain functions in our tests, in order to allow testing using Echidna:

Contract InternalTestOverridingSignatures is System {

    function verifySignature(..) public returns (bool) override {
      return true; // signatures are always valid
    }
 
    function echidna_state_greater_than_X() public returns (bool) {
       executeSomethingWithSignature(..)
       return stateVar > X;
    }
}

Model testing: if the system is not modular enough, then we will need a different approach. Instead of using the code as it is, we will create a “model” of the system in Solidity, using mostly the original code. While there is no defined list of steps to build a model, we can show a generic example. Let’s assume we have a complex system that include this piece of code:

Contract System {
    … 
    function calculateSomething() public returns (uint256) {
       if (booleanState) {
           stateSomething = (uint256State1 * uint256State2) / 2**128;
           return stateSomething / uint128State;
       } 
       …
    }
}

Where boolState, uint256State1, uint256State2 and stateSomething are state variables of our system to test. We are going to create a model (e.g. copy, paste and modify the original code in a new contract), where each state variable is transformed into a parameter:

Contract SystemModel {

    function calculateSomething(bool boolState, uint256 uint256State1, …) public returns (uint256) {
       if (boolState) {
           stateSomething = (uint256State1 * uint256State2) / 2**128;
           return stateSomething / uint128State;
       } 
       …
    }
}

At this point, we should be able to compile our model without any dependency from the original codebase (everything necessary should be included in the model). Then, we can insert assertions to detect when the returned value exceeds a certain threshold.

While developers or auditors can be tempted to quickly create tests using this technique there are certain disadvantages when creating models:

  • The code tested can be very different from the one we want to test: this can either introduce issues that are not real (false positives) or hide real issues from the original code (false negatives). In the example, it is unclear if the state variable can take arbitrary values.

  • The model will have a limited value if the code is changed since any modification in the original model will force a rebuild of the model, and this should be manually performed.

In any case, developers should be warned that their code is difficult to test and it should be refactored to avoid this issue in the future.

Filtering functions to call during a fuzzing campaign

Table of contents:

Introduction

We will see how to filter the functions to be fuzzed. The target is the following smart contract (../example/multi.sol):

contract C {
  bool state1 = false;
  bool state2 = false;
  bool state3 = false;
  bool state4 = false;

  function f(uint x) public {
    require(x == 12);
    state1 = true;
  }

  function g(uint x) public {
    require(state1);
    require(x == 8);
    state2 = true;
  }

  function h(uint x) public {
    require(state2);
    require(x == 42);
    state3 = true;
  }

 function i() public {
    require(state3);
    state4 = true;
  }

  function reset1() public {
    state1 = false;
    state2 = false;
    state3 = false;
    return;
  }

  function reset2() public {
    state1 = false;
    state2 = false;
    state3 = false;
    return;
  }

  function echidna_state4() public returns (bool) {
    return (!state4);
  }

}

This small example forces Echidna to find a certain sequence of transactions to change a state variable. This is hard for a fuzzer (it is recommended to use a symbolic execution tool like Manticore). We can run Echidna to verify this:

$ echidna-test multi.sol 
...
echidna_state4: passed! 🎉
Seed: -3684648582249875403

Filtering functions

Echidna has trouble finding the correct sequence to test this contract because the two reset functions (reset1 and reset2) will set all the state variables to false. However, we can use a special Echidna feature to either blacklist the reset functions or to whitelist only the f, g, h and i functions.

To blacklist functions, we can use this configuration file:

filterBlacklist: true
filterFunctions: ["C.reset1()", "C.reset2()"]

Another approach to filter functions is to list the whitelisted functions. To do that, we can use this configuration file:

filterBlacklist: false
filterFunctions: ["C.f(uint256)", "C.g(uint256)", "C.h(uint256)", "C.i()"]
  • filterBlacklist is true by default.
  • Filtering will be performed by full function name (contract name + "." + ABI function signature). If you have f() and f(uint256), you can specify exactly which function to filter.

Run Echidna

To run Echidna with a configuration file blacklist.yaml:

$ echidna-test multi.sol --config blacklist.yaml 
...
echidna_state4: failed!💥  
  Call sequence:
    f(12)
    g(8)
    h(42)
    i()

Echidna will find the sequence of transactions to falsify the property almost immediately.

Summary: Filtering functions

Echidna can either blacklist or whitelist functions to call during a fuzzing campaign using:

filterBlacklist: true
filterFunctions: ["C.f1()", "C.f2()", "C.f3()"]
$ echidna-test contract.sol --config config.yaml 
...

According to the value of the filterBlacklist boolean, Echidna will start a fuzzing campaign by either blacklisting C.f1(), C.f2() and C.f3() or by only calling those functions.

How to test assertions with Echidna

Table of contents:

Introduction

In this short tutorial, we will show you how to use Echidna to check assertions in smart contracts. For this example, make sure you use Solidity 0.7.x or older. If you run them with Solidity 0.8.x, the test will never fail.

Write an assertion

Let's suppose we have a contract like this one:

contract Incrementor {
  uint private counter = 2**200;

  function inc(uint val) public returns (uint){
    uint tmp = counter;
    counter += val;
    // tmp <= counter
    return (counter - tmp);
  }
}

We want to make sure that tmp is less than or equal to counter after returning its difference. We could write an Echidna property, but we will need to store the tmp value somewhere. Instead, we could use an assertion like this one (../example/assert.sol):

contract Incrementor {
  uint private counter = 2**200;

  function inc(uint val) public returns (uint){
    uint tmp = counter;
    counter += val;
    assert (tmp <= counter);
    return (counter - tmp);
  }
}

We could also use a special event called AssertionFailed with any number of parameters to let Echidna know about a failed assertion without using assert. This will work in any contract. For instance:

contract Incrementor {
  event AssertionFailed(uint);
  uint private counter = 2**200;

  function inc(uint val) public returns (uint){
    uint tmp = counter;
    counter += val;
    if (tmp > counter)
      emit AssertionFailed(counter);
    return (counter - tmp);
  }
}

Run Echidna

To enable the assertion failure testing in Echidna, you can use --test-mode assertion directly from the command line.

Otherwise, you can create an Echidna configuration file, config.yaml, with testMode set for assertion checking:

testMode: assertion

When we run this contract with Echidna, we obtain the expected results:

$ echidna-test assert.sol --test-mode assertion
Analyzing contract: assert.sol:Incrementor
assertion in inc: failed!💥  
  Call sequence, shrinking (2596/5000):
    inc(21711016731996786641919559689128982722488122124807605757398297001483711807488)
    inc(7237005577332262213973186563042994240829374041602535252466099000494570602496)
    inc(86844066927987146567678238756515930889952488499230423029593188005934847229952)

Seed: 1806480648350826486

As you can see, Echidna reports an assertion failure in the inc function. Adding more than one assertion per function is possible, however, Echidna cannot tell which assertion failed.

When and how to use assertions

Assertions can be used as alternatives to explicit properties if the conditions to check are directly related to the correct use of some operation f. Adding assertions after some code will enforce that the check happens immediately after it was executed:

function f(..) public {
    // some complex code
    ...
    assert (condition);
    ...
}

On the contrary, using an explicit boolean property will randomly execute transactions and there is no easy way to enforce exactly when it will be checked. It is still possible to do this workaround:

function echidna_assert_after_f() public returns (bool) {
    f(..); 
    return(condition);
}

However, there are some issues:

  • It does not compile if f is declared as internal or external
  • It is unclear which arguments should be used to call f
  • The property will fail if f reverts,

Assertions can help to overcome this possible issues. For instance, they can be easily detected when calling internal or public functions:

function f(..) public {
    // some complex code
    ...
    g(..) // this contains an assert
    ...
}

If g is external, then assertion failure can be only detected in Solidity 0.8.x or later.

function f(..) public {
    // some complex code
    ...
    contract.g(..) // this contains an assert
    ...
}

In general, we recommend following John Regehr's advice on using assertions:

  • Do not force any side effects during the assertion checking. For instance: assert(ChangeStateAndReturn() == 1)
  • Do not assert obvious statements. For instance assert(var >= 0) where var is declared as uint.

Finally, please do not use require instead of assert, since Echidna will not be able to detect it (but the contract will revert anyway).

Summary: Assertion Checking

The following summarizes the run of Echidna on our example (remember to use 0.7.x or older):

contract Incrementor {
  uint private counter = 2**200;

  function inc(uint val) public returns (uint){
    uint tmp = counter;
    counter += val;
    assert (tmp <= counter);
    return (counter - tmp);
  }
}
$ echidna-test assert.sol --test-mode assertion
Analyzing contract: assert.sol:Incrementor
assertion in inc: failed!💥  
  Call sequence, shrinking (2596/5000):
    inc(21711016731996786641919559689128982722488122124807605757398297001483711807488)
    inc(7237005577332262213973186563042994240829374041602535252466099000494570602496)
    inc(86844066927987146567678238756515930889952488499230423029593188005934847229952)

Seed: 1806480648350826486

Echidna found that the assertion in inc can fail if this function is called multiple times with large arguments.

How to write good properties step by step

Table of contents:

Introduction

In this short tutorial, we will detail some ideas to write interesting or useful properties using Echidna. At each step, we will iteratively improve our properties.

A first approach

One of the simplest properties to write using Echidna is to throw an assertion when some function is expected to revert/return.

Let's suppose we have a contract interface like the one below:

interface DeFi {
  ERC20 t;
  function getShares(address user) external returns (uint256)
  function createShares(uint256 val) external returns (uint256)
  function depositShares(uint256 val) external
  function withdrawShares(uint256 val) external
  function transferShares(address to) external
  ...

In this example, users can deposit tokens using depositShares, mint shares using createShares, withdraw shares using withdrawShares, transfer all shares to another user using transferShares, and get the number of shares for any account using getShares. We will start with very basic properties:

contract Test {
  DeFi c;
  ERC20 token;

  constructor() {
    c = DeFi(..);
    token.mint(address(this), ...);
  }
  
  function getShares_never_reverts() public {
      (bool b,) = c.call(abi.encodeWithSignature("getShares(address)", address(this)));
      assert(b);
  }

  function depositShares_never_reverts(uint256 val) public {
    if (token.balanceOf(address(this)) >= val) {
        (bool b,) = c.call(abi.encodeWithSignature("depositShares(uint256)", val));
        assert(b);
    }
  }
  
  function withdrawShares_never_reverts(uint256 val) public {
    if (c.getShares(address(this)) >= val) {
        (bool b,) = c.call(abi.encodeWithSignature("withdrawShares(uint256)", val));
        assert(b);
    }
  }
  
  function depositShares_can_revert(uint256 val) public {
    if (token.balanceOf(address(this)) < val) {
        (bool b,) = c.call(abi.encodeWithSignature("depositShares(uint256)", val));
        assert(!b);
    }
  }
  
  function withdrawShares_can_revert(uint256 val) public {
    if (c.getShares(address(this)) < val) {
        (bool b,) = c.call(abi.encodeWithSignature("withdrawShares(uint256)", val));
        assert(!b);
    }
  }
  
}

After you have writen your first version of properties, run Echidna to make sure they work as expected. During this tutorial, we will improve them step by step. It is strongly recommended to run the fuzzer at each step to increase the probability of detecting any potential issues.

Perhaps you think these properties are too low level to be useful, particularly if the code has a good coverage in terms of unit tests. But you will be surprised how often an unexpected revert/return uncovers a complex and severe issue. Moreover, we will see how these properties can be improved to cover more complex post-conditions.

Before we continue, we will improve these properties using try/catch. The use of a low-level call forces us to manually encode the data, which can be error prone (an error will always cause calls to revert). Note, this will only work if the codebase is using solc 0.6.0 or later:

  ...
  function depositShares_never_reverts(uint256 val) public {
    if (token.balanceOf(address(this)) >= val) {
        try c.depositShares(val) { /* not reverted */ } catch { assert(false); }
    }
  }
  
  function depositShares_can_revert(uint256 val) public {
    if (token.balanceOf(address(this)) < val) {
        try c.depositShares(val) { assert(false); } catch { /* reverted */ }
    }
  }
  ...
  
}

Enhacing postcondition checks

If the previous properties are passing, this means that the pre-conditions are good enough, however the post-conditions are not very precise. Avoiding reverts doesn't mean that the contract is in a valid state. Let's add some basic preconditions:

  ...
  function depositShares_never_reverts(uint256 val) public {
    if (token.balanceOf(address(this)) >= val) {
        try c.depositShares(val) { /* not reverted */ } catch { assert(false); }
        assert(c.getShares(address(this)) > 0);
    }
  }
  
  function withdrawShares_never_reverts(uint256 val) public {
    if (c.getShares(address(this)) >= val) {
        try c.withdrawShares(val) { /* not reverted */ } catch { assert(false); }
        assert(token.balanceOf(address(this)) > 0);
    }
  }
  ...
  
}

Uhm, it looks like it is not that easy to specify the value of shares/tokens obtained after each deposit/withdrawal. At least we can say that we must receive something, right?

Combining properties

In this generic example, it is unclear if there is a way to calculate how many shares or tokens we should receive after executing the deposit/withdraw operations. Of course, if we have that information, we should use it. In any case, what we can do here is to combine these two properties into a single one to be able check more precisely it's preconditions.

  ...
  function deposit_withdraw_shares_never_reverts(uint256 val) public {
    uint256 original_balance = token.balanceOf(address(this)); 
    if (original_balance >= val) {
        try c.depositShares(val) { /* not reverted */ } catch { assert(false); }
        uint256 shares = c.getShares(address(this);
        assert(shares > 0);
        try c.withdrawShares(shares) { /* not reverted */ } catch { assert(false); }
        assert(token.balanceOf(address(this)) == original_balance);
    }
  }
  ...
  
}

The resulting property checks that calls to deposit/withdraw shares will never revert and once they execute, the original number of tokens remains the same. Keep in mind that this property should consider fees and any tolerated loss of precision (e.g. when the computation requires a division).

Final considerations

Two important considerations for this example:

We want Echidna to spend most of the execution exploring the contract to test. So, in order to make the properties more efficient, we should avoid dead branches where there is nothing to do. That's why we can improve depositShares_never_reverts to use:

  function depositShares_never_reverts(uint256 val) public {
    if(token.balanceOf(address(this)) > 0) {
      val = val % (token.balanceOf(address(this)) + 1);
      try c.depositShares(val) { /* not reverted */ } catch { assert(false); }
      assert(c.getShares(address(this)) > 0);
    } else {
      ... // code to test depositing zero tokens
    }
  }

Additionally, combining properties does not mean that we will have to remove simpler ones. For instance, if we want to write withdraw_deposit_shares_never_reverts, in which we reverse the order of operations (withdraw and then deposit, instead of deposit and then withdraw), we will have to make sure c.getShares(address(this)) can be positive. An easy way to do it is to keep depositShares_never_reverts, since this code allows Echidna to deposit tokens from address(this) (otherwise, this is impossible).

Summary: How to write good properties

It is usually a good idea to start writing simple properties first and then improving them to make them more precise and easier to read. At each step you should run a short fuzzing campaign to make sure they work as expected and try to catch issues early during the development of your smart contracts.

Advanced

Collecting, visualizing and modifying an Echidna corpus

Table of contents:

Introduction

We will see how to collect and use a corpus of transactions with Echidna. The target is the following smart contract (../example/magic.sol):

contract C {
  bool value_found = false;
  function magic(uint magic_1, uint magic_2, uint magic_3, uint magic_4) public {
    require(magic_1 == 42);
    require(magic_2 == 129);
    require(magic_3 == magic_4+333);
    value_found = true;
    return;
  }

  function echidna_magic_values() public returns (bool) {
    return !value_found;
  }

}

This small example forces Echidna to find certain values to change a state variable. This is hard for a fuzzer (it is recommended to use a symbolic execution tool like Manticore). We can run Echidna to verify this:

$ echidna-test magic.sol 
...

echidna_magic_values: passed! 🎉

Seed: 2221503356319272685

However, we can still use Echidna to collect corpus when running this fuzzing campaign.

Collecting a corpus

To enable the corpus collection, create a corpus directory:

$ mkdir corpus-magic

And an Echidna configuration file config.yaml:

corpusDir: "corpus-magic"

Now we can run our tool and check the collected corpus:

$ echidna-test magic.sol --config config.yaml 

Echidna still cannot find the correct magic value. We can verify where it gets stuck reviewing the corpus-magic/covered.*.txt file:

r   |contract C {
  bool value_found = false;
r   |  function magic(uint magic_1, uint magic_2, uint magic_3, uint magic_4) public {
r   |    require(magic_1 == 42);
r   |    require(magic_2 == 129);
r   |    require(magic_3 == magic_4+333);
    value_found = true;
    return;
  }

  function echidna_magic_values() public returns (bool) {
    return !value_found;
  }

}

The label r on the left of each line shows that Echidna is able to reach these lines but it ends in a revert. As you can see, the fuzzer gets stuck at the last require.

To find a workaround, let's take a look at the corpus it collected. For instance, one of these files was:

[
   {
      "_gas'" : "0xffffffff",
      "_delay" : [
         "0x13647",
         "0xccf6"
      ],
      "_src" : "00a329c0648769a73afac7f9381e08fb43dbea70",
      "_dst" : "00a329c0648769a73afac7f9381e08fb43dbea72",
      "_value" : "0x0",
      "_call" : {
         "tag" : "SolCall",
         "contents" : [
            "magic",
            [
               {
                  "contents" : [
                     256,
                     "93723985220345906694500679277863898678726808528711107336895287282192244575836"
                  ],
                  "tag" : "AbiUInt"
               },
               {
                  "contents" : [
                     256,
                     "334"
                  ],
                  "tag" : "AbiUInt"
               },
               {
                  "contents" : [
                     256,
                     "68093943901352437066264791224433559271778087297543421781073458233697135179558"
                  ],
                  "tag" : "AbiUInt"
               },
               {
                  "tag" : "AbiUInt",
                  "contents" : [
                     256,
                     "332"
                  ]
               }
            ]
         ]
      },
      "_gasprice'" : "0xa904461f1"
   }
]

Clearly, this input will not trigger the failure in our property. In the next step, we will see how to modify it for that.

Seeding a corpus

Echidna needs some help in order to deal with the magic function. We are going to copy and modify the input to use suitable parameters for it:

$ cp corpus-magic/coverage/2712688662897926208.txt corpus-magic/coverage/new.txt

We will modify new.txt to call magic(42,129,333,0). Now, we can re-run Echidna:

$ echidna-test magic.sol --config config.yaml 
...
echidna_magic_values: failed!💥  
  Call sequence:
    magic(42,129,333,0)


Unique instructions: 142
Unique codehashes: 1
Seed: -7293830866560616537

This time, the property is violated immediately. We can verify that another covered.*.txt file is created showing another trace (labeled with *) that Echidna executed which finished with a return at the end of the magic function.

*r  |contract C {
  bool value_found = false;
*r  |  function magic(uint magic_1, uint magic_2, uint magic_3, uint magic_4) public {
*r  |    require(magic_1 == 42);
*r  |    require(magic_2 == 129);
*r  |    require(magic_3 == magic_4+333);
*   |    value_found = true;
    return;
  }

  function echidna_magic_values() public returns (bool) {
    return !value_found;
  }

}

Using optimization mode to find local maximums

Table of contents:

Introduction

We will see how to perform function optimization using Echidna. This tutorial will require Echidna 2.0.5 or greater, so make sure you have update it before starting.

Optimization mode is a experimental feature that allows to define a special function which takes no arguments and returns a int256. Echidna will try find a sequence of transactions to maximize the value returned:

    function echidna_opt_function() public view returns (int256) {
        // if it reverts, Echidna will assumed it returned type(int256).min
        return ..;
    }

Optimizing with Echidna

In this example, the target is the following smart contract (../example/opt.sol):

contract TestDutchAuctionOptimization {
    int256 maxPriceDifference;

    function setMaxPriceDifference(
        uint256 startPrice,
        uint256 endPrice,
        uint256 startTime,
        uint256 endTime
    ) public {
        if (endTime < (startTime + 900)) {
            revert();
        }
        if (startPrice <= endPrice) {
            revert();
        }
        uint256 numerator = (startPrice - endPrice) *
            (block.timestamp - startTime);
        uint256 denominator = endTime - startTime;
        uint256 stepDecrease = numerator / denominator;
        uint256 currentAuctionPrice = startPrice - stepDecrease;
        if (currentAuctionPrice < endPrice) {
            maxPriceDifference = int256(endPrice - currentAuctionPrice);
        }
        if (currentAuctionPrice > startPrice) {
            maxPriceDifference = int256(currentAuctionPrice - startPrice);
        }
    }

    function echidna_opt_price_difference() public view returns (int256) {
        return maxPriceDifference;
    }
}

This small example forces Echidna to maximize certain price difference given some preconditions. If the preconditions are not met, the function will revert, without changing the actual value.

To run this example:

$ echidna-test opt.sol --test-mode optimization --test-limit 100000 --seq-len 1 --corpus-dir corpus --shrink-limit 50000
...
echidna_opt_price_difference: max value: 1076841

  Call sequence, shrinking (42912/50000):
    setMaxPriceDifference(1349752405,1155321,609,1524172858) Time delay: 603902 seconds Block delay: 21

The resulting max value is not unique, running in longer campaign will likely result in a larger value.

Regarding the command line, the optimization mode is enabled using --test-mode optimization. additionally, we included the following tweaks:

  1. Use only one transaction (we know that the function is stateless)
  2. Use a large shrink limit in order to obtain a better value during the minimization of the complexity of the input.

Every time Echidna is executed using the corpus directory, the last input that produces the maximum value should be re-used from the reproducers directory:

$ echidna-test opt.sol --test-mode optimization --test-limit 100000 --seq-len 1 --corpus-dir corpus --shrink-limit 50000
Loaded total of 1 transactions from corpus/reproducers/
Loaded total of 9 transactions from corpus/coverage/
Analyzing contract: /home/g/Code/echidna/opt.sol:TestDutchAuctionOptimization
echidna_opt_price_difference: max value: 1146878

  Call sequence:
    setMaxPriceDifference(1538793592,1155321,609,1524172858) Time delay: 523701 seconds Block delay: 49387

Finding transactions with high gas consumption

Table of contents:

Introduction

We will see how to find the transactions with high gas consumption with Echidna. The target is the following smart contract (../example/gas.sol):

contract C {
  uint state;

  function expensive(uint8 times) internal {
    for(uint8 i=0; i < times; i++)
      state = state + i;
  }

  function f(uint x, uint y, uint8 times) public {
    if (x == 42 && y == 123)
      expensive(times);
    else
      state = 0;
  }

  function echidna_test() public returns (bool) {
    return true;
  }

}

Here expensive can have a large gas consumption.

Currently, Echidna always needs a property to test: here echidna_test always returns true. We can run Echidna to verify this:

$ echidna-test gas.sol
...
echidna_test: passed! 🎉

Seed: 2320549945714142710

Measuring Gas Consumption

To enable Echidna's gas consumption feature, create a configuration file ../example/gas.yaml:

estimateGas: true

In this example, we will also reduce the size of the transaction sequence to make the results easier to understand:

seqLen: 2
estimateGas: true

Run Echidna

Once we have the configuration file created, we can run Echidna like this:

$ echidna-test 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 gas shown is an estimation provided by HEVM.

Filtering Out Gas-Reducing Calls

The tutorial on filtering functions to call during a fuzzing campaign shows how to remove some functions during testing.
This can be critical for getting an accurate gas estimate. Consider the following example (example/pushpop.sol):

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(0x0);
  }
  function echidna_test() public returns (bool) {
      return true;
  }
}

If Echidna uses this config.yaml, it can call all functions and won't easily find transactions with high gas cost:

$ echidna-test 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

That's because the cost depends on the size of addrs and random calls tend to leave the array almost empty. Blacklisting pop and clear, however, gives us much better results (../example/blacklistpushpop.yaml):

estimateGas: true
filterBlacklist: true
filterFunctions: ["C.pop()", "C.clear()"]
$ echidna-test pushpop.sol --config config.yaml
...
push used a maximum of 40839 gas
...
check used a maximum of 1484472 gas

Summary: Finding transactions with high gas consumption

Echidna can find transactions with high gas consumption using the estimateGas configuration option:

estimateGas: true
$ echidna-test contract.sol --config config.yaml 
...

Once the fuzzing campaign is over, Echidna will report a sequence with the maximum gas consumption for every function.

Smart contract Fuzzing at scale with Echidna

In this tutorial we will review how we can create a dedicated server for fuzzing smart contracts using Echidna.

Workflow:

  1. Install and setup a dedicated server
  2. Start a short fuzzing campaign
  3. Start a continuous fuzzing campaign
  4. Add properties, check coverage and modify the code if necessary
  5. Ending the campaign

1. Install and setup a dedicated server

First, obtain a dedicated server with at least 32 GB of RAM and as many cores as possible. Start by creating a user for the fuzzing campaign. Only use the root acount to create an unprivileged user:

# adduser echidna
# usermod -aG sudo echidna

Then, using that user (echidna), install some basic dependencies:

$ sudo apt install unzip python3-pip

Then install everything necessary to build your smart contract(s) as well slither and echidna-parade. For instance:

$ pip3 install solc-select
$ solc-select install all
$ pip3 install slither_analyzer
$ pip3 install echidna_parade

Add $PATH=$PATH:/home/echidna/.local/bin at the end of /home/echidna/.bashrc

Next, install Echidna. The easiest way is to download the latest precompiled Echidna release, uncompress it, and move it to /home/echidna/.local/bin:

$ wget "https://github.com/crytic/echidna/releases/download/v2.0.0/echidna-test-2.0.0-Ubuntu-18.04.tar.gz"
$ tar -xf echidna-test-2.0.0-Ubuntu-18.04.tar.gz
$ mv echidna-test /home/echidna/.local/bin

2. Start a short fuzzing campaign

Select a contract to test and provide an initialization if needed. It does not have to be perfect, just start with some basic stuff and iterate over the result. Before starting this campaign, modify your echidna config to define a corpus directory to use. For instance:

corpusDir: "corpus-exploration"

This directory will be automatically created but since we are starting a new campaign, please remove the corpus directory if it was created by a previous echidna campaign. If you don't have any properties to test, you can use:

testMode: exploration

to allow Echidna to run without any properties.

We will start a very short Echidna run (5 minutes), to check that everything looks fine. To do that, use the following config:

testLimit: 100000000000
timeout: 300 # 5 minutes

After it runs, check the coverage file, located in corpus-exploration/covered.*.txt. If the initialization is wrong, clean the corpus-exploration directory and restart the campaign.

3. Starting a continuous fuzzing campaign

When you are satisfied with the first iteration of the initialization, we can start a "continuous campaign" for exploration and testing using echidna-parade. Before starting, double check your config file. For instance, if you added properties, do not forget to remove benchmarkMode.

echidna-parade is a tool is used to launch multiple Echidna instances at the same time, keeping track of the corpora of each one. Each instance will be configured to run for a certain amount of time, with different parameters, in order to maximize the chance to reach new code.

We will show it with an example, where:

  • the initial corpus is empty
  • the base config file will be exploration.yaml
  • the time to run the initial instance will be 3600 seconds (1 hour)
  • the time to run each "generation" will be 1800 seconds (30 minutes)
  • the campaign will run in continuous mode (if timeout is -1, it means run forever)
  • the number of Echidna instances per generation will be 8. This should be adjusted according to the number of available cores, but avoid using all your cores if you don't want to kill your server
  • the target contract is named C
  • the file that contains the contract is test.sol

Finally, we will log the stdout and stderr in parade.log and parade.err and fork the process to let it run forever.

$ echidna-parade test.sol --config exploration.yaml --initial_time 3600 --gen_time 1800 --timeout -1 --ncores 8 --contract C > parade.log 2> parade.err &

After you run this command, exit the shell so you won't kill it accidentally if your connection fails.

4. Add more properties, check coverage and modify the code if necessary

In this step, we can add more properties while Echidna is exploring the contracts. Keep in mind that you should avoid changing the ABI of the contracts (otherwise the quality of the corpus will degrade).

Also, we can tweak the code to improve coverage, but before starting that, we need to know how to monitor our fuzzing campaign. We can use this command:

$ watch "grep 'COLLECTING NEW COVERAGE' parade.log | tail -n 30"

When new coverage is found, you will see something like this:

COLLECTING NEW COVERAGE: parade.181140/gen.30.10/corpus/coverage/-3538310549422809236.txt
COLLECTING NEW COVERAGE: parade.181140/gen.35.9/corpus/coverage/5960152130200926175.txt
COLLECTING NEW COVERAGE: parade.181140/gen.35.10/corpus/coverage/3416698846701985227.txt
COLLECTING NEW COVERAGE: parade.181140/gen.36.6/corpus/coverage/-3997334938716772896.txt
COLLECTING NEW COVERAGE: parade.181140/gen.37.7/corpus/coverage/323061126212903141.txt
COLLECTING NEW COVERAGE: parade.181140/gen.37.6/corpus/coverage/6733481703877290093.txt

you can verify the corresponding covered file, such as parade.181140/gen.37.6/corpus/covered.1615497368.txt.

For examples on how to help Echidna to improve its coverage, please review the improving coverage tutorial.

To monitor failed properties, use this command:

$ watch "grep 'FAIL' parade.log | tail -n 30"

When failed properties are found, you will see something like this:

NEW FAILURE: assertion in f: failed!💥
parade.181140/gen.179.0 FAILED
parade.181140/gen.179.3 FAILED
parade.181140/gen.180.2 FAILED
parade.181140/gen.180.4 FAILED
parade.181140/gen.180.3 FAILED
...

4. Ending the campaign

When you are satisfied with the coverage results, you can terminate the continuous campaign using:

$ killall echidna-parade echidna

How to test bytecode only contracts

Table of contents:

Introduction

We will see how to fuzz a contract without any provided source code. The technique can also be used to perform differential fuzzing (i.e. compare multiple implementations) between a Solidity contract and a Vyper contract.

Consider the following bytecode:

608060405234801561001057600080fd5b506103e86000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055506103e86001819055506101fa8061006e6000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c806318160ddd1461004657806370a0823114610064578063a9059cbb146100bc575b600080fd5b61004e61010a565b6040518082815260200191505060405180910390f35b6100a66004803603602081101561007a57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610110565b6040518082815260200191505060405180910390f35b610108600480360360408110156100d257600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610128565b005b60015481565b60006020528060005260406000206000915090505481565b806000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540392505081905550806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540192505081905550505056fe

For which we only know the ABI:

interface Target{
  function totalSupply() external returns(uint);
  function balanceOf(address) external returns(uint);
  function transfer(address, uint) external;
}

We want to test if it is possible to have more tokens than the total supply.

Proxy pattern

Because we don't have the source code, we cannot directly add the property in the contract. Instead we will use a proxy contract:

interface Target{
  function totalSupply() external returns(uint);
  function balanceOf(address) external returns(uint);
  function transfer(address, uint) external;
}

contract TestBytecodeOnly{
    Target t;

    constructor() public{
        address target_addr;
        // init bytecode
        bytes memory target_bytecode = hex"608060405234801561001057600080fd5b506103e86000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055506103e86001819055506101fa8061006e6000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c806318160ddd1461004657806370a0823114610064578063a9059cbb146100bc575b600080fd5b61004e61010a565b6040518082815260200191505060405180910390f35b6100a66004803603602081101561007a57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610110565b6040518082815260200191505060405180910390f35b610108600480360360408110156100d257600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610128565b005b60015481565b60006020528060005260406000206000915090505481565b806000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540392505081905550806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540192505081905550505056fe";

        uint size = target_bytecode.length;

        assembly{
            target_addr := create(0, 0xa0, size) // 0xa0 was manually computed. It might require different value according to the compiler version
        }
        t = Target(target_addr);
    }

    function transfer(address to, uint amount) public {
        t.transfer(to, amount);
    }

    function echidna_test_balance() public returns(bool){
        return t.balanceOf(address(this)) <= t.totalSupply();
    }
}

The proxy:

  • Deploys the bytecode in its constructor
  • Has one function that will call the target's transfer function
  • Has one Echidna property t.balanceOf(address(this)) <= t.totalSupply()

Run Echidna

$ echidna-test bytecode_only.sol --contract TestBytecodeOnly
echidna_test_balance: failed!💥  
  Call sequence:
    transfer(0x0,1002)

Here Echidna found that by calling transfer(0, 1002) anyone can mint tokens.

Target source code

The actual source code of the target is:

contract C{
    mapping(address => uint) public balanceOf;
    uint public totalSupply;

    constructor() public{
        balanceOf[msg.sender] = 1000;
        totalSupply = 1000;
    }

    function transfer(address to, uint amount) public{
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
    }
}

Echidna correctly found the bug: lack of overflow checks in transfer.

Differential fuzzing

Consider the following Vyper and Solidity contracts:

@view
@external
def my_func(a: uint256, b: uint256, c: uint256) -> uint256:
    return a * b / c
contract SolidityVersion{
    function my_func(uint a, uint b, uint c) view public{
        return a * b / c;
    }
}

We can test that they return always the same values using the proxy pattern:

interface Target{
  function my_func(uint, uint, uint) external returns(uint);
}

contract SolidityVersion{
    Target t;

    constructor() public{
        address target_addr;

        // vyper bytecode
        bytes memory target_bytecode = hex"61007756341561000a57600080fd5b60043610156100185761006d565b600035601c52630ff198a3600051141561006c57600435602435808202821582848304141761004657600080fd5b80905090509050604435808061005b57600080fd5b82049050905060005260206000f350005b5b60006000fd5b61000461007703610004600039610004610077036000f3";

        uint size = target_bytecode.length;

        assembly{
            target_addr := create(0, 0xa0, size) // 0xa0 was manually computed. It might require different value according to the compiler version
        }
        t = Target(target_addr);
    }

    function test(uint a, uint b, uint c) public returns(bool){
        assert(my_func(a, b, c) == t.my_func(a, b, c));
    }

    function my_func(uint a, uint b, uint c) view internal returns(uint){
        return a * b / c;
    }
}

Here we run Echidna with the assertion mode:

$ echidna-test  vyper.sol --config config.yaml --contract SolidityVersion --test-mode assertion
assertion in test: passed! 🎉

Generic Proxy code

Adapt the following code to your needs:

interface Target{
  // public/external functions
}

contract TestBytecodeOnly{
    Target t;

    constructor() public{
        address target_addr;
        // init bytecode
        bytes memory target_bytecode = hex"";

        uint size = target_bytecode.length;

        assembly{
            target_addr := create(0, 0xa0, size) // 0xa0 was manually computed. It might require different value according to the compiler version
        }
        t = Target(target_addr);
    }

    // Add helper functions to call the target's functions from the proxy

    function echidna_test() public returns(bool){
      // The property to test
    }
}

Summary: Testing contracts without source code

Echidna can fuzz contracts without source code using a proxy contract. This technique can be also used to compare implementations written in Solidity and Vyper.

Using HEVM Cheats To Test Permit

Introduction

EIP 2612 introduces the function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) to the ERC20 abi. This function essentially takes in signature parameters generated through ECDSA combined with the EIP 712 standard for typed data hashing, and recovers the author of the signature through ecrecover(). It then sets allowances[owner][spender] to value.

Uses

This is a new way of allocating allowances, as signatures can be computed offchain and passed to a contract. This allows a relayer to pay the entire gas fee of the permit transaction in exchange for a fee and thus allows completely gasless transactions for a user. Furthermore this removes the typical approve() -> transferFrom() pattern that forces users to send two transactions instead of just one via this new method.

Do note that for the permit function to work, a valid signature is needed. This example will show how we can use hevm's sign cheatcode to sign data with a private key. More generally, you can use this cheatcode to test anything that requires valid signatures.

Example

We use solmate’s implementation of the ERC20 standard that includes the permit function. Notice that there are also values for the PERMIT_TYPEHASH and a mapping(address -> uint256) public nonces , the former is a part of the EIP712 standard and the latter is used to prevent signature replay attacks.

In our TestDepositWithPermit contract, we need to have the signature signed by an owner for validation. To do this, we can use hevm’s sign cheatcode, which takes in a message and a private key and creates a valid signature. For this example, we use the private key 0x02 and the following signed message, which essentially represents the permit signature following EIP 712:

    keccak256(
        abi.encodePacked(
                "\x19\x01",
                asset.DOMAIN_SEPARATOR(),
                keccak256(
                    abi.encode(
                        keccak256(
                            "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
                        ),
                        owner,
                        spender,
                        assetAmount,
                        asset.nonces(owner),
                        block.timestamp
                    )
                )
            )
        );

The helper function getSignature(address owner, address spender, uint256 assetAmount) returns a valid signature generated via the sign cheatcode. Note that the sign cheatcode leaks the private key and thus it is best to use dummy keys when testing. Our keypair data was taken from this site. Now to test out the signature, we will mint a random amount to the OWNER address, the address corresponding to private key 0x02, which was the signer of the permit signature. We then see if we can use that signature to transfer the owner’s tokens to ourselves.

First we will call permit() on our Mock ERC20 token with the signature generated in getSignature(), and then call transferFrom(). If our permit request and transfer was successful, our balance of the mock ERC20 should be increased by the amount permitted and OWNER's balance should decrease as well. For simplicity, we'll transfer all the minted tokens so that OWNER's balance should be 0, and our balance should be amount.

Code

The full example code can be found here.

End-to-End testing with Echidna (Part I)

When smart contracts require a complex initialization and the time to do it is short, we want to avoid manually recreating a deployment for a fuzzing campaign with Echidna. That's why we have a new approach for testing using Echidna based on the deployments and execution of tests directly from ganache.

Requirements:

This approach needs a smart contract project, with the following constraints:

  • It should use Solidity: Vyper is not supported, since Slither/Echidna is not very effective at running these (e.g. no AST is included).
  • It should have tests or at least, a complete deployment script.
  • It should work with Slither. If it fails, please report the issue.

For this tutorial, we used the drizzle-box example.

Getting started:

Before stating, make sure you have the latest releases from Echidna and Etheno installed.

Then, install the packages to compile the project:

$ git clone https://github.com/truffle-box/drizzle-box
$ cd drizzle-box
$ npm i truffle

If ganache is not installed, add it manually. In our example, we will run:

$ npm -g i ganache

Other projects using yarn will require:

$ yarn global add ganache

Ensure that $ ganache --version outputs ganache v7.3.2 or greater.

It is also important to select one test script from the available tests. Ideally, this test will deploy all (or most) contracts, including mock/test ones. For this example, we are going to take a look to the SimpleStorage contract:

contract SimpleStorage {
    event StorageSet(string _message);

    uint256 public storedData;

    function set(uint256 x) public {
        storedData = x;

        emit StorageSet("Data stored successfully!");
    }
}

This small contract allows to set the storedData state variable. As expected, we have a unit test that deploys and tests this contract (simplestorage.js):

const SimpleStorage = artifacts.require("SimpleStorage");

contract("SimpleStorage", accounts => {
  it("...should store the value 89.", async () => {
    const simpleStorageInstance = await SimpleStorage.deployed();

    // Set value of 89
    await simpleStorageInstance.set(89, { from: accounts[0] });

    // Get stored value
    const storedData = await simpleStorageInstance.storedData.call();

    assert.equal(storedData, 89, "The value 89 was not stored.");
  });
});

Capturing transactions

Before starting to write interesting properties, it is necessary to to collect an Etheno trace to replay it inside Echidna:

First, start Etheno:

$ etheno --ganache --ganache-args="--miner.blockGasLimit 10000000" -x init.json

By default the following Ganache arguments are set via Etheno:

  • -d: Ganache will use a pre-defined, deterministic seed to create all accounts.
  • --chain.allowUnlimitedContractSize: Allows unlimited contract sizes while debugging. This is set so that there is no size limitations on the contracts that are going to be deployed
  • -p <port_num>: The port_num will be set to (1) the value of --ganache-port or (2) Etheno will choose the smallest port number higher than the port number on which Etheno’s JSON RPC server is running.

NOTE: If you are using Docker to run etheno, the commands should be:

$ docker run -it -p 8545:8545 -v ~/etheno:/home/etheno/ trailofbits/etheno
(you will now be working within the Docker instance)
$ etheno --ganache --ganache-args="--miner.blockGasLimit 10000000" -x init.json
  • The -p in the first command publishes (i.e. exposes) port 8545 from inside the Docker container out to port 8545 on the host.
  • The -v in the first command maps a directory from inside the Docker container to one outside the Docker container. After Etheno exits, the init.json file will now be in the ~/etheno folder on the host.

Note that if the deployment fails to complete successfully due to a ProviderError: exceeds block gas limit exception, increasing the --miner.blockGasLimit value can help. This is especially helpful for large contract deployments. Learn more about the various Ganache command-line arguments that can be set by clicking here.

Additionally, if Etheno fails to produce any output, then it failed to execute ganache under-the-hood. Check if ganache (with the associated command-line arguments) can be executed correctly from your terminal without the use of Etheno.

Meanwhile, in another terminal, run one test or the deployment process. How to run it depends on how the project was developed. For instance, for truffle, use:

$ truffle test test/test.js

for buidler:

$ buidler test test/test.js --network localhost

In the Drizzle example, we will run:

$ truffle test test/simplestorage.js --network develop.

After Etheno finishes, gently kill it by using Ctrl+c (or Command+C on Mac). It will save the init.json file. If your test fails for some reason or you want to run a different one, restart Etheno and re-run the test.

Writing and running a property

Once we have a json file with saved transactions, we can verify that the SimpleStorage contract is deployed at 0x871DD7C2B4b25E1Aa18728e9D5f2Af4C4e431f5c, so we can easily write a contract (./contracts/crytic/E2E.sol) with a simple a property to test it:

import "../SimpleStorage.sol";

contract E2E {
        SimpleStorage st = SimpleStorage(0x871DD7C2B4b25E1Aa18728e9D5f2Af4C4e431f5c);
        function crytic_const_storage() public returns(bool) {
            return st.storedData() == 89;
        }
}

For large, multi-contract deployments, using console.log to print out the deployed, contract addresses can be valuable in quickly setting up the Echidna testing contract.

This simple property checks if the stored data remains constant. To run it you will need the following echidna config file (echidna.yaml):

prefix: crytic_
initialize: init.json
multi-abi: true
cryticArgs: ['--truffle-build-directory', 'app/src/contracts/'] # needed by drizzle

Then, running Echidna shows the results immediately:

$ echidna-test . --contract E2E --config echidna.yaml
...
crytic_const_storage: failed!💥  
  Call sequence:
    (0x871dd7c2b4b25e1aa18728e9d5f2af4c4e431f5c).set(0) from: 0x0000000000000000000000000000000000010000

For this last step, make sure you are using . as a target for echidna-test. If you use the path to the E2E.sol file instead, then Echidna will not be able get information from all the deployed contracts to call the set(uint256) function and the property will never fail.

Key considerations:

When using Etheno with Echidna, note that there are two edge cases that may cause unexpected behavior:

  1. Function calls that use ether: The accounts that are created and used for testing in Ganache are not the same as the accounts that are used to send transactions in Echidna. Thus, the account balances of the Ganache accounts do not carry over to the accounts used by Echidna. So, if there is a function call logged by Etheno that requires the transfer of some ether from an account that exists in Ganache, this call will fail in Echidna.
  2. Fuzz tests that rely on block.timestamp: The concept of time is different between Ganache and Echidna. Echidna always starts with a fixed timestamp while Etheno will use Ganache's concept of time. This means that assertions or requirements in a fuzz test that rely on timestamp comparisons / evaluations may fail in Echidna.

In the next part of this tutorial, we will explore how to easily find where contracts are deployed with a specific tool based on Slither. This will be useful if the deployment process is complex and we need to test an specific contract.

Understanding and using multi-abi in Echidna

Table of contents:

Introduction

This tutorial is written as a hands-on guide to using multi-abi testing in Echidna. You will learn what multi-abi testing is, how to use it in your tests, and what to expect from its usage.

What is multi-abi testing?

It is a testing mode that allows Echidna to call functions from any contract not directly under test. The ABI for the contract must be known, and it must have been deployed by the contract under test.

When and how to use multi-abi

By default, Echidna calls functions from the contract to be analyzed, sending the transactions randomly from addresses 0x10000, 0x20000 and 0x30000.

In some systems, the user has to interact with other contracts prior to calling a function on the fuzzed contract. A common example is when you want to provide liquidity to a DeFi protocol, you will first need to approve the protocol for spending your tokens. This transaction has to be initiated from your account before actually interacting with the protocol contract.

A fuzzing campaign meant to test this example protocol contract won't be able to modify users allowances, therefore most of the interactions with the protocol won't be tested correctly.

This is where multi-abi testing is useful: It allows Echidna to call functions from other contracts (not just from the contract under test), sending the transactions from the same accounts that will interact with the target contract.

Run Echidna

We will use a simple example to show how multi-abi works. We will be using two contracts, Flag and EchidnaTest, both available in ../example/multiabi.sol.

The Flag contract contains a boolean flag that is only set if flip() is called, and a getter function that returns the value of the flag. For now, ignore test_fail(), we will talk about this function later.

contract Flag {

   bool flag = false;

   function flip() public {
       flag = !flag;
   }

   function get() public returns (bool) {
        return flag;
   }

   function test_fail() public {
       assert(false);
   }
}

The test harness will instantiate a new Flag, and the invariant under test will be that f.get() (that is, the boolean value of the flag) is always false.

contract EchidnaTest {
   Flag f;

   constructor() {
      f = new Flag();
   }

   function test_flag_is_false() public {
      assert(f.get() == false);
   }

}

In a non multi-abi fuzzing campaign, Echidna is not able to break the invariant, because it only interacts with EchidnaTest functions. However, if we use the following configuration file, enabling multi-abi testing, the invariant is broken. You can access ../example/multiabi.yaml here.

testMode: assertion
testLimit: 50000
multi-abi: true

To run the Echidna tests, run echidna-test multiabi.sol --contract EchidnaTest --config multiabi.yaml from the example directory. Alternatively, you can specify --multi-abi in the command line instead of using a configuration file.

Example run with multi-abi set to false

$ echidna-test multiabi.sol --contract EchidnaTest --config multiabi.yaml 
Analyzing contract: building-secure-contracts/program-analysis/echidna/example/multiabi.sol:EchidnaTest
test_flag_is_false():  passed! 🎉
AssertionFailed(..):  passed! 🎉

Unique instructions: 282
Unique codehashes: 2
Corpus size: 2
Seed: -8252538430849362039

Example run with multi-abi set to true

$ echidna-test multiabi.sol --contract EchidnaTest --config multiabi.yaml 
Analyzing contract: building-secure-contracts/program-analysis/echidna/example/multiabi.sol:EchidnaTest
test_flag_is_false(): failed!💥  
  Call sequence:
    flip()
    flip()
    flip()
    test_flag_is_false()

Event sequence: Panic(1)
AssertionFailed(..):  passed! 🎉

Unique instructions: 368
Unique codehashes: 2
Corpus size: 6
Seed: -6168343983565830424

Use cases and conclusions

Testing with multi-abi is a useful tool for complex systems that require the user to interact with more than one contract, as we mentioned earlier. Another use case is for deployed contracts that require interactions to be initiated by specific addresses: for those, specifying the sender configuration setting allows to send the transactions from the correct account.

A side-effect of using multi-abi is that the search space grows with the number of functions that can be called. This, combined with high values of sequence lengths, can make the fuzzing test not so thorough, because the dimension of the search space is simply too big to reasonably explore. Finally, adding more functions as fuzzing candidates makes the campaigns to take up more execution time.

A final remark is that multi-abi testing in assertion mode ignores all assert failures from the contracts not under test. This is shown in Flag.test_fail() function: even though it explicitly asserts false, the Echidna test ignores it.

Fuzzing Tips

The following describe fuzzing tips to make Echidna more efficient:

Filtering inputs

To filter inputs, % is more efficient than adding require or if statements. For example, if you are a fuzzing a operation(uint256 index, ..) where index is supposed to be less than 10**18, use:

function operation(uint index, ...) public{
   index = index % 10**18
   ...
}

If require(index <= 10**18) is used instead, many transactions generated will revert, slowing the fuzzer.

This can also be generalized define a min and max range, for example:

function operation(uint balance, ...) public{
   balance = MIN_BALANCE + balance % (MAX_BALANCE - MIN_BALANCE);
   ...
}

Will ensure that balance is always between MIN_BALANCE and MAX_BALANCE, without discarding any generated transactions. As expected, this will speed up the exploration, but at the cost of avoiding certain paths in your code. To overcome this issue, the usual solution is to have two functions:

function operation(uint balance, ...) public{
   ... // original code
}

function safeOperation(uint balance, ...) public{
   balance = MIN_BALANCE + balance % (MAX_BALANCE - MIN_BALANCE); // safe balance
   ...
}

So Echidna is free to use any of these, exploring safe and unsafe usage of the input data.

Dealing with dynamic arrays

When a dynamic array is used as input, Echidna will limit the size of it to 32 elements:

function operation(uint256[] data, ...) public{
   ... // use of data
}

This is because deserializing dynamic arrays is slow and can take some amount of memory during the execution. Dynamic arrays are also problematic since they are hard to mutate. Despite this, Echidna includes some specific mutators to remove/repeat elements or cut elements. These mutators are performed using the collected corpus. In general, we suggest the use of push(..) and pop() functions to handle the exploration of dynamic arrays used as inputs:

private uint256[] data;
function push(uint256 x) public{
   data.push(x);
}

function pop() public{
   data.pop();
}

function operation(...) public{
   ... // use of data
}

This will work well to test arrays with a small amount of elements; however, it introduces an unexpected bias in the exploration: since push an pop are functions that will be selected with equal probability, the chance of building large arrays (e.g. more than 64 elements) is very very small. One quick solution could be to blacklist the pop() function during a short campaign:

filterFunctions: ["C.pop()"]

This is enough for small scale testing. A more general solution is available using a specific testing technique called swarm testing. This allows to run a long testing campaign with some tool but randomly shuffling the configuration of it. In case of Echidna, swarm testing runs with different config files, where it blacklists some number of random functions from the contract before testing. We offer swarm testing and scalability with echidna-parade, our dedicated tool for fuzzing smart contracts. A specific tutorial in the use of echidna-parade is available here.

Frequently asked questions about Echidna

This list contains answers to frequent questions related with the usage of Echidna. If this looks too difficult to understand for you, you need to make sure you already reviewed all the other Echidna documentation.

Echidna fails to start or compile my contract, what should I do?

Start testing if crytic-compile can compile your contracts. If you are using a compilation framework such as truffle or hardhat, use:

crytic-compile .

And check for any errors. If there is an unexpected error, please report it in the crytic-compile issue tracker.

If crytic-compile works fine, test slither to see if there is any issues with the information that this tool extract for running Echidna. Again, if you are using a compilation framework, use:

slither . --print echidna

If that command works correctly, it should print a json file with some information from your contracts, otherwise, report any error in the slither issue tracker. If everything here works, but Echidna still fails, please open an issue in our issue tracker or ask in the #ethereum channel of the EmpireHacking slack.

How long should I run Echidna?

Echidna uses fuzzing testing which runs for a fixed amount of transactions (or a global timeout). Users should specify a suitable number of transactions or a timeout (in seconds), depending on the amount of resources available for a fuzzing campaign and the complexity of the code. Determining the best amount of time to run a fuzzer is still an open research question, however, monitoring the code coverage of your smart contracts can be a good way to determinate if the fuzzing campaign should be extended.

Why has Echidna not implemented fuzzing of smart contract constructors with parameters?

Echidna is focused on security testing during audits. When we perform testing, we adjust the fuzzing campaign to test with a limited number of possible constructor parameters (normally, the ones that are going to be used for the real deployment). We are not focused on issues that depend on alternative deployments that should not happen. On top of that, redeploying contracts during the fuzzing campaign has a performance impact and the sequences of transactions that we collect in the corpus could be more difficult (or even impossible) to reuse and mutate in different contexts.

How does Echidna know which sequence of transactions should be added into the corpus?

Echidna starts generating a sequence with a number of transactions to execute. It will execute them one by one, collecting coverage information for every transaction. If a transaction adds new coverage, then the complete sequence (up to that transaction), will be added into the corpus.

How is coverage information used?

Coverage information is used to determine if a sequence of transactions has reached a new program state and added into the internal corpus.

What is coverage information exactly?

Coverage is a combination of the following information:

  • Echidna reached a certain program counter in a given contract.
  • The execution ended, either with stop, revert or a variety of errors (e.g. assertion failed, out of gas, not enough ether balance, etc)
  • The number of EVM frames when the execution ended (in other words, how deep ends the execution in terms of internal transactions)

How is the corpus used?

The corpus is used as the primary source of transactions to replay and mutate during a fuzzing campaign. The probability of using a sequence of transactions to replay and mutate is directly proportional to the number of transactions needed to add it into the corpus. In other words, more rare sequences are replayed and mutated more often during a fuzzing campaign.

When a new sequence of transactions is added into the corpus, does this mean that a new line of code is always reached?

Not always, it means we reached some program state given our coverage definition.

Why not use coverage per individual transaction, instead of per sequence of transactions?

Coverage per isolated transaction will be possible, however, it is incomplete vision of the coverage since some code requires previous transaction to reach some specific lines.

How to know which type of testing should be used? (boolean properties, assertions, etc)

Check the tutorial on selecting the right test mode

Why does Echidna return “Property X failed with no transactions made” when running one or more tests?

Before starting a fuzzing campaign, Echidna tests the properties with no transactions at all to check if they fail. In that case, a property can be detected to fail in the initial state (after the contract is deployed). You should check that the property is correct to know why it fails without any transactions.

How can I know how a property or assertion failed?

Echidna indicates the cause of a failed test in the UI. For instance, if a boolean property X fails because of a revert, Echidna will show “property X FAILED! with ErrorRevert”, otherwise, it should show “property X FAILED! with ErrorReturnFalse”. Assertion can only fail because with “ErrorUnrecognizedOpcode”, which is how Solidity implements assertions in EVM.

How can I know exactly where and how property or assertion failed?

Events are an easy way to output values from the EVM. You can use them to get information in the code that has the failed property or assertion. Only the events of transaction triggering the failure will be shown (this will be improved in the near future). Also, events are collected and displayed, even if the transaction reverted (despite the Yellow Paper states that the event log should be cleaned).

Another way to see where an assertion failed is using the coverage information. This requires to enable the corpus collection (e.g. --corpus-dir X) and check the coverage.*.txt file to see something like this:

*e  |   function test(int x, address y, address z) public {
*e  |     require(x > 0 || x <= 0);
*e  |     assert(z != address(0x0));
*   |     assert(y != z);
*   |     state = x;
    |   }

The e marker indicates that Echidna collected a trace that ends with an assertion failure. As we can see, the path ends in the assert, so it should fail there.

Why coverage information seems incorrect or incomplete?

Coverage mappings can be imprecesie, however, if it fails completely, it could be that you are using the viaIR optimization option, which seems to have some unexpected impact on the solc maps that we are still investigating. As a workaround, disable viaIR.

Echidna crashes showing NonEmpty.fromList: empty list

Echidna relies on the Solidity metadata to detect where each contract is deployed. Please do not disable it. If this is not the case, please open an issue in our issue tracker.

Echidna stopped working for some reason. How can I debug it?

Use --format text and open an issue with the error you see in your console or ask in the #ethereum channel at the EmpireHacking slack.

I am not getting expected results from Echidna tests. What can I do?

Sometimes it is useful to create small properties or assertions to test that the tool executed them correctly. For instance, for property mode:

    function echidna_test() public returns (bool) {
       return false;
    }

And for assertion mode:

    function test_assert_false() public {
       assert(false); 
    }

If these are not failing, please open an issue so we can take a look.

Exercises

Exercise 1

Table of contents:

Join the team on Slack at: https://empireslacking.herokuapp.com/ #ethereum

Targeted contract

We will test the following contract ./exercise1/token.sol:

 contract Ownership{
    address owner = msg.sender;
    function Owner(){
         owner = msg.sender;
     }
     modifier isOwner(){
         require(owner == msg.sender);
         _;
      }
   }

  contract Pausable is Ownership{
     bool is_paused;
     modifier ifNotPaused(){
              require(!is_paused);
              _;
      }

      function paused() isOwner public{
          is_paused = true;
      }

      function resume() isOwner public{
          is_paused = false;
      }
   }

   contract Token is Pausable{
      mapping(address => uint) public balances;
      function transfer(address to, uint value) ifNotPaused public{
           balances[msg.sender] -= value;
           balances[to] += value;
       }
    }

Testing a token balance

Goals

  • Add a property to check that echidna_caller cannot have more than an initial balance of 10000.
  • Once Echidna finds the bug, fix the issue, and re-check your property with Echidna.

The skeleton for this exercise is (./exercise1/template.sol):

     import "token.sol";
     contract TestToken is Token {
       address echidna_caller = msg.sender;

        constructor() public{
            balances[echidna_caller] = 10000;
         }
         // add the property
        function echidna_test_balance() public view returns (bool) {}
      }

Solution

This solution can be found in ./exercise1/solution.sol

Exercise 2

This exercise requires to finish the exercise 1.

Table of contents:

Join the team on Slack at: https://empireslacking.herokuapp.com/ #ethereum

Targeted contract

We will test the following contract ./exercise2/token.sol:

 contract Ownership{
    address owner = msg.sender;
    function Owner() public {
         owner = msg.sender;
     }
     modifier isOwner(){
         require(owner == msg.sender);
         _;
      }
   }

  contract Pausable is Ownership{
     bool is_paused;
     modifier ifNotPaused(){
               require(!is_paused);
               _;
      }

      function paused() isOwner public{
          is_paused = true;
      }

      function resume() isOwner public{
          is_paused = false;
      }
   }

   contract Token is Pausable{
      mapping(address => uint) public balances;
      function transfer(address to, uint value) ifNotPaused public{
           balances[msg.sender] -= value;
           balances[to] += value;
       }
    }

Testing access control

Goals

  • Consider paused() to be called at deployment, and the ownership removed.
  • Add a property to check that the contract cannot be unpaused.
  • Once Echidna finds the bug, fix the issue, and re-try your property with Echidna.

The skeleton for this exercise is (./exercise2/template.sol):

   import "token.sol";
   contract TestToken is Token {
      address echidna_caller = msg.sender;
      constructor(){
         paused(); // pause the contract
         owner = 0x0; // lose ownership
       }
      // add the property
      function echidna_no_transfer() public view returns (bool) {}
   }

Solution

This solution can be found in ./exercise2/solution.sol

Exercise 3

This exercise requires to finish exercise 1 and exercise 2

Table of contents:

Join the team on Slack at: https://empireslacking.herokuapp.com/ #ethereum

Targeted contract

We will test the following contract ./exercise3/token.sol:

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

  contract Pausable is Ownership{
     bool is_paused;
     modifier ifNotPaused(){
          require(!is_paused);
          _;
      }

      function paused() isOwner public{
          is_paused = true;
      }

      function resume() isOwner public{
          is_paused = false;
      }
   }

   contract Token is Pausable{
      mapping(address => uint) public balances;
      function transfer(address to, uint value) ifNotPaused public{
            require(balances[msg.sender] >= value);
            balances[msg.sender] -= value;
            balances[to] += value;
       }
    }

Testing with custom initialization

Consider the following extension of the token (./exercise3/mintable.sol):

   import "token.sol";
   contract MintableToken is Token{
      int totalMinted;
      int totalMintable;

      constructor(int _totalMintable) public {
         totalMintable = _totalMintable;
      }

      function mint(uint value) isOwner() public{
          require(int(value) + totalMinted < totalMintable);
          totalMinted += int(value);
          balances[msg.sender] += value;
       }
    }

The version of token.sol contains the fixes of the previous exercises.

Goals

  • Create a scenario, where echidna_caller (msg.sender) becomes the owner of the contract at construction, and totalMintable is set to 10,000. Recall that Echidna needs a constructor without argument.
  • Add a property to check if echidna_caller can mint more than 10,000 tokens.
  • Once Echidna finds the bug, fix the issue, and re-try your property with Echidna.

The skeleton for this exercise is (./exercise3/template.sol):

   import "mintable.sol";
   contract TestToken is MintableToken {
      address echidna_caller = msg.sender;

      // update the constructor
      constructor() public {}

      // add the property
      function echidna_test_balance() public view returns (bool) {}
   }

Solution

This solution can be found in ./exercise3/solution.sol

Exercise 4

Table of contents:

This exercise is based on the tutorial How to test assertions.

Join the team on Slack at: https://empireslacking.herokuapp.com/ #ethereum

Targeted contract

We will test the following contract ./exercise4/token.sol:

 contract Ownership{
    address owner = msg.sender;
    function Owner() public{
         owner = msg.sender;
     }
     modifier isOwner(){
         require(owner == msg.sender);
         _;
      }
   }

  contract Pausable is Ownership{
     bool is_paused;
     modifier ifNotPaused(){
              require(!is_paused);
              _;
      }

      function paused() isOwner public{
          is_paused = true;
      }

      function resume() isOwner public{
          is_paused = false;
      }
   }

   contract Token is Pausable{
      mapping(address => uint) public balances;
      function transfer(address to, uint value) ifNotPaused public{
           balances[msg.sender] -= value;
           balances[to] += value;
       }
    }

Exercise

Goals

Add asserts to ensure that after calling transfer:

  • msg.sender must have its initial balance or less.
  • to must have its initial balance or more.

Once Echidna finds the bug, fix the issue, and re-try your assertion with Echidna.

This exercise is similar to the first one, but using assertions instead of explicit properties.
However, in this exercise, it is easier to modify the original token contract (./exercise4/token.sol).

Solution

This solution can be found in ./exercise4/solution.sol

Exercise 5

Table of contents:

Join the team on Slack at: https://empireslacking.herokuapp.com/ #ethereum

Setup

  1. Clone the repo: git clone https://github.com/crytic/damn-vulnerable-defi-echidna
  2. install the dependencies via yarn install.

Context

The challenge is described here: https://www.damnvulnerabledefi.xyz/challenges/2.html, we assume that the reader is familiar with it.

Goals

  • Setup the testing environment with the right contracts and necessary balances.
  • Analyze the before function in test/naive-receiver/naive-receiver.challenge.js to identify what initial setup needs to be done.
  • Add a property to check whether the balance of the FlashLoanReceiver contract can change.
  • Create a config.yaml with the necessary configuration option(s).
  • Once Echidna finds the bug, fix the issue, and re-try your property with Echidna.

Only the following contracts are relevant:

  • contracts/naive-receiver/FlashLoanReceiver.sol
  • contracts/naive-receiver/NaiveReceiverLenderPool.sol

Hints

We recommend to first try without reading the following hints. The hints are in the hints branch.

Solution

This solution can be found in solutions branch.

Solution Explained (spoilers ahead)

The goal of the naive receiver challenge is to realize that an arbitrary user can call request a flash loan for FlashLoanReceiver. In fact, this can be done even if the arbitrary user has no ether.

Echidna found this by simply calling NaiveReceiverLenderPool.flashLoan() with the address of FlashLoanReceiver and any arbitrary amount.

See example output below from Echidna:

$ echidna-test . --contract NaiveReceiverEchidna --config naivereceiver.yaml
...

echidna_test_contract_balance: failed!💥  
  Call sequence:
    flashLoan(0x62d69f6867a0a084c6d313943dc22023bc263691,353073667)

...

Exercise 6

Table of contents:

Join the team on Slack at: https://empireslacking.herokuapp.com/ #ethereum

Setup

  1. Clone the repo: git clone https://github.com/crytic/damn-vulnerable-defi-echidna
  2. install the dependencies via yarn install.

Context

The challenge is described here: https://www.damnvulnerabledefi.xyz/challenges/1.html, we assume that the reader is familiar with it.

Goals

  • Setup the testing environment with the right contracts and necessary balances.
  • Analyze the before function in test/unstoppable/unstoppable.challenge.js to identify what initial setup needs to be done.
  • Add a property to check whether UnstoppableLender can always provide flash loans.
  • Create a config.yaml with the necessary configuration option(s).
  • Once Echidna finds the bug, fix the issue, and re-try your property with Echidna.

Only the following contracts are relevant:

  • contracts/DamnValuableToken.sol
  • contracts/unstoppable/UnstoppableLender.sol
  • contracts/unstoppable/ReceiverUnstoppable.sol

Hints

We recommend to first try without reading the following hints. The hints are in the hints branch.

Solution

This solution can be found in the solutions branch.

Solution Explained (spoilers ahead)

Note: Please make sure that you have placed solution.sol (or UnstoppableEchidna.sol) in contracts/unstoppable.

The goal of the unstoppable challenge is to realize that UnstoppableLender has two modes of tracking its balance: poolBalance and damnValuableToken.balanceOf(address(this)).

poolBalance is added to when someone calls depositTokens().

However, a user can call damnValuableToken.transfer() directly and increase the balanceOf(address(this)) without increasing poolBalance.

Now, the two balance trackers are out-of-sync.

When Echidna calls pool.flashLoan(10), the assertion assert(poolBalance == balanceBefore) in UnstoppableLender will break and the pool can no longer provide flash loans.

See example output below from Echidna:

$ echidna-test . --contract UnstoppableEchidna --config unstoppable.yaml

...

echidna_testFlashLoan: failed!💥  
  Call sequence:
    transfer(0x62d69f6867a0a084c6d313943dc22023bc263691,1296000)

...

Exercise 7

Table of contents:

Join the team on Slack at: https://empireslacking.herokuapp.com/ #ethereum

Setup

Setup

  1. Clone the repo: git clone https://github.com/crytic/damn-vulnerable-defi-echidna
  2. install the dependencies via yarn install.
  3. Analyze the before function in test/side-entrance/side-entrance.challenge.js to identify what initial setup needs to be done.
  4. Create a contract to be used for the property testing by Echidna.

No skeleton will be provided for this exercise.

Goals

  • Setup the testing environment with the right contracts and necessary balances.
  • Add a property to check whether the balance of the SideEntranceLenderPool contract has changed.
  • Create a config.yaml with the necessary configuration option(s).
  • Once Echidna finds the bug, fix the issue, and re-try your property with Echidna.

Hint: It might help to start with doing a manual flashloan to get familiar with the workings of the target contract.

Solution

This solution can be found in ./exercise7/solution.sol

Solution Explained (spoilers ahead)

The goal of the side entrance challenge is to realize that the contract's accounting of its ETH balance is misconfigured. balanceBefore is used to track the balance of the contract before the flash loan BUT address(this).balance is used to track the balance of the contract after the flash loan. Thus, you can use the deposit function to repay your flash loan while still maintaining that the contract's total balance of ETH has not changed (i.e. address(this).balance >= balanceBefore). Since the ETH that was deposited is now owned by you, you can now also withdraw it and drain all the funds from the contract.

In order for Echidna to be able to interact with the SideEntranceLenderPool, it has to be deployed first. However, deploying and funding it from the contract to be used by Echidna won't work, as the funding transaction's msg.sender is the contract itself. This means that the owner of the funds is the Echidna contract and therefore it can remove the funds by calling withdraw(), without the need for the exploit.

To prevent that issue, a simple factory contract has to be created to deploy the pool without setting the Echidna property testing contract as the owner of the funds. This factory has a public function that deploys a SideEntranceLenderPool, funds it with the given amount, and return its address. Now, since the Echidna testing contract is not the owner of the funds, it cannot call withdraw() to empty the pool.

Now that the challenge is set up as intended, we proceed to solve it by instructing Echidna to do a flashloan. Using the setEnableWithdraw and setEnableDeposit Echidna will search for function(s) to call inside the flashloan callback to try and break the testPoolBalance property.

At some point Echidna will identify that if (1) deposit is used to pay back the flash loan and (2) withdraw is called right after, the testPoolBalance property breaks.

Example Echidna output:

$ echidna-test . --contract EchidnaSideEntranceLenderPool --config config.yaml
...
testPoolBalance(): failed!💥  
  Call sequence:
    execute() Value: 0x103
    setEnableDeposit(true,256)
    flashLoan(1)
    setEnableWithdraw(true)
    setEnableDeposit(false,0)
    execute()
    testPoolBalance()
...

Exercise 8

Table of contents:

Join the team on Slack at: https://empireslacking.herokuapp.com/ #ethereum

Setup

  1. Clone the repo: git clone https://github.com/crytic/damn-vulnerable-defi-echidna
  2. install the dependencies via yarn install.

Context

The challenge is described here: https://www.damnvulnerabledefi.xyz/challenges/5.html, we assume that the reader is familiar with it.

Goals

  • Setup the testing environment with the right contracts and necessary balances.
  • Analyze the before function in test/the-rewarder/the-rewarder.challenge.js to identify what initial setup needs to be done.
  • Add a property to check whether the attacker can get almost whole reward (let us say more than 99 %) from the TheRewarderPool contract.
  • Create a config.yaml with the necessary configuration option(s).
  • Once Echidna finds the bug, .... well, this time to fix the issue would mean to apply completely different reward logic as in this particular solution is rather naive implementation.

Only the following contracts are relevant:

  • contracts/the-rewarder/TheRewarderPool.sol
  • contracts/the-rewarder/FlashLoanerPool.sol

Hints

We recommend to first try without reading the following hints. The hints are in the hints branch.

  • The invariant that we are looking for is "an attacker cannot get almost whole amount of rewards"
  • Read what is the multi abi option
  • A config file is provided in the-rewarder.yaml

Solution

This solution can be found in solutions branch.

Solution Explained (spoilers ahead)

The goal of the rewarder challenge is to realize that an arbitrary user can call request a flash loan from FlashLoanerPool and borrow the whole amount of Damn Valuable Tokens (DVT) available. Then this amount of DVT can deposit into TheRewarderPool. By doing this, the user affects total proportion of tokens deposited in the TheRewarderPool (and thus gets the most of the percentage of deposited asset in that particular time on his/her side). Furthermore, if the user schedules it in the right time (once the REWARDS_ROUND_MIN_DURATION is reached), snapshot of users deposits is taken, the user repay immediately the loan (i.e., in the same transaction) and gets almost whole reward in return. In fact, this can be done even if the arbitrary user has no DVT.

Echidna reveals this vulnerability by finding the right order of two function, simply calling (1) TheRewarderPool.deposit() (with prior approval) and (2) TheRewarderPool.withdraw() with the max amount of DVT borrowed in flash loan in both functions mentioned.

See example output below from Echidna:

$ echidna-test . --contract EchidnaRewarder --config ./the-rewarder.yaml
...

testRewards(): failed!💥
  Call sequence:
    *wait* Time delay: 441523 seconds Block delay: 9454
    setEnableDeposit(true) from: 0x0000000000000000000000000000000000030000
    setEnableWithdrawal(true) from: 0x0000000000000000000000000000000000030000
    flashLoan(39652220640884191256808) from: 0x0000000000000000000000000000000000030000
    testRewards() from: 0x0000000000000000000000000000000000030000

...

Manticore Tutorial

The aim of this tutorial is to show how to use Manticore to automatically find bugs in smart contracts.

The first part introduces a set of the basic features of Manticore: running under Manticore and manipulating smart contracts through API, getting throwing path, adding constraints. The second part is exercise to solve.

Table of contents:

Join the team on Slack at: https://empireslacking.herokuapp.com/ #ethereum, #manticore

Installation

Manticore requires >= python 3.6. It can be installed through pip or using docker.

Manticore through docker

docker pull trailofbits/eth-security-toolbox
docker run -it -v "$PWD":/home/training trailofbits/eth-security-toolbox

The last command runs eth-security-toolbox in a docker that has access to your current directory. You can change the files from your host, and run the tools on the files from the docker

Inside docker, run:

solc-select 0.5.11
cd /home/trufflecon/

Manticore through pip

pip3 install --user manticore

solc 0.5.11 is recommended.

Running a script

To run a python script with python 3:

python3 script.py

Introduction to dynamic symbolic execution

Manticore is a dynamic symbolic execution tool, we described in our previous blogposts (1, 2, 3, 4).

Dynamic Symbolic Execution in a Nutshell

Dynamic symbolic execution (DSE) is a program analysis technique that explores a state space with a high degree of semantic awareness. This technique is based on the discovery of "program paths", represented as mathematical formulas called path predicates. Conceptually, this technique operates on path predicates in two steps:

  1. They are constructed using constraints on the program's input.
  2. They are used to generate program inputs that will cause the associated paths to execute.

This approach produces no false positives in the sense that all identified program states can be triggered during concrete execution. For example, if the analysis finds an integer overflow, it is guaranteed to be reproducible.

Path Predicate Example

To get an insigh of how DSE works, consider the following example:

function f(uint a){
  
  if (a == 65) {
      // A bug is present
  }
  
}

As f() contains two paths, a DSE will construct two differents path predicates:

  • Path 1: a == 65
  • Path 2: Not (a == 65)

Each path predicate is a mathematical formula that can be given to a so-called SMT solver, which will try to solve the equation. For Path 1, the solver will say that the path can be explored with a = 65. For Path 2, the solver can give a any value other than 65, for example a = 0.

Verifying properties

Manticore allows a full control over all the execution of each path. As a result, it allows to add arbirtray contraints to almost anything. This control allows for the creation of properties on the contract.

Consider the following example:

function unsafe_add(uint a, uint b) returns(uint c){
  c = a + b; // no overflow protection
  return c;
}

Here there is only one path to explore in the function:

  • Path 1: c = a + b

Using Manticore, you can check for overflow, and add constraitns to the path predicate:

  • c = a + b AND (c < a OR c < b)

If it is possible to find a valuation of a and b for which the path predicate above is feasible, it means that you have found an overflow. For example the solver can generate the input a = 10 , b = MAXUINT256.

If you consider a fixed version:

function safe_add(uint a, uint b) returns(uint c){
  c = a + b; 
  require(c>=a);
  require(c>=b);
  return c;
}

The associated formula with overflow check would be:

  • c = a + b AND (c >= a) AND (c=>b) AND (c < a OR c < b)

This formula cannot be solved; in other words this is a proof that in safe_add, c will always increase.

DSE is thereby a powerful tool, that can verify arbitrary constraints on your code.

Running under Manticore

Table of contents:

Introduction

We will see how to explore a smart contract with the Manticore API. The target is the following smart contract (examples/example.sol):

pragma solidity >=0.4.24 <0.6.0;
contract Simple {
    function f(uint a) payable public{
        if (a == 65) {
            revert();
        }
    }
}

Run a standalone exploration

You can run Manticore directly on the smart contract by the following command (project can be a Solidity File, or a project directory):

$ manticore project

You will get the output of testcases like this one (the order may change):

...
... m.c.manticore:INFO: Generated testcase No. 0 - STOP
... m.c.manticore:INFO: Generated testcase No. 1 - REVERT
... m.c.manticore:INFO: Generated testcase No. 2 - RETURN
... m.c.manticore:INFO: Generated testcase No. 3 - REVERT
... m.c.manticore:INFO: Generated testcase No. 4 - STOP
... m.c.manticore:INFO: Generated testcase No. 5 - REVERT
... m.c.manticore:INFO: Generated testcase No. 6 - REVERT
... m.c.manticore:INFO: Results in /home/ethsec/workshops/Automated Smart Contracts Audit - TruffleCon 2018/manticore/examples/mcore_t6vi6ij3
...

Without additional information, Manticore will explore the contract with new symbolic transactions until it does not explore new paths on the contract. Manticore does not run new transactions after a failing one (e.g: after a revert).

Manticore will output the information in a mcore_* directory. Among other, you will find in this directory:

  • global.summary: coverage and compiler warnings
  • test_XXXXX.summary: coverage, last instruction, account balances per test case
  • test_XXXXX.tx: detailed list of transactions per test case

Here Manticore founds 7 test cases, which correspond to (the filename order may change):

Transaction 0Transaction 1Transaction 2Result
test_00000000.txContract creationf(!=65)f(!=65)STOP
test_00000001.txContract creationfallback functionREVERT
test_00000002.txContract creationRETURN
test_00000003.txContract creationf(65)REVERT
test_00000004.txContract creationf(!=65)STOP
test_00000005.txContract creationf(!=65)f(65)REVERT
test_00000006.txContract creationf(!=65)fallback functionREVERT

Exploration summary f(!=65) denotes f called with any value different than 65.

As you can notice, Manticore generates an unique test case for every successful or reverted transaction.

Use the --quick-mode flag if you want fast code exploration (it disable bug detectors, gas computation, ...)

Manipulate a smart contract through the API

This section describes details how to manipulate a smart contract through the Manticore Python API. You can create new file with python extension *.py and write the necessary code by adding the API commands (basics of which will be described below) into this file and then run it with the command $ python3 *.py. Also you can execute the commands below directly into the python console, to run the console use the command $ python3.

Creating Accounts

The first thing you should do is to initiate a new blockchain with the following commands:

from manticore.ethereum import ManticoreEVM

m = ManticoreEVM()

A non-contract account is created using m.create_account:

user_account = m.create_account(balance=1 * 10**18)

A Solidity contract can be deployed using m.solidity_create_contract:

source_code = '''
pragma solidity >=0.4.24 <0.6.0;
contract Simple {
    function f(uint a) payable public{
        if (a == 65) {
            revert();
        }
    }
}
'''
# Initiate the contract
contract_account = m.solidity_create_contract(source_code, owner=user_account)

Summary

Executing transactions

Manticore supports two types of transaction:

  • Raw transaction: all the functions are explored
  • Named transaction: only one function is explored

Raw transaction

A raw transaction is executed using m.transaction:

m.transaction(caller=user_account,
              address=contract_account,
              data=data,
              value=value)

The caller, the address, the data, or the value of the transaction can be either concrete or symbolic:

For example:

symbolic_value = m.make_symbolic_value()
symbolic_data = m.make_symbolic_buffer(320)
m.transaction(caller=user_account,
              address=contract_address,
              data=symbolic_data,
              value=symbolic_value)

If the data is symbolic, Manticore will explore all the functions of the contract during the transaction execution. It will be helpful to see the Fallback Function explanation in the Hands on the Ethernaut CTF article for understanding how the function selection works.

Named transaction

Functions can be executed through their name. To execute f(uint var) with a symbolic value, from user_account, and with 0 ether, use:

symbolic_var = m.make_symbolic_value()
contract_account.f(symbolic_var, caller=user_account, value=0)

If value of the transaction is not specified, it is 0 by default.

Summary

  • Arguments of a transaction can be concrete or symbolic
  • A raw transaction will explore all the functions
  • Function can be called by their name

Workspace

m.workspace is the directory used as output directory for all the files generated:

print("Results are in {}".format(m.workspace))

Terminate the Exploration

To stop the exploration use m.finalize(). No further transactions should be sent once this method is called and Manticore generates test cases for each of the path explored.

Summary: Running under Manticore

Putting all the previous steps together, we obtain:

from manticore.ethereum import ManticoreEVM

m = ManticoreEVM()

with open('example.sol') as f:
    source_code = f.read()

user_account = m.create_account(balance=1*10**18)
contract_account = m.solidity_create_contract(source_code, owner=user_account)

symbolic_var = m.make_symbolic_value()
contract_account.f(symbolic_var)

print("Results are in {}".format(m.workspace))
m.finalize() # stop the exploration

All the code above you can find into the examples/example_run.py

The next step is to accessing the paths.

Getting Throwing Path

Table of contents:

Introduction

We will now improve the previous example and generate specific inputs for the paths raising an exception in f(). The target is still the following smart contract (examples/example.sol):

pragma solidity >=0.4.24 <0.6.0;
contract Simple {
    function f(uint a) payable public{
        if (a == 65) {
            revert();
        }
    }
}

Using state information

Each path executed has its state of the blockchain. A state is either ready or it is killed, meaning that it reaches a THROW or REVERT instruction:

  • m.ready_states: the list of states that are ready (they did not execute a REVERT/INVALID)
  • m.killed_states: the list of states that are ready (they did not execute a REVERT/INVALID)
  • m.all_states: all the states
for state in m.all_states:
    # do something with state

You can access state information. For example:

  • state.platform.get_balance(account.address): the balance of the account
  • state.platform.transactions: the list of transactions
  • state.platform.transactions[-1].return_data: the data returned by the last transaction

The data returned by the last transaction is an array, which can be converted to a value with ABI.deserialize, for example:

data = state.platform.transactions[0].return_data
data = ABI.deserialize("uint", data)

How to generate testcase

Use m.generate_testcase(state, name) to generate testcase:

m.generate_testcase(state, 'BugFound')

Summary

  • You can iterate over the state with m.all_states
  • state.platform.get_balance(account.address) returns the account’s balance
  • state.platform.transactions returns the list of transactions
  • transaction.return_data is the data returned
  • m.generate_testcase(state, name) generate inputs for the state

Summary: Getting Throwing Path

from manticore.ethereum import ManticoreEVM

m = ManticoreEVM()

with open('example.sol') as f:
    source_code = f.read()

user_account = m.create_account(balance=1*10**18)
contract_account = m.solidity_create_contract(source_code, owner=user_account)

symbolic_var = m.make_symbolic_value()
contract_account.f(symbolic_var)

## Check if an execution ends with a REVERT or INVALID
for state in m.terminated_states:
    last_tx = state.platform.transactions[-1]
    if last_tx.result in ['REVERT', 'INVALID']:
        print('Throw found {}'.format(m.workspace))
        m.generate_testcase(state, 'ThrowFound')

All the code above you can find into the examples/example_throw.py

The next step is to add constraints to the state.

Note we could have generated a much simpler script, as all the states returned by terminated_state have REVERT or INVALID in their result: this example was only meant to demonstrate how to manipulate the API.

Adding Constraints

Table of contents:

Introduction

We will see how to constrain the exploration. We will make the assumption that the documentation of f() states that the function is never called with a == 65, so any bug with a == 65 is not a real bug. The target is still the following smart contract (examples/example.sol):

pragma solidity >=0.4.24 <0.6.0;
contract Simple {
    function f(uint a) payable public{
        if (a == 65) {
            revert();
        }
    }
}

Operators

The Operators module facilitates the manipulation of constraints, among other it provides:

  • Operators.AND,
  • Operators.OR,
  • Operators.UGT (unsigned greater than),
  • Operators.UGE (unsigned greater than or equal to),
  • Operators.ULT (unsigned lower than),
  • Operators.ULE (unsigned lower than or equal to).

To import the module use the following:

from manticore.core.smtlib import Operators

Operators.CONCAT is used to concatenate an array to a value. For example, the return_data of a transaction needs to be changed to a value to be checked against another value:

last_return = Operators.CONCAT(256, *last_return)

Constraints

You can use constraints globally or for a specific state.

Global constraint

Use m.constrain(constraint) to add a global cosntraint. For example, you can call a contract from a symbolic address, and restraint this address to be specific values:

symbolic_address = m.make_symbolic_value()
m.constraint(Operators.OR(symbolic == 0x41, symbolic_address == 0x42))
m.transaction(caller=user_account,
              address=contract_account,
              data=m.make_symbolic_buffer(320),
              value=0)

State constraint

Use state.constrain(constraint) to add a constraint to a specific state It can be used to constrain the state after its exploration to check some property on it.

Checking Constraint

Use solver.check(state.constraints) to know if a constraint is still feasible. For example, the following will constraint symbolic_value to be different from 65 and check if the state is still feasible:

state.constrain(symbolic_var != 65)
if solver.check(state.constraints):
    # state is feasible

Summary: Adding Constraints

Adding constraint to the previous code, we obtain:

from manticore.ethereum import ManticoreEVM
from manticore.core.smtlib.solver import Z3Solver

solver = Z3Solver.instance()

m = ManticoreEVM()

with open("example.sol") as f:
    source_code = f.read()

user_account = m.create_account(balance=1*10**18)
contract_account = m.solidity_create_contract(source_code, owner=user_account)

symbolic_var = m.make_symbolic_value()
contract_account.f(symbolic_var)

no_bug_found = True

## Check if an execution ends with a REVERT or INVALID
for state in m.terminated_states:
    last_tx = state.platform.transactions[-1]
    if last_tx.result in ['REVERT', 'INVALID']:
        # we do not consider the path were a == 65
        condition = symbolic_var != 65
        if m.generate_testcase(state, name="BugFound", only_if=condition):
            print(f'Bug found, results are in {m.workspace}')
            no_bug_found = False

if no_bug_found:
    print(f'No bug found')

All the code above you can find into the examples/example_constraint.py

The next step is to follow the exercises.

Manticore Exercises

Example: Arithmetic overflow

This scenario is given as an example. You can follow its structure to solve the exercises.

my_token.py uses manticore to find for an attacker to generate tokens during a transfer on Token (my_token.sol).

## Proposed scenario

We use the pattern initilization, exploration and property for our scripts.

Initialization

  • Create one user account
  • Create the contract account

Exploration

  • Call balances on the user account
  • Call transfer with symbolic destination and value
  • Call balances on the user account

Property

  • Check if the user can have more token after the transfer than before.

Exercice 1 : Arithmetic

Use Manticore to find an input allowing an attacker to generate free tokens in exercise1/token.sol. Propose a fix of the contract, and test your fix using your Manticore script.

Proposed scenario

Follow the pattern initilization, exploration and property for the script.

Initialization

  • Create one account
  • Create the contract account

Exploration

  • Call is_valid_buy with two symbolic values for tokens_amount and wei_amount

Property

  • An attack is found if on a state alive wei_amount == 0 and tokens_amount >= 1

Hints

  • m.ready_states returns the list of state alive
  • Operators.AND(a, b) allows to create and AND condition
  • You can use the template proposed in exercise1/template.py

Solution

exercise1/solution.py

Exercice 2 : Arithmetic on multi transactions

Use Manticore to find if an overflow is possible in Overflow.add. Propose a fix of the contract, and test your fix using your Manticore script.

Proposed scenario

Follow the pattern initilization, exploration and property for the script.

Initialization

  • Create one user account
  • Create the contract account

Exploration

  • Call two times add with two symbolic values
  • Call sellerBalance()

Property

  • Check if it is possible for the value returned by sellerBalance() to be lower than the first input.

## Hints:

  • The value returned by the last transaction can be accessed through:
state.platform.transactions[-1].return_data
  • The data returned needs to be deserialized:
data = ABI.deserialize("uint", data)

Solution

exercise2/solution.py.

Slither

The aim of this tutorial is to show how to use Slither to automatically find bugs in smart contracts.

Once you feel you understand the material in this README, proceed to the exercises:

Watch Slither's code walkthrough to learn about its code structure.

Installation

Slither requires Python >= 3.8. It can be installed through pip or using docker.

Slither through pip:

pip3 install --user slither-analyzer

Docker

Slither through docker:

docker pull trailofbits/eth-security-toolbox
docker run -it -v "$PWD":/home/trufflecon trailofbits/eth-security-toolbox

The last command runs eth-security-toolbox in a docker that has access to your current directory. You can change the files from your host, and run the tools on the files from the docker

Inside docker, run:

solc-select 0.5.11
cd /home/trufflecon/

Command line

Command line versus user-defined scripts. Slither comes with a set of predefined detectors that find many common bugs. Calling Slither from the command line will run all the detectors, no detailed knowledge of static analysis needed:

slither project_paths

In addition to detectors, Slither has code review capabilities through its printers and tools.

Static analysis

The capabilities and design of the Slither static analysis framework has been described in blog posts (1, 2) and an academic paper.

Static analysis exists in different flavors. You most likely realize that compilers like clang and gcc depend on these research techniques, but it also underpins (Infer, CodeClimate, FindBugs and tools based on formal methods like Frama-C and Polyspace.

We won't be exhaustively reviewing static analysis techniques and researcher here. Instead, we'll focus on what is needed to understand how Slither works so you can more effectively use it to find bugs and understand code.

Code representation

In contrast to a dynamic analysis, which reasons about a single execution path, static analysis reasons about all the paths at once. To do so, it relies on a different code representation. The two most common ones are the abstract syntax tree (AST) and the control flow graph (CFG).

Abstract Syntax Trees (AST)

AST are used every time the compiler parses code. It is probably the most basic structure upon which static analysis can be performed.

In a nutshell, an AST is a structured tree where, usually, each leaf contains a variable or a constant and internal nodes are operands or control flow operations. Consider the following code:

function safeAdd(uint a, uint b) pure internal returns(uint){
    if(a + b <= a){
        revert();
    }
    return a + b;
}

The corresponding AST is shown in:

AST

Slither uses the AST exported by solc.

While simple to build, the AST is a nested structure. At times, this is not the most straightforward to analyze. For example, to identify the operations used by the expression a + b <= a, you must first analyze <= and then +. A common approach is to use the so-called visitor pattern, which navigates through the tree recursively. Slither contains a generic visitor in ExpressionVisitor.

The following code uses ExpressionVisitor to detect if the expression contains an addition:

from slither.visitors.expression.expression import ExpressionVisitor
from slither.core.expressions.binary_operation import BinaryOperationType

class HasAddition(ExpressionVisitor):

    def result(self):
        return self._result

    def _post_binary_operation(self, expression):
        if expression.type == BinaryOperationType.ADDITION:
            self._result = True

visitor = HasAddition(expression) # expression is the expression to be tested
print(f'The expression {expression} has a addition: {visitor.result()}')

Control Flow Graph (CFG)

The second most common code representation is the control flow graph (CFG). As its name suggests, it is a graph-based representation which exposes all the execution paths. Each node contains one or multiple instructions. Edges in the graph represent the control flow operations (if/then/else, loop, etc). The CFG of our previous example is:

CFG

The CFG is the representation on top of which most of the analyses are built.

Many other code representations exist. Each representation has advantages and drawbacks according to the analysis you want to perform.

Analysis

The simplest type of analyses you can perform with Slither are syntactic analyses.

Syntax analysis

Slither can navigate through the different components of the code and their representation to find inconsistencies and flaws using a pattern matching-like approach.

For example the following detectors look for syntax-related issues:

Semantic analysis

In contrast to syntax analysis, a semantic analysis will go deeper and analyze the “meaning” of the code. This family includes some broad types of analyses. They lead to more powerful and useful results, but are also more complex to write.

Semantic analyses are used for the most advanced vulnerability detections.

Data dependency analysis

A variable variable_a is said to be data-dependent of variable_b if there is a path for which the value of variable_a is influenced by variable_b.

In the following code, variable_a is dependent of variable_b:

// ...
variable_a = variable_b + 1;

Slither comes with built-in data dependency capabilities, thanks to its intermediate representation (discussed in a later section).

An example of data dependency usage can be found in the dangerous strict equality detector. Here Slither will look for strict equality comparison to a dangerous value (incorrect_strict_equality.py#L86-L87), and will inform the user that it should use >= or <= rather than ==, to prevent an attacker to trap the contract. Among other, the detector will consider as dangerous the return value of a call to balanceOf(address) (incorrect_strict_equality.py#L63-L64), and will use the data dependency engine to track its usage.

Fixed-point computation

If your analysis navigates through the CFG and follows the edges, you are likely to see already visited nodes. For example, if a loop is presented as shown below:

for(uint i; i < range; ++){
    variable_a += 1
}

Your analysis will need to know when to stop. There are two main strategies here: (1) iterate on each node a finite number of times, (2) compute a so-called fixpoint. A fixpoint basically means that analyzing this node does not provide any meaningful information.

An example of fixpoint used can be found in the reentrancy detectors: Slither explores the nodes, and look for externals calls, write and read to storage. Once it has reached a fixpoint (reentrancy.py#L125-L131), it stops the exploration, and analyze the results to see if a reentrancy is present, through different reentrancy patterns (reentrancy_benign.py, reentrancy_read_before_write.py, reentrancy_eth.py).

Writing analyses using efficient fixed point computation requires a good understanding of how the analysis propagates its information.

Intermediate representation

An intermediate representation (IR) is a language meant to be more amenable to static analysis than the original one. Slither translates Solidity to its own IR: SlithIR.

Understanding SlithIR is not necessary if you only want to write basic checks. However, it will come in handy if you plan to write advanced semantic analyses. The SlithIR and SSA printers will help you to understand how the code is translated.

API Basics

Slither has an API that lets you explore basic attributes of the contract and its functions.

To load a codebase:

from slither import Slither
slither = Slither('/path/to/project')

Exploring contracts and functions

A Slither object has:

  • contracts (list(Contract): list of contracts
  • contracts_derived (list(Contract): list of contracts that are not inherited by another contract (subset of contracts)
  • get_contract_from_name (str): Return a list of contract matching the name

A Contract object has:

  • name (str): Name of the contract
  • functions (list(Function)): List of functions
  • modifiers (list(Modifier)): List of functions
  • all_functions_called (list(Function/Modifier)): List of all the internal functions reachable by the contract
  • inheritance (list(Contract)): List of inherited contracts
  • get_function_from_signature (str): Return a Function from its signature
  • get_modifier_from_signature (str): Return a Modifier from its signature
  • get_state_variable_from_name (str): Return a StateVariable from its name

A Function or a Modifier object has:

  • name (str): Name of the function
  • contract (contract): the contract where the function is declared
  • nodes (list(Node)): List of the nodes composing the CFG of the function/modifier
  • entry_point (Node): Entry point of the CFG
  • variables_read (list(Variable)): List of variables read
  • variables_written (list(Variable)): List of variables written
  • state_variables_read (list(StateVariable)): List of state variables read (subset of variables`read)
  • state_variables_written (list(StateVariable)): List of state variables written (subset of variables`written)

Example: Print Basic Information

print_basic_information.py shows how to print basic information about a project.

Exercice 1 : Function overridden protection

The goal is to create a script that fills a missing feature of Solidity: function overriding protection.

exercises/exercise1/coin.sol contains a function that must never be overridden:

_mint(address dst, uint val)

Use Slither to ensure that no contract that inherits Coin overrides this function.

Proposed algorithm

Get the coin contract
    For each contract of the project:
        If Coin is in the list of inherited contract:
            Get the mint function
            If the contract declaring the mint function is != Coin:
                A bug is found.

Hints

  • To get a specific contract, use slither.get_contract_from_name (note: it returns a list)
  • To get a specific function, use contract.get_function_from_signature

Solution

See exercises/exercise1/solution.py.

Exercice 2: Access control

exercises/exercise2/coin.sol possess an access control with the onlyOwner modifier. A frequent mistake is to forget to add the modifier to a critical function. We are going to see how to implement a conservative access control approach with Slither.

The goal is to create a script that will ensure that all the public and external function calls onlyOwner, except for the functions whitelisted.

Proposed algorithm

Create a whitelist of signatures
Explore all the functions
    If the function is in the whitelist of signatures:
        Skip
    If the function is public or external:
        If onlyOwner is not in the modifiers:
            A bug is found

Solution

See exercises/exercise2/solution.py.

Trail of Bits blogposts

The following contains the blockchain related blogposts made by Trail of Bits.

Consensus algorithms

Research in the distributes systems area

DateTitleDescription
2021/11/11Motivating global stabilizationReview of Fischer, Lynch, and Paterson’s classic impossibility result and global stabilization time assumption
2019/10/25Formal Analysis of the CBC Casper Consensus Algorithm with TLA+Verification of finality of the Correct By Construction (CBC) PoS consensus protocol
2019/07/12On LibraBFT’s use of broadcastsLiveness of LibraBFT and HotStuff algorithms
2019/07/02State of the Art Proof-of-Work: RandomXSummary of our audit of ASIC and GPU-resistant PoW algorithm
2018/10/12Introduction to Verifiable Delay Functions (VDFs)Basics of VDFs - a class of hard to compute, not paralelizable, but easily verifiable functions

Fuzzing compilers

Our work in the topic of fuzzing the solc compiler

DateTitleDescription
2021/03/23A Year in the Life of a Compiler Fuzzing CampaignResults and feature of fuzzing solc
2020/06/05Breaking the Solidity Compiler with a FuzzerOur approach to fuzzing solc

General

Security research, analyses, announcements, and writeups

DateTitleDescription
2022/10/12Porting the Solana eBPF JIT compiler to ARM64Low-level writeup of the work done to make Solana compiler work on ARM64
2022/06/24Managing risk in blockchain deploymentsSummary of "Do You Really Need a Blockchain? An Operational Risk Assessment" report
2022/06/21Are blockchains decentralized?Summary of "Are Blockchains Decentralize? Unintended Centralities in Distributed Ledgers" report
2020/08/05Accidentally stepping on a DeFi legoWriteup of a vulnerability in yVault project
2020/05/15Bug Hunting with CryticDescription of 9 bugs found by Trail of Bits tools in public projects
2019/11/13Announcing the Crytic $10k Research PrizeAcademic research prize promoting open source work
2019/10/24Watch Your Language: Our First Vyper AuditPros and cons of Vyper language and disclosure of vulnerability in the Vyper's compiler
2019/08/08246 Findings From our Smart Contract Audits: An Executive SummaryPublication of data aggregated from our audits. Discussion about possibility of automatic and manual detection of vulnerabilities, and usefulness of unit tests
2018/11/19Return of the Blockchain Security Empire Hacking
2018/02/09Parity Technologies engages Trail of Bits
2017/11/06Hands on the Ethernaut CTFFirst write-up on Ethernaut

Guidance

General guidance

DateTitleDescription
2021/02/05Confessions of a smart contract paper reviewerSix requirements for a good research paper
2018/11/2710 Rules for the Secure Use of Cryptocurrency Hardware WalletsRecommendations for the secure use of hardware wallets.
2018/10/04Ethereum security guidance for allAnnouncement of office hours, Blockchain Security Contacts, and Awesome Ethereum Security
2018/04/06How to prepare for a security reviewChecklist for before having a security audit

Presentations

Talks, videos, and slides

DateTitleDescription
2019/01/18Empire Hacking: Ethereum Edition 2Talks include: Anatomy of an unsafe smart contract programming language, Evaluating digital asset security fundamentals, Contract upgrade risks and recommendations, How to buidl an enterprise-grade mainnet Ethereum client, Failures in on-chain privacy, Secure micropayment protocols, Designing the Gemini dollar: a regulated, upgradeable, transparent stablecoin, Property testing with Echidna and Manticore for secure smart contracts, Simple is hard: Making your awesome security thing usable
2018/11/16Trail of Bits @ Devcon IV RecapTalks include: Using Manticore and Symbolic Execution to Find Smart Contract Bugs, Blockchain Autopsies, Current State of Security
2017/12/22Videos from Ethereum-focused Empire HackingTalks include: A brief history of smart contract security, A CTF Field Guide for smart contracts, Automatic bug finding for the blockchain, Addressing infosec needs with blockchain technology

Tooling

Description of our tools and their use cases

DateToolTitleDescription
2022/08/17slitherUsing mutants to improve SlitherInserting random bugs into smart contracts and detecting them with various static analysis tools - to improve Slither's detectors
2022/07/28slitherShedding smart contract storage with SlitherAnnouncement of the slither-read-storage tool
2022/04/20Amarna: Static analysis for Cairo programsOverview of Cairo footguns and announcement of the new static analysis tool
2022/03/02echidnaOptimizing a smart contract fuzzerMeasuring and improving performance of Echidna (Haskell code)
2021/12/16slitherDetecting MISO and Opyn’s msg.value reuse vulnerability with SlitherDescription of Slither's new detectors: delegatecall-loop and msg-value-loop
2021/04/02Solar: Context-free, interactive analysis for SolidityProof-of-concept static analysis framework
2020/10/23slitherEfficient audits with machine learning and Slither-similDetect similar Solidity functions with Slither and ML
2020/08/17echidnaUsing Echidna to test a smart contract libraryDesigning and testing properties with differential fuzzing
2020/07/12manticoreContract verification made easierRe-use Echidna properties with Manticore with manticore-verifier
2020/06/12slitherUpgradeable contracts made safer with Crytic17 new Slither detectors for upgradeable contracts
2020/03/30echidnaAn Echidna for all SeasonsAnnouncement of new features in Echidna
2020/03/03manticoreManticore discovers the ENS bugUsing symbolic analysis to find vulnerability in Ethereum Name Service contract
2020/01/31manticoreSymbolically Executing WebAssembly in ManticoreUsing symbolic analysis on an artificial WASM binary
2019/08/02Crytic: Continuous Assurance for Smart ContractsNew product that integrates static analysis with GitHub pipeline
2019/07/03slitherAvoiding Smart Contract "Gridlock" with SlitherDescription of a DoS vulnerability resulting from a strict equality check, and Slither's dangerous-strict-equality detector
2019/05/27slitherSlither: The Leading Static Analyzer for Smart ContractsSlither design and comparison with other static analysis tools
2018/10/19slitherSlither – a Solidity static analysis frameworkIntroduction to Slither's API and printers
2018/09/06rattleRattle – an Ethereum EVM binary analysis frameworkTurn EVM bytecode to infinite-register SSA form
2018/05/03echidnaState Machine Testing with EchidnaExample use case of Echidna's Haskell API
2018/03/23Use our suite of Ethereum security toolsOverview of our tools and documents: Not So Smart Contracts, Slither, Echidna, Manticore, EVM Opcode Database, Ethersplay, IDA-EVM, Rattle
2018/03/09echidnaEchidna, a smart fuzzer for EthereumFirst release and introduction to Echidna
2017/04/27manticoreManticore: Symbolic execution for humansFirst release and introduction to Manticore (not adopted for EVM yet)

Upgradeability

Our work related to contracts upgradeability

DateTitleDescription
2020/12/16Breaking Aave UpgradeabilityDescription of Delegatecall Proxy vulnerability in formally-verified Aave contracts
2020/10/30Good idea, bad design: How the Diamond standard falls shortAudit of Diamond standard's implementation
2018/10/29How contract migration worksAlternative to upgradability mechanism - moving data to a new contract
2018/09/05Contract upgrade anti-patternsDiscussion of risks and recommendations for Data Separation and Delegatecall Proxy patterns. Disclosure of vulnerability in Zeppelin Proxy contract.

Zero-knowledge

Our work in Zero-Knowledge Proofs space

DateTitleDescription
2022/04/18The Frozen Heart vulnerability in PlonK
2022/04/15The Frozen Heart vulnerability in Bulletproofs
2022/04/14The Frozen Heart vulnerability in Girault’s proof of knowledge
2022/04/13Coordinated disclosure of vulnerabilities affecting Girault, Bulletproofs, and PlonKIntroducing new "Frozen Heart" class of vulnerabilities
2021/12/21Disclosing Shamir’s Secret Sharing vulnerabilities and announcing ZKDocs
2021/02/19Serving up zero-knowledge proofsFiat-Shamir transformation explained
2020/12/14Reverie: An optimized zero-knowledge proof systemRust implementation of the MPC-in-the-head proof system
2020/05/21Reinventing Vulnerability Disclosure using Zero-knowledge ProofsAnnouncement of DARPA sponsored work on ZK proofs of exploitability
2019/10/04Multi-Party Computation on Machine LearningImplementation of 3-party computation protocol for perceptron and support vector machine (SVM) algorithms