Using Simbolik for Solidity Debugging

Posted on November 4th, 2024 by Raoul Schaffranek
Last updated on November 4th, 2024

simbolik-screenshot.png

Solidity is the most popular language for developing smart contracts for Ethereum Mainnet, Optimism, Base, Mantle, and virtually every EVM compatible chain. Simbolik is a debugger designed to make Solidity debugging for all these chains as easy as possible. In this spirit it automates much of the repetitive work, including compiling your contracts, creating and deploying to a local testnet, and simulating user transactions. Whenever feasible, Simbolik can launch a debugging session with a single click. However, there are cases where some manual setup is required, such as when debugging complex systems with multiple interacting contracts or scenarios involving user interactions. This post will guide you through setting up these more realistic scenarios. Simbolik's guiding principle is to avoid repetitive tasks: you should only write the deployment or simulation code once, and then reuse it as needed. Equally important is that all configuration happens in Solidity, so you never have to switch to another language.

For this guide, we’ll debug a constant product automated market maker (AMM). Specifically, we'll simulate what happens when Alice, a liquidity provider, adds liquidity to the market and when Bob, a trader, swaps tokens. We'll borrow the AMM from solidity-by-example.org - an excellent learning resource.

Getting Started

You don’t need to install anything locally to follow this guide — just head over to Simbolik's browser version. If you want to modify the code, clone the simbolik-examples repository and follow the Getting Started Guide to set up Simbolik locally.

Setting Up the Contracts

The AMM contract depends on two ERC20 tokens representing tradeable assets. While manual deployment is an option, it’s inefficient and not very scalable, especially if others on your team need to replicate your setup or if the system includes more than just a hanful of contracts.

Instead, we create a deployment script in Solidity. In Simbolik, deployment scripts are just standard Solidity smart contracts, designed to be self-contained:

contract DebugCPAMM { ERC20 public token0; ERC20 public token1; CPAMM public cpamm; constructor() { token0 = new ERC20("token0", "TK0", 0); token1 = new ERC20("token1", "TK1", 0); cpamm = new CPAMM(address(token0), address(token1)); } }

Simulating User Behavior

To observe how Alice and Bob interact with the market, we could manually set up accounts and send transactions. However, for the same reasons we avoid manual deployment, automating these interactions is more efficient. In Simbolik, we use "digital actors"—smart contracts that simulate human users. Just like deployment scripts digital actors are plain-old Solidity contracts. Here’s what an actor for a liquidity provider might look like:

contract LiquidityProvider { CPAMM cpamm; constructor(CPAMM _cpamm) { cpamm = _cpamm; } function addLiquidity(uint256 amountA, uint256 amountB) external returns (uint256) { cpamm.token0().approve(address(cpamm), amountA); cpamm.token1().approve(address(cpamm), amountB); return cpamm.addLiquidity(amountA, amountB); } function removeLiquidity(uint256 amount) external { cpamm.removeLiquidity(amount); } }

Adding liquidity involves three steps: approving the AMM to spend the tokens and then calling the addLiquidity function.

Deploying the Complete System

We deploy the ERC20 tokens, the AMM, and the digital actors in one script and fund the actors to prepare for interactions:

contract DebugCPAMM { ERC20 public token0; ERC20 public token1; CPAMM public cpamm; LiquidityProvider public alice; Swapper public bob; constructor() { token0 = new ERC20("token0", "TK0", 0); token1 = new ERC20("token1", "TK1", 0); cpamm = new CPAMM(address(token0), address(token1)); alice = new LiquidityProvider(cpamm); bob = new Swapper(cpamm); // Fund Alice and Bob with tokens token0.mint(address(alice), 1000); token1.mint(address(alice), 1000); token0.mint(address(bob), 1000); token1.mint(address(bob), 1000); } }

Creating Debugging Scenarios

Now that the system is deployed, we can create debugging scenarios. Here’s a scenario where Alice adds liquidity. A scneario is just plain-old Solidity functions.

contract DebugCPAMM { // ... constructor code ... function debugAddLiquidity() public { uint256 aliceShares = alice.addLiquidity(100, 100); } }

You can start debugging the scenario by clicking the ▷ Debug that appears above the function.

The following debugSwap function outlines a more complex scenario where Alice adds liquidity, Bob swaps tokens, and Alice eventually withdraws her liquidity. We also check balances before and after these operations to understand how the market actions impact users:

function debugSwap() public { // Balances before operations uint256 alice0Before = token0.balanceOf(address(alice)); uint256 alice1Before = token1.balanceOf(address(alice)); uint256 bob0Before = token0.balanceOf(address(bob)); uint256 bob1Before = token1.balanceOf(address(bob)); // Alice adds liquidity uint256 aliceShares = alice.addLiquidity(100, 100); // Bob swaps 50 tokens bob.swap(address(token0), 50); // Alice removes liquidity alice.removeLiquidity(aliceShares); // Balances after operations uint256 alice0After = token0.balanceOf(address(alice)); uint256 alice1After = token1.balanceOf(address(alice)); uint256 bob0After = token0.balanceOf(address(bob)); uint256 bob1After = token1.balanceOf(address(bob)); }

The Advantages of Debugging with Simbolik

Using digital actors to simulate behavior has clear advantages: your debugging sessions become reproducible, easily shareable, and help onboard new team members more efficiently. Debugging scenarios outline exactly how a system should be deployed and interacted with, which is especially valuable during security audits. Auditors can start analyzing your code immediately with pre-configured scenarios. For best practices, consider committing your debugging scripts to your codebase, similar to unit tests.

Start Experimenting!

Now it’s your turn to experiment with these debugging scenarios. Observe how balances shift as you tweak parameters, and try building new scenarios.

If you have any questions, refer to our documentation, join our Discord, or our Telegram group. We’d love to hear from you! Also, don’t miss meeting our team at DevCon Bangkok.