Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions scenarios/uniswap-swaps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

Execute Uniswap V2 or V3 swaps with configurable parameters. This scenario allows you to test Uniswap DEX interactions by performing buy and sell transactions.

The scenario self-deploys everything it needs on a fresh network: WETH9, mock DAI tokens, two factories (each with its own router) and seeded liquidity. Select the version with `--uniswap-version` (default: 2). In v3 mode each DAI gets a pool on both factories at the configured `--fee-tier`, and swaps are routed through the canonical Uniswap v3 `SwapRouter`.
The scenario self-deploys everything it needs on a fresh network: two factories (each with its own router), a shared mock quote token, one mock DAI token per pair, and seeded liquidity. Every pair trades a DAI token against the quote token; both are plain ERC20s with a public `mint`, so **no ETH capital is needed beyond gas**: liquidity is minted into the pools by the liquidity-provider helper, and each child wallet mints its own quote tokens (see `--quote-funding`) both at startup and whenever it runs low. Select the version with `--uniswap-version` (default: 2). In v3 mode each DAI gets a pool on both factories at the configured `--fee-tier`, and swaps are routed through the canonical Uniswap v3 `SwapRouter`.

Every pool is seeded with 2000 quote tokens and 20,000,000 DAI, so the initial price is 10,000 DAI per quote token. Swap sizes (`--min-swap`, `--max-swap`, `--sell-threshold`) are denominated in DAI.

## Usage

Expand Down Expand Up @@ -30,21 +32,24 @@ spamoor uniswap-swaps [flags]
### Swap Settings
- `--uniswap-version` - Uniswap version to use, 2 or 3 (default: 2)
- `--fee-tier` - Uniswap v3 fee tier in hundredths of a bip: 500, 3000 or 10000 (default: 3000)
- `--pair-count` - Number of uniswap pairs to deploy (default: 1)
- `--min-swap` - Minimum swap amount in wei (default: 100000000000000000)
- `--max-swap` - Maximum swap amount in wei (default: 1000000000000000000000)
- `--buy-ratio` - Ratio of buy vs sell swaps (0-100, default: 50)
- `--pair-count` - Number of uniswap pairs (DAI tokens) to deploy (default: 1)
- `--min-swap` - Minimum swap amount in DAI wei (default: 100000000000000000)
- `--max-swap` - Maximum swap amount in DAI wei (default: 1000000000000000000000)
- `--buy-ratio` - Ratio of buy vs sell swaps (0-100, default: 40)
- `--slippage` - Slippage tolerance in basis points (default: 50)
- `--slippage-min` - Min per-trade slippage in basis points; 0 disables and uses the fixed `--slippage` (default: 0)
- `--slippage-max` - Max per-trade slippage in basis points; when greater than `--slippage-min`, each trade draws a uniform-random tolerance in `[min, max]` (default: 0)
- `--sell-threshold` - DAI balance threshold to force sell in wei (default: 100000000000000000000000)
- `--sell-threshold` - DAI balance threshold to force sell in wei (default: 50000000000000000000000)
- `--quote-funding` - Quote token amount in wei minted to each child wallet at startup and whenever a wallet cannot afford a buy (default: 5000000000000000000, i.e. 5 quote tokens = 50,000 DAI at the seeded price)

### Wallet Management
- `--max-wallets` - Maximum number of child wallets to use
- `--refill-amount` - ETH amount to fund each child wallet (default: 5)
- `--refill-balance` - Minimum ETH balance before refilling (default: 2)
- `--refill-interval` - Seconds between balance checks (default: 300)

Child wallets only spend ETH on gas, so the refill settings can be kept low.

### Client Settings
- `--client-group` - Client group to use for sending transactions
- `--deploy-client-group` - Client group to use for deployment transactions (same as --client-group if empty)
Expand All @@ -69,4 +74,9 @@ spamoor uniswap-swaps -p "<PRIVKEY>" -h http://rpc-host:8545 -t 2 --buy-ratio 0
Send Uniswap v3 swaps at the 0.3% fee tier:
```bash
spamoor uniswap-swaps -p "<PRIVKEY>" -h http://rpc-host:8545 -t 2 --uniswap-version 3 --fee-tier 3000
```
```

## Troubleshooting

- **Wallets keep minting instead of swapping**: `--quote-funding` is too small relative to `--max-swap`. A buy of N DAI costs roughly N / 10,000 quote tokens, so make sure the funding covers many buys.
- **Swaps revert with insufficient output**: the pool price moved more than the slippage tolerance allows between quoting and execution. Raise `--slippage` or lower `--max-swap` so single trades have less price impact.
109 changes: 20 additions & 89 deletions scenarios/uniswap-swaps/contract/PairLiquidityProvider.go

Large diffs are not rendered by default.

286 changes: 31 additions & 255 deletions scenarios/uniswap-swaps/contract/PairLiquidityProvider.sol
Original file line number Diff line number Diff line change
@@ -1,101 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

interface IERC721Receiver {
/**
* @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom}
* by `operator` from `from`, this function is called.
*
* It must return its Solidity selector to confirm the token transfer.
* If any other value is returned or the interface is not implemented by the recipient, the transfer will be
* reverted.
*
* The selector can be obtained in Solidity with `IERC721Receiver.onERC721Received.selector`.
*/
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}

interface IERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
// Minimal helper for the spamoor uniswap-swaps scenario (v2 mode).
//
// It seeds an ERC20/ERC20 pair on both routers in a single tx, minting both
// tokens on demand. The scenario's tokens are mock tokens with a public mint,
// so the helper never needs to be funded: the caller only pays gas.

function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
interface IMintableERC20 {
function approve(address spender, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
}

interface IDai is IERC20 {
function mint(address to, uint256 amount) external;
}

library TransferHelper {
/// @notice Transfers tokens from the targeted address to the given destination
/// @notice Errors with 'STF' if transfer fails
/// @param token The contract address of the token to be transferred
/// @param from The originating address from which the tokens will be transferred
/// @param to The destination address of the transfer
/// @param value The amount to be transferred
function safeTransferFrom(
address token,
address from,
address to,
uint256 value
) internal {
(bool success, bytes memory data) =
token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'STF');
}

/// @notice Transfers tokens from msg.sender to a recipient
/// @dev Errors with ST if transfer fails
/// @param token The contract address of the token which will be transferred
/// @param to The recipient of the transfer
/// @param value The value of the transfer
function safeTransfer(
address token,
address to,
uint256 value
) internal {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'ST');
}

/// @notice Approves the stipulated contract to spend the given allowance in the given token
/// @dev Errors with 'SA' if transfer fails
/// @param token The contract address of the token to be approved
/// @param to The target of the approval
/// @param value The amount of the given token the target will be allowed to spend
function safeApprove(
address token,
address to,
uint256 value
) internal {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.approve.selector, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'SA');
}

/// @notice Transfers ETH to the recipient address
/// @dev Fails with `STE`
/// @param to The destination of the transfer
/// @param value The value to be transferred
function safeTransferETH(address to, uint256 value) internal {
(bool success, ) = to.call{value: value}(new bytes(0));
require(success, 'STE');
}
}

interface IUniswapV2Router01 {
function factory() external pure returns (address);
function WETH() external pure returns (address);

interface IUniswapV2Router02 {
function addLiquidity(
address tokenA,
address tokenB,
Expand All @@ -106,183 +23,42 @@ interface IUniswapV2Router01 {
address to,
uint deadline
) external returns (uint amountA, uint amountB, uint liquidity);
function addLiquidityETH(
address token,
uint amountTokenDesired,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) external payable returns (uint amountToken, uint amountETH, uint liquidity);
function removeLiquidity(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external returns (uint amountA, uint amountB);
function removeLiquidityETH(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) external returns (uint amountToken, uint amountETH);
function removeLiquidityWithPermit(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external returns (uint amountA, uint amountB);
function removeLiquidityETHWithPermit(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external returns (uint amountToken, uint amountETH);
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts);
function swapTokensForExactTokens(
uint amountOut,
uint amountInMax,
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts);
function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
external
payable
returns (uint[] memory amounts);
function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
external
returns (uint[] memory amounts);
function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
external
returns (uint[] memory amounts);
function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
external
payable
returns (uint[] memory amounts);

function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB);
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut);
function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn);
function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts);
function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts);
}

interface IUniswapV2Router02 is IUniswapV2Router01 {
function removeLiquidityETHSupportingFeeOnTransferTokens(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline
) external returns (uint amountETH);
function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
address token,
uint liquidity,
uint amountTokenMin,
uint amountETHMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external returns (uint amountETH);

function swapExactTokensForTokensSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external;
function swapExactETHForTokensSupportingFeeOnTransferTokens(
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external payable;
function swapExactTokensForETHSupportingFeeOnTransferTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external;
}

contract PairLiquidityProvider is IERC721Receiver {
address private _owner1;
address private _owner2;
contract PairLiquidityProvider {
address private _owner;
address private _router1;
address private _router2;
address private _weth9;
mapping(address => bool) private _deployedLiquidity;
mapping(address => mapping(address => bool)) private _deployedLiquidity;

constructor(address owner1, address owner2, address router1, address router2, address weth9) {
_owner1 = owner1;
_owner2 = owner2;
constructor(address owner, address router1, address router2) {
_owner = owner;
_router1 = router1;
_router2 = router2;
_weth9 = weth9;
}

receive() external payable {
}
// providePairLiquidity mints amountA of tokenA and amountB of tokenB and
// splits them evenly between the tokenA/tokenB pair on router1 and the one
// on router2 (creating the pairs if needed). The LP tokens stay locked in
// this contract.
function providePairLiquidity(address tokenA, address tokenB, uint256 amountA, uint256 amountB) external {
require(msg.sender == _owner, "not owner");
require(!_deployedLiquidity[tokenA][tokenB], "liquidity already deployed");
_deployedLiquidity[tokenA][tokenB] = true;

function onERC721Received(address, address, uint256, bytes calldata) public pure override returns (bytes4) {
return this.onERC721Received.selector;
}
uint256 amountAPerPair = amountA / 2;
uint256 amountBPerPair = amountB / 2;

function call(address addr, bytes calldata data) public payable {
require(msg.sender == _owner1 || msg.sender == _owner2, "not owner");
IMintableERC20(tokenA).mint(address(this), amountAPerPair * 2);
IMintableERC20(tokenB).mint(address(this), amountBPerPair * 2);

(bool sent, ) = payable(addr).call{value: msg.value}(data);
require(sent, "call failed");
_addLiquidity(_router1, tokenA, tokenB, amountAPerPair, amountBPerPair);
_addLiquidity(_router2, tokenA, tokenB, amountAPerPair, amountBPerPair);
}

function providePairLiquidity(address dai, uint256 daiDesired) public payable {
require(msg.sender == _owner1 || msg.sender == _owner2, "not owner");
require(!_deployedLiquidity[dai], "liquidity already deployed");
_deployedLiquidity[dai] = true;

uint256 daiPerPair = daiDesired/2;
daiDesired = daiPerPair * 2;

// mint dai for both pairs
IDai(dai).mint(address(this), daiDesired);

// approve dai for both pairs
TransferHelper.safeApprove(dai, _router1, daiPerPair);
TransferHelper.safeApprove(dai, _router2, daiPerPair);

// provide liquidity to router1
IUniswapV2Router02(_router1).addLiquidityETH{value: msg.value/2}(dai, daiPerPair, 0, 0, address(this), block.timestamp);

// provide liquidity to router2
IUniswapV2Router02(_router2).addLiquidityETH{value: msg.value/2}(dai, daiPerPair, 0, 0, address(this), block.timestamp);

// return remaining eth back to sender
if(address(this).balance > 0) {
(bool sent, ) = payable(tx.origin).call{value: address(this).balance}("");
require(sent, "failed to send refund");
}
function _addLiquidity(address router, address tokenA, address tokenB, uint256 amountA, uint256 amountB) private {
require(IMintableERC20(tokenA).approve(router, amountA), "approve A failed");
require(IMintableERC20(tokenB).approve(router, amountB), "approve B failed");
IUniswapV2Router02(router).addLiquidity(tokenA, tokenB, amountA, amountB, 0, 0, address(this), block.timestamp);
}

}
}
Loading