Skip to main content

Use Case P03: Uniswap V2 Stop Order Demo

Overview

In this lesson, we’ll go through the demo of a simple Reactive smart contract that implements a stop order upon a Uniswap V2 pool. This RSC listens for Sync events that we’ve discussed earlier and runs the order on the destination contract, thus effectively implementing a stop order.

By the conclusion of this lesson, we will have covered:

  • What the setup looks like - the source smart contract, the reactive smart contract, the destination smart contract.

  • How it works and what exactly the reactive smart contract does.

  • How to deploy the reactive smart contract and test it.

This demo builds on the basic reactive example outlined in src/demos/basic/README.md. Refer to that document for an outline of fundamental concepts and architecture of reactive applications.

The demo implements simple stop orders for Uniswap V2 liquidity pools. The application monitors the specified Uniswap V2 pair, and as soon as the exchange rate reaches a given threshold, initiates a sale of assets through that same pair.

Origin Chain Contract

The stop order application subscribes to Sync log records produced by a given Uniswap V2 token pair contract, normally emitted on swaps and deposition or withdrawal of liquidity from the pool. Additionally, the reactive contract subscribes to events from its callback contract to deactivate the stop order on completion. We expect this loopback of events from the callback contract to be an important part of many reactive applications.

Reactive Contract

The reactive contract for stop orders subscribes to the specified L1 pair contract. Sync events received allow the reactive to compute the current exchange rate for the two tokens in the pool. As soon as the rate reaches the threshold given on the contract's deployment, it requests a callback to L1 to sell the assets.

Uniswap V2 Stop Order

Upon initiating the order's execution, the reactive contract begins waiting for the Stop event from the L1 contract (which serves as both source and destination in this case), indicating the successful completion of the order. Having received that, the reactive contract goes dormant, reverting all calls to it. Unlike the simple contract in the basic demo, this reactive contract is stateful.

The reactive contract is fully configurable and can be used with any Uniswap V2-compatible pair contract. The reactive contract for this demo is implemented in UniswapDemoStopOrderReactive.sol.

Destination Chain Contract

This contract is responsible for the actual execution of the stop order. The client must allocate the token allowance for the callback contract, and ensure that they have sufficient tokens for the order's execution. Once the Reactive Network performs the callback transaction, this contract verifies the caller's address to prevent misuse or abuse, checks the current exchange rate against the given threshold, checks the allowance and token balance, and then performs an exact token swap through the Uniswap V2 router contract, returning the earned tokens to the client. Once the order has been executed, the callback contract emits a Stop log record picked up by the reactive contract as described in the section above.

The callback contract is stateless and may be used by any number of reactive stop order contracts, as long as they use the same router contract. The callback contract is implemented in UniswapDemoStopOrderCallback.sol.

Further Considerations

While this demo covers a fairly realistic use case, it's not a production-grade implementation, which would require more safety and sanity checks and use a more complicated flow of state for its reactive contract. Instead, this demo is intended to demonstrate several more features of Reactive Network compared to the basic demo, namely:

  • Subscription to heterogeneous L1 events.

  • Stateful reactive contracts.

  • Loopback data flow between the reactive contract and the destination chain contract.

  • Basic sanity checks are required in destination chain contracts, both for security reasons and because callback execution is not synchronous with the execution of the reactive contract.

Nonetheless, a few further improvements could be made to bring this implementation closer to a practical stop order implementation, in particular:

  • Leveraging dynamic event subscription to allow a single reactive contract to handle multiple arbitrary orders.

  • Additional sanity checks and retry policy in the reactive contract.

  • Support for arbitrary routers on the destination side.

  • More elaborate data flow between reactive contract and destination chain contract.

  • Support for alternate DEXes.

Deployment & Testing

You will need the following environment variables configured appropriately to follow this script:

  • SEPOLIA_RPC
  • SEPOLIA_PRIVATE_KEY
  • REACTIVE_RPC
  • REACTIVE_PRIVATE_KEY
  • SYSTEM_CONTRACT_ADDR

If you want to test this live, you will need some tokens and a Uniswap V2 liquidity pool for them. You can use any pre-existing tokens and pair, or you can deploy your own, e.g., the barebones ERC20 token provided in UniswapDemoToken.sol.

The destination chain contract is deployed to Sepolia at 0x7B7FDD139DaCF06d236C999E23cF2eac36C349C1. It is configured to use the Uniswap V2 router at 0xC532a74256D3Db42D0Bf7a0400fEFDbad7694008, which is associated with the factory contract at 0x7E0987E5b3a30e3f2828572Bb659A548460a3003. It also doesn't validate the caller's address to simplify testing. If you can use the pairs deployed by this factory, you do not need to deploy your own instance of the contract. In case you do need your own destination chain contract, deploy as follows (recommended rpc-url: https://rpc2.sepolia.org):

forge create --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY src/demos/uniswap-v2-stop-order/UniswapDemoStopOrderCallback.sol:UniswapDemoStopOrderCallback --constructor-args $AUTHORIZED_CALLER_ADDRESS $UNISWAP_V2_ROUTER_ADDRESS

Where the AUTHORIZED_CALLER_ADDRESS should contain the address you want to authorize to perform the callbacks, or 0x0000000000000000000000000000000000000000 to skip this check. UNISWAP_V2_ROUTER_ADDRESS should point to the V2-compatible router you want to use.

To initiate a new stop order, you should authorize your destination chain contract to spend your tokens like so:

cast send $TOKEN_ADDRESS 'approve(address,uint256)' --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_EY $CALLBACK_CONTRACT_ADDR 1000000000000000000

The last parameter is the raw amount you want to authorize. For tokens with 18 decimal places, the above is equivalent to allowing the callback to spend a single token.

Now you need to deploy the reactive stop order contract to the Reactive Network (Kopli Testnet Network Information):

forge create --rpc-url $REACTIVE_RPC --private-key $REACTIVE_PRIVATE_KEY src/demos/uniswap-v2-stop-order/UniswapDemoStopOrderReactive.sol:UniswapDemoStopOrderReactive --constructor-args $SYSTEM_CONTRACT_ADDRESS $UNISWAP_V2_PAIR_ADDRESS $CALLBACK_CONTRACT_ADDRESS $CLIENT_WALLET $DIRECTION_BOOLEAN $EXCHANGE_RATE_DENOMINATOR $EXCHANGE_RATE_NUMERATOR

The SYSTEM_CONTRACT_ADDR should point to the system contract handling event subscriptions. The rest of the variables should be self-explanatory, except for DIRECTION_BOOLEAN, which specifies whether the order is for selling token0 or token1 of the pair in question. You're all set!

Performing the swaps through the pair being monitored, you should be able to bring the exchange rate below the threshold, which should initiate the execution of your stop order. You may want to test separate components manually, feeding events to the reactive contract or activating the callback manually like so:

cast call $REACTIVE_CONTRACT_ADDRESS 'react(uint256,address,uint256,uint256,uint256,uint256,bytes)' --trace --verbose --rpc-url $REACTIVE_RPC --private-key $REACTIVE_PRIVATE_KEY 0 0xED32ba8b09Ced902b1c49E2a1F384AfC98C1330C 0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1 0 0 0 0x00000000000000000000000000000000000000000000000098a7d9b8314c000000000000000000000000000000000000000000000000000083d6c7aab6360000

The parameters being fed to the react() method here are as follows:

  • Chain ID, which can normally be ignored while testing.

  • Originating contract's address, set to the monitored pair's address in the example above.

  • The four topics of the log record, with the example above corresponding to the Uniswap V2's Sync event.

  • Payload, containing the pair's remaining reserves for the Sync event. These could be encoded by using any ABI lib, or pulled from an actual Sync event using any block explorer software.

Assuming the exchange rate is below the threshold, the call should produce a trace similar to the following:

Traces:
[20319] 0x0c189A26E0AD06f8E12179280d9e8fB0EE1648C2::90dfa8f4(0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ed32ba8b09ced902b1c49e2a1f384afc98c1330c1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000098a7d9b8314c000000000000000000000000000000000000000000000000000083d6c7aab6360000)
├─ emit topic 0: 0x8dd725fa9d6cd150017ab9e60318d40616439424e2fade9c1c58854950917dfc
│ topic 1: 0x0000000000000000000000000000000000000000000000000000000000000000
│ topic 2: 0x0000000000000000000000007b7fdd139dacf06d236c999e23cf2eac36c349c1
│ topic 3: 0x00000000000000000000000000000000000000000000000000000000000f4240
│ data: 0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a40ac73c12000000000000000000000000ed32ba8b09ced902b1c49e2a1f384afc98c1330c000000000000000000000000afefa3fec75598e868b8527231db8c431e51c2ae00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000
└─ ← ()

This log record indicates to the Reactive Network that the contract wants to perform a callback to L1. See the technical reference for more details.

Calling the destination chain contract is quite straightforward:

cast send $CALLBACK_CONTRACT_ADDRESS 'stop(address,address,bool,uint256,uint256)' --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PRIVATE_KEY $UNISWAP_V2_PAIR_ADDRESS $CLIENT_WALLET $DIRECTION_BOOLEAN $EXCHANGE_RATE_DENOMINATOR $EXCHANGE_RATE_NUMERATOR

The environment variables here are the same as in the example above for deploying the reactive contract. Note that the destination chain contract is unaware of the contracts deployed to the Reactive Network. As long as the call passes the caller address check, and there is both an allowance of the specified tokens from the $CLIENT_WALLET and sufficient tokens in their account, the callback contract will perform any sale below the specified rate.

Conclusion

Through this lesson, we've seen the prowess of Reactive Smart Contracts (RSCs) in real-world applications, demonstrating their ability to interact with and respond to on-chain events seamlessly. In our specific case, we dissected how RSCs are utilized to automate stop orders on Uniswap V2, providing a clear view of their operational mechanics and the integration with callback contracts for order completion.

We've learned about the intricate dance between source chain contracts and reactive contracts, how Sync events drive the reactive mechanism, and the stateful nature of reactive contracts in managing stop orders. This exploration not only showcases what RSCs are capable of but also equips us with practical insights into building and deploying responsive, decentralized financial tools in the blockchain ecosystem.