Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ deployments/**/31337.*
broadcast/**/dry-run/**
script/deploy/tasks/1337/**
script/deploy/tasks/31337/**
# Smoke test broadcast logs are transient validation artifacts, not deployment records
broadcast/V2SmokeTest.s.sol/**
broadcast/V3SmokeTest.s.sol/**
broadcast/V4SmokeTest.s.sol/**

debug/
Cargo.lock
Expand Down
3 changes: 2 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ fs_permissions = [
{ access = "read-write", path = ".forge-snapshots"},
{ access = "read", path = "script/" },
{ access = "read-write", path = "script/deploy/tasks" },
{ access = "read", path = "out/" }
{ access = "read", path = "out/" },
{ access = "read", path = "deployments/" }
]

skip = [
Expand Down
140 changes: 140 additions & 0 deletions script/smoke/V2SmokeTest.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Script, console2 as console} from 'forge-std/Script.sol';

interface IERC20Min {
function approve(address spender, uint256 amount) external returns (bool);
function balanceOf(address) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
}

interface IWETH9 is IERC20Min {
function deposit() external payable;
}

interface IV2Factory {
function getPair(address tokenA, address tokenB) external view returns (address);
}

interface IV2Router02 {
function WETH() external view returns (address);

function addLiquidity(
address tokenA,
address tokenB,
uint256 amountADesired,
uint256 amountBDesired,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline
) external returns (uint256 amountA, uint256 amountB, uint256 liquidity);

function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external returns (uint256[] memory amounts);
}

contract TestToken {
string public name = 'SmokeV2 Token';
string public symbol = 'SMK2';
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;

constructor(uint256 _supply) {
totalSupply = _supply;
balanceOf[msg.sender] = _supply;
}

function approve(address s, uint256 a) external returns (bool) {
allowance[msg.sender][s] = a;
return true;
}

function transfer(address to, uint256 a) external returns (bool) {
balanceOf[msg.sender] -= a;
balanceOf[to] += a;
return true;
}

function transferFrom(address from, address to, uint256 a) external returns (bool) {
if (allowance[from][msg.sender] != type(uint256).max) allowance[from][msg.sender] -= a;
balanceOf[from] -= a;
balanceOf[to] += a;
return true;
}
}

contract V2SmokeTest is Script {
function run() public {
// Chain-agnostic address resolution
string memory chainIdStr = vm.toString(block.chainid);
string memory path = string.concat('./deployments/json/', chainIdStr, '.json');
string memory json = vm.readFile(path);

address v2Factory = vm.parseJsonAddress(json, '.latest.UniswapV2Factory.address');
address v2Router = vm.parseJsonAddress(json, '.latest.UniswapV2Router02.address');
// Derive WETH from the deployed Router02 — works on any chain
address weth = IV2Router02(v2Router).WETH();

require(v2Factory != address(0), 'UniswapV2Factory not in deployments JSON');
require(v2Router != address(0), 'UniswapV2Router02 not in deployments JSON');
require(weth != address(0), 'WETH not derivable from Router');

vm.startBroadcast();
address me = msg.sender;
console.log('Chain:', block.chainid);
console.log('Deployer:', me);
console.log('V2 Factory:', v2Factory);
console.log('V2 Router02:', v2Router);
console.log('WETH:', weth);

IWETH9(weth).deposit{value: 0.0001 ether}();
console.log('Wrapped 0.0001 ETH; WETH balance:', IERC20Min(weth).balanceOf(me));

TestToken test = new TestToken(1_000_000 ether);
console.log('Test token:', address(test));

IWETH9(weth).approve(v2Router, type(uint256).max);
test.approve(v2Router, type(uint256).max);

(uint256 amtA, uint256 amtB, uint256 liq) = IV2Router02(v2Router)
.addLiquidity(weth, address(test), 0.000_05 ether, 1000 ether, 0, 0, me, block.timestamp + 3600);
console.log('Added v2 liquidity:');
console.log(' amountA (WETH):', amtA);
console.log(' amountB (TEST):', amtB);
console.log(' LP tokens:', liq);
require(liq > 0, 'no LP minted');

address pair = IV2Factory(v2Factory).getPair(weth, address(test));
console.log('Pair address:', pair);
require(pair != address(0), 'pair address should be non-zero');

uint256 wethBefore = IERC20Min(weth).balanceOf(me);
address[] memory swapPath = new address[](2);
swapPath[0] = address(test);
swapPath[1] = weth;

uint256[] memory amounts =
IV2Router02(v2Router).swapExactTokensForTokens(1 ether, 0, swapPath, me, block.timestamp + 3600);
uint256 wethAfter = IERC20Min(weth).balanceOf(me);

console.log('Swap: 1 TEST -> WETH');
console.log(' amount in:', amounts[0]);
console.log(' amount out:', amounts[1]);
console.log(' WETH delta:', wethAfter - wethBefore);
require(amounts[1] > 0, 'swap returned zero');
require(wethAfter > wethBefore, "WETH balance didn't increase");

console.log('');
console.log('SUCCESS: v2 pair created, liquidity added, swap completed');
vm.stopBroadcast();
}
}
195 changes: 195 additions & 0 deletions script/smoke/V3SmokeTest.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Script, console2 as console} from 'forge-std/Script.sol';

interface IERC20Min {
function approve(address spender, uint256 amount) external returns (bool);
function balanceOf(address) external view returns (uint256);
}

interface IWETH9 is IERC20Min {
function deposit() external payable;
}

interface IV3Factory {
function createPool(address tokenA, address tokenB, uint24 fee) external returns (address);
function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address);
}

interface IUniswapV3Pool {
function initialize(uint160 sqrtPriceX96) external;
function slot0() external view returns (uint160, int24, uint16, uint16, uint16, uint8, bool);
}

interface INonfungiblePositionManager {
function WETH9() external view returns (address);

struct MintParams {
address token0;
address token1;
uint24 fee;
int24 tickLower;
int24 tickUpper;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
address recipient;
uint256 deadline;
}
function mint(MintParams calldata) external payable returns (uint256, uint128, uint256, uint256);
}

interface ISwapRouter02 {
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}
function exactInputSingle(ExactInputSingleParams calldata) external payable returns (uint256);
}

contract TestToken {
string public name = 'SmokeV3 Token';
string public symbol = 'SMK3';
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;

constructor(uint256 _s) {
totalSupply = _s;
balanceOf[msg.sender] = _s;
}

function approve(address s, uint256 a) external returns (bool) {
allowance[msg.sender][s] = a;
return true;
}

function transfer(address to, uint256 a) external returns (bool) {
balanceOf[msg.sender] -= a;
balanceOf[to] += a;
return true;
}

function transferFrom(address f, address t, uint256 a) external returns (bool) {
if (allowance[f][msg.sender] != type(uint256).max) allowance[f][msg.sender] -= a;
balanceOf[f] -= a;
balanceOf[t] += a;
return true;
}
}

contract V3SmokeTest is Script {
uint24 constant FEE = 3000;
int24 constant TICK_LOWER = -887_220;
int24 constant TICK_UPPER = 887_220;
uint160 constant SQRT_PRICE_1_1 = 79_228_162_514_264_337_593_543_950_336;

function run() public {
string memory chainIdStr = vm.toString(block.chainid);
string memory path = string.concat('./deployments/json/', chainIdStr, '.json');
string memory json = vm.readFile(path);

address v3Factory = vm.parseJsonAddress(json, '.latest.UniswapV3Factory.address');
address npm = vm.parseJsonAddress(json, '.latest.NonfungiblePositionManager.address');
address swapRouter = vm.parseJsonAddress(json, '.latest.SwapRouter02.address');
address weth = INonfungiblePositionManager(npm).WETH9();

require(v3Factory != address(0), 'UniswapV3Factory not in deployments JSON');
require(npm != address(0), 'NonfungiblePositionManager not in deployments JSON');
require(swapRouter != address(0), 'SwapRouter02 not in deployments JSON');

vm.startBroadcast();
address me = msg.sender;
console.log('Chain:', block.chainid);
console.log('Deployer:', me);
console.log('V3 Factory:', v3Factory);
console.log('NPM:', npm);
console.log('SwapRouter02:', swapRouter);
console.log('WETH:', weth);

IWETH9(weth).deposit{value: 0.0001 ether}();
TestToken test = new TestToken(1_000_000 ether);
console.log('Test token:', address(test));

address pool = _createPool(v3Factory, address(test), weth);
_mintPosition(npm, address(test), weth, me);
_doSwap(swapRouter, address(test), weth, me);
pool; // silence unused warning

console.log('');
console.log('SUCCESS: v3 pool created, position minted, swap completed');
vm.stopBroadcast();
}

function _createPool(address v3Factory, address test, address weth) internal returns (address pool) {
(address t0, address t1) = weth < test ? (weth, test) : (test, weth);
pool = IV3Factory(v3Factory).createPool(t0, t1, FEE);
IUniswapV3Pool(pool).initialize(SQRT_PRICE_1_1);
(uint160 sqrtPx,,,,,,) = IUniswapV3Pool(pool).slot0();
console.log('Pool:', pool);
console.log('Pool sqrtPriceX96:', sqrtPx);
}

function _mintPosition(address npm, address test, address weth, address me) internal {
IWETH9(weth).approve(npm, type(uint256).max);
TestToken(test).approve(npm, type(uint256).max);

(address t0, address t1) = weth < test ? (weth, test) : (test, weth);
uint256 amt0 = t0 == weth ? uint256(0.000_05 ether) : uint256(1000 ether);
uint256 amt1 = t1 == weth ? uint256(0.000_05 ether) : uint256(1000 ether);

INonfungiblePositionManager.MintParams memory mp = INonfungiblePositionManager.MintParams({
token0: t0,
token1: t1,
fee: FEE,
tickLower: TICK_LOWER,
tickUpper: TICK_UPPER,
amount0Desired: amt0,
amount1Desired: amt1,
amount0Min: 0,
amount1Min: 0,
recipient: me,
deadline: block.timestamp + 3600
});

(uint256 tokenId, uint128 liq, uint256 a0, uint256 a1) = INonfungiblePositionManager(npm).mint(mp);
console.log('Minted v3 position:');
console.log(' tokenId:', tokenId);
console.log(' liquidity:', liq);
console.log(' amount0:', a0);
console.log(' amount1:', a1);
require(liq > 0, 'no liquidity minted');
}

function _doSwap(address swapRouter, address test, address weth, address me) internal {
TestToken(test).approve(swapRouter, type(uint256).max);
uint256 wethBefore = IERC20Min(weth).balanceOf(me);

ISwapRouter02.ExactInputSingleParams memory sp = ISwapRouter02.ExactInputSingleParams({
tokenIn: test,
tokenOut: weth,
fee: FEE,
recipient: me,
amountIn: 1 ether,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
});

uint256 out = ISwapRouter02(swapRouter).exactInputSingle(sp);
uint256 wethAfter = IERC20Min(weth).balanceOf(me);

console.log('Swap: 1 TEST -> WETH');
console.log(' amount out:', out);
console.log(' WETH delta:', wethAfter - wethBefore);
require(out > 0, 'swap returned zero');
require(wethAfter > wethBefore, "WETH balance didn't increase");
}
}
Loading
Loading