Skip to content

Commit 64b383e

Browse files
razwwclaude
andcommitted
feat(periphery): rename position NFT + add hardened BSC mainnet scripts
- NonfungiblePositionManager.initialize: collection name -> 'Lista V3 Positions NFT', symbol -> 'LISTA-V3' (was '...NFT-V1' / 'LIS-V3-POS'). EIP-712 permit nameHash derives from the same string, so it follows automatically. FullFlowTest name/symbol/DOMAIN_SEPARATOR assertions updated. - script/DeployBscMainnet.s.sol: chain-56-pinned full-stack deploy incl. a fresh NFT descriptor. Deployer defaults to all privileged roles and is printed pre-broadcast. Impl deploy+initialize is atomic via NpmImplDeployer, closing the impl-initialization front-run window. - script/CreateSlisBnbPool.s.sol: enable the 0.01% fee tier and create+initialize the slisBNB/WBNB pool at the live slisBNB exchange rate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 666d977 commit 64b383e

4 files changed

Lines changed: 257 additions & 4 deletions

File tree

script/CreateSlisBnbPool.s.sol

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
pragma solidity =0.7.6;
3+
pragma abicoder v2;
4+
5+
import 'forge-std/Script.sol';
6+
import 'forge-std/console.sol';
7+
8+
import {IListaV3Factory} from '../src/core/interfaces/IListaV3Factory.sol';
9+
import {IPoolInitializer} from '../src/periphery/interfaces/IPoolInitializer.sol';
10+
11+
/// @dev Minimal view into Lista's slisBNB StakeManager for the live slisBNB->BNB rate.
12+
interface IStakeManager {
13+
function convertSnBnbToBnb(uint256 amount) external view returns (uint256);
14+
}
15+
16+
/// @title Create the slisBNB/BNB 0.01% pool on Lista V3 (BSC mainnet)
17+
/// @notice Initializes the pool at the *current* slisBNB exchange rate read on-chain at run time, so
18+
/// the starting price tracks slisBNB's BNB value. fee = 100 (0.01% / 1bp). The 1bp fee tier is not
19+
/// seeded by the factory, so this script enables it first (owner-only; the deployer owns the factory).
20+
///
21+
/// slisBNB (0xB0b8...A1B) < WBNB (0xbb4C...95c), so token0 = slisBNB, token1 = WBNB, and the pool price
22+
/// (token1/token0) = WBNB per slisBNB = convertSnBnbToBnb(1e18)/1e18. Both tokens are 18 decimals, so
23+
/// no decimal adjustment is needed.
24+
///
25+
/// Usage:
26+
/// forge script script/CreateSlisBnbPool.s.sol:CreateSlisBnbPool \
27+
/// --rpc-url $BSC_RPC --private-key $PRIVATE_KEY --broadcast
28+
contract CreateSlisBnbPool is Script {
29+
uint256 internal constant BSC_MAINNET_CHAIN_ID = 56;
30+
31+
// Lista V3 mainnet (canonical hardened deploy)
32+
IListaV3Factory internal constant FACTORY = IListaV3Factory(0xcb010ed373523942706F730b89792aA1C1597b20);
33+
IPoolInitializer internal constant NPM = IPoolInitializer(0x31677537685EBDF1B695eDa46eC385845395f5dD);
34+
35+
// Tokens (BSC mainnet)
36+
address internal constant SLISBNB = 0xB0b84D294e0C75A6abe60171b70edEb2EFd14A1B;
37+
address internal constant WBNB = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
38+
39+
// slisBNB exchange-rate source
40+
IStakeManager internal constant STAKE_MANAGER = IStakeManager(0x1adB950d8bB3dA4bE104211D5AB038628e477fE6);
41+
42+
uint24 internal constant FEE = 100; // 0.01% (1bp)
43+
int24 internal constant TICK_SPACING = 1; // canonical tick spacing for the 1bp tier
44+
45+
function run() external returns (address pool, uint160 sqrtPriceX96) {
46+
uint256 chainId;
47+
assembly {
48+
chainId := chainid()
49+
}
50+
require(chainId == BSC_MAINNET_CHAIN_ID, 'not BSC mainnet (expected chainid 56)');
51+
require(SLISBNB < WBNB, 'token order'); // token0 must be the lower address
52+
53+
// Live rate: BNB returned for 1 slisBNB. Price (token1/token0) = WBNB per slisBNB.
54+
uint256 amountIn = 1e18;
55+
uint256 amountOut = STAKE_MANAGER.convertSnBnbToBnb(amountIn);
56+
require(amountOut > 0, 'rate=0');
57+
require(amountOut < (uint256(1) << 64), 'rate too large for fixed-point'); // keeps the <<192 safe
58+
59+
// sqrtPriceX96 = sqrt(price) * 2^96 = sqrt(amountOut * 2^192 / amountIn).
60+
uint256 ratioX192 = (amountOut << 192) / amountIn;
61+
sqrtPriceX96 = uint160(_sqrt(ratioX192));
62+
63+
console.log('--- create slisBNB/BNB pool (0.01%) ---');
64+
console.log('deployer:', msg.sender);
65+
console.log('token0 (slisBNB):', SLISBNB);
66+
console.log('token1 (WBNB):', WBNB);
67+
console.log('rate: BNB per slisBNB (x1e18):', amountOut);
68+
console.log('sqrtPriceX96:', uint256(sqrtPriceX96));
69+
70+
vm.startBroadcast();
71+
72+
// Enable the 1bp fee tier if it isn't already (owner-only). Permanent: V3 tiers can't be removed.
73+
if (FACTORY.feeAmountTickSpacing(FEE) == 0) {
74+
FACTORY.enableFeeAmount(FEE, TICK_SPACING);
75+
}
76+
77+
pool = NPM.createAndInitializePoolIfNecessary(SLISBNB, WBNB, FEE, sqrtPriceX96);
78+
79+
vm.stopBroadcast();
80+
81+
console.log('pool:', pool);
82+
}
83+
84+
/// @dev Integer square root (Babylonian). Converges for the full uint256 domain used here.
85+
function _sqrt(uint256 x) internal pure returns (uint256 y) {
86+
if (x == 0) return 0;
87+
uint256 z = (x + 1) / 2;
88+
y = x;
89+
while (z < y) {
90+
y = z;
91+
z = (x / z + z) / 2;
92+
}
93+
}
94+
}

script/DeployBscMainnet.s.sol

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
pragma solidity =0.7.6;
3+
pragma abicoder v2;
4+
5+
import 'forge-std/Script.sol';
6+
import 'forge-std/console.sol';
7+
8+
import {ListaV3Factory} from '../src/core/ListaV3Factory.sol';
9+
import {NonfungiblePositionManager} from '../src/periphery/NonfungiblePositionManager.sol';
10+
import {NonfungibleTokenPositionDescriptor} from '../src/periphery/NonfungibleTokenPositionDescriptor.sol';
11+
import {SwapRouter} from '../src/periphery/SwapRouter.sol';
12+
13+
import {ProxyAdmin} from 'lib/openzeppelin-contracts/contracts/proxy/ProxyAdmin.sol';
14+
import {TransparentUpgradeableProxy} from 'lib/openzeppelin-contracts/contracts/proxy/TransparentUpgradeableProxy.sol';
15+
16+
/// @title Deploy the full Lista V3 stack on BNB Chain mainnet (chainId 56)
17+
/// @notice Self-contained, chain-pinned counterpart to Deploy.s.sol. Everything BSC-mainnet-specific
18+
/// is hardcoded here (WBNB, native-currency label) and the run() hard-requires chainid == 56 so a
19+
/// mis-set RPC can never broadcast mainnet bytecode to the wrong chain. The deploy sequence matches
20+
/// the audited Deploy.s.sol exactly; only the chain config is fixed.
21+
///
22+
/// The NFT collection name/symbol ("Lista V3 Positions NFT" / "LISTA-V3") are baked into the NPM
23+
/// implementation's initialize() — see NonfungiblePositionManager.sol — not set here.
24+
///
25+
/// All privileged roles (factory owner / fee-tier admin, and ProxyAdmin owner) default to the
26+
/// broadcasting deployer EOA. The deployer address is printed before any broadcast so it can be
27+
/// confirmed against the funded signer.
28+
///
29+
/// Optional env (with defaults):
30+
/// OWNER = factory owner / fee-tier admin (defaults to the deployer EOA).
31+
/// PROXY_ADMIN_OWNER = owner of the ProxyAdmin that controls the NPM proxy (defaults to OWNER).
32+
/// TOKEN_DESCRIPTOR = existing NFT position descriptor to reuse. If unset (0x0), a fresh
33+
/// NonfungibleTokenPositionDescriptor is deployed against THIS build's
34+
/// PoolAddress/init-code-hash and wired into the NPM. Only pass an address
35+
/// that was built from THIS repo — never a foreign fork.
36+
///
37+
/// Usage:
38+
/// forge script script/DeployBscMainnet.s.sol:DeployBscMainnet \
39+
/// --rpc-url $BSC_RPC --broadcast --verify --etherscan-api-key $BSCSCAN_API_KEY --slow
40+
contract DeployBscMainnet is Script {
41+
uint256 internal constant BSC_MAINNET_CHAIN_ID = 56;
42+
/// @dev Canonical WBNB on BNB Chain mainnet.
43+
address internal constant WBNB = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
44+
/// @dev Native-currency label shown for WBNB positions in the freshly-deployed descriptor.
45+
bytes32 internal constant NATIVE_CURRENCY_LABEL = bytes32('BNB');
46+
47+
struct Deployment {
48+
address proxyAdmin;
49+
address factory;
50+
address tokenDescriptor;
51+
address npmImpl;
52+
address npmProxy;
53+
address swapRouter;
54+
}
55+
56+
function run() external returns (Deployment memory out) {
57+
uint256 chainId;
58+
assembly {
59+
chainId := chainid()
60+
}
61+
require(chainId == BSC_MAINNET_CHAIN_ID, 'not BSC mainnet (expected chainid 56)');
62+
63+
// The deployer is the broadcasting signer; all privileged roles default to it.
64+
address deployer = msg.sender;
65+
address owner = vm.envOr('OWNER', deployer);
66+
address proxyAdminOwner = vm.envOr('PROXY_ADMIN_OWNER', owner);
67+
address tokenDescriptor = vm.envOr('TOKEN_DESCRIPTOR', address(0));
68+
69+
require(owner != address(0), 'OWNER=0');
70+
require(proxyAdminOwner != address(0), 'PROXY_ADMIN_OWNER=0');
71+
72+
bool deployDescriptor = tokenDescriptor == address(0);
73+
74+
console.log('--- Lista V3 deploy (BSC mainnet) ---');
75+
console.log('chainId:', chainId);
76+
console.log('deployer:', deployer);
77+
console.log('owner:', owner);
78+
console.log('proxyAdminOwner:', proxyAdminOwner);
79+
console.log('WBNB:', WBNB);
80+
console.log('deploy descriptor?:', deployDescriptor);
81+
if (!deployDescriptor) {
82+
console.log('tokenDescriptor (reused):', tokenDescriptor);
83+
}
84+
85+
vm.startBroadcast();
86+
87+
// ProxyAdmin: owner() is the broadcaster from its constructor; transfer only if a different
88+
// owner was requested.
89+
ProxyAdmin proxyAdmin = new ProxyAdmin();
90+
if (proxyAdminOwner != proxyAdmin.owner()) {
91+
proxyAdmin.transferOwnership(proxyAdminOwner);
92+
}
93+
94+
// Factory: plain deploy. owner = broadcaster; hand off to the requested owner if different.
95+
ListaV3Factory factory = new ListaV3Factory();
96+
if (owner != msg.sender) {
97+
factory.setOwner(owner);
98+
}
99+
100+
// Descriptor: fresh deploy only when TOKEN_DESCRIPTOR was not provided. Built from THIS repo's
101+
// PoolAddress/POOL_INIT_CODE_HASH so it derives pool addresses correctly for this deployment.
102+
// forge auto-deploys and links the NFTDescriptor library it depends on.
103+
if (deployDescriptor) {
104+
NonfungibleTokenPositionDescriptor descriptor =
105+
new NonfungibleTokenPositionDescriptor(WBNB, NATIVE_CURRENCY_LABEL);
106+
tokenDescriptor = address(descriptor);
107+
}
108+
109+
// NPM: impl + proxy. factory/WETH9 are constructor immutables on the impl; every future
110+
// upgrade MUST re-pass the exact same (factory, WBNB) to the new impl.
111+
//
112+
// The impl is created AND its initializer consumed inside NpmImplDeployer's constructor —
113+
// a single CREATE tx — so there is no separate initialize() tx for a bot to front-run
114+
// (an earlier non-atomic `new impl(); impl.initialize(0xdead)` was front-run on mainnet).
115+
address npmImpl = address(new NpmImplDeployer(address(factory), WBNB).impl());
116+
117+
bytes memory npmInit = abi.encodeWithSelector(NonfungiblePositionManager.initialize.selector, tokenDescriptor);
118+
TransparentUpgradeableProxy npmProxy =
119+
new TransparentUpgradeableProxy(npmImpl, address(proxyAdmin), npmInit);
120+
121+
// SwapRouter is not upgradeable; plain deploy against the factory.
122+
SwapRouter swapRouter = new SwapRouter(address(factory), WBNB);
123+
124+
vm.stopBroadcast();
125+
126+
out = Deployment({
127+
proxyAdmin: address(proxyAdmin),
128+
factory: address(factory),
129+
tokenDescriptor: tokenDescriptor,
130+
npmImpl: npmImpl,
131+
npmProxy: address(npmProxy),
132+
swapRouter: address(swapRouter)
133+
});
134+
135+
console.log('--- deployed ---');
136+
console.log('ProxyAdmin:', out.proxyAdmin);
137+
console.log('Factory:', out.factory);
138+
console.log('TokenDescriptor:', out.tokenDescriptor);
139+
console.log('NPM impl:', out.npmImpl);
140+
console.log('NPM proxy:', out.npmProxy);
141+
console.log('SwapRouter:', out.swapRouter);
142+
}
143+
}
144+
145+
/// @title Atomic NPM implementation deployer
146+
/// @notice Deploys a NonfungiblePositionManager implementation and consumes its initializer within
147+
/// this contract's constructor — i.e. inside a single CREATE transaction. Because the deploy and the
148+
/// `initialize(0xdead)` happen in the same tx, there is no intervening block in which a bot can call
149+
/// `initialize()` on the fresh impl. Read `impl()` afterwards for the deployed implementation address.
150+
contract NpmImplDeployer {
151+
NonfungiblePositionManager public immutable impl;
152+
153+
constructor(address factory_, address weth9_) {
154+
NonfungiblePositionManager i = new NonfungiblePositionManager(factory_, weth9_);
155+
// Close the impl-takeover window atomically; harmless even though NPM has no selfdestruct.
156+
i.initialize(address(0xdead));
157+
impl = i;
158+
}
159+
}

src/periphery/NonfungiblePositionManager.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ contract NonfungiblePositionManager is
7676
constructor(address _factory, address _WETH9) PeripheryImmutableState(_factory, _WETH9) {}
7777

7878
function initialize(address _tokenDescriptor_) external initializer {
79-
__ERC721Permit_init('Lista V3 Positions NFT-V1', 'LIS-V3-POS', '1');
79+
__ERC721Permit_init('Lista V3 Positions NFT', 'LISTA-V3', '1');
8080
_tokenDescriptor = _tokenDescriptor_;
8181
_nextId = 1;
8282
_nextPoolId = 1;

test/periphery/FullFlowTest.t.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,8 @@ contract FullFlowTest is Test {
140140
assertEq(npm.WETH9(), address(weth));
141141

142142
// ERC721 metadata set by __ERC721_init through initialize()
143-
assertEq(npm.name(), 'Lista V3 Positions NFT-V1');
144-
assertEq(npm.symbol(), 'LIS-V3-POS');
143+
assertEq(npm.name(), 'Lista V3 Positions NFT');
144+
assertEq(npm.symbol(), 'LISTA-V3');
145145

146146
// EIP-165 registrations written to proxy storage during initialize()
147147
assertTrue(npm.supportsInterface(0x01ffc9a7)); // ERC165
@@ -159,7 +159,7 @@ contract FullFlowTest is Test {
159159
keccak256(
160160
abi.encode(
161161
0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f,
162-
keccak256(bytes('Lista V3 Positions NFT-V1')),
162+
keccak256(bytes('Lista V3 Positions NFT')),
163163
keccak256(bytes('1')),
164164
ChainId.get(),
165165
address(npm)

0 commit comments

Comments
 (0)