DEV Community

Cover image for Building a Trading Executor in Solidity for Robinhood Chain
hamssog
hamssog

Posted on Originally published at hamssog.substack.com

Building a Trading Executor in Solidity for Robinhood Chain

A trading bot can be written almost entirely in TypeScript.

For a simple application, that can be enough:

Strategy
   ↓
TypeScript
   ↓
Router
   ↓
Robinhood Chain
Enter fullscreen mode Exit fullscreen mode

But as the system becomes more complex, there is another engineering layer worth designing deliberately:

the Solidity execution layer.

A custom smart contract can provide an onchain boundary for authorized execution, token handling, protocol adapters, validation, and event emission.

The application above it can still handle:

market data
strategy
risk
wallet orchestration
monitoring
reconciliation
Enter fullscreen mode Exit fullscreen mode

That gives us a complete architecture:

Trading Strategy
       ↓
Risk Engine
       ↓
TypeScript Backend
       ↓
Solidity Executor
       ↓
EVM
       ↓
Robinhood Chain
Enter fullscreen mode Exit fullscreen mode

Robinhood Chain is fully EVM-compatible. Its current documentation states that Solidity and Vyper contracts can be deployed using standard Ethereum tooling, including Foundry and Hardhat. Robinhood Chain mainnet currently uses chain ID 4663.

This article focuses on how I would build the Solidity layer for a trading system.


Why put part of a trading system onchain?

Not every trading bot needs a custom contract.

For some systems:

TypeScript
   ↓
Existing Protocol Contract
Enter fullscreen mode Exit fullscreen mode

is perfectly reasonable.

A custom Solidity layer becomes more interesting when the application needs an explicit execution boundary.

For example:

TypeScript
   ↓
Trade Intent
   ↓
Solidity Executor
   ↓
Protocol
Enter fullscreen mode Exit fullscreen mode

The Solidity contract can enforce rules such as:

authorized executor
allowed assets
allowed routers
maximum trade amount
minimum output
deadline
pause state
Enter fullscreen mode Exit fullscreen mode

Meanwhile, the application layer handles:

market discovery
strategy
pricing
risk scoring
wallet management
transaction monitoring
Enter fullscreen mode Exit fullscreen mode

The contract is therefore not the trading strategy.

It is the onchain enforcement and execution layer.


The architecture

A production-oriented implementation can be split into:

                    TRADING APPLICATION
                           |
            +--------------+--------------+
            |                             |
        Strategy                       Risk Engine
            |                             |
            +--------------+--------------+
                           |
                      Trade Intent
                           |
                    TypeScript Backend
                           |
                      Solidity Executor
                           |
                    Protocol Adapter
                           |
                         EVM
                           |
                   Robinhood Chain
Enter fullscreen mode Exit fullscreen mode

After the transaction:

Robinhood Chain
       ↓
Events / Receipt
       ↓
Transaction Monitor
       ↓
Reconciliation
       ↓
Position State
Enter fullscreen mode Exit fullscreen mode

This creates a complete round trip.


Start with a constrained executor

I would avoid designing a contract that accepts arbitrary calldata from an untrusted caller.

A better starting point is a constrained executor.

For example:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IERC20 {
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) external returns (bool);
}

interface IRouter {
    function swap(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        uint256 minAmountOut,
        address recipient
    ) external returns (uint256 amountOut);
}

contract TradingExecutor {
    address public immutable owner;

    mapping(address => bool) public allowedRouter;
    mapping(address => bool) public allowedToken;

    bool public paused;

    error Unauthorized();
    error Paused();
    error RouterNotAllowed();
    error TokenNotAllowed();
    error InvalidAmount();
    error SlippageExceeded();

    event RouterUpdated(address indexed router, bool allowed);
    event TokenUpdated(address indexed token, bool allowed);
    event Paused(bool value);

    event TradeExecuted(
        bytes32 indexed operationId,
        address indexed router,
        address indexed tokenIn,
        address tokenOut,
        uint256 amountIn,
        uint256 amountOut
    );

    modifier onlyOwner() {
        if (msg.sender != owner) revert Unauthorized();
        _;
    }

    constructor(address initialOwner) {
        owner = initialOwner;
    }

    function setRouter(
        address router,
        bool allowed
    ) external onlyOwner {
        allowedRouter[router] = allowed;
        emit RouterUpdated(router, allowed);
    }

    function setToken(
        address token,
        bool allowed
    ) external onlyOwner {
        allowedToken[token] = allowed;
        emit TokenUpdated(token, allowed);
    }

    function setPaused(
        bool value
    ) external onlyOwner {
        paused = value;
        emit Paused(value);
    }

    function executeTrade(
        bytes32 operationId,
        address router,
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        uint256 minAmountOut,
        address recipient
    ) external onlyOwner returns (uint256 amountOut) {
        if (paused) revert Paused();
        if (!allowedRouter[router]) revert RouterNotAllowed();
        if (!allowedToken[tokenIn]) revert TokenNotAllowed();
        if (!allowedToken[tokenOut]) revert TokenNotAllowed();
        if (amountIn == 0) revert InvalidAmount();

        IERC20(tokenIn).transferFrom(
            msg.sender,
            address(this),
            amountIn
        );

        IERC20(tokenIn).approve(
            router,
            amountIn
        );

        amountOut = IRouter(router).swap(
            tokenIn,
            tokenOut,
            amountIn,
            minAmountOut,
            recipient
        );

        if (amountOut < minAmountOut) {
            revert SlippageExceeded();
        }

        emit TradeExecuted(
            operationId,
            router,
            tokenIn,
            tokenOut,
            amountIn,
            amountOut
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

This is still only a simplified example.

The important design principle is the constraint:

Caller
 ↓
Authorization
 ↓
Allowed Router
 ↓
Allowed Token
 ↓
Trade Parameters
 ↓
Execution
 ↓
Event
Enter fullscreen mode Exit fullscreen mode

The contract does not accept unlimited arbitrary execution.


Access control comes first

Trading contracts can potentially move valuable assets.

So the first question should be:

Who is allowed to call the execution function?

The simplest model is:

Owner
   ↓
Executor
   ↓
Trade
Enter fullscreen mode Exit fullscreen mode

But larger systems may need:

Admin
   ↓
Configuration

Operator
   ↓
Trading Execution

Guardian
   ↓
Emergency Pause
Enter fullscreen mode Exit fullscreen mode

The right model depends on custody and product requirements.

A multi-wallet system will usually need a different signer architecture from a single-wallet application.


The contract should validate what matters onchain

Some rules belong in TypeScript.

Some rules are safer when enforced directly by Solidity.

For example, market scanning belongs naturally offchain:

price feeds
technical indicators
historical analysis
strategy logic
Enter fullscreen mode Exit fullscreen mode

But an onchain executor can enforce:

allowed router
allowed token
maximum amount
minimum output
authorized caller
deadline
pause state
Enter fullscreen mode Exit fullscreen mode

This gives:

Offchain Strategy
       ↓
Onchain Enforcement
Enter fullscreen mode Exit fullscreen mode

rather than trying to put the entire trading strategy inside a contract.


Slippage should be explicit

Suppose a strategy wants to buy a token.

The application can calculate:

Expected Output = 1,250
Enter fullscreen mode Exit fullscreen mode

but the contract should not simply accept whatever comes back.

Instead:

Expected Output
       ↓
Minimum Acceptable Output
       ↓
Solidity
       ↓
Execute
Enter fullscreen mode Exit fullscreen mode

For example:

uint256 amountOut = router.swap(
    tokenIn,
    tokenOut,
    amountIn,
    minAmountOut,
    recipient
);

if (amountOut < minAmountOut) {
    revert SlippageExceeded();
}
Enter fullscreen mode Exit fullscreen mode

The exact router interface will depend on the protocol being integrated.

The important concept is that the acceptable execution boundary is explicit.


Deadlines are another useful boundary

A transaction that was valid when a strategy generated it may no longer be desirable several minutes later.

The TypeScript layer can calculate a deadline:

signal generated
      ↓
quote generated
      ↓
deadline = now + 15 seconds
Enter fullscreen mode Exit fullscreen mode

The contract can then reject execution after the deadline.

Conceptually:

error DeadlineExpired();

if (block.timestamp > deadline) {
    revert DeadlineExpired();
}
Enter fullscreen mode Exit fullscreen mode

This is especially useful for trading systems where quotes can become stale quickly.


Token approvals need careful handling

A common EVM interaction looks like:

Token
 ↓
approve
 ↓
Router
 ↓
swap
Enter fullscreen mode Exit fullscreen mode

But approvals are part of the security model.

A trading contract should avoid giving an arbitrary address unlimited approval.

A better architecture is:

Approved Router
      ↓
Token Approval
      ↓
Specific Execution
Enter fullscreen mode Exit fullscreen mode

The allowed router list can be controlled by the contract administrator.

For sensitive applications, token approvals should also be considered part of the deployment and operational review.


Events connect Solidity to the backend

The smart contract should emit structured events.

For example:

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

The TypeScript backend can then consume:

TradeExecuted
      ↓
Event Decoder
      ↓
Trade Record
      ↓
Position Engine
      ↓
Portfolio
Enter fullscreen mode Exit fullscreen mode

This gives the backend a reliable link between the requested operation and the onchain result.


Operation IDs

I like giving each trade a deterministic application-level operation ID.

For example:

strategy
+
wallet
+
nonce
+
timestamp
Enter fullscreen mode Exit fullscreen mode

can produce:

operationId
Enter fullscreen mode Exit fullscreen mode

The contract records it in the event:

TradeExecuted(operationId, ...)
Enter fullscreen mode Exit fullscreen mode

The backend can then connect:

Strategy Signal
      ↓
Trade Intent
      ↓
operationId
      ↓
Transaction
      ↓
Event
      ↓
Position
Enter fullscreen mode Exit fullscreen mode

This becomes extremely useful when debugging execution.


Idempotent event processing

A blockchain indexer can receive the same event again.

For example:

RPC connection
    ↓
disconnect
    ↓
reconnect
    ↓
event replay
Enter fullscreen mode Exit fullscreen mode

If the backend simply processes every event as a new trade, the position may be doubled.

So I would construct an event identity from:

chainId
+
transactionHash
+
logIndex
Enter fullscreen mode Exit fullscreen mode

For example:

const eventId =
  `${chainId}:${transactionHash}:${logIndex}`;
Enter fullscreen mode Exit fullscreen mode

Then store that value with a database uniqueness constraint.

The logic becomes:

Event
 ↓
Already processed?
 ├── yes → ignore
 └── no  → process
Enter fullscreen mode Exit fullscreen mode

This is a small implementation detail with a large operational impact.


Transaction state still belongs in TypeScript

The Solidity contract can emit the final execution event.

The application still needs to track transaction state before that event exists.

I would use something like:

type TransactionState =
  | "CREATED"
  | "SIGNED"
  | "SUBMITTED"
  | "PENDING"
  | "CONFIRMED"
  | "FAILED"
  | "UNKNOWN";
Enter fullscreen mode Exit fullscreen mode

Normal execution:

CREATED
   ↓
SIGNED
   ↓
SUBMITTED
   ↓
PENDING
   ↓
CONFIRMED
Enter fullscreen mode Exit fullscreen mode

Failure path:

SUBMITTED
   ↓
RPC TIMEOUT
   ↓
UNKNOWN
   ↓
RECONCILIATION
Enter fullscreen mode Exit fullscreen mode

This is where smart-contract engineering meets backend engineering.


Never equate RPC failure with transaction failure

Consider:

Application
   ↓
broadcast transaction
   ↓
RPC timeout
Enter fullscreen mode Exit fullscreen mode

The timeout only tells us that the application did not receive the expected response.

It does not necessarily tell us:

transaction failed
Enter fullscreen mode Exit fullscreen mode

The transaction may already exist onchain.

So the correct flow is:

UNKNOWN
   ↓
Find transaction
   ↓
Check receipt
   ↓
Check events
   ↓
Update state
Enter fullscreen mode Exit fullscreen mode

Only then should the application decide what to do next.


Reconciliation

Suppose the application expected:

BUY 100 tokens
Enter fullscreen mode Exit fullscreen mode

The transaction completes.

The contract emits:

amountOut = 63
Enter fullscreen mode Exit fullscreen mode

The position engine must record:

63
Enter fullscreen mode Exit fullscreen mode

not:

100
Enter fullscreen mode Exit fullscreen mode

The reconciliation system can compare:

Database
   ↓
Transaction Receipt
   ↓
Trade Events
   ↓
Token Balance
   ↓
Canonical Position
Enter fullscreen mode Exit fullscreen mode

That gives the system a recovery mechanism after:

RPC failures
missed events
process crashes
partial execution
delayed confirmations
Enter fullscreen mode Exit fullscreen mode

Pons is a good example of why protocol adapters matter

The same architecture applies to Pons, but the protocol adapter needs to understand which Pons version is being used.

The current Pons documentation describes V2 as:

Create
  ↓
Bonding Curve
  ↓
Trade
  ↓
Graduate
  ↓
Uniswap V4 Pool
Enter fullscreen mode Exit fullscreen mode

Pons V2 also has protocol-specific launch and trading behavior such as its bonding-curve reserves, snipe-tax rules, and launch lifecycle.

That means a trading executor should not bury Pons-specific assumptions everywhere.

Instead:

Trading Engine
      ↓
Pons Adapter
      ↓
Pons Contract
Enter fullscreen mode Exit fullscreen mode

This keeps protocol-specific logic isolated.


Stock Tokens show another side of EVM integration

Robinhood's current documentation describes Stock Tokens as standard ERC-20 contracts with 18 decimals and onchain Chainlink price feeds. (docs.robinhood.com)

That means the same application architecture can be reused:

Stock Token
     ↓
ERC-20 Adapter
     ↓
Trading Executor
     ↓
Portfolio
Enter fullscreen mode Exit fullscreen mode

The strategy can remain separate from the contract implementation.

That allows the same backend to support:

arbitrage
momentum
rebalancing
signal-based trading
Enter fullscreen mode Exit fullscreen mode

without rebuilding the entire execution layer.


Contract testing with Foundry

A contract that compiles is not proof that it is safe.

I would test at multiple levels.

Unit Tests
    ↓
Integration Tests
    ↓
Fork Tests
    ↓
Testnet
    ↓
Mainnet
Enter fullscreen mode Exit fullscreen mode

For a trading executor, the minimum test surface should include:

authorized caller
unauthorized caller
allowed router
blocked router
allowed token
blocked token
zero amount
slippage failure
deadline failure
paused state
successful execution
event emission
Enter fullscreen mode Exit fullscreen mode

For example:

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

    vm.expectRevert(
        TradingExecutor.Unauthorized.selector
    );

    executor.executeTrade(
        operationId,
        router,
        tokenIn,
        tokenOut,
        amountIn,
        minAmountOut,
        recipient
    );
}
Enter fullscreen mode Exit fullscreen mode

The exact test setup will depend on the project.

The principle is more important:

test the rejection paths, not only the happy path.

Robinhood's current deployment guide recommends testing on testnet before deploying to mainnet.


Fork testing

Fork tests are particularly useful for protocol integrations.

Instead of mocking every dependency, a fork can reproduce a realistic EVM environment.

Conceptually:

Robinhood Chain State
        ↓
Fork
        ↓
Your Contract
        ↓
Real Protocol Contracts
        ↓
Test Execution
Enter fullscreen mode Exit fullscreen mode

This is useful when validating:

router behavior
token behavior
balances
allowances
events
execution paths
Enter fullscreen mode Exit fullscreen mode

before sending transactions against the live network.


Gas optimization

Gas matters.

But I would optimize in this order:

1. Correctness
2. Security
3. Test coverage
4. Observability
5. Gas optimization
Enter fullscreen mode Exit fullscreen mode

Useful optimization techniques may include:

custom errors
efficient storage
avoiding unnecessary storage reads
careful calldata usage
efficient event design
Enter fullscreen mode Exit fullscreen mode

But I would never sacrifice a clear security boundary just to save a small amount of gas.


Contract structure

A larger project could separate the Solidity code like this:

contracts/
├── executors/
│   └── TradingExecutor.sol
│
├── adapters/
│   ├── TokenAdapter.sol
│   ├── PonsAdapter.sol
│   └── RouterAdapter.sol
│
├── interfaces/
│   ├── IERC20.sol
│   └── IRouter.sol
│
├── libraries/
│   ├── Validation.sol
│   └── Types.sol
│
└── mocks/
Enter fullscreen mode Exit fullscreen mode

The TypeScript application can then sit beside it:

src/
├── strategy/
├── risk/
├── execution/
├── monitoring/
├── portfolio/
├── reconciliation/
└── api/
Enter fullscreen mode Exit fullscreen mode

This gives:

contracts/
     +
TypeScript
     =
Trading Infrastructure
Enter fullscreen mode Exit fullscreen mode

Multi-wallet systems

Multi-wallet trading adds another layer.

Imagine:

Wallet A
Wallet B
Wallet C
Wallet D
Enter fullscreen mode Exit fullscreen mode

The backend might coordinate all four.

But the contract should have an explicit authorization model.

For example:

Admin
  ↓
Authorized Executor
  ↓
Allowed Operation
  ↓
Protocol
Enter fullscreen mode Exit fullscreen mode

The exact architecture depends on whether the wallets are:

user-owned
operator-owned
custodial
non-custodial
Enter fullscreen mode Exit fullscreen mode

That decision should happen before contract design.


Emergency controls

Trading infrastructure can benefit from a pause mechanism.

For example:

function setPaused(
    bool value
) external onlyOwner {
    paused = value;
    emit Paused(value);
}
Enter fullscreen mode Exit fullscreen mode

Then:

Normal
 ↓
Trading Enabled

Emergency
 ↓
Pause

Investigation
 ↓
Reconciliation

Recovery
 ↓
Unpause
Enter fullscreen mode Exit fullscreen mode

A pause mechanism is not a replacement for security.

It is an additional operational control.


The complete execution lifecycle

Putting everything together:

MARKET DATA
     ↓
STRATEGY
     ↓
TRADE INTENT
     ↓
RISK ENGINE
     ↓
QUOTE
     ↓
TYPE SCRIPT
     ↓
SOLIDITY EXECUTOR
     ↓
PROTOCOL
     ↓
ROBINHOOD CHAIN
     ↓
TRANSACTION
     ↓
EVENTS
     ↓
RECONCILIATION
     ↓
POSITION
     ↓
PORTFOLIO
Enter fullscreen mode Exit fullscreen mode

This is the architecture I want to demonstrate when I say I build trading systems.

The smart contract is one layer.

The TypeScript engine is another.

The important part is how the two work together.


Where this fits into Pons and Stock Token products

The same engineering model can support:

Pons Sniper Bot
Pons Bundler
Pons Copy Trading Bot
Pons Trading Terminal
Stock Token Trading Bot
Stock Token Arbitrage Bot
Multi-Wallet Trading
Custom Execution Infrastructure
Enter fullscreen mode Exit fullscreen mode

For example:

Pons Sniper
    ↓
Launch Detection
    ↓
Strategy
    ↓
Risk
    ↓
Pons Adapter
    ↓
Execution
Enter fullscreen mode Exit fullscreen mode

or:

Stock Token Arbitrage
    ↓
Price Normalization
    ↓
Opportunity
    ↓
Risk
    ↓
Execution Contract
    ↓
Onchain Trade
Enter fullscreen mode Exit fullscreen mode

The application changes.

The underlying engineering principles stay similar.


My preferred development stack

For Robinhood Chain trading systems, the stack can look like:

Solidity
    ↓
Foundry
    ↓
EVM
    ↓
Robinhood Chain
    ↓
viem
    ↓
TypeScript
    ↓
PostgreSQL
    ↓
API / Dashboard
Enter fullscreen mode Exit fullscreen mode

That allows work across several levels:

Smart Contract
      ↓
Protocol Integration
      ↓
Trading Engine
      ↓
Backend
      ↓
Frontend
Enter fullscreen mode Exit fullscreen mode

This is the engineering surface I want my public projects to demonstrate.


Public implementation proof

I already work with this broader architecture across Pons and Stock Token projects.

Pons Sniper Bot

Pons V2 launch detection, curve execution, risk controls, transaction monitoring, and reconciliation.

GitHub: github.com/0xhamssog/pons-sniper-bot

Pons Bundler

Pons V2 launch-and-buy and multi-wallet execution.

GitHub: github.com/0xhamssog/pons-bundler

Stock Token Arbitrage Bot

Stock Token pricing, opportunity detection, and automated trading infrastructure.

GitHub: github.com/0xhamssog/robinhood-stock-token-arbitrage-bot

These projects provide a practical foundation for adding more protocol integrations and custom execution modules.


Smart Contract Development

Custom Solidity contracts, adapters, execution contracts, access control, events, and testing.

Trading Bot Development

Strategy engines, risk systems, execution, transaction monitoring, and reconciliation.

Pons Development

Sniper bots, bundlers, copy trading, wallet tracking, launch monitoring, and trading terminals.

Stock Token Development

Trading bots, arbitrage systems, portfolio tools, and automated execution.

Full Trading Applications

Frontend + API + trading engine + Solidity + wallet infrastructure + monitoring.

The starting point does not have to be a complete platform.

It can be one contract, one execution module, or one trading strategy.


Final Thoughts

A trading bot is often described as:

API
+
Strategy
+
Transaction
Enter fullscreen mode Exit fullscreen mode

But a more complete system is:

Strategy
     ↓
Risk
     ↓
Execution
     ↓
Solidity
     ↓
    EVM
     ↓
Transaction
     ↓
Events
     ↓
Reconciliation
     ↓
Position
Enter fullscreen mode Exit fullscreen mode

That is why I am expanding my Robinhood Chain work beyond application-level bots.

I want the public work to demonstrate the full engineering stack:

Solidity.

EVM.

TypeScript.

Protocol integration.

Trading infrastructure.

Risk management.

Transaction state.

Reconciliation.

For clients building on Robinhood Chain and Pons, that means I can work not only on the bot interface, but on the smart-contract and execution infrastructure underneath it.

Need a custom Robinhood Chain trading system?

I build custom Solidity/EVM contracts, Pons integrations, Stock Token trading bots, execution engines, multi-wallet systems, trading terminals, and full-stack trading applications.

Top comments (0)