Relearning Solidity
Motivation
I've been doing protocol engineering for nearly 4 years now with a handful of projects, some of which have had millions of dollars in TVL, but I am still kinda mid.
This is my how I go from a mid solidity engineer to a kinda good one.
Smart Contract Design Patterns
Github Repositories
https://github.com/sablier-labs/v2-core
Seaport
Openzeppelin
Rate Limiter
- events need to match types
Masterchef or synthetix staking
uniswap v2
some airdrop claiming contract
Useful solidity patterns/trivia
https://github.com/dragonfly-xyz/useful-solidity-patterns
- You can slice calldata using calldata[4:] but you cannot for memory arrays. You need to manually copy the bytes over using assembly
Many times, people use assembly to look cool or to optimize gas. Here are some legitimate use cases for assembly:
- Handling Reverts - When an external call reverts while calling it from a
try/catch
, call
, delegatecall
, or staticcall
, the revert message will just be some bytes. If you want to bubble this revert up and revert the transaction with the bytes that came from the external call's revert, you can use the following pattern
- Saving Gas when Hashing together 32 byte values - The non-assembly method would be to do
keccak256(abi.encode(bytes32 a, uint256 b))
. However, abi.encode allocates memory to store the result of abi.encode(bytes32 a, uint256 b)
. The optimal solution would be to use bytes of scratch space, as follows
- Casting between different structs/arrays - If you have structs/arrays with the same underlying type(ie
address[]
vs interface[]
), you can use assembly to cast between them. You can also do the same thing with structs, as long as the elements in the structs are the exact same.
- Proxy Patterns
- Consists of a Logic Contract and a Proxy Contract
- The proxy contract has two functions, the default fallback function and a delegate call function. When you want to make a call, the calldata gets passed to the delegate call function via the fallback function(which catches everything)
- To make a proxy upgradable, you can allow the proxy contract to change the address of the logic contract. However, there are several pitfalls to this pattern.
- If you change up the order of how variables are declared, the compiler will put them into different storage slots and you will have variables overwriting stuff. Even worse, you could overwrite your logic contract address storage slot. This is why we have stuff like ERC-1967, which declares which storage slot your logic contract address is supposed to be in.
- Also, you can't use a constructor, so you use an initializer. But, if someone manages to reinitialize your logic contract, you will get hacked
- SSTORE2 - If you want to store data that is larger than 32 bytes, you can store the data in a contract's bytecode to save gas on reads and writes. The premise is to store a bytes array or string into a contract, and read from it using
address(contract).code
- Of course, you want to check to make sure the contract doesn't actually have the ability to execute, so make sure to prefix the bytecode with any opcode except selfdestruct
- Bitmaps - If you want to store a nonce for each order on an orderbook, you can use a bitmap to save gas. By packing nonces within a 256 bit word, you can ensure that users only write to a new slot once every 255 times.
- ERC20 Compatibility - Certain ERC20 tokens such as USDT and BNB do not conform to the ERC20 specification of returning a boolean indicating success or failure. When trying to decode the returned boolean, the transaction will revert as there is no data there. However, we can use SafeERC20 to solve this problem, as it first checks if the call has enough data to decode before decoding it
Additionally, there are ERC20 tokens that only allow approvals if the user currently has an approval balance of zero. This is to bypass the frontrunning approval attack.
Solidity v0.8
Changes introduced between Solidity v0.8.0 and v0.8.23:
- Exponentiation is right associative.
a**b**c
is now parsed as a**(b**c)
.
- Custom Errors
- New Opcodes for 1559 ie
block.basefee
, block.maxPriorityFee
, block.maxFeePerGas
- User Defined Value Types(UDVT) ie
type Decimals18 is uint256;
- The previous solution was
struct Decimals18{ uint256 decimals}
which had extra gas overhead
- Use
abi.encodeCall
over abi.encodeWithSelector
when encoding calldata as it checks that the function's types match the supplied arguments
- User Defined Value Types now have user defined operators. This allows you to use custom functions as operators for your UDVTs
Foundry Notes
Read 1-29 of https://book.getfoundry.sh/
For Defi, understand the following examples. These are good design patterns
https://solidity-by-example.org/defi/staking-rewards/
https://solidity-by-example.org/defi/constant-sum-amm/
https://solidity-by-example.org/defi/vault/
Articles
https://cmichel.io/how-to-become-a-smart-contract-auditor/
https://solodit.xyz/
Code arena reports
https://code4rena.com/reports/2023-01-opensea
TODO
Environment Diagram practice for solidity