Skip to content
Open
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
16 changes: 16 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# REQUIRED: RPC Configuration
RPC_URL=https://unichain-mainnet.g.alchemy.com/v2/YOUR_API_KEY

# REQUIRED: Pool Manager Address
POOL_MANAGER=0x1F98400000000000000000000000000000000004

# OPTIONAL: Real Pool Key for Integration Tests
# If POOL_CURRENCY0 and POOL_CURRENCY1 are set, tests will use real pool
# If not set, tests will use mock tokens
# POOL_CURRENCY0=0x...
# POOL_CURRENCY1=0x...

# REQUIRED if using real pool (POOL_CURRENCY0 and POOL_CURRENCY1 are set)
POOL_FEE=3000
POOL_TICK_SPACING=60
POOL_HOOKS=0x0000000000000000000000000000000000000000
225 changes: 225 additions & 0 deletions test/BaseTest.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

import {Test} from "@forge-std/Test.sol";
import {Yoga} from "../src/Yoga.sol";
import {IERC20} from "@forge-std/interfaces/IERC20.sol";
import {Currency} from "@uniswapv4/types/Currency.sol";
import {PoolKey} from "@uniswapv4/types/PoolKey.sol";
import {IHooks} from "@uniswapv4/interfaces/IHooks.sol";
import {IPoolManager} from "@uniswapv4/interfaces/IPoolManager.sol";
import {TickMath} from "@uniswapv4/libraries/TickMath.sol";

contract BaseTest is Test {
Yoga public yoga;
IPoolManager public poolManager;

IERC20 public token0;
IERC20 public token1;
PoolKey public testKey;

address public alice;
address public bob;
address public charlie;

bool public usingNativeETH;

function setUp() public virtual {
string memory rpcUrl = vm.envString("RPC_URL");
vm.createSelectFork(rpcUrl);

yoga = new Yoga();

address poolManagerAddr = vm.envAddress("POOL_MANAGER");
poolManager = IPoolManager(poolManagerAddr);

alice = makeAddr("alice");
bob = makeAddr("bob");
charlie = makeAddr("charlie");

_setupTokensAndPool();
}

function _setupTokensAndPool() internal {
try vm.envAddress("POOL_CURRENCY0") returns (address currency0) {
try vm.envAddress("POOL_CURRENCY1") returns (address currency1) {
_setupRealPool(currency0, currency1);
return;
} catch {}
} catch {}

_setupMockTokenPool();
}

function _setupRealPool(address currency0, address currency1) internal {
usingNativeETH = (currency0 == address(0) || currency1 == address(0));

if (currency0 == address(0)) {
token0 = IERC20(address(new NativeETHWrapper()));
} else {
token0 = IERC20(currency0);
}

if (currency1 == address(0)) {
token1 = IERC20(address(new NativeETHWrapper()));
} else {
token1 = IERC20(currency1);
}

uint24 fee = uint24(vm.envUint("POOL_FEE"));
int24 tickSpacing = int24(vm.envInt("POOL_TICK_SPACING"));
address hooks = vm.envAddress("POOL_HOOKS");

testKey = PoolKey({
currency0: Currency.wrap(currency0),
currency1: Currency.wrap(currency1),
fee: fee,
tickSpacing: tickSpacing,
hooks: IHooks(hooks)
});

if (currency0 == address(0)) {
deal(alice, 10000 ether);
deal(bob, 10000 ether);
deal(charlie, 10000 ether);
} else {
deal(currency0, alice, 10000 ether);
deal(currency0, bob, 10000 ether);
deal(currency0, charlie, 10000 ether);
}

if (currency1 == address(0)) {
deal(alice, 10000 ether);
deal(bob, 10000 ether);
deal(charlie, 10000 ether);
} else {
deal(currency1, alice, 10000 ether);
deal(currency1, bob, 10000 ether);
deal(currency1, charlie, 10000 ether);
}

if (!usingNativeETH) {
deal(alice, 100 ether);
deal(bob, 100 ether);
deal(charlie, 100 ether);
}

_tryInitializePool();
}

function _setupMockTokenPool() internal {
usingNativeETH = false;

MockERC20 mockToken0 = new MockERC20("Mock Token 0", "MTK0");
MockERC20 mockToken1 = new MockERC20("Mock Token 1", "MTK1");

if (address(mockToken0) < address(mockToken1)) {
token0 = IERC20(address(mockToken0));
token1 = IERC20(address(mockToken1));
} else {
token0 = IERC20(address(mockToken1));
token1 = IERC20(address(mockToken0));
}

testKey = PoolKey({
currency0: Currency.wrap(address(token0)),
currency1: Currency.wrap(address(token1)),
fee: 3000,
tickSpacing: 60,
hooks: IHooks(address(0))
});

deal(address(token0), alice, 10000 ether);
deal(address(token1), alice, 10000 ether);
deal(address(token0), bob, 10000 ether);
deal(address(token1), bob, 10000 ether);
deal(address(token0), charlie, 10000 ether);
deal(address(token1), charlie, 10000 ether);

deal(alice, 100 ether);
deal(bob, 100 ether);
deal(charlie, 100 ether);

_tryInitializePool();
}

function _tryInitializePool() internal {
poolManager.initialize(testKey, TickMath.getSqrtPriceAtTick(0));
}
}

contract NativeETHWrapper is IERC20 {
function name() external pure returns (string memory) {
return "Native ETH";
}

function symbol() external pure returns (string memory) {
return "ETH";
}

function decimals() external pure returns (uint8) {
return 18;
}

function balanceOf(address account) external view returns (uint256) {
return account.balance;
}

function approve(address, uint256) external pure returns (bool) {
return true;
}

function transfer(address, uint256) external pure returns (bool) {
revert("Use native ETH transfer");
}

function transferFrom(address, address, uint256) external pure returns (bool) {
revert("Use native ETH transfer");
}

function totalSupply() external pure returns (uint256) {
return type(uint256).max;
}

function allowance(address, address) external pure returns (uint256) {
return type(uint256).max;
}
}

contract MockERC20 is IERC20 {
string public name;
string public symbol;
uint8 public constant decimals = 18;
uint256 public totalSupply;

mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;

constructor(string memory _name, string memory _symbol) {
name = _name;
symbol = _symbol;
}

function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}

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

function transferFrom(address from, address to, uint256 amount) external returns (bool) {
if (allowance[from][msg.sender] != type(uint256).max) {
allowance[from][msg.sender] -= amount;
}
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
}
47 changes: 47 additions & 0 deletions test/unit/YogaBasic.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

import {BaseTest} from "../BaseTest.sol";

contract YogaBasicTest is BaseTest {
function test_InitialState() public view {
assertEq(yoga.nextTokenId(), 1, "Initial token ID should be 1");
}

function test_PoolManagerAddress() public view {
assertEq(
address(yoga.POOL_MANAGER()),
address(poolManager),
"Pool manager address should match env"
);
}

function test_PoolManagerExists() public view {
assertTrue(
address(poolManager).code.length > 0,
"Pool manager should have code"
);
}

function test_Name() public view {
assertEq(yoga.name(), "YogaPosition", "Name should be YogaPosition");
}

function test_Symbol() public view {
assertEq(yoga.symbol(), "YP", "Symbol should be YP");
}

function test_SupportsERC721Interface() public view {
bytes4 erc721InterfaceId = 0x80ac58cd;
assertTrue(yoga.supportsInterface(erc721InterfaceId), "Should support ERC721");
}

function test_SupportsERC165Interface() public view {
bytes4 erc165InterfaceId = 0x01ffc9a7;
assertTrue(yoga.supportsInterface(erc165InterfaceId), "Should support ERC165");
}

function test_TokenURI() public view {
assertEq(yoga.tokenURI(1), "/dev/null", "Token URI should be /dev/null");
}
}
Loading
Loading