DEV Community

Cover image for Building a Solidity Trading Executor with Foundry and TypeScript
hamssog
hamssog

Posted on Originally published at hamssog.substack.com

Building a Solidity Trading Executor with Foundry and TypeScript

Most trading bot examples stop at the strategy layer:

Price update
   ↓
Signal
   ↓
Buy / Sell
Enter fullscreen mode Exit fullscreen mode

That is enough to demonstrate an idea.

It is not enough to build reusable trading infrastructure.

Once a strategy starts executing real transactions, the system needs to deal with authorization, approved contracts, slippage, deadlines, transaction state, events, failed execution, and reconciliation.

For this project, I am building a reusable EVM Trading Executor with:

  • Solidity
  • Foundry
  • TypeScript
  • EVM-compatible protocols

The executor is designed to sit underneath different applications rather than being tied to one trading strategy.

Strategy
   ↓
Risk Checks
   ↓
Execution Request
   ↓
Solidity Trading Executor
   ↓
Protocol
   ↓
On-chain Result
   ↓
Events
   ↓
Reconciliation
Enter fullscreen mode Exit fullscreen mode

Why build an execution layer?

A strategy answers:

Should I trade?

An execution system answers:

Can I execute that trade under the required constraints?

Those are different responsibilities.

For example, a strategy may produce:

tokenIn
tokenOut
amountIn
minimumAmountOut
deadline
router
Enter fullscreen mode Exit fullscreen mode

The execution layer should validate those parameters before interacting with an external contract.

That gives the system a clear boundary:

+----------------------+
| Strategy             |
|                      |
| Signal / Opportunity |
+----------+-----------+
           |
           v
+----------------------+
| Trading Executor     |
|                      |
| Validation           |
| Security             |
| Execution            |
+----------+-----------+
           |
           v
+----------------------+
| EVM Protocol         |
+----------------------+
Enter fullscreen mode Exit fullscreen mode

Project structure

A simple project structure can look like:

evm-trading-executor/
├── src/
│   ├── TradingExecutor.sol
│   ├── interfaces/
│   │   └── ITradingExecutor.sol
│   └── libraries/
├── test/
│   ├── TradingExecutor.t.sol
│   ├── TradingExecutorFuzz.t.sol
│   └── TradingExecutorInvariant.t.sol
├── script/
│   └── Deploy.s.sol
├── lib/
├── foundry.toml
└── ts/
    ├── executor.ts
    └── types.ts
Enter fullscreen mode Exit fullscreen mode

The Solidity side owns the execution rules.

The TypeScript side handles application-level orchestration.


The Solidity executor

The first version should keep the core interface small.

interface ITradingExecutor {
    function executeTrade(
        address router,
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        uint256 amountOutMinimum,
        uint256 deadline
    ) external returns (uint256 amountOut);
}
Enter fullscreen mode Exit fullscreen mode

The important part is the separation of responsibilities.

The executor receives a trade request.

It does not need to know why the strategy generated it.


Access control

The executor should not allow arbitrary addresses to submit trades.

A basic implementation can use an executor role:

bytes32 public constant EXECUTOR_ROLE =
    keccak256("EXECUTOR_ROLE");
Enter fullscreen mode Exit fullscreen mode

Then the execution function can enforce the role:

function executeTrade(
    address router,
    address tokenIn,
    address tokenOut,
    uint256 amountIn,
    uint256 amountOutMinimum,
    uint256 deadline
) external onlyRole(EXECUTOR_ROLE) returns (uint256 amountOut) {
    // validation and execution
}
Enter fullscreen mode Exit fullscreen mode

This creates an explicit trust boundary.

A production architecture can separate:

Admin
 ├── configuration
 └── role management

Executor
 └── trade execution

Emergency Operator
 └── pause / recovery
Enter fullscreen mode Exit fullscreen mode

Keeping these responsibilities separate makes the system easier to operate.


Token allowlists

A reusable executor should not blindly interact with arbitrary tokens.

A simple allowlist is:

mapping(address => bool) public allowedTokens;
Enter fullscreen mode Exit fullscreen mode

Then validate both sides of a trade:

require(
    allowedTokens[tokenIn],
    "TOKEN_IN_NOT_ALLOWED"
);

require(
    allowedTokens[tokenOut],
    "TOKEN_OUT_NOT_ALLOWED"
);
Enter fullscreen mode Exit fullscreen mode

This is a small feature with an important purpose.

If a configuration error or compromised service generates an unexpected token address, the executor can reject the request.


Router allowlists

The same approach applies to protocol routers.

mapping(address => bool) public allowedRouters;
Enter fullscreen mode Exit fullscreen mode

Before execution:

require(
    allowedRouters[router],
    "ROUTER_NOT_ALLOWED"
);
Enter fullscreen mode Exit fullscreen mode

Instead of:

execute arbitrary target
Enter fullscreen mode Exit fullscreen mode

the architecture becomes:

Executor
   ↓
Approved Router
   ↓
Approved Protocol
Enter fullscreen mode Exit fullscreen mode

That is much easier to reason about from a security perspective.


Slippage protection

Automated trading should not assume that a quote remains valid indefinitely.

A trade request should include a minimum acceptable output:

uint256 amountOutMinimum;
Enter fullscreen mode Exit fullscreen mode

The executor can then pass the constraint into the underlying protocol.

Conceptually:

Expected output:     100 tokens
Minimum accepted:    98 tokens
Actual output:      101 tokens
Enter fullscreen mode Exit fullscreen mode

The trade can proceed.

But:

Expected output:     100 tokens
Minimum accepted:    98 tokens
Actual output:       94 tokens
Enter fullscreen mode Exit fullscreen mode

should fail.

This is one of the most important differences between a trading executor and a simple transaction sender.


Deadline protection

Execution parameters can also become stale.

The executor should therefore validate the deadline:

require(
    block.timestamp <= deadline,
    "DEADLINE_EXPIRED"
);
Enter fullscreen mode Exit fullscreen mode

This makes time part of the execution contract.

An old request should not remain valid forever.


Validation before external calls

I prefer validating everything possible before making an external call.

A simplified execution flow is:

1. Check caller
2. Check amount
3. Check token allowlist
4. Check router allowlist
5. Check deadline
6. Prepare execution
7. Call protocol
8. Emit event
Enter fullscreen mode Exit fullscreen mode

This has two advantages:

First, invalid requests fail early.

Second, the external protocol is only called after the executor has verified its own rules.


Events

The execution contract should provide a useful event stream.

For example:

event TradeExecuted(
    address indexed executor,
    address indexed router,
    address indexed tokenIn,
    address tokenOut,
    uint256 amountIn,
    uint256 amountOut
);
Enter fullscreen mode Exit fullscreen mode

The off-chain system can consume this event and update application state.

Solidity Executor
      ↓
TradeExecuted
      ↓
  Indexer
      ↓
Position Engine
      ↓
     PnL
      ↓
Dashboard / Alerts
Enter fullscreen mode Exit fullscreen mode

This is much cleaner than trying to reconstruct every execution entirely from application logs.


TypeScript execution layer

The TypeScript service sits above the contract.

Its job is to prepare and submit execution requests.

A simplified structure is:

type TradeRequest = {
  router: `0x${string}`;
  tokenIn: `0x${string}`;
  tokenOut: `0x${string}`;
  amountIn: bigint;
  amountOutMinimum: bigint;
  deadline: bigint;
};
Enter fullscreen mode Exit fullscreen mode

The service can then construct the transaction:

const hash = await walletClient.writeContract({
  address: executorAddress,
  abi: executorAbi,
  functionName: "executeTrade",
  args: [
    request.router,
    request.tokenIn,
    request.tokenOut,
    request.amountIn,
    request.amountOutMinimum,
    request.deadline,
  ],
});
Enter fullscreen mode Exit fullscreen mode

The exact client library can vary.

The important architecture is:

TypeScript
   ↓
Build execution request
   ↓
Validate application state
   ↓
Call Solidity executor
   ↓
Monitor transaction
Enter fullscreen mode Exit fullscreen mode

Type safety matters

Trading systems move a lot of numeric values:

amountIn
amountOut
price
slippage
gas
fees
balances
reserves
Enter fullscreen mode Exit fullscreen mode

Using JavaScript floating-point numbers for on-chain amounts is dangerous.

I keep token amounts in integer form:

const amountIn = 1000000000000000000n;
Enter fullscreen mode Exit fullscreen mode

and convert only at presentation boundaries.

That keeps calculations aligned with EVM integer arithmetic.


Transaction state

Calling:

writeContract(...)
Enter fullscreen mode Exit fullscreen mode

does not mean the trade is completed.

A useful state machine is:

READY
  ↓
SUBMITTED
  ↓
CONFIRMED
  ↓
RECONCILED
Enter fullscreen mode Exit fullscreen mode

With failure paths:

SUBMITTED
   ├── REVERTED
   ├── DROPPED
   ├── REPLACED
   └── CONFIRMED
Enter fullscreen mode Exit fullscreen mode

The application should distinguish these states.

For example:

transaction submitted
Enter fullscreen mode Exit fullscreen mode

is not equivalent to:

position updated
Enter fullscreen mode Exit fullscreen mode

The latter requires reconciliation with on-chain state.


Reconciliation

After a transaction is confirmed, the off-chain application should verify what actually happened.

A simplified process:

Transaction receipt
       ↓
Execution event
       ↓
Actual token amounts
       ↓
Position update
       ↓
PnL update
Enter fullscreen mode Exit fullscreen mode

This matters particularly for partial fills, refunds, fee deductions, or protocol-specific execution behavior.

A reliable system should use actual on-chain results rather than trusting only the parameters that were originally requested.


Foundry tests

The contract layer needs more than one successful test.

My testing structure is:

Tests
├── Unit
├── Fuzz
├── Invariant
└── Fork
Enter fullscreen mode Exit fullscreen mode

Unit testing

Start with deterministic behavior.

For example:

function test_RejectsUnauthorizedCaller() public {
    vm.prank(attacker);

    vm.expectRevert();
    executor.executeTrade(
        router,
        tokenIn,
        tokenOut,
        amountIn,
        minimumOut,
        deadline
    );
}
Enter fullscreen mode Exit fullscreen mode

Other unit tests should cover:

authorized execution
blocked token
blocked router
expired deadline
invalid amount
successful execution
reverted execution
Enter fullscreen mode Exit fullscreen mode

Fuzz testing

Trading values have a huge input space.

Fuzz testing allows Foundry to generate different values automatically.

For example:

function testFuzz_AmountValidation(
    uint256 amountIn
) public {
    // validate execution behavior
}
Enter fullscreen mode Exit fullscreen mode

This helps find edge cases around:

  • zero values
  • very large amounts
  • boundary conditions
  • unexpected combinations of parameters

Invariant testing

Some properties should always remain true.

For example:

Unauthorized accounts cannot execute trades.
Enter fullscreen mode Exit fullscreen mode

Another:

Blocked routers cannot be used.
Enter fullscreen mode Exit fullscreen mode

And:

Expired execution requests cannot execute.
Enter fullscreen mode Exit fullscreen mode

These properties are stronger than testing one specific transaction.

They define the rules the executor is supposed to preserve across many state transitions.


Fork testing

Mocks are useful for isolated contract tests.

Fork tests provide another layer.

The idea is:

Real EVM state
      ↓
   Fork
      ↓
Trading Executor
      ↓
Real protocol contracts
Enter fullscreen mode Exit fullscreen mode

This helps expose integration problems that do not appear in fully mocked environments.

For a trading system, integration behavior is critical.


Protocol adapters

One executor may eventually need to support multiple protocols.

I prefer keeping protocol-specific code separate.

                 Trading Executor
                        |
        +---------------+---------------+
        |               |               |
        v               v               v
     Adapter A       Adapter B       Adapter C
        |               |               |
        v               v               v
     Protocol A      Protocol B      Protocol C
Enter fullscreen mode Exit fullscreen mode

The executor owns shared rules such as:

authorization
allowlists
slippage
deadlines
events
Enter fullscreen mode Exit fullscreen mode

The adapter owns protocol-specific behavior.

That keeps the core contract from becoming one enormous protocol-specific implementation.


Where the executor fits in a real trading system

The smart contract is only one layer.

A larger application might look like:

+---------------------------+
| Frontend / Trading UI     |
+-------------+-------------+
              |
              v
+---------------------------+
| API / Trading Service     |
+-------------+-------------+
              |
              v
+---------------------------+
| Strategy + Risk Engine    |
+-------------+-------------+
              |
              v
+---------------------------+
| Transaction Manager       |
+-------------+-------------+
              |
              v
+---------------------------+
| Solidity Trading Executor |
+-------------+-------------+
              |
              v
+---------------------------+
| EVM Protocol              |
+---------------------------+
              |
              v
+---------------------------+
| Indexer / Reconciliation  |
+---------------------------+
Enter fullscreen mode Exit fullscreen mode

Each layer has a different responsibility.

That separation makes debugging much easier.


Reusing the same infrastructure

Once the execution layer is isolated, different applications can use it.

For example:

                    EVM Trading Executor
                            |
        +-------------------+-------------------+
        |                   |                   |
        v                   v                   v
   Sniper Bot         Copy Trading         Arbitrage
        |                   |                   |
        +-------------------+-------------------+
                            |
                            v
                      EVM Execution
Enter fullscreen mode Exit fullscreen mode

This is useful because the strategy can evolve without rewriting the fundamental execution controls.

The same pattern can also support:

  • Pons trading systems
  • Stock Token applications
  • automated arbitrage
  • copy trading
  • trading terminals
  • custom EVM products

What I am actually proving with this project

The main purpose of this repository is not to show that I can write:

contract TradingExecutor {}
Enter fullscreen mode Exit fullscreen mode

The goal is to demonstrate the complete engineering boundary around execution.

That means:

Solidity
+
EVM architecture
+
TypeScript integration
+
Access control
+
Protocol validation
+
Slippage protection
+
Transaction lifecycle
+
Event design
+
Foundry testing
+
Reconciliation
Enter fullscreen mode Exit fullscreen mode

That is the infrastructure I would reuse when turning a trading idea into an actual application.


GitHub

The project is being developed as:

evm-trading-executor

Recommended repository layout:

src/
test/
script/
ts/
foundry.toml
README.md
Enter fullscreen mode Exit fullscreen mode

The README should include:

Architecture
Installation
Environment variables
Deployment
Contract interface
TypeScript usage
Testing
Fork testing
Failure handling
Enter fullscreen mode Exit fullscreen mode

A repository is much more useful to a potential client when it explains both what was built and how the system is intended to be extended.


Final architecture

The complete flow is:

Market / On-chain Data
          ↓
       Strategy
          ↓
      Risk Checks
          ↓
   Execution Request
          ↓
+-----------------------+
| Solidity Executor     |
|                       |
| Access Control        |
| Allowlists            |
| Slippage              |
| Deadline              |
| Validation            |
| Execution             |
| Events                |
+-----------+-----------+
            |
            v
      EVM Protocol
            |
            v
     Transaction Result
            |
            v
       Reconciliation
            |
            v
     Position / PnL State
Enter fullscreen mode Exit fullscreen mode

The strategy decides what should happen.

The execution layer makes sure the requested action follows the application's rules.

That distinction is the foundation I use when building reusable EVM trading infrastructure.


Next extensions

The next iterations can add:

Protocol adapters
Nonce management
Gas policies
Pause / emergency controls
Multi-wallet execution
Execution simulation
MEV-aware routing
Advanced monitoring
Enter fullscreen mode Exit fullscreen mode

The same executor architecture can then become part of a larger custom trading product rather than remaining an isolated Solidity demo.

Top comments (0)