@sentrix-labs/canonical-contracts 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,70 @@
1
+ # Changelog
2
+
3
+ All notable changes to Sentrix canonical contracts are documented in this file.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Tag pattern `vX.Y.Z`. Each release tag corresponds to a coordinated deploy to mainnet (chain 7119) + testnet (chain 7120).
6
+
7
+ ---
8
+
9
+ ## [Unreleased]
10
+
11
+ ### Documentation
12
+
13
+ - **`docs/ADDRESSES.md`** — added "SentrixSafe ownership" section documenting the 2026-04-28 migration history and the final 1-of-1 state.
14
+ - **`script/TransferOwnership.s.sol`** — rewritten to document the completed migration with `AUTHORITY` / `SAFE_MAINNET` / `SAFE_TESTNET` constants. Read-only print of final state.
15
+ - **`README.md`** — SentrixSafe row links to the ownership section in `docs/ADDRESSES.md`.
16
+
17
+ ### Governance
18
+
19
+ - **SentrixSafe ownership migration FINAL (2026-04-28)** — both Safes now 1-of-1 with the Sentrix Labs authority signer `0xa25236925bc10954e0519731cc7ba97f4bb5714b` (threshold=1). Bootstrap deployer retired from Safe ownership.
20
+
21
+ | Step | Chain | Tx | Block |
22
+ |---|---|---|---|
23
+ | addOwner(authority, 1) | testnet 7120 | `0xb70a83eb416e…` | 881639 |
24
+ | addOwner(authority, 1) | mainnet 7119 | `0xd17400c35f07…` | 755821 |
25
+ | removeOwner(deployer, 1) | testnet 7120 | `0xb0c69e89252c…` | 884599 |
26
+ | removeOwner(deployer, 1) | mainnet 7119 | `0x8e9ca8b4cbe0…` | 757829 |
27
+
28
+ ---
29
+
30
+ ## [1.0.0] — 2026-04-27 — Initial deploy to Sentrix mainnet (7119) + testnet (7120)
31
+
32
+ > **All four canonical contracts deployed.** Single coordinated deploy session by `0x5acb04058fc4dfa258f29ce318282377cac176fd` (bootstrap deployer EOA, retired from Safe ownership 2026-04-28). All addresses immutable.
33
+
34
+ ### Deployed — chain 7119 (Sentrix mainnet)
35
+
36
+ | Contract | Address | Block | Deploy tx |
37
+ |---|---|---|---|
38
+ | WSRX | `0x4693b113e523A196d9579333c4ab8358e2656553` | 716787 | `0xc5b5016338ba2de65ebb631374724bcc33db63b9a570e77d455d896b40f103fb` |
39
+ | Multicall3 | `0xFd4b34b5763f54a580a0d9f7997A2A993ef9ceE9` | 717078 | `0x64633e100f952d845970590fda32786118cf5e6b29b56c281d8d1e4b8e889f0a` |
40
+ | TokenFactory | `0xc753199b723649ab92c6db8A45F158921CFDEe49` | 717392 | `0xfda6219d60219e223d049158bca734d77e475c0b6b0c02074beeba0c701be112` |
41
+ | SentrixSafe | `0x6272dC0C842F05542f9fF7B5443E93C0642a3b26` | 717618 | `0xc67fb31dd135051732a41530e26897fb7c10eaec1fc6cb9334b596073758cb0f` |
42
+
43
+ ### Deployed — chain 7120 (Sentrix testnet)
44
+
45
+ | Contract | Address | Block | Deploy tx |
46
+ |---|---|---|---|
47
+ | WSRX | `0x85d5E7694AF31C2Edd0a7e66b7c6c92C59fF949A` | 723183 | `0xfcfd2e0c1b3b4e61a2166a35cbb780d8370e9d1d6c67f7137aeb66c52260e8b3` |
48
+ | Multicall3 | `0x7900826De548425c6BE56caEbD4760AB0155Cd54` | 723191 | `0x1f8d6749f7ffdcbbaabfad21167d5133593944be6c59025c69c3f6543cb7f6c2` |
49
+ | TokenFactory | `0x7A2992af0d4979aDD076347666023d66d29276Fc` | 723195 | `0xe68e5553af080a97181e09279782873f884477759656e51c03f522c94cb9da47` |
50
+ | SentrixSafe | `0xc9D7a61D7C2F428F6A055916488041fD00532110` | 723511 | `0x514863050498a545fa627d696aa82cdd2558bc35470418d7f83af8f1c0a12176` |
51
+
52
+ ### Initial scaffold (pre-deploy work, all merged)
53
+
54
+ - `contracts/WSRX.sol` — Wrapped SRX (ERC-20, 18-decimal, native bridge)
55
+ - `contracts/Multicall3.sol` — Standard Multicall3 (read/write batch)
56
+ - `contracts/SentrixSafe.sol` — Minimal multi-sig (Gnosis Safe v1.4.1-derived; array-based owners with `addOwner` / `removeOwner` / `execTransaction`)
57
+ - `contracts/TokenFactory.sol` — ERC-20 deployer (open factory, anyone may call)
58
+ - `script/Deploy*.s.sol` — Foundry deploy scripts (per-contract) + `CheckDeployment.s.sol` smoke test
59
+ - `test/*.t.sol` — unit + fuzz + invariant + integration tests
60
+ - `deployments/{7119,7120}.json` — populated with deployed addresses
61
+ - `deployments/abi/*.json` — ABI exports refreshed via `script/copy-abi.sh`
62
+ - `docs/ADDRESSES.md` — auto-generated from deployments JSON via `script/GenerateAddressDocs.sh`
63
+ - CI: `forge build + test + snapshot`, slither static analysis, CodeQL — all green
64
+
65
+ ### Notes
66
+
67
+ - WSRX, Multicall3, and TokenFactory have **no owner role** (immutable). Only SentrixSafe has owner-set governance via `execTransaction`.
68
+ - SentrixSafe deployed initially as a **1-of-1 multisig owned by the bootstrap deployer**. Migrated to 1-of-1 with authority signer post-deploy (see Unreleased §Governance above).
69
+
70
+ Multicall3 deployed at a non-canonical address — the cross-chain canonical `0xcA11bde05977b3631167028862bE2a173976CA11` (via Create2 deployer) is **not** in use here. Initial Sentrix deploy used plain CREATE for v1; future revisions may add a Create2 path if cross-chain tooling demands it.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sentrix Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE ADDED
@@ -0,0 +1,19 @@
1
+ Sentrix Canonical Contracts
2
+ Copyright 2026 Sentrix Labs
3
+
4
+ This product includes software developed by Sentrix Labs, distributed under
5
+ the Business Source License 1.1 (BUSL-1.1) — see LICENSE.
6
+
7
+ Change Date: 2030-01-01
8
+ On the Change Date, the License terms convert to the Apache License,
9
+ Version 2.0, as published by the Apache Software Foundation.
10
+
11
+ This product mirrors third-party software:
12
+ - contracts/Multicall3.sol — verbatim from github.com/mds1/multicall
13
+ (MIT License, copyright Matt Solomon). License preserved in the
14
+ file's SPDX header.
15
+
16
+ Trademark notice: SENTRIX and SENTRIX CHAIN are trademarks of Sentrix
17
+ Labs. Use of these marks in the unmodified canonical contracts is
18
+ permitted under the BUSL-1.1 use grant. Use of the marks in derivative
19
+ works requires written permission.
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ <p align="center">
2
+ <img src="https://cdn.jsdelivr.net/gh/sentrix-labs/brand-kit@master/png-transparent/sentrix-labs-256.png" alt="Sentrix Labs" width="120">
3
+ </p>
4
+
5
+ <h1 align="center">Sentrix Canonical Contracts</h1>
6
+
7
+ <p align="center"><strong>Production EVM contracts for <a href="https://sentrixchain.com">Sentrix Chain</a> — WSRX, Multicall3, SentrixSafe, TokenFactory.</strong></p>
8
+
9
+ <p align="center">
10
+ <a href="https://github.com/sentrix-labs/canonical-contracts/actions/workflows/ci.yml"><img src="https://github.com/sentrix-labs/canonical-contracts/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
11
+ <a href="https://github.com/sentrix-labs/canonical-contracts/actions/workflows/security.yml"><img src="https://github.com/sentrix-labs/canonical-contracts/actions/workflows/security.yml/badge.svg" alt="Security"></a>
12
+ <img src="https://img.shields.io/badge/license-BUSL--1.1-black" alt="License">
13
+ <img src="https://img.shields.io/badge/solc-0.8.24-blue" alt="Solidity">
14
+ <img src="https://img.shields.io/badge/foundry-stable-orange" alt="Foundry">
15
+ </p>
16
+
17
+ ---
18
+
19
+ ## What's in here
20
+
21
+ ### Deployed on both chains (mainnet 7119 + testnet 7120)
22
+
23
+ | Contract | Purpose |
24
+ |---|---|
25
+ | [`WSRX`](contracts/WSRX.sol) | Wrapped SRX — ERC-20 (18 decimals) backed 1:1 by native SRX. Lets EVM dApps hold SRX as a token. |
26
+ | [`Multicall3`](contracts/Multicall3.sol) | Standard Multicall3 ([mds1/multicall](https://github.com/mds1/multicall)) for batched read/write calls. |
27
+ | [`SentrixSafe`](contracts/SentrixSafe.sol) | Minimal multi-sig wallet (Gnosis Safe v1.4.1-derived) for treasury management. Currently configured 1-of-1 with the Sentrix Labs authority signer (`0xa25236925bc10954e0519731cc7ba97f4bb5714b`) on both chains — see [`docs/ADDRESSES.md`](docs/ADDRESSES.md#sentrixsafe-ownership). |
28
+ | [`TokenFactory`](contracts/TokenFactory.sol) | Deploys minimal ERC-20 tokens via a single function call. v1 + v1.1.0 both deployed (see addresses doc). |
29
+
30
+ ### Deployed on mainnet (7119) only
31
+
32
+ | Contract | Purpose |
33
+ |---|---|
34
+ | [`CoinBlastCurve`](contracts/CoinBlastCurve.sol) | Bonding-curve token contract. The CBLAST genesis token launched via this curve on 2026-05-01 — first on-chain CoinBlast bonding-curve launch. |
35
+
36
+ ### In-tree but not yet deployed
37
+
38
+ | Contract | Purpose |
39
+ |---|---|
40
+ | [`CoinBlastFactory`](contracts/CoinBlastFactory.sol) | Launchpad-style factory that spawns new `CoinBlastCurve` instances. Code-complete; deployment gated on the launchpad UX surface landing. |
41
+ | [`MerkleAirdrop`](contracts/MerkleAirdrop.sol) | Merkle-root airdrop distribution contract for the planned eligibility-based SRX drops. Code-complete; deployment gated on the airdrop campaign go-signal. |
42
+ | [`StrategicReserveTimelock`](contracts/StrategicReserveTimelock.sol) | Time-locked treasury vault for the Strategic Reserve allocation. Code-complete; deployment gated on Strategic Reserve EOA → contract migration plan. |
43
+
44
+ See [`docs/ADDRESSES.md`](docs/ADDRESSES.md) for deployed addresses on each chain.
45
+
46
+ ## Quickstart
47
+
48
+ ```bash
49
+ git clone --recurse-submodules https://github.com/sentrix-labs/canonical-contracts.git
50
+ cd canonical-contracts
51
+
52
+ # Install Foundry: https://getfoundry.sh
53
+ curl -L https://foundry.paradigm.xyz | bash && foundryup
54
+
55
+ # Install dependencies
56
+ make install
57
+
58
+ # Build + test
59
+ make build
60
+ make test
61
+
62
+ # Coverage
63
+ make coverage # outputs coverage/lcov.info
64
+ ```
65
+
66
+ ## Integrate
67
+
68
+ ```bash
69
+ npm install @sentrix-labs/canonical-contracts ethers
70
+ ```
71
+
72
+ ```ts
73
+ import { ethers } from "ethers";
74
+ import abi from "@sentrix-labs/canonical-contracts/deployments/abi/WSRX.json";
75
+ import deployments from "@sentrix-labs/canonical-contracts/deployments/7119.json";
76
+
77
+ const provider = new ethers.JsonRpcProvider("https://rpc.sentrixchain.com");
78
+ const wsrx = new ethers.Contract(deployments.WSRX.address, abi.abi, provider);
79
+ console.log("totalSupply:", await wsrx.totalSupply());
80
+ ```
81
+
82
+ Full integration guide → [`docs/INTEGRATION.md`](docs/INTEGRATION.md).
83
+
84
+ ## Deploy
85
+
86
+ End-to-end runbook: [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md). High-level flow:
87
+
88
+ ```bash
89
+ cp .env.example .env
90
+ $EDITOR .env
91
+
92
+ forge script script/DeployWSRX.s.sol --rpc-url sentrix_testnet --broadcast --private-key $DEPLOYER_PRIVATE_KEY
93
+ forge script script/DeployWSRX.s.sol --rpc-url sentrix_mainnet --broadcast --private-key $DEPLOYER_PRIVATE_KEY
94
+
95
+ # Update deployments/7119.json + 7120.json + CHANGELOG.md
96
+ # Tag release
97
+ git tag v1.0.0 && git push --tags
98
+ ```
99
+
100
+ CI auto-creates a GitHub Release from the CHANGELOG entry.
101
+
102
+ ## Verify
103
+
104
+ Sourcify-equivalent verification is on the ecosystem readiness Tier 1 backlog. Until that lands, run manual verification per [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) §12.
105
+
106
+ Health-check a deployment:
107
+
108
+ ```bash
109
+ WSRX_ADDR=0x... MULTICALL3_ADDR=0x... SAFE_ADDR=0x... FACTORY_ADDR=0x... \
110
+ forge script script/CheckDeployment.s.sol --rpc-url sentrix_testnet
111
+ ```
112
+
113
+ ## Security
114
+
115
+ - All contracts immutable (no upgrade proxy — see [`docs/SECURITY_MODEL.md`](docs/SECURITY_MODEL.md))
116
+ - Pre-merge: `forge test`, `forge build --sizes`, `slither`, `gitleaks`
117
+ - Daily: scheduled slither + mythril runs ([`security.yml`](.github/workflows/security.yml))
118
+ - Vulnerability disclosure: `security@sentrixchain.com` ([`SECURITY.md`](SECURITY.md))
119
+
120
+ ## Docs
121
+
122
+ | Doc | What it covers |
123
+ |---|---|
124
+ | [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | Contract relationships + 8↔18 decimal conversion |
125
+ | [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) | Step-by-step deploy runbook |
126
+ | [`docs/INTEGRATION.md`](docs/INTEGRATION.md) | Code examples (ethers, wagmi) |
127
+ | [`docs/SECURITY_MODEL.md`](docs/SECURITY_MODEL.md) | Trust assumptions + threat model |
128
+ | [`docs/ADDRESSES.md`](docs/ADDRESSES.md) | Deployed addresses (auto-gen) |
129
+ | [`docs/FAQ.md`](docs/FAQ.md) | Common questions |
130
+ | [`docs/STORAGE_LAYOUT.md`](docs/STORAGE_LAYOUT.md) | Storage slots per contract |
131
+ | [`docs/AUDIT.md`](docs/AUDIT.md) | Audit status + findings (when available) |
132
+
133
+ ## Contributing
134
+
135
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md). PRs welcome — `forge test` + `forge fmt --check` + `slither --fail-high` must pass before merge.
136
+
137
+ ## Community
138
+
139
+ - **GitHub Discussions** — https://github.com/sentrix-labs/canonical-contracts/discussions for integration questions, contract design feedback, deployment help.
140
+ - **Org profile** — https://github.com/sentrix-labs
141
+
142
+ ## License
143
+
144
+ BUSL-1.1 (see [`LICENSE`](LICENSE) + [`NOTICE`](NOTICE)). Change Date: 2030-01-01 → MIT.
145
+
146
+ `Multicall3.sol` is a verbatim mirror of [mds1/multicall](https://github.com/mds1/multicall) (MIT) — license preserved in the file's SPDX header.
147
+
148
+ ---
149
+
150
+ <p align="center"><sub>Built by <a href="https://github.com/sentrix-labs">Sentrix Labs</a> for <a href="https://sentrixchain.com">Sentrix Chain</a>.</sub></p>
@@ -0,0 +1,369 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity 0.8.24;
3
+
4
+ import {FactoryToken} from "./TokenFactory.sol";
5
+
6
+ /// Minimal interface for the Sentrix V2 router/factory at graduation time.
7
+ /// We use these only for read-only checks (audit H5 shadow-pair guard);
8
+ /// the actual addLiquiditySRX call is still done via low-level call so the
9
+ /// router ABI evolves independently.
10
+ interface IRouterMinimal {
11
+ function factory() external view returns (address);
12
+ }
13
+
14
+ interface IFactoryMinimal {
15
+ function getPair(address tokenA, address tokenB) external view returns (address);
16
+ }
17
+
18
+ /// @title CoinBlastCurve
19
+ /// @author Sentrix Labs
20
+ /// @notice On-chain linear bonding curve for the CoinBlast launchpad. One
21
+ /// instance per launched token. Buyers send native SRX, get tokens
22
+ /// along the curve P(s) = P0 × (1 + K × s/S). When SRX raised hits
23
+ /// the graduation threshold, anyone can call `graduate()` which
24
+ /// seeds the canonical Sentrix V2 DEX pool with the raised SRX +
25
+ /// remaining curve inventory and burns the resulting LP forever.
26
+ ///
27
+ /// @dev Designed to match the frontend's bonding-curve.ts arithmetic
28
+ /// exactly so off-chain estimates equal on-chain settlements.
29
+ ///
30
+ /// Security model:
31
+ /// - Immutable post-deploy: no admin keys, no upgrade path, no
32
+ /// parameter mutation. Once SRX is raised it can only exit via
33
+ /// a sell() back along the curve OR via graduate() into locked LP.
34
+ /// - nonReentrant guards every state-changing entry point so the
35
+ /// ERC-20 token's transfer hooks (or a malicious recipient on
36
+ /// SRX refund) cannot re-enter the curve mid-trade.
37
+ /// - On graduation, the launchpad calls into the DEX router and
38
+ /// immediately burns the LP receipt — the raised SRX is locked
39
+ /// as DEX liquidity forever.
40
+ contract CoinBlastCurve {
41
+ // ── Immutable curve parameters ────────────────────────────────────
42
+ /// @notice ERC-20 token sold by this curve. The curve owns the entire
43
+ /// initial supply at construction; tokens leave via buy() and
44
+ /// return via sell()/graduate().
45
+ FactoryToken public immutable token;
46
+ /// @notice Total token supply (also the curve's denominator). Constant.
47
+ uint256 public immutable curveSupply;
48
+ /// @notice Base price numerator/denominator in SRX-wei per whole token.
49
+ /// price0 = basePriceNum / basePriceDen (whole-token units).
50
+ uint256 public immutable basePriceNum;
51
+ uint256 public immutable basePriceDen;
52
+ /// @notice Curve steepness — k = kNum / kDen (e.g. 1/2 = 0.5).
53
+ uint256 public immutable kNum;
54
+ uint256 public immutable kDen;
55
+ /// @notice Once `srxRaised` reaches this value, graduation unlocks.
56
+ uint256 public immutable graduationSrxThreshold;
57
+ /// @notice Trading-fee taker (in basis points; capped at 500 = 5%).
58
+ address public immutable feeRecipient;
59
+ uint256 public immutable feeBps;
60
+ /// @notice Sentrix V2 router used for graduation. Pinned at construction
61
+ /// so a future router upgrade can't redirect graduation flow.
62
+ address public immutable router;
63
+ /// @notice WSRX address — needed by router.addLiquiditySRX. Pinned.
64
+ address public immutable wsrx;
65
+
66
+ /// @dev Hard cap on fees so a misconfigured deploy can't drain users.
67
+ uint256 public constant MAX_FEE_BPS = 500; // 5%
68
+ /// @dev Bound on basePriceNum × kNum so the curve-cost product stays
69
+ /// under 2^256 across the full supply range (see _curveCost notes).
70
+ uint256 public constant MAX_PRICE_NUM = 1e18;
71
+ uint256 public constant MAX_K_NUM = 1e3;
72
+ uint256 public constant MAX_CURVE_SUPPLY = 1e30; // 1T tokens × 1e18
73
+
74
+ // ── Mutable curve state ───────────────────────────────────────────
75
+ /// @notice Tokens currently held by buyers (= curveSupply - balanceOf(this)).
76
+ uint256 public tokensSold;
77
+ /// @notice Cumulative native SRX received (net of fees + sells).
78
+ uint256 public srxRaised;
79
+ /// @notice Latched true after graduate() runs. Locks buy/sell forever.
80
+ bool public graduated;
81
+
82
+ /// @dev Reentrancy lock — initialised to 1 so the first acquire is cheap.
83
+ uint256 private _locked = 1;
84
+
85
+ // ── Events ────────────────────────────────────────────────────────
86
+ event Buy(address indexed buyer, uint256 srxIn, uint256 fee, uint256 tokensOut);
87
+ event Sell(address indexed seller, uint256 tokensIn, uint256 fee, uint256 srxOut);
88
+ event Graduated(address indexed pair, uint256 srxLiquidity, uint256 tokenLiquidity, uint256 lpBurned);
89
+
90
+ // ── Errors ────────────────────────────────────────────────────────
91
+ error AlreadyGraduated();
92
+ error NotGraduatable();
93
+ error ZeroValue();
94
+ error Slippage();
95
+ error InsufficientReserve();
96
+ error TransferFailed();
97
+ error Reentrancy();
98
+ error FeeTooHigh();
99
+ error InvalidParams();
100
+
101
+ modifier nonReentrant() {
102
+ if (_locked != 1) revert Reentrancy();
103
+ _locked = 2;
104
+ _;
105
+ _locked = 1;
106
+ }
107
+
108
+ modifier active() {
109
+ if (graduated) revert AlreadyGraduated();
110
+ _;
111
+ }
112
+
113
+ /// @notice One-shot construction parameters — packed into a struct so the
114
+ /// constructor stays under solc's stack-depth limit (12 args
115
+ /// tripped the EVM-stack frame for the via-Yul codegen).
116
+ struct InitParams {
117
+ string name;
118
+ string symbol;
119
+ uint256 curveSupply;
120
+ uint256 basePriceNum;
121
+ uint256 basePriceDen;
122
+ uint256 kNum;
123
+ uint256 kDen;
124
+ uint256 graduationSrxThreshold;
125
+ address feeRecipient;
126
+ uint256 feeBps;
127
+ address router;
128
+ address wsrx;
129
+ }
130
+
131
+ // ── Construction ──────────────────────────────────────────────────
132
+ constructor(InitParams memory p) {
133
+ if (p.curveSupply == 0 || p.basePriceDen == 0 || p.kDen == 0) revert InvalidParams();
134
+ if (p.graduationSrxThreshold == 0) revert InvalidParams();
135
+ if (p.router == address(0) || p.wsrx == address(0) || p.feeRecipient == address(0)) revert InvalidParams();
136
+ if (p.feeBps > MAX_FEE_BPS) revert FeeTooHigh();
137
+ // Magnitude bounds — see _curveCost. Without these, the slope-term
138
+ // product can overflow uint256 for extreme parameter combinations.
139
+ if (p.basePriceNum > MAX_PRICE_NUM || p.kNum > MAX_K_NUM) revert InvalidParams();
140
+ if (p.curveSupply > MAX_CURVE_SUPPLY) revert InvalidParams();
141
+
142
+ token = new FactoryToken(p.name, p.symbol, p.curveSupply, address(this));
143
+ curveSupply = p.curveSupply;
144
+ basePriceNum = p.basePriceNum;
145
+ basePriceDen = p.basePriceDen;
146
+ kNum = p.kNum;
147
+ kDen = p.kDen;
148
+ graduationSrxThreshold = p.graduationSrxThreshold;
149
+ feeRecipient = p.feeRecipient;
150
+ feeBps = p.feeBps;
151
+ router = p.router;
152
+ wsrx = p.wsrx;
153
+ }
154
+
155
+ // ── Curve math ────────────────────────────────────────────────────
156
+ //
157
+ // Linear curve: P(s) = (basePriceNum / basePriceDen) × (1 + (kNum/kDen) × s / curveSupply)
158
+ //
159
+ // Cost to move from `a` tokens-sold to `b` tokens-sold (with b > a):
160
+ // ∫[a,b] P(s) ds
161
+ // = (basePriceNum / basePriceDen) × (b − a) // base term
162
+ // + (basePriceNum / basePriceDen) × (kNum / kDen) × (b² − a²) / (2 × curveSupply) // slope term
163
+ //
164
+ // We multiply through to keep everything in SRX-wei and uint256-safe. With
165
+ // curveSupply ≤ 1e30 (1B tokens × 1e18), b² ≤ 1e60 still fits into uint256.
166
+
167
+ /// @notice SRX-wei cost to move from `a` to `b` tokens (b ≥ a, both in token-wei).
168
+ /// @dev Linear bonding curve P(s) = P0 × (1 + K × s/S) integrates to:
169
+ /// Cost = P0 × Δ + P0 × K × Δ × (a+b) / (2S)
170
+ /// where Δ = b - a, S = curveSupply.
171
+ ///
172
+ /// To avoid uint256 overflow on the slope term we apply the
173
+ /// constructor-enforced bounds:
174
+ /// basePriceNum ≤ 1e18, kNum ≤ 1e3, curveSupply ≤ 1e30
175
+ /// The largest intermediate is `basePriceNum × kNum × delta × sum`
176
+ /// which is ≤ 1e18 × 1e3 × 1e30 × 2e30 = 2e81 — ABOVE 2^256.
177
+ /// So we factor and divide eagerly: compute `delta × sum / (2S)`
178
+ /// first (≤ S = 1e30, safe), then multiply by `basePriceNum × kNum`
179
+ /// (≤ 1e21), giving a final ≤ 1e51 → safe.
180
+ function _curveCost(uint256 a, uint256 b) internal view returns (uint256) {
181
+ if (b <= a) return 0;
182
+ uint256 delta = b - a;
183
+
184
+ // Base term: P0 × Δ
185
+ uint256 baseTerm = (basePriceNum * delta) / basePriceDen;
186
+
187
+ // Slope term in three steps with eager division to keep intermediates
188
+ // bounded:
189
+ // step1 = (delta × (a + b)) / (2 × curveSupply) ≤ curveSupply
190
+ // step2 = step1 × basePriceNum × kNum ≤ 1e51
191
+ // step3 = step2 / (basePriceDen × kDen) = result
192
+ //
193
+ // Two ordering caveats:
194
+ // - delta × (a + b) can reach 2 × curveSupply² = 2 × 1e60. Still
195
+ // under 2^256 (≈ 1.16e77), so the multiplication itself is safe.
196
+ // - Eager division by (2 × curveSupply) discards up to (2S - 1)
197
+ // wei of precision per term. Test suite asserts settlement
198
+ // within ≤ 1 wei of the TS reference for typical params.
199
+ uint256 sum = a + b;
200
+ uint256 slopeTerm = (delta * sum) / (2 * curveSupply);
201
+ slopeTerm = slopeTerm * basePriceNum;
202
+ slopeTerm = (slopeTerm * kNum) / (basePriceDen * kDen);
203
+
204
+ return baseTerm + slopeTerm;
205
+ }
206
+
207
+ /// @notice Cost in SRX-wei for buying enough tokens to move from
208
+ /// `tokensSold` → `tokensSold + tokensOut`. View helper.
209
+ function quoteBuy(uint256 tokensOut) public view returns (uint256 grossSrxIn, uint256 fee) {
210
+ if (tokensOut == 0) return (0, 0);
211
+ if (tokensSold + tokensOut > curveSupply) revert InsufficientReserve();
212
+ uint256 baseCost = _curveCost(tokensSold, tokensSold + tokensOut);
213
+ // Fee is taken on top of the curve cost so the user pays
214
+ // grossSrxIn = baseCost + fee.
215
+ fee = (baseCost * feeBps) / (10_000 - feeBps);
216
+ grossSrxIn = baseCost + fee;
217
+ }
218
+
219
+ /// @notice Tokens received when selling along the curve.
220
+ function quoteSell(uint256 tokensIn) public view returns (uint256 srxOut, uint256 fee) {
221
+ if (tokensIn == 0) return (0, 0);
222
+ if (tokensIn > tokensSold) revert InsufficientReserve();
223
+ uint256 baseRefund = _curveCost(tokensSold - tokensIn, tokensSold);
224
+ fee = (baseRefund * feeBps) / 10_000;
225
+ srxOut = baseRefund - fee;
226
+ }
227
+
228
+ // ── Trade entrypoints ─────────────────────────────────────────────
229
+
230
+ /// @notice Buy along the curve. Caller specifies `minTokensOut` for slippage
231
+ /// protection. Excess SRX is refunded.
232
+ function buy(uint256 minTokensOut) external payable active nonReentrant returns (uint256 tokensOut) {
233
+ if (msg.value == 0) revert ZeroValue();
234
+
235
+ // Binary search for the largest tokensOut such that quoteBuy(tokensOut) ≤ msg.value.
236
+ // For a strictly-monotone-increasing curve this converges in ≤256 iterations.
237
+ uint256 lo = 0;
238
+ uint256 hi = curveSupply - tokensSold;
239
+ while (lo < hi) {
240
+ uint256 mid = lo + (hi - lo + 1) / 2;
241
+ (uint256 cost,) = quoteBuy(mid);
242
+ if (cost <= msg.value) lo = mid;
243
+ else hi = mid - 1;
244
+ }
245
+ tokensOut = lo;
246
+ if (tokensOut < minTokensOut) revert Slippage();
247
+ if (tokensOut == 0) revert ZeroValue();
248
+
249
+ (uint256 grossPaid, uint256 fee) = quoteBuy(tokensOut);
250
+
251
+ tokensSold += tokensOut;
252
+ srxRaised += (grossPaid - fee);
253
+
254
+ // Forward fee to recipient; refund any dust to buyer.
255
+ if (fee > 0) _safeSendSRX(feeRecipient, fee);
256
+ uint256 refund = msg.value - grossPaid;
257
+ if (refund > 0) _safeSendSRX(msg.sender, refund);
258
+
259
+ require(token.transfer(msg.sender, tokensOut), "CoinBlast: TOKEN_TRANSFER");
260
+
261
+ emit Buy(msg.sender, grossPaid, fee, tokensOut);
262
+ }
263
+
264
+ /// @notice Sell tokens back along the curve. Caller must have approved
265
+ /// the curve for `tokensIn` beforehand.
266
+ function sell(uint256 tokensIn, uint256 minSrxOut)
267
+ external
268
+ active
269
+ nonReentrant
270
+ returns (uint256 srxOut)
271
+ {
272
+ if (tokensIn == 0) revert ZeroValue();
273
+ (uint256 srxNet, uint256 fee) = quoteSell(tokensIn);
274
+ if (srxNet < minSrxOut) revert Slippage();
275
+
276
+ require(token.transferFrom(msg.sender, address(this), tokensIn), "CoinBlast: TOKEN_TRANSFER_FROM");
277
+ tokensSold -= tokensIn;
278
+
279
+ // Audit H4 (HIGH, 2026-05-07): srxRaised is a derived figure that
280
+ // can drift from address(this).balance because quoteBuy/quoteSell fee
281
+ // math truncates sub-wei lossage on each trade. The pre-fix line was
282
+ // `srxRaised -= (srxNet + fee)` which could underflow → revert →
283
+ // sells permanently locked until graduation threshold met. Clamp the
284
+ // debit at zero so the underflow is impossible; address(this).balance
285
+ // remains the true source of truth for outflows below.
286
+ uint256 debit = srxNet + fee;
287
+ srxRaised = srxRaised >= debit ? srxRaised - debit : 0;
288
+
289
+ if (fee > 0) _safeSendSRX(feeRecipient, fee);
290
+ _safeSendSRX(msg.sender, srxNet);
291
+
292
+ emit Sell(msg.sender, tokensIn, fee, srxNet);
293
+ srxOut = srxNet;
294
+ }
295
+
296
+ // ── Graduation ────────────────────────────────────────────────────
297
+
298
+ /// @notice Anyone may trigger graduation once the SRX raised meets the
299
+ /// threshold. Migrates raised SRX + remaining tokens into a fresh
300
+ /// Sentrix V2 pool and burns the LP forever.
301
+ function graduate() external active nonReentrant {
302
+ if (srxRaised < graduationSrxThreshold) revert NotGraduatable();
303
+
304
+ // Audit H5 (HIGH, 2026-05-07): graduate() was unauthenticated and
305
+ // someone could front-run by pre-creating a (token, wsrx) pair on
306
+ // the V2 factory with skewed reserves before this call. addLiquidity
307
+ // would then mint LP at a manipulated ratio, leaking value to whoever
308
+ // arbitraged the first post-graduation swap. Guard: read the pinned
309
+ // router's factory, ask if a pair already exists for our (token, wsrx),
310
+ // and revert if so. This forces the curve to be the SOLE creator of
311
+ // its launch pair.
312
+ address factoryAddr = IRouterMinimal(router).factory();
313
+ require(
314
+ IFactoryMinimal(factoryAddr).getPair(address(token), wsrx) == address(0),
315
+ "CoinBlast: PAIR_EXISTS"
316
+ );
317
+
318
+ graduated = true;
319
+
320
+ uint256 tokenLiquidity = token.balanceOf(address(this));
321
+ // Surfaced during testnet smoke 2026-05-01: integer rounding inside
322
+ // quoteBuy/quoteSell can leave srxRaised a few wei *above* the
323
+ // actual native balance (the fee math discards sub-wei lossage on
324
+ // each trade, accumulated). Sourcing srxLiquidity from
325
+ // address(this).balance avoids router.call{value: srxLiquidity}
326
+ // reverting with insufficient-native-funds at the very last step.
327
+ // We still zero srxRaised so any future read sees 0 — the curve
328
+ // is graduated either way.
329
+ uint256 srxLiquidity = address(this).balance;
330
+ srxRaised = 0;
331
+
332
+ // Approve router to pull tokens, then add liquidity. LP receipt is
333
+ // sent to address(0) → permanently locked.
334
+ require(token.approve(router, tokenLiquidity), "CoinBlast: APPROVE");
335
+ (bool ok, bytes memory ret) = router.call{value: srxLiquidity}(
336
+ abi.encodeWithSignature(
337
+ "addLiquiditySRX(address,uint256,uint256,uint256,address,uint256)",
338
+ address(token),
339
+ tokenLiquidity,
340
+ tokenLiquidity, // accept any positive token amount (we control supply)
341
+ srxLiquidity, // accept any positive SRX amount (we control raise)
342
+ address(0xdEaD), // LP receipt destination — burnt forever
343
+ block.timestamp + 1 hours
344
+ )
345
+ );
346
+ require(ok, "CoinBlast: ADD_LIQUIDITY");
347
+ // ret = (uint amountToken, uint amountSRX, uint liquidity); we only
348
+ // need the LP amount for the event log.
349
+ (,, uint256 lpBurned) = abi.decode(ret, (uint256, uint256, uint256));
350
+
351
+ // Compute the pair address by re-using the factory we already
352
+ // looked up at the H5 guard above (no need for two factory() calls).
353
+ address pair = IFactoryMinimal(factoryAddr).getPair(address(token), wsrx);
354
+
355
+ emit Graduated(pair, srxLiquidity, tokenLiquidity, lpBurned);
356
+ }
357
+
358
+ // ── Internal helpers ──────────────────────────────────────────────
359
+ function _safeSendSRX(address to, uint256 amount) internal {
360
+ (bool ok,) = to.call{value: amount}("");
361
+ if (!ok) revert TransferFailed();
362
+ }
363
+
364
+ // Reject stray native SRX so accidental sends don't pollute the curve's
365
+ // accounting. Buyers must use buy().
366
+ receive() external payable {
367
+ if (msg.sender != router) revert TransferFailed();
368
+ }
369
+ }