Conviction Lockers (CL20's) are part of the XBT2027 eco-system for XBT, 2027 and D17. This code is open-source with an MIT License.
This repo contains a minimal Uniswap V2 locker that buys a token, records its own entry price from the pool, and unlocks only when a later on-chain price probe reaches a target multiple.
There is no hosted backend, oracle, menu system, or external API. The contract uses the same pool for the initial baseline check and later unlock checks, so the unlock condition is reproducible from on-chain swaps.
This repository ships with a Sepolia 2027 example based on the token by x.com/XBT2027. The same contract pattern can be adapted to other Uniswap V2 pools by changing the RPC, router, token, quote token, and pair addresses. For now, use testnet only unless you have reviewed the contract and scripts yourself.
This contract is not production ready. Expect changes before 1.0 releases.
src/MinimalConvictionLocker.sol: owner-only locker contract source.scripts/deploy-and-lock.mjs: deploys a fresh locker, buys with ETH, records the baseline probe, and creates the first lock.scripts/inspect-lock.mjs: reads the lock state and target values.scripts/verify-withdraw.mjs: verifies the price target and optionally withdraws in the same transaction..env.example: Sepolia defaults and the main settings to change.package.json/package-lock.json: Hardhat, ethers, and dotenv setup.
The detailed Sepolia run report is a separate artifact:
SEPOLIA-CL-20-FLOW.md
This repo does not include or require the 2027 token contract. 2027 is only the token/pool used for the Sepolia example configuration. The locker can target any compatible Uniswap V2 pair by changing the env addresses.
MinimalConvictionLocker deploys a new owner-only vault and can create the first lock in the deployment transaction.
With ETH_IN=0.001, LOCK_BPS=10000, and PROBE_AMOUNT=1:
- The constructor deploys a vault owned by your wallet.
- It swaps
ETH_INthrough Uniswap V2 to buy the locked token. - It allocates
LOCK_BPSof the bought tokens to the lock.10000means 100%. - It sells exactly
PROBE_AMOUNTof the bought token as the baseline price probe. - It records the baseline value in WETH/native quote terms, or USDT terms if configured.
- It locks the remaining bought tokens.
Later, the owner calls verify or verifyAndWithdraw. If the target has not already been reached, the locker pulls another owner-funded PROBE_AMOUNT, sells it into the same pool, and compares the output to the stored target.
For example:
TARGET_MULTIPLE=5.00
stores targetMultipleX100 = 500, meaning a 5x target. If you prefer percent language:
TARGET_INCREASE_PERCENT=400
also means 5x, because a 400% gain is original capital plus 400%.
| Setting | Address |
|---|---|
| Token bought and locked | 0x368a8b834464D1B28bdeB4d2437D016de4F5EA67 |
| Price quote pair | 0xD7850331ad7090C7c9bD2883e9b42945C13Dfa4f |
| Quote token | Sepolia WETH 0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9 |
| Router | Sepolia Uniswap V2 router 0xC532a74256D3Db42D0Bf7a0400fEFDbad7694008 |
Original mainnet reference values from the 2027 example:
| Setting | Address |
|---|---|
| Token | 0x3483FE3baC9Ca981f53E92f05603E1B32cd1b3cC |
| Pair | 0x7C3ef649FbfDb54c9bB31Dbc1229DC772C000EC8 |
| WETH | 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 |
| Optional WETH/USDT V3 pool | 0x11b815efB8f581194ae79006d24E0d814B7697F6 |
Mainnet is not the recommended starting point. The scripts default to Sepolia and require an explicit confirmation string before sending any transaction.
Use Node.js 22.13 or newer.
npm install
npm run compile
cp .env.example .envFill in .env with a fresh testnet wallet:
OWNER_PRIVATE_KEY=your_testnet_private_key
OWNER_ADDRESS=0xYourWalletAddress
The owner wallet is the only wallet that can verify, withdraw, or sweep tokens from its locker. The private key never needs to be pasted into a script file; the scripts load it from .env with dotenv.
At minimum, a Sepolia run needs:
OWNER_PRIVATE_KEY=
OWNER_ADDRESS=
RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
CHAIN_ID=11155111
NETWORK_NAME=sepolia
ETH_IN=0.001
TARGET_MULTIPLE=5.00
PROBE_AMOUNT=1
MAX_TOTAL_COST_ETH=0.005
The token, pair, quote token, and router values are already set in .env.example for the Sepolia 2027 example. To point the kit at another pool, change:
LOCKED_TOKEN=
QUOTE_TOKEN=
UNISWAP_V2_PAIR=
UNISWAP_V2_ROUTER=
Recommended wallet hygiene:
- Use a fresh wallet with no real funds.
- You can get free Sepolia eth for Testnet from https://cloud.google.com/application/web3/faucet/ethereum/sepolia
- Do not use a wallet connected to your trading activity.
- Ideally use a separate macOS or Windows profile from the one you trade from.
- Treat every mainnet setting as dangerous until you have tested the full flow on testnet.
- Never commit
.env. It is ignored by.gitignore.
Dry run first:
npm run deploy-lockSend the Sepolia transaction:
EXECUTE=I_UNDERSTAND_SEPOLIA npm run deploy-lockThe script prints LOCKER_ADDRESS=... and the lock ID. Add them to .env:
LOCKER_ADDRESS=0x...
LOCK_ID=0
npm run inspectThis prints the locked amount, baseline quote, last quote, target quote, unlock basis, fallback unlock time, and whether the lock is already withdrawable.
Dry run:
npm run verify-withdrawExecute verification and withdraw in one transaction if the price target is met:
EXECUTE=I_UNDERSTAND_SEPOLIA VERIFY_ACTION=verifyAndWithdraw npm run verify-withdrawRun verification without withdrawing:
EXECUTE=I_UNDERSTAND_SEPOLIA VERIFY_ACTION=verify npm run verify-withdrawWithdraw after the fallback time has passed:
EXECUTE=I_UNDERSTAND_SEPOLIA VERIFY_ACTION=withdraw npm run verify-withdrawImportant: price verification sells another PROBE_AMOUNT from the owner wallet. The owner must hold and approve that probe amount. The script can create the approval when executing, but a dry run may stop early if approval is missing because gas cannot be estimated yet.
The main env values are:
RPC_URL=
PRIVATE_RPC_URL=
NETWORK_NAME=
CHAIN_ID=
UNISWAP_V2_ROUTER=
LOCKED_TOKEN=
QUOTE_TOKEN=
UNISWAP_V2_PAIR=
WETH_USDT_V3_POOL=
For mainnet, you would change these values to mainnet RPC and mainnet contract addresses, and the confirmation string becomes:
EXECUTE=I_UNDERSTAND_MAINNET npm run deploy-lockThat is supported as an escape hatch, not as a recommendation. Testnet first.
- The initial deploy-lock flow is one transaction: contract creation, ETH buy, baseline probe, and lock creation.
- Only the owner can verify, withdraw, or sweep unlocked leftovers.
- The withdrawal destination is always the owner wallet.
- The contract is not upgradeable.
- Existing pre-owned ERC-20 tokens cannot be pulled during constructor deployment unless the future contract address was approved in advance or a permit path is added. This kit focuses on the ETH-buy-and-lock path.
- A 5x trigger does not guarantee a 5x realized exit on the full position. The trigger is a marginal probe price; selling a larger amount can move the AMM price.
- This is unaudited experimental code.
- Deployment and verification cost gas.
- Public mempool swaps can be sandwiched. Use a protected RPC where available.
- Shallow liquidity, LP removal, token migrations, or malicious token behavior can make verification fail or make a target easier to hit.
- The design intentionally uses live pool conditions. It does not protect against market manipulation.
- A contract bug, compromised key, or wrong env value can create loss.