# Building Smart Contracts the Smart Way
I've spent a lot of time recently writing and reviewing solidity, which has been refreshing. Ironically, life as a Smart Contract engineer doesn't always consist of writing solidity. In fact, it has been almost six months since the last time I was writing this many new smart contracts, so I had to spend some time getting acquainted with the latest and greatest in Solidity tools and best practices.
Now that I'm feeling a bit reacquainted with the Solidity landscape, I'd like to document some up-to-date tips for writing smart contracts in 2023.
These tips will probably be outdated by 2024, as the smart contract landscape continues to progress at a mind-numbing pace, so I've tried my best to compile advice that could be considered more evergreen. This essay is designed to be read by any developer who wants to launch a contract system on Mainnet. This is the time where your decisions matter the most--decisions about framework (e.g. Hardhat or Foundry), about upgradeability, and about contract architecture. Once you deploy to Mainnet, you go past the point of no (easy) return and you enter the dark and stormy seas where you must defend against MEV monsters lurking below the depths.
Spend more time upfront on design and security, and harvet the rewards later.
This essay is organized as follows:
- **Pre development tips 📝**: *TLDR*: Pick a framework carefully.
- **Mid development tips 🧘🏻♀️**: Spend a lot of time designing contracts and writing interfaces before you actually start coding. Pay attention to your coding style.
- **Post development tips 🏋🏿♀️**: This is where you really think about the security model and stress tests with vigor.
- **Tips on various topics 🎁**: Bring it home, make sure you've checked all the boxes. I won't call this an addendum because these topics are really important and you should read through them all.
***Who am I?*** I lead the engineering team at [Across Protocol](https://docs.across.to/), a cross-chain bridge spun out of [UMA](https://umaproject.org/) and I've written a lot of smart contract code in the last four years. Smart contract engineering has evolved a lot since 2018 and I've had a front row seat.

## Lifecycle of writing smart contracts
Before you write your first `pragma`, ask yourself: *are you cut out for this life?*
Deploying a production application based on smart contracts to Mainnet means that you're going to be writing a lot of non-Solidity code.
My experience as a smart contract engineer has consisted of multiple month sprints of writing only solidity, followed by contract deployment, and then many more months writing off-chain monitoring and relaying systems that interact with those contracts.
A smart contract is really just server-side code that you deploy on a blockchain instead of the cloud or on a physical machine. Building robust server-side code means writing a lot of additional code that monitors the server, asking it questions like *"are you alive?"* and *"why are you dead?"*.
Here are some graphs of the LOC (lines of code) changes per week for the the [Across](https://docs.across.to/) smart contract and off-chain repos. Note how all of the contracts work is front-loaded and is followed by the the off-chain work which realistically never ends. (Good) contracts live forever, while off-chain code must continually be updated.
So, this is why I think the last month of writing Solidity has truly been refreshing. After all, if you wrote solid, safe Solidity, then if all goes well you won't touch that Solidity for a long time!


## Pre development:
*At this stage you are*: someone with an idea for a project that needs smart contracts.
You have one job at this stage, pick a smart contract framework and choose wisely. In 2023, your best choices are:
- [Hardhat](https://hardhat.org/hardhat-runner/docs/getting-started#overview) with [hardhat-deploy](zttps://github.com/wighawag/hardhat-deploy#readme)
- [Foundry](https://book.getfoundry.sh/getting-started/first-steps)
I recommend Hardhat and Foundry because they are the most common and supported so when you google questions about why your code isn't working, you'll find more answers on public forums. You'll also have more friends to commiserate with and share syntax complaints with.
Both frameworks will have you writing contracts in Solidity. The difference is in what languages you'll write your tests, deployment scripts, and other helper scripts in:
- Hardhat = Typescript/Javascript
- Foundry = Solidity
Hardhat has a great plugin system that makes it very easy to add helper modules to, including `hardhat-deploy` to help with deploying contracts. Hardhat is also more established than Foundry. Hardhat essentially replaced Truffle (RIP) which in 2018/2019 was the standard for frameworks. Hardhat was faster and simpler than Truffle, and now Foundry is coming for the throne.
Foundry is cool right now, in the same way Hardhat was cool in 2019, and you can be sure there will be another framework in the future. Pick the best tool for the job and consider:
- ease of use
- language
- documentation
- public support
Foundry is a lot faster than Hardhat when compiling and writing tests, which makes a big difference when you have a lot of tests (as you should!). Writing tests in Solidity, as you do in Foundry, also feels "safer" than in Typescript. Once your contracts are deployed on Mainnet, clients in any language can interact with them so if you've tested against them in Typescript, you might not have 100% certainty how they would react when called with Python. Or, if you've used the [`ethers.js`](https://docs.ethers.org/v5/) library in your tests, then you might have a bit of concern when a client using [`web3.js`](https://web3js.readthedocs.io/en/v1.8.2/) interacts with them.
Generally, smart contracts should respond the same regardless of client implementation, but there are some worries when testing with typescript. For example, calling `myFunction(uint256 x)` with `ethers.js` means that you're implicitly instructing `ethers.js` to convert `x` into a `uint256` type that can be passed to the contract, and `ethers.js` might handle this slightly differently than `web3.js` for really large or small values of `x`.
When you write your tests in Solidity, then you can have a bit more reassurance about how your contracts will behave regardless of client implementation. Now, this idea ignores the reality that all clients will be written in some non-Solidity language. So, it is important that your client calls the contracts as expected and its good to test this. So, I would recommend a stack of contract unit tests in Solidity, and client tests in the client's language.
- Contracts written and tested in *Solidity*
- Frontend written in `Next.js/React.js` and tested in *Typescript*
- Middleware written and tested in `Typescript/Rust/Golang/[you name a language]`
#### Can't decide on a framework? Go with Foundry
Foundry has enough support at this stage that all of your common questions have already been asked and documented publicly. Foundry has great native support for fuzz and invariant testing, and has solid built-in tools for formatting and deployment. Moreover, you can attach `hardhat` support to your repo later to write tests that more closely resemble the front end code you'll eventually write.
I found starting with [this template](https://github.com/paulrberg/foundry-template) to be helpful.
### Other decisions to make before you even think about writing contracts:
- Pick a network that you'll deploy on. This choice will inform how gas optimized you build your smart contracts. Mainnet is more secure than L2's and more expensive. Have you considered building a [cross-chain contract system where the most secure contracts are deployed on Mainnet and the common user-facing ones are deployed on L2's](https://hackmd.io/@nicholaspai/Syr9IG-Ls)?
- Decide early on if you want your contracts to be upgradeable. This means upgrading your contract code without having to change their addresses. You probably want them upgradeable. More on this [later](#Upgradeability).
Now onwards, let us code.
## The development lifecycle I follow, summarized:
1. Write pseudo code description of the components you need.
- ex: I want to build a cross-chain bridge, so I will need contracts that comprise the bridging logic, store the configuration parameters that control the bridge's behavior, and the governance system that can modify those configuration parameters.
3. Transform those components into smart contracts, describe each in English.
- ex: The bridging logic will be split into a Hub and Spoke model where liquidity providers interact with a passive pool of liquidity in the Hub, and bridgers interact with the Spokes. Each Spoke will be deployed on a different L2 network, and the Hub will be deployed on Mainnet.
- ex: The configuration store will store settings that shape the curve on which fees are charged based on utilization of the bridge. A separate contract, or perhaps specifically a library, will have contain the fee curve mathematics itself.
5. Write interfaces first and think about functions.
- Writing out the function interfaces will give you a better idea of which contracts you will need to write and how they will interact. They will also indicate which contracts you thought you had to write and no longer need to write.
```solidity
// Contains liquidity used to pay back relayers
interface Hub {
// Adds and removes liquidity to Hub for `token`.
addLiquidity(uint256 amount, address token);
removeLiquidity(uint256 amount, address token);
struct Fill {
uint256 originChainId;
uint256 depositId;
uint256 destinationChainId;
};
// Proposer posts a bond to optimistically propose a list of fills that should be refunded. Off-chain validators will validate that each fill corresponds to a fill sent on the SpokePool at`destinationChainId` and a deposit on the SpokePool at `originChainId`. If all fills are validated, then `executeProposedRefunds` can be used to send tokens out of the contract to each of the SpokePools to pay out refunds.
proposeRefundsForFIlls(Fill[] fills) returns (uint256 challengePeriodEndTime);
executeRefunds();
}
interface Spoke {
// Bridge `amount` of `token` to Spoke on destination chain with ID `destinationChainId`.
// Returns to caller unique ID representing deposit. Deposits are uniquely identified by origin
// chain (i.e. this contract's chain ID) and depositId.
deposit(uint256 amount, uint256 destinationChainId, address token) returns (uint256 depositId)
// Pulls `amount` of `token` from caller and sends to `recipient`. If `amount` and `token`
// correctly map to a deposit on the `originChainId` for a deposit with ID `depositId`, then the
// msg.sender will get refunded their amount + fees.
fillDeposit(int256 originChainId, uint256 depositId, uint256 amount, address token, address recipient)
}
```
7. Start implementing the interfaces.
8. At this point, you'll likely have to go back to the drawingboard (as far back as step 1) and re-write contracts as you realize you missed some things you wish, or perhaps a different smart contract architecture would suit your project better.
- This is normal to go backwards at this point! This is OK and healthy. This is the time to not be OK with architectural compromises and build things the right way from the outset. Future maintainers (i.e. probably future You) will thank you.
9. Start writing tests as you complete contracts. Write *simple* unit tests at first so you have something to test. Do this for each contract slowly.
- The beautiful thing about building up tests as you go is that when you make changes, you can run the unit tests to make sure your changes didn't break anything.
- ex. having the following test means that no matter what I change about the `Spoke` contract will not violate the rule that `amount > 0`
```solidity
contract SpokeTest {
function test_deposit() public {
// The following `deposit` call should revert since you can't deposit 0 tokens.
vm.expectRevert("Amount > 0");
spoke.deposit(0, destinationChainId, token)
}
}
```
10. Keep implementing contract code followed by adding unit tests. Your flow at this point should be run unit tests after chunks of code are implemented.
## Mid development tips
### Reuse code
I think its important to try to re-use existing, battle-tested code where possible. Take advantage of Lindyness. Use [OpenZeppelin libraries](https://github.com/OpenZeppelin/openzeppelin-contracts) wherever possible as these are used in many production systems today. (If you are building upgradeable contracts, then [here](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable) are the upgradeable version of the OZ contracts).
This will reduce the amount of code you end up writing and also reduce the amount of code that you pay auditors to review. Most importantly this will reduce the amount of complexity you need to reason about and write tests for.
### Choice of contract libraries
Why OZ versus [solmate](https://github.com/transmissions11/solmate), which is gas optimized? Call me old fashioned but I subscribe to the KISS philosophy: keep it simple stupid. I find OZ's contracts much easier to understand and I'm willing to trade off higher gas costs for *most* contract applications.
Some apps really need to be gas optimal, but these are rare cases.
### Style: we all know good style
Keep your contracts as simple as possible and add comments---more than you think you need.
Clean code is easier to review, easier to maintain, and easier to integrate with.
As an auditor, well commented functions are a joy to read, meaning that it will be easier for me to catch bugs.
There is no right way to name variables. This is how I do it:
- private/internal functions begin with "_" for example `deposit()` might call `_deposit()`.
- constants are in ALL_CAPS
- Function names should describe actions like `stake` or `transfer`
- State variables should be nouns that functions act upon like `mapping(address => uint) moneysForPersons`.
- Modifiers should begin with `onlyX` so that functions can have `onlyX` modifiers making it VERY clear when they can be called
These rules make it easy to run the following quick checks:
- Are all public/external functions reentrancy guarded? Are any functions that don't begin with an "_" not reentrancy guarded?
- Are all public/external functions (i.e. all contracts in the contract interface) commented in Natspec format?
#### Nits
- Functions should be ordered: external, public, internal, private
- If a block of code is repeated, make it a `private` function and re-use it. You'll thank yourself when you need to edit that logic and don't have to make the edit 5 times.
- Natspec comments: use them. It helps clients render comments and I'm pretty sure etherscan.io renders natspec comments above function params. GitHub Copilot makes commenting easy.
## Finished implementing all contracts and have some tests? Time to add more complex tests:
1. Add [Fuzz testing](https://book.getfoundry.sh/forge/fuzz-testing).
2. Add [Invariant testing](https://book.getfoundry.sh/forge/invariant-testing#invariant-testing)
4. Run Solidity static analyzers like [Slither](https://github.com/crytic/slither)
5. Write [deployment scripts](https://book.getfoundry.sh/tutorials/solidity-scripting) and add tests for them.
6. Add interface comments in [NatSpec format](https://docs.soliditylang.org/en/v0.8.19/natspec-format.html!).
One of the reasons why I recommend Foundry is that it supports Fuzz and Invariant testing natively.
## Hire an external auditor
I'm not going to recommend specific auditing firms/individuals but I do think you want at least opinions from at least two outside sources. A good auditor will ask questions as they read through the code. To get the most out of an audit, make sure you send them:
- A one pager explaining at a high level what your contracts are supposed to do, and what the contracts specifically implement
- Which contract functionality you think are the most vulnerable to attacks
## Selected topics you really should think about it
### WETH/ETH
Can your contract work with ETH or WETH or both? Should all of its custodied ETH be wrapped into WETH?
If it needs to unwrap WETH then it must have a [fallback](https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/fallback-functions/) function. If it has a fallback, make sure there is some way to withdraw ETH that it holds so it doesn't blackhole its balances.
Similarly, make sure there is some way to withdraw any ERC20's accidentally to it.
### Multicaller
Its great UX if a client can batch together multiple functions on your contract at the same time. For example, relayers love bridges where they can fill multiple deposits at the same time (e.g. batch together `fillDeposit()` calls into a single atomic transaction). To support this, simply have your contracts extend [`Multicall`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Multicall.sol).
### Governance
What points of control exist in your system? Can these points be eventually decentralized?
I think gradual decentralization makes sense: where the control is initially owned by a multisig so that bugs can be patched quickly (or worst case pause the contracts). Eventually, this ownership can be transferred to a more [optimistic](https://docs.umaproject.org/developers/osnap) [style](https://wiki.tally.xyz/docs/compound-governor) of [governance](https://github.com/gnosis/zodiac#modules).
Many people ask: is your protocol decentralized? I think this is the wrong question in evaluating a protocol's long term potential as a decentralized protocol.
Instead ask: can this protocol become truly decentralized? Usually this amounts to: can the initial multisig owner call `transferOwnership` or some other set of functions to irrevocably give up control?
Now if you're asking yourself why you should be able to give up control, then read this [timeless piece](https://medium.com/@VitalikButerin/the-meaning-of-decentralization-a0c92b76a274). Its a security consideration, not a moral one. Decentralized protocols are more secure because they are:
- lack central points of weakness
- are resistant to human lapses in judgement
### Playing nicely with Multisigs
This is a good opportunity to make sure that muiltisigs can interact with your contract.
Do users need to sign messages and execute in your contracts? If so, make sure you support [EIP1271](https://eips.ethereum.org/EIPS/eip-1271) so that contracts can sign messages in additionto EOA's. Moreover, make sure you support typed structured data, [EIP712](https://eips.ethereum.org/EIPS/eip-712).
### Upgradeability
Consider which contracts (usually not all of them!) might need to be upgraded in the future. This doesn't give you license to make mistakes at the outset. When you upgrade contracts its probably not going to be possible to remove functions or anything in the contract interfaces, instead its much easier if you only *add* functionality in the future.
Use OZ's [upgradeable proxies](https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies). I recommend the following type of proxy patterns:
- UUPS
- Clones
UUPS should be used for most contracts that are designed to be deployed once.
Clones should be used if one contract is meant to be deployed many times, for example the [Uniswap Pool](https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol) contract. Clones enables upgrading them all in unison.
Upgradeability power should be given to same ownership that [administrates the system](#Governance).
Its very simple to add a timelock to the upgradeability process by adding logic to the [`upgradeTo` function](https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable-upgradeTo-address-).
### Reentrancy
Anytime your contract calls another contract, assume that that contract wants to steal your funds. To do so, it usually must call back into your contract.
Don't allow this unless you wrote that other contract or trust it!
By default, add [reentrancy guards](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/security/ReentrancyGuardUpgradeable.sol) to every public/external function. The added marginal gas costs are worth the peace of mind!
And again, don't roll your own reentrancy guards unless you have a strong reason to do so! Use OZ's.
### Compiling with an optimizer
Should [optimize](https://docs.soliditylang.org/en/v0.8.19/internals/optimizer.html) your contracts at compile time? Probably yes.
- Pros: Reduces gas cost at run-time for users.
- Cons: Produces larger contract size. A concern if you're bumping up against contract size limit of approximately 24.5kB.
Practical recommendation: optimize if space allows. Set `runs` really high. I usually default to `1_000_000` runs and reduce at as needed as I run against the contract size limit.
### Formal verification
I recommend doing this sort of analysis for any implementations of more advanced math functions. For example, AMM's and lending markets implementing fee curves should use this.
Most contracts implement pure math functions at some point, so they should use formal analysis in some way. If you don't do this, then at least fuzz the contracts so you have confidence that certain variable states can't be reached! This will help counter underflow/overflow, divide-by-zero, and other silent killers.
I've only recently started to work with [Certora](https://www.certora.com/) so I'll write more about this experience in the future.
### Common errors encountered when compiling contracts
#### Stack-too-deep errors
You can sometimes avoid these by using the `viaIR` solidity compiler [option](https://docs.soliditylang.org/en/v0.8.17/ir-breaking-changes.html).
#### Contract size too large
This one is very annoying. Some tips:
- Call other contracts via their interface rather than importing the full contract. For example if you need to call `ERC20(someToken).transfer` make sure you call `IERC20(someToken)` instead of `ERC20(someToken)`.
- Internal [`library`](https://docs.soliditylang.org/en/v0.8.17/introduction-to-smart-contracts.html#delegatecall-and-libraries) functions contribute to bytecode. External `library` functions do not but add gas since they are external contract calls.
- Double check that all `require` statements are needed. Some are overkill like checking if an input `address != address(0)`, especially for permissioned functions (i.e. `onlyOwner` functions) where the caller is trusted.
- Separate functionality where possible into separate contracts. This will increase gas costs as making calls to external contracts adds overhead, but reduces contract size.
- When you are really close to being under the limit, lower the number of runs in the optimizer until you get there.
Other tips:
- https://ethereum.org/en/developers/tutorials/downsizing-contracts-to-fight-the-contract-size-limit/
- https://blog.polymath.network/solidity-tips-and-tricks-to-save-gas-and-reduce-bytecode-size-c44580b218e6
### Other resources
- https://book.getfoundry.sh/tutorials/best-practices