polkamarkets-js 3.4.5 → 4.0.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.
Files changed (60) hide show
  1. package/.claude/settings.local.json +29 -0
  2. package/CLOB_AUDIT_REPORT.md +305 -0
  3. package/CLOB_SDK_REFERENCE.md +654 -0
  4. package/abis/AdminRegistry.json +1 -0
  5. package/abis/ConditionalTokens.json +1 -0
  6. package/abis/FeeModule.json +1 -0
  7. package/abis/MockERC20.json +1 -1
  8. package/abis/Multicall3.json +1 -0
  9. package/abis/MyriadCTFExchange.json +1 -0
  10. package/abis/NegRiskAdapter.json +1 -0
  11. package/abis/PredictionMarketV3ManagerCLOB.json +1 -0
  12. package/abis/WrappedCollateral.json +1 -0
  13. package/contracts/AdminRegistry.sol +74 -0
  14. package/contracts/ConditionalTokens.sol +129 -0
  15. package/contracts/FeeModule.sol +163 -0
  16. package/contracts/IMarketOracle.sol +18 -0
  17. package/contracts/IMyriadMarketManager.sol +28 -0
  18. package/contracts/MyriadCTFExchange.sol +887 -0
  19. package/contracts/NegRiskAdapter.sol +446 -0
  20. package/contracts/Outcomes.sol +10 -0
  21. package/contracts/PredictionMarketV3ManagerCLOB.sol +397 -0
  22. package/contracts/Swap11.sol +23 -0
  23. package/contracts/WrappedCollateral.sol +68 -0
  24. package/contracts/oracles/RealitioOracle.sol +90 -0
  25. package/foundry.toml +1 -1
  26. package/package.json +1 -1
  27. package/script/CreateCLOBMarket.s.sol +63 -0
  28. package/script/CreateNegRiskEvent.s.sol +98 -0
  29. package/script/DeployCLOB.s.sol +118 -0
  30. package/script/DeployNegRiskAdapter.s.sol +85 -0
  31. package/script/DeployOracles.s.sol +31 -0
  32. package/script/DeployRealitioTest.s.sol +30 -0
  33. package/script/ResolveMarket.s.sol +51 -0
  34. package/script/ResolveNegRiskEvent.s.sol +43 -0
  35. package/script/SetCLOBFees.s.sol +79 -0
  36. package/script/SetupCLOBOperator.s.sol +79 -0
  37. package/script/SubmitRealitioAnswer.s.sol +50 -0
  38. package/scripts/create_clob_markets.sh +411 -0
  39. package/scripts/resolve_clob_market.sh +246 -0
  40. package/scripts/setup_clob_operator.sh +54 -0
  41. package/src/Application.js +96 -0
  42. package/src/interfaces/index.js +6 -0
  43. package/src/models/AdminRegistryContract.js +28 -0
  44. package/src/models/ConditionalTokensContract.js +32 -0
  45. package/src/models/FeeModuleContract.js +75 -0
  46. package/src/models/Multicall3Contract.js +14 -0
  47. package/src/models/MyriadCTFExchangeContract.js +20 -0
  48. package/src/models/PredictionMarketV3ManagerCLOBContract.js +74 -0
  49. package/src/models/index.js +13 -1
  50. package/test/AdminRegistry.t.sol +326 -0
  51. package/test/ConditionalTokens.t.sol +579 -0
  52. package/test/FeeModule.t.sol +508 -0
  53. package/test/MyriadCTFExchange.t.sol +939 -0
  54. package/test/NegRiskAdapter.t.sol +1267 -0
  55. package/test/POC.t.sol +227 -0
  56. package/test/PredictionMarket.t.sol +895 -895
  57. package/test/PredictionMarketCLOB.t.sol +2948 -0
  58. package/test/PredictionMarketManager.t.sol +891 -891
  59. package/test/RealitioOracle.t.sol +361 -0
  60. /package/abis/{Test.json → test.json} +0 -0
@@ -0,0 +1,29 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebFetch(domain:polygonscan.com)",
5
+ "WebFetch(domain:raw.githubusercontent.com)",
6
+ "WebSearch",
7
+ "Bash(git log:*)",
8
+ "Bash(forge build)",
9
+ "Bash(forge build --contracts test/PredictionMarketCLOB.t.sol)",
10
+ "Bash(forge build --contracts test/AdminRegistry.t.sol)",
11
+ "Bash(python3 -c \":*)",
12
+ "Bash(forge test --match-path \"test/AdminRegistry.t.sol\" -v)",
13
+ "Bash(forge test --match-test \"testGrantRoleByNonAdminReverts|testAfterTransfer\" -vvv --match-path \"test/AdminRegistry.t.sol\")",
14
+ "Bash(forge test --match-path \"test/FeeModule.t.sol\" -v)",
15
+ "Bash(forge test --match-path \"test/ConditionalTokens.t.sol\" -v)",
16
+ "Bash(forge test --match-path \"test/MyriadCTFExchange.t.sol\" -v)",
17
+ "Bash(forge test --match-path \"test/MyriadCTFExchange.t.sol\" --match-test \"testMatchOrdersWithFeesByNonOperatorReverts|testMarketMismatchReverts\" -vvv)",
18
+ "Bash(forge test --match-path \"test/AdminRegistry.t.sol|test/FeeModule.t.sol|test/ConditionalTokens.t.sol|test/MyriadCTFExchange.t.sol\" -v)",
19
+ "Bash(forge test --match-contract \"AdminRegistryTest|FeeModuleTest|ConditionalTokensTest|MyriadCTFExchangeTest\" -v)",
20
+ "Bash(forge test -v)",
21
+ "Bash(cast sig:*)",
22
+ "Bash(pip3 install:*)",
23
+ "Bash(pip install:*)",
24
+ "Bash(node -e \":*)",
25
+ "Bash(forge inspect:*)",
26
+ "Bash(curl -s \"https://api.github.com/repos/Polkamarkets/polkamarkets-js/releases?per_page=5\")"
27
+ ]
28
+ }
29
+ }
@@ -0,0 +1,305 @@
1
+ # Myriad CLOB Smart Contract Audit Report
2
+
3
+ - Review date: 2026-04-08
4
+ - Repository: `polkamarkets/bepro-js`
5
+ - Review target: `/contracts` diff against `main`
6
+ - Reviewed branch: `fix-135-mint-merge-match-settling`
7
+ - Reviewer: Codex
8
+
9
+ ## Executive Summary
10
+
11
+ This review covered the newly introduced CLOB contract stack added in the `/contracts` diff against `main`, with particular focus on:
12
+
13
+ - `MyriadCTFExchange`
14
+ - `ConditionalTokens`
15
+ - `PredictionMarketV3ManagerCLOB`
16
+ - `NegRiskAdapter`
17
+ - `WrappedCollateral`
18
+ - `FeeModule`
19
+ - `AdminRegistry`
20
+ - `RealitioOracle`
21
+
22
+ The system introduces a substantial new settlement surface: off-chain signed orders matched on-chain, ERC1155 conditional positions, per-market fee routing, pluggable oracle resolution, and a neg-risk adapter that uses unbacked wrapped collateral minting as an internal accounting primitive.
23
+
24
+ The review identified one high-severity issue and three medium-severity issues. The most important finding is a solvency failure in neg-risk void handling: malformed but currently allowed event-level void payouts can leave permanent unbacked `WCOL` in circulation and let those tokens drain future deposits.
25
+
26
+ ## Scope
27
+
28
+ The following contracts were in scope because they are newly added or materially changed relative to `main`:
29
+
30
+ - `contracts/AdminRegistry.sol`
31
+ - `contracts/ConditionalTokens.sol`
32
+ - `contracts/FeeModule.sol`
33
+ - `contracts/IMarketOracle.sol`
34
+ - `contracts/IMyriadMarketManager.sol`
35
+ - `contracts/MyriadCTFExchange.sol`
36
+ - `contracts/NegRiskAdapter.sol`
37
+ - `contracts/Outcomes.sol`
38
+ - `contracts/PredictionMarketV3ManagerCLOB.sol`
39
+ - `contracts/WrappedCollateral.sol`
40
+ - `contracts/oracles/RealitioOracle.sol`
41
+
42
+ `CLOB_SDK_REFERENCE.md` was also reviewed because it describes expected system behavior and trust assumptions for launch.
43
+
44
+ ## Methodology
45
+
46
+ The review was performed as a manual adversarial code audit focused on:
47
+
48
+ - asset accounting and solvency
49
+ - trust boundaries and role separation
50
+ - order lifecycle and stale-order risk
51
+ - settlement invariants across split, merge, redeem, and cross-market matching
52
+ - neg-risk event resolution and cleanup
53
+ - privileged operations, upgradeability, and wiring assumptions
54
+
55
+ `forge build` succeeds locally. `forge test` could not be executed in this environment because the installed Foundry nightly crashes before test execution in `system_configuration::dynamic_store`. As a result, findings are based on source review plus compile verification rather than a successful local test run.
56
+
57
+ ## Severity Definitions
58
+
59
+ - High: direct loss of funds, persistent insolvency, or a launch-blocking safety failure
60
+ - Medium: meaningful integrity or availability risk, especially when combined with normal operator/admin actions
61
+ - Low: limited impact issue or hardening gap
62
+ - Informational: non-exploitable risk, documentation mismatch, or operational observation
63
+
64
+ ## Findings Summary
65
+
66
+ | ID | Severity | Title |
67
+ | --- | --- | --- |
68
+ | H-01 | High | Neg-risk voiding can leave unbacked `WCOL` in circulation and drain future deposits |
69
+ | M-01 | Medium | Market admins can reopen naturally closed markets and revive stale GTC orders |
70
+ | M-02 | Medium | Market admins can swap the oracle after close and seize settlement |
71
+ | M-03 | Medium | Rotating the global neg-risk adapter can strand live events |
72
+
73
+ ## Detailed Findings
74
+
75
+ ### H-01: Neg-risk voiding can leave unbacked `WCOL` in circulation and drain future deposits
76
+
77
+ **Severity:** High
78
+
79
+ **Affected contracts:**
80
+
81
+ - `contracts/NegRiskAdapter.sol`
82
+ - `contracts/WrappedCollateral.sol`
83
+ - `contracts/PredictionMarketV3ManagerCLOB.sol`
84
+
85
+ **Description**
86
+
87
+ The neg-risk system relies on `WrappedCollateral` (`WCOL`) as a wrapper over real collateral plus an adapter-only unbacked mint path used during conversion and cross-market minting.
88
+
89
+ That design is only solvent if event-level resolution preserves the accounting invariant between:
90
+
91
+ - user-held YES sets
92
+ - adapter-held NO inventory
93
+ - adapter-minted unbacked `WCOL`
94
+
95
+ `voidEvent()` currently validates only each market in isolation:
96
+
97
+ - each per-market pair is forced to sum to `1e18` via `manager.adminVoidMarket(...)`
98
+ - but there is no event-level bound on `sum(yesPayouts)`
99
+
100
+ Relevant code:
101
+
102
+ - `contracts/NegRiskAdapter.sol`
103
+ - `voidEvent()` accepts arbitrary `yesPayouts` arrays with only a length check
104
+ - each market is then voided with `YES = yesPayouts[i]` and `NO = 1e18 - yesPayouts[i]`
105
+ - `contracts/PredictionMarketV3ManagerCLOB.sol`
106
+ - `adminVoidMarket()` validates only that each market's two payouts sum to `1e18`
107
+ - `contracts/NegRiskAdapter.sol`
108
+ - `redeemNOPositions()` burns `min(mintedWcolPerEvent[eventId], wcolRecovered)` and then zeroes the event accounting regardless of whether all minted `WCOL` was actually recovered
109
+ - `contracts/WrappedCollateral.sol`
110
+ - `unwrap()` is global and permissionless, so any surviving unbacked `WCOL` can later redeem against unrelated backing
111
+
112
+ For converted or cross-market-created full YES baskets, solvency requires `sum(yesPayouts) <= 1e18`. If the sum exceeds `1e18`, YES holders can redeem more `WCOL` than the adapter can recover from its NO positions. The deficit is not tracked after cleanup because `mintedWcolPerEvent[eventId]` is zeroed even when recovery is incomplete.
113
+
114
+ **Impact**
115
+
116
+ This can create persistent bad debt inside `WCOL`:
117
+
118
+ 1. a neg-risk event is voided with an over-allocating YES vector
119
+ 2. YES holders redeem more `WCOL` than the system can safely back
120
+ 3. cleanup burns only the recoverable portion of minted `WCOL`
121
+ 4. leftover unbacked `WCOL` remains transferable and unwrap-capable
122
+ 5. future wrappers or unrelated users provide fresh underlying backing
123
+ 6. the unbacked `WCOL` drains that new backing
124
+
125
+ This is a direct cross-user and cross-event solvency failure.
126
+
127
+ **Exploit sketch**
128
+
129
+ For a 3-outcome event:
130
+
131
+ - Alice deposits `100`
132
+ - Alice converts into a full YES basket across all three markets
133
+ - admin voids the event with `yesPayouts = [0.5, 0.5, 0.5]`
134
+ - Alice redeems `150 WCOL`
135
+ - adapter NO cleanup recovers only `150 WCOL` against `200 WCOL` minted during conversion, burns `150`, and zeroes the accounting
136
+ - `WCOL` supply still exceeds real underlying by `50`
137
+ - Alice can later drain a new user's wrapped deposit using the surviving unbacked `WCOL`
138
+
139
+ The repository now includes a proof-of-concept test at `test/POC.t.sol` demonstrating this behavior.
140
+
141
+ **Why this is especially concerning**
142
+
143
+ The current tests explicitly treat `50/50/50` event void payouts as acceptable for a 3-outcome event:
144
+
145
+ - `test/NegRiskAdapter.t.sol`
146
+ - `testVoidEvent5050`
147
+ - `testVoidEventRedeemNOPositions`
148
+
149
+ That makes the issue more than a hypothetical malicious-admin corner case. The repository already encodes an event-level payout shape that breaks wrapper solvency.
150
+
151
+ **Recommendations**
152
+
153
+ At minimum:
154
+
155
+ 1. enforce an event-level invariant in `voidEvent()`:
156
+ - `sum(yesPayouts) <= 1e18`
157
+ 2. do not zero `mintedWcolPerEvent[eventId]` unless full recovery occurred
158
+ 3. if partial recovery is ever possible, preserve explicit bad-debt accounting and prevent unrelated backing from being consumed silently
159
+
160
+ Stronger mitigations:
161
+
162
+ 1. silo wrapper accounting per event rather than sharing one global redeemable wrapper
163
+ 2. treat over-allocating void vectors as invalid resolution inputs rather than as treasury/accounting events
164
+ 3. add invariant tests asserting `underlying.balanceOf(wcol) >= wcol.totalSupply()` after every resolution path
165
+
166
+ ---
167
+
168
+ ### M-01: Market admins can reopen naturally closed markets and revive stale GTC orders
169
+
170
+ **Severity:** Medium
171
+
172
+ **Affected contracts:**
173
+
174
+ - `contracts/PredictionMarketV3ManagerCLOB.sol`
175
+ - `contracts/MyriadCTFExchange.sol`
176
+
177
+ **Description**
178
+
179
+ `adminSetClosesAt()` can move `closesAt` forward on any unresolved market, even if the market has already closed by time.
180
+
181
+ The manager does not persist a closed state transition. Instead, close status is derived at read time:
182
+
183
+ - if `market.state == open` and `block.timestamp >= closesAt`, `getMarketState()` returns `closed`
184
+ - `isMarketTradeable()` similarly depends on `block.timestamp < closesAt`
185
+
186
+ If a market has naturally closed and an admin later extends `closesAt`, the market becomes tradeable again.
187
+
188
+ **Impact**
189
+
190
+ The SDK documents `expiration = 0` as GTC order behavior. That means old signed orders can remain valid indefinitely. Reopening a closed market can therefore reactivate stale orders under new information, which is a serious integrity problem for an orderbook launch.
191
+
192
+ This is especially dangerous when:
193
+
194
+ - users rely on market close as a hard boundary for stale orders
195
+ - off-chain infra preserves order validity until explicit expiration or cancellation
196
+ - admins use close-time edits operationally without realizing they are reviving execution rights
197
+
198
+ **Recommendations**
199
+
200
+ 1. forbid extending `closesAt` after a market has already closed
201
+ 2. if edits are needed, allow only shortening while the market is still open
202
+ 3. alternatively, permanently latch close once first reached
203
+ 4. if reopening must exist, invalidate all outstanding orders for that market as part of the action
204
+
205
+ ---
206
+
207
+ ### M-02: Market admins can swap the oracle after close and seize settlement
208
+
209
+ **Severity:** Medium
210
+
211
+ **Affected contracts:**
212
+
213
+ - `contracts/PredictionMarketV3ManagerCLOB.sol`
214
+
215
+ **Description**
216
+
217
+ `updateMarketOracle()` is callable by `MARKET_ADMIN_ROLE` on any unresolved market. There is no restriction that the market must still be open.
218
+
219
+ As a result, after trading has ended but before settlement, a market admin can:
220
+
221
+ - replace the oracle with a malicious oracle that returns a chosen result
222
+ - replace the oracle with a non-resolving oracle and freeze permissionless settlement
223
+ - reinitialize oracle-side state with new `oracleData`
224
+
225
+ `resolveMarket()` then trusts the currently configured oracle.
226
+
227
+ **Impact**
228
+
229
+ This collapses the trust boundary between market administration and resolution. In the current role model, market creation/maintenance and final settlement are presented as separate concerns, but the contract lets a market admin rewrite the settlement source after trading is over.
230
+
231
+ That creates a meaningful integrity risk even if `RESOLUTION_ADMIN_ROLE` is intended to be tightly controlled.
232
+
233
+ **Recommendations**
234
+
235
+ 1. freeze oracle changes once the market closes
236
+ 2. preferably freeze oracle changes immediately after market creation unless there is a clearly documented emergency path
237
+ 3. if emergency oracle replacement is required, gate it behind `RESOLUTION_ADMIN_ROLE` plus a timelock or pause
238
+ 4. emit stronger operational guidance that oracle changes alter settlement trust assumptions
239
+
240
+ ---
241
+
242
+ ### M-03: Rotating the global neg-risk adapter can strand live events
243
+
244
+ **Severity:** Medium
245
+
246
+ **Affected contracts:**
247
+
248
+ - `contracts/PredictionMarketV3ManagerCLOB.sol`
249
+ - `contracts/MyriadCTFExchange.sol`
250
+
251
+ **Description**
252
+
253
+ The manager and exchange each store a single mutable `negRiskAdapter` address:
254
+
255
+ - manager uses it to authorize neg-risk market creation and neg-risk admin resolution
256
+ - exchange uses it for cross-market minting
257
+
258
+ However, neg-risk events are stateful:
259
+
260
+ - the adapter itself stores event membership and question text
261
+ - the adapter holds NO inventory
262
+ - the adapter tracks minted `WCOL` per event
263
+
264
+ If governance rotates `negRiskAdapter` after events are already live:
265
+
266
+ - the old adapter still owns the state and positions
267
+ - the manager stops recognizing the old adapter as the authorized neg-risk resolver
268
+ - the exchange starts calling the new adapter for cross-market operations
269
+ - the new adapter has no knowledge of old events
270
+
271
+ **Impact**
272
+
273
+ This can strand live events and freeze event-specific flows even if all parties are honest. It is an availability and operational safety issue with funds in flight.
274
+
275
+ **Recommendations**
276
+
277
+ 1. make the adapter immutable once the first neg-risk event is created
278
+ 2. or store the adapter address per event/market at creation time and always route through that stored adapter
279
+ 3. document migration explicitly if adapter replacement is intended
280
+
281
+ ## Informational Observations
282
+
283
+ ### I-01: SDK/reference documentation is materially out of sync with the contracts
284
+
285
+ The reviewed `CLOB_SDK_REFERENCE.md` does not match the current contract surface in several important places:
286
+
287
+ - unresolved outcome docs describe `-2`, while the manager returns `-3`
288
+ - voided redemption example calls `redeemPositions`, while the contract exposes `redeemVoided`
289
+ - fee docs describe array-based fee schedules and a 3-argument `withdrawFees`, while the contract uses tier structs and a 2-argument withdrawal
290
+
291
+ This mismatch increases integration risk and could lead client code to make unsafe assumptions about settlement and fee behavior.
292
+
293
+ ### I-02: The new stack depends on transient storage reentrancy guards
294
+
295
+ `PredictionMarketV3ManagerCLOB`, `MyriadCTFExchange`, and `NegRiskAdapter` use OpenZeppelin transient reentrancy guards, which require EIP-1153 support. Public BNB Chain sources indicate support is present, so this is not an immediate launch blocker for BSC, but it should be treated as a deployment precondition for any other target network.
296
+
297
+ ### I-03: Local test execution was blocked by a Foundry nightly crash
298
+
299
+ The installed Foundry nightly panics before test execution in `system_configuration::dynamic_store`. `forge build` succeeds. The included PoC was therefore added and compile-checked, but not executed in this environment.
300
+
301
+ ## Conclusion
302
+
303
+ The new CLOB contracts are thoughtfully structured and cover many important behaviors with tests, but the neg-risk accounting model currently has a launch-blocking solvency issue. The medium-severity findings also show that a few privileged operations remain too powerful relative to the role model described in the SDK and expected by traders.
304
+
305
+ I would not recommend launching the neg-risk component in its current form before H-01 is fixed and the privileged lifecycle behaviors are narrowed or explicitly accepted as protocol trust assumptions.