@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 +70 -0
- package/LICENSE +21 -0
- package/NOTICE +19 -0
- package/README.md +150 -0
- package/contracts/CoinBlastCurve.sol +369 -0
- package/contracts/CoinBlastFactory.sol +76 -0
- package/contracts/MerkleAirdrop.sol +142 -0
- package/contracts/Multicall3.sol +125 -0
- package/contracts/SentrixSafe.sol +252 -0
- package/contracts/StrategicReserveTimelock.sol +104 -0
- package/contracts/TokenFactory.sol +106 -0
- package/contracts/WSRX.sol +95 -0
- package/contracts/interfaces/ISentrixSafe.sol +42 -0
- package/contracts/interfaces/ITokenFactory.sol +27 -0
- package/contracts/interfaces/IWSRX.sol +30 -0
- package/contracts/mocks/MockERC20.sol +60 -0
- package/contracts/mocks/MockSRX.sol +28 -0
- package/deployments/7119.json +75 -0
- package/deployments/7120.json +51 -0
- package/deployments/README.md +70 -0
- package/deployments/abi/FactoryToken.json +1 -0
- package/deployments/abi/Multicall3.json +1 -0
- package/deployments/abi/SentrixSafe.json +1 -0
- package/deployments/abi/TokenFactory.json +1 -0
- package/deployments/abi/WSRX.json +1 -0
- package/deployments/abi/index.js +11 -0
- package/dist/generated.d.ts +3181 -0
- package/dist/generated.d.ts.map +1 -0
- package/dist/index.cjs +1429 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1415 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
- package/src/generated.ts +1441 -0
- package/src/index.ts +41 -0
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
|
+
}
|