Quickstart

Get up and running with RealSafe in minutes. This guide covers basic setup, deployment, and usage for both V2 and V3 lockers.

Prerequisites

Required Tools

# Install Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup

# Verify installation
forge --version
cast --version

Environment Setup

Create a .env file in the contracts/ directory:

# Private key (without 0x prefix)
PRIVATE_KEY=your_private_key_here

# RPC URLs
MONAD_TESTNET_RPC=https://testnet-rpc.monad.xyz

# Block explorers (optional)
MONAD_EXPLORER_API_KEY=your_api_key

Installation

# Clone the repository
git clone https://github.com/realsafe/locker.git
cd locker/contracts

# Install dependencies
forge install

# Build contracts
forge build

Deployment

Deploy on Monad Testnet

# Deploy contracts
forge script script/DeployMonad.s.sol \
  --rpc-url monad_testnet \
  --broadcast

# Output will show deployment details

Usage Examples

Example 1: Lock a V3 Position

Step 1: Create a Pool and Position

# Deploy test tokens
forge script script/DeployTwoTokens.s.sol \
  --rpc-url monad_testnet \
  --broadcast

# Create pool and mint position
forge script script/CreatePool.s.sol \
  --rpc-url monad_testnet \
  --broadcast

Step 2: Lock the Position

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

import "forge-std/Script.sol";
import {V3Locker} from "../src/V3Locker.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";

contract LockMyPosition is Script {
    address constant V3_LOCKER = 0x0b4619Ed28429a392C79aed87E6572F34ab6199e;
    address constant POSITION_MANAGER = 0x46A15B0b27311cedF172AB29E4f4766fbE7F4364;
    
    function run() external {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        
        // Your NFT token ID
        uint256 myTokenId = 83;
        
        // Lock for 30 days
        uint256 unlockTime = block.timestamp + 30 days;
        
        vm.startBroadcast(deployerPrivateKey);
        
        // Approve NFT
        IERC721(POSITION_MANAGER).approve(V3_LOCKER, myTokenId);
        
        // Lock position
        uint256 creationFee = V3Locker(V3_LOCKER).creationFee();
        V3Locker(V3_LOCKER).lockPosition{value: creationFee}(
            myTokenId,
            unlockTime,
            msg.sender,  // collect address
            address(0)   // no condition
        );
        
        vm.stopBroadcast();
        
        console.log("Position locked successfully!");
    }
}

Run the script:

forge script script/LockMyPosition.s.sol \
  --rpc-url monad_testnet \
  --broadcast

Example 2: Lock V2 LP Tokens

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

import "forge-std/Script.sol";
import {V2Locker} from "../src/V2Locker.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract LockLPTokens is Script {
    address constant V2_LOCKER = 0x527713F26D0769E5EaE5A77b530511025fB4FDe7;
    
    function run() external {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        
        // Your LP token details
        address lpToken = 0x324cc0b7E34882A41aFdf1a623529E6ce6BA9623;
        uint256 amount = 1000 * 1e18; // 1000 LP tokens
        
        // Lock for 90 days
        uint256 unlockTime = block.timestamp + 90 days;
        
        vm.startBroadcast(deployerPrivateKey);
        
        // Approve LP tokens
        IERC20(lpToken).approve(V2_LOCKER, amount);
        
        // Lock tokens
        uint256 creationFee = V2Locker(V2_LOCKER).creationFee();
        V2Locker(V2_LOCKER).lockLP{value: creationFee}(
            lpToken,
            amount,
            unlockTime,
            false,       // not permanent
            address(0)   // no condition
        );
        
        vm.stopBroadcast();
        
        console.log("LP tokens locked successfully!");
    }
}

Example 3: Claim Fees from Locked V3 Position

Note: This only applies to V3 positions. V2 LP tokens don't have a separate fee claiming function because trading fees automatically accrue to the LP token's value. You receive all accrued fees when you unlock your V2 LP tokens.

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

import "forge-std/Script.sol";
import {V3Locker} from "../src/V3Locker.sol";

contract ClaimFeesFromLock is Script {
    address constant V3_LOCKER = 0x0b4619Ed28429a392C79aed87E6572F34ab6199e;
    
    function run() external {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        
        // Your lock ID
        uint256 myLockId = 0;
        
        vm.startBroadcast(deployerPrivateKey);
        
        // Claim all available fees (V3 only)
        V3Locker(V3_LOCKER).claimFees(
            myLockId,
            type(uint128).max,  // max token0
            type(uint128).max   // max token1
        );
        
        vm.stopBroadcast();
        
        console.log("Fees claimed successfully!");
    }
}

Using Cast (Command Line)

Get Your Lock IDs

# Get all your lock IDs
cast call 0x0b4619Ed28429a392C79aed87E6572F34ab6199e \
  "getOwnerLockIds(address)" \
  YOUR_ADDRESS \
  --rpc-url monad_testnet

Check Lock Details

# Get specific lock details
cast call 0x0b4619Ed28429a392C79aed87E6572F34ab6199e \
  "getLock(uint256)" \
  0 \
  --rpc-url monad_testnet

Check if You Can Unlock

# Check unlock status
cast call 0x0b4619Ed28429a392C79aed87E6572F34ab6199e \
  "canUnlock(uint256)" \
  0 \
  --rpc-url monad_testnet

Unlock Your Position

# Unlock when ready
cast send 0x0b4619Ed28429a392C79aed87E6572F34ab6199e \
  "withdraw(uint256,address)" \
  0 \
  YOUR_ADDRESS \
  --private-key $PRIVATE_KEY \
  --rpc-url monad_testnet

Testing

Run All Tests

# Run full test suite
forge test

# Run with gas reporting
forge test --gas-report

# Run specific test
forge test --match-test testLockPosition -vvv

Test V3 Locker

forge test --match-contract V3LockerTest -vvv

Test V2 Locker

forge test --match-contract V2LockerTest -vvv

Verification

Verify on Monad Explorer

# Verify V3 Locker
forge verify-contract \
  0x0b4619Ed28429a392C79aed87E6572F34ab6199e \
  src/V3Locker.sol:V3Locker \
  --chain-id 84532 \
  --constructor-args $(cast abi-encode "constructor(address,address,address,uint256,uint256,uint256)" \
    0x46A15B0b27311cedF172AB29E4f4766fbE7F4364 \
    0x735c60c7FA167a8F69Ae631CD7d020942d9923B6 \
    0x735c60c7FA167a8F69Ae631CD7d020942d9923B6 \
    100000000000000 \
    100 \
    100)

Common Issues & Solutions

Issue: "Insufficient Creation Fee"

Solution: Check the current creation fee:

cast call V3_LOCKER_ADDRESS "creationFee()" --rpc-url base_sepolia

Send the correct amount with --value flag.

Issue: "NFT Transfer Failed"

Solution: Approve the NFT first:

cast send POSITION_MANAGER \
  "approve(address,uint256)" \
  V3_LOCKER_ADDRESS \
  TOKEN_ID \
  --private-key $PRIVATE_KEY \
  --rpc-url monad_testnet

Issue: "Lock Not Unlocked"

Solution: Check the unlock time:

# Get lock details
cast call V3_LOCKER_ADDRESS "getLock(uint256)" LOCK_ID --rpc-url base_sepolia

# Compare unlockDate with current timestamp
date +%s

Next Steps

Support

Last updated