Most trading bot examples stop at the strategy layer:
Price update
↓
Signal
↓
Buy / Sell
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
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
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 |
+----------------------+
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
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);
}
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");
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
}
This creates an explicit trust boundary.
A production architecture can separate:
Admin
├── configuration
└── role management
Executor
└── trade execution
Emergency Operator
└── pause / recovery
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;
Then validate both sides of a trade:
require(
allowedTokens[tokenIn],
"TOKEN_IN_NOT_ALLOWED"
);
require(
allowedTokens[tokenOut],
"TOKEN_OUT_NOT_ALLOWED"
);
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;
Before execution:
require(
allowedRouters[router],
"ROUTER_NOT_ALLOWED"
);
Instead of:
execute arbitrary target
the architecture becomes:
Executor
↓
Approved Router
↓
Approved Protocol
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;
The executor can then pass the constraint into the underlying protocol.
Conceptually:
Expected output: 100 tokens
Minimum accepted: 98 tokens
Actual output: 101 tokens
The trade can proceed.
But:
Expected output: 100 tokens
Minimum accepted: 98 tokens
Actual output: 94 tokens
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"
);
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
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
);
The off-chain system can consume this event and update application state.
Solidity Executor
↓
TradeExecuted
↓
Indexer
↓
Position Engine
↓
PnL
↓
Dashboard / Alerts
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;
};
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,
],
});
The exact client library can vary.
The important architecture is:
TypeScript
↓
Build execution request
↓
Validate application state
↓
Call Solidity executor
↓
Monitor transaction
Type safety matters
Trading systems move a lot of numeric values:
amountIn
amountOut
price
slippage
gas
fees
balances
reserves
Using JavaScript floating-point numbers for on-chain amounts is dangerous.
I keep token amounts in integer form:
const amountIn = 1000000000000000000n;
and convert only at presentation boundaries.
That keeps calculations aligned with EVM integer arithmetic.
Transaction state
Calling:
writeContract(...)
does not mean the trade is completed.
A useful state machine is:
READY
↓
SUBMITTED
↓
CONFIRMED
↓
RECONCILED
With failure paths:
SUBMITTED
├── REVERTED
├── DROPPED
├── REPLACED
└── CONFIRMED
The application should distinguish these states.
For example:
transaction submitted
is not equivalent to:
position updated
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
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
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
);
}
Other unit tests should cover:
authorized execution
blocked token
blocked router
expired deadline
invalid amount
successful execution
reverted execution
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
}
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.
Another:
Blocked routers cannot be used.
And:
Expired execution requests cannot execute.
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
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
The executor owns shared rules such as:
authorization
allowlists
slippage
deadlines
events
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 |
+---------------------------+
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
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 {}
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
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
The README should include:
Architecture
Installation
Environment variables
Deployment
Contract interface
TypeScript usage
Testing
Fork testing
Failure handling
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
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
The same executor architecture can then become part of a larger custom trading product rather than remaining an isolated Solidity demo.
Top comments (0)