diff --git a/onchain/src/OptimizerV3Push3.sol b/onchain/src/OptimizerV3Push3.sol new file mode 100644 index 0000000..2cd14b3 --- /dev/null +++ b/onchain/src/OptimizerV3Push3.sol @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +/** + * @title OptimizerV3Push3 + * @notice Auto-generated from optimizer_v3.push3 via Push3→Solidity transpiler. + * Implements the same isBullMarket logic as OptimizerV3. + */ +contract OptimizerV3Push3 { + /** + * @notice Determines if the market is in bull configuration. + * @param percentageStaked Percentage of authorized stake in use (0 to 1e18). + * @param averageTaxRate Normalized average tax rate from Stake contract (0 to 1e18). + * @return bull True if bull config, false if bear. + */ + function isBullMarket( + uint256 percentageStaked, + uint256 averageTaxRate + ) public pure returns (bool bull) { + require(percentageStaked <= 1e18, "Invalid percentage staked"); + uint256 taxrate = uint256(averageTaxRate); + uint256 staked = uint256(((percentageStaked * 100) / 1000000000000000000)); + bool b33; + if ((staked > 91)) { + uint256 deltas = uint256((100 - staked)); + uint256 r28; + if ((taxrate <= 206185567010309)) { + r28 = uint256(0); + } else { + uint256 r27; + if ((taxrate <= 412371134020618)) { + r27 = uint256(1); + } else { + uint256 r26; + if ((taxrate <= 618556701030927)) { + r26 = uint256(2); + } else { + uint256 r25; + if ((taxrate <= 1030927835051546)) { + r25 = uint256(3); + } else { + uint256 r24; + if ((taxrate <= 1546391752577319)) { + r24 = uint256(4); + } else { + uint256 r23; + if ((taxrate <= 2164948453608247)) { + r23 = uint256(5); + } else { + uint256 r22; + if ((taxrate <= 2783505154639175)) { + r22 = uint256(6); + } else { + uint256 r21; + if ((taxrate <= 3608247422680412)) { + r21 = uint256(7); + } else { + uint256 r20; + if ((taxrate <= 4639175257731958)) { + r20 = uint256(8); + } else { + uint256 r19; + if ((taxrate <= 5670103092783505)) { + r19 = uint256(9); + } else { + uint256 r18; + if ((taxrate <= 7216494845360824)) { + r18 = uint256(10); + } else { + uint256 r17; + if ((taxrate <= 9278350515463917)) { + r17 = uint256(11); + } else { + uint256 r16; + if ((taxrate <= 11855670103092783)) { + r16 = uint256(12); + } else { + uint256 r15; + if ((taxrate <= 15979381443298969)) { + r15 = uint256(13); + } else { + uint256 r14; + if ((taxrate <= 22164948453608247)) { + r14 = uint256(14); + } else { + uint256 r13; + if ((taxrate <= 29381443298969072)) { + r13 = uint256(15); + } else { + uint256 r12; + if ((taxrate <= 38144329896907216)) { + r12 = uint256(16); + } else { + uint256 r11; + if ((taxrate <= 49484536082474226)) { + r11 = uint256(17); + } else { + uint256 r10; + if ((taxrate <= 63917525773195876)) { + r10 = uint256(18); + } else { + uint256 r9; + if ((taxrate <= 83505154639175257)) { + r9 = uint256(19); + } else { + uint256 r8; + if ((taxrate <= 109278350515463917)) { + r8 = uint256(20); + } else { + uint256 r7; + if ((taxrate <= 144329896907216494)) { + r7 = uint256(21); + } else { + uint256 r6; + if ((taxrate <= 185567010309278350)) { + r6 = uint256(22); + } else { + uint256 r5; + if ((taxrate <= 237113402061855670)) { + r5 = uint256(23); + } else { + uint256 r4; + if ((taxrate <= 309278350515463917)) { + r4 = uint256(24); + } else { + uint256 r3; + if ((taxrate <= 402061855670103092)) { + r3 = uint256(25); + } else { + uint256 r2; + if ((taxrate <= 520618556701030927)) { + r2 = uint256(26); + } else { + uint256 r1; + if ((taxrate <= 680412371134020618)) { + r1 = uint256(27); + } else { + uint256 r0; + if ((taxrate <= 886597938144329896)) { + r0 = uint256(28); + } else { + r0 = uint256(29); + } + r1 = uint256(r0); + } + r2 = uint256(r1); + } + r3 = uint256(r2); + } + r4 = uint256(r3); + } + r5 = uint256(r4); + } + r6 = uint256(r5); + } + r7 = uint256(r6); + } + r8 = uint256(r7); + } + r9 = uint256(r8); + } + r10 = uint256(r9); + } + r11 = uint256(r10); + } + r12 = uint256(r11); + } + r13 = uint256(r12); + } + r14 = uint256(r13); + } + r15 = uint256(r14); + } + r16 = uint256(r15); + } + r17 = uint256(r16); + } + r18 = uint256(r17); + } + r19 = uint256(r18); + } + r20 = uint256(r19); + } + r21 = uint256(r20); + } + r22 = uint256(r21); + } + r23 = uint256(r22); + } + r24 = uint256(r23); + } + r25 = uint256(r24); + } + r26 = uint256(r25); + } + r27 = uint256(r26); + } + r28 = uint256(r27); + } + uint256 dup29 = uint256(r28); + uint256 r32; + if ((dup29 >= 14)) { + uint256 dup30 = uint256((dup29 + 1)); + uint256 r31; + if ((dup30 > 29)) { + r31 = uint256(29); + } else { + r31 = uint256(dup30); + } + r32 = uint256(r31); + } else { + r32 = uint256(dup29); + } + uint256 effidx = uint256(r32); + b33 = (((((deltas * deltas) * deltas) * effidx) / 20) < 50); + } else { + b33 = false; + } + bull = b33; + } +} diff --git a/onchain/test/OptimizerV3Push3.t.sol b/onchain/test/OptimizerV3Push3.t.sol new file mode 100644 index 0000000..8ea5ea3 --- /dev/null +++ b/onchain/test/OptimizerV3Push3.t.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import { OptimizerV3 } from "../src/OptimizerV3.sol"; +import { OptimizerV3Push3 } from "../src/OptimizerV3Push3.sol"; +import "forge-std/Test.sol"; + +/** + * @title OptimizerV3Push3Test + * @notice Verifies that the Push3-transpiled OptimizerV3Push3 produces + * identical results to the hand-written OptimizerV3 for all test cases. + * + * Tests mirror OptimizerV3.t.sol to ensure equivalence. + */ +contract OptimizerV3Push3Test is Test { + OptimizerV3 ref; // reference (hand-written) + OptimizerV3Push3 push3; // transpiled from Push3 + + uint256[30] TAX_RATES = + [uint256(1), 3, 5, 8, 12, 18, 24, 30, 40, 50, 60, 80, 100, 130, 180, 250, 320, 420, 540, 700, 920, 1200, 1600, 2000, 2600, 3400, 4400, 5700, 7500, 9700]; + uint256 constant MAX_TAX = 9700; + + function setUp() public { + ref = new OptimizerV3(); + push3 = new OptimizerV3Push3(); + } + + function _norm(uint256 taxIdx) internal view returns (uint256) { + return TAX_RATES[taxIdx] * 1e18 / MAX_TAX; + } + + function _pct(uint256 pct) internal pure returns (uint256) { + return pct * 1e18 / 100; + } + + // ---- Equivalence against reference ---- + + function testEquivalenceAllTaxRates() public view { + uint256[7] memory staked = [uint256(0), 50, 91, 92, 95, 96, 100]; + for (uint256 s = 0; s < staked.length; s++) { + for (uint256 t = 0; t < 30; t++) { + bool r = ref.isBullMarket(_pct(staked[s]), _norm(t)); + bool p = push3.isBullMarket(_pct(staked[s]), _norm(t)); + assertEq(p, r, string.concat( + "Mismatch at staked=", vm.toString(staked[s]), + "% taxIdx=", vm.toString(t) + )); + } + } + } + + function testEquivalence93to99Percent() public view { + for (uint256 s = 93; s <= 99; s++) { + for (uint256 t = 0; t < 30; t++) { + bool r = ref.isBullMarket(_pct(s), _norm(t)); + bool p = push3.isBullMarket(_pct(s), _norm(t)); + assertEq(p, r, string.concat( + "Mismatch at staked=", vm.toString(s), + "% taxIdx=", vm.toString(t) + )); + } + } + } + + // ---- Direct correctness tests (mirror OptimizerV3.t.sol) ---- + + function testAlwaysBearAt0Percent() public view { + for (uint256 t = 0; t < 30; t++) { + assertFalse(push3.isBullMarket(0, _norm(t))); + } + } + + function testAlwaysBearAt91Percent() public view { + for (uint256 t = 0; t < 30; t++) { + assertFalse(push3.isBullMarket(_pct(91), _norm(t))); + } + } + + function testBoundary92PercentLowestTax() public view { + // deltaS=8, effIdx=0 → penalty=0 < 50 → BULL + assertTrue(push3.isBullMarket(_pct(92), _norm(0))); + } + + function testBoundary92PercentTaxIdx1() public view { + // deltaS=8, effIdx=1 → penalty=512*1/20=25 < 50 → BULL + assertTrue(push3.isBullMarket(_pct(92), _norm(1))); + } + + function testBoundary92PercentTaxIdx2() public view { + // deltaS=8, effIdx=2 → penalty=512*2/20=51 >= 50 → BEAR + assertFalse(push3.isBullMarket(_pct(92), _norm(2))); + } + + function testAt95PercentTaxIdx7() public view { + // deltaS=5, effIdx=7 → penalty=125*7/20=43 < 50 → BULL + assertTrue(push3.isBullMarket(_pct(95), _norm(7))); + } + + function testAt95PercentTaxIdx8() public view { + // deltaS=5, effIdx=8 → penalty=125*8/20=50 NOT < 50 → BEAR + assertFalse(push3.isBullMarket(_pct(95), _norm(8))); + } + + function testAt97PercentHighTax() public view { + // deltaS=3, effIdx=29 → penalty=27*29/20=39 < 50 → BULL + assertTrue(push3.isBullMarket(_pct(97), _norm(29))); + } + + function testAt100PercentAlwaysBull() public view { + for (uint256 t = 0; t < 30; t++) { + assertTrue(push3.isBullMarket(1e18, _norm(t))); + } + } + + function testEffIdxShiftAtBoundary() public view { + // taxIdx=13: effIdx=13, penalty=64*13/20=41 < 50 → BULL + assertTrue(push3.isBullMarket(_pct(96), _norm(13))); + // taxIdx=14: effIdx=15 (shift!), penalty=64*15/20=48 < 50 → BULL + assertTrue(push3.isBullMarket(_pct(96), _norm(14))); + // taxIdx=15: effIdx=16, penalty=64*16/20=51 >= 50 → BEAR + assertFalse(push3.isBullMarket(_pct(96), _norm(15))); + } + + function testRevertsAbove100Percent() public { + vm.expectRevert("Invalid percentage staked"); + push3.isBullMarket(1e18 + 1, 0); + } + + // ---- Fuzz ---- + + function testFuzzEquivalence(uint256 percentageStaked, uint256 averageTaxRate) public view { + percentageStaked = bound(percentageStaked, 0, 1e18); + averageTaxRate = bound(averageTaxRate, 0, 1e18); + bool r = ref.isBullMarket(percentageStaked, averageTaxRate); + bool p = push3.isBullMarket(percentageStaked, averageTaxRate); + assertEq(p, r, "Push3 result must match reference for all inputs"); + } + + function testFuzzNeverReverts(uint256 percentageStaked, uint256 averageTaxRate) public view { + percentageStaked = bound(percentageStaked, 0, 1e18); + averageTaxRate = bound(averageTaxRate, 0, 1e18); + push3.isBullMarket(percentageStaked, averageTaxRate); + } +} diff --git a/tools/push3-transpiler/optimizer_v3.push3 b/tools/push3-transpiler/optimizer_v3.push3 new file mode 100644 index 0000000..97503f9 --- /dev/null +++ b/tools/push3-transpiler/optimizer_v3.push3 @@ -0,0 +1,201 @@ +;; OptimizerV3 in Push3 +;; +;; Computes isBullMarket(percentageStaked_1e18, averageTaxRate_1e18) +;; +;; Inputs on DYADIC stack (top to bottom when called): +;; top: percentageStaked (0 to 1e18, where 1e18 = 100%) +;; below: averageTaxRate (0 to 1e18, normalized from Stake contract) +;; +;; Output on BOOLEAN stack: +;; top: TRUE if bull market, FALSE if bear market +;; +;; Logic mirrors OptimizerV3.isBullMarket: +;; stakedPct = percentageStaked * 100 / 1e18 (0-100) +;; if stakedPct <= 91 → FALSE (always bear) +;; deltaS = 100 - stakedPct +;; effIdx = _taxRateToEffectiveIndex(averageTaxRate) (0-29, with +1 shift at >=14) +;; penalty = deltaS^3 * effIdx / 20 +;; return penalty < 50 + +( + ;; Step 1: Bind inputs to names. + ;; Stack on entry: [percentageStaked_1e18 (top), averageTaxRate_1e18 (below)] + DYADIC.SWAP + ;; Stack: [averageTaxRate_1e18 (top), percentageStaked_1e18 (below)] + TAXRATE DYADIC.DEFINE + ;; Stack: [percentageStaked_1e18] + + ;; Step 2: Compute stakedPct = percentageStaked * 100 / 1e18 (integer 0..100) + 100 DYADIC.* + 1000000000000000000 DYADIC./ + ;; Stack: [stakedPct] + STAKED DYADIC.DEFINE + ;; Stack: [] + + ;; Step 3: Main conditional — stakedPct > 91? + STAKED 91 DYADIC.> + ;; bool_stack: [stakedPct > 91] + + EXEC.IF + + ;; TRUE branch: stakedPct > 91 — compute penalty + ( + ;; deltaS = 100 - stakedPct + 100 STAKED DYADIC.- + ;; Stack: [deltaS] + DELTAS DYADIC.DEFINE + ;; Stack: [] + + ;; Compute raw tax index via 30-way threshold lookup. + ;; Each level: if TAXRATE <= threshold then push index, else go deeper. + TAXRATE 206185567010309 DYADIC.<= + EXEC.IF + 0 + ( TAXRATE 412371134020618 DYADIC.<= + EXEC.IF + 1 + ( TAXRATE 618556701030927 DYADIC.<= + EXEC.IF + 2 + ( TAXRATE 1030927835051546 DYADIC.<= + EXEC.IF + 3 + ( TAXRATE 1546391752577319 DYADIC.<= + EXEC.IF + 4 + ( TAXRATE 2164948453608247 DYADIC.<= + EXEC.IF + 5 + ( TAXRATE 2783505154639175 DYADIC.<= + EXEC.IF + 6 + ( TAXRATE 3608247422680412 DYADIC.<= + EXEC.IF + 7 + ( TAXRATE 4639175257731958 DYADIC.<= + EXEC.IF + 8 + ( TAXRATE 5670103092783505 DYADIC.<= + EXEC.IF + 9 + ( TAXRATE 7216494845360824 DYADIC.<= + EXEC.IF + 10 + ( TAXRATE 9278350515463917 DYADIC.<= + EXEC.IF + 11 + ( TAXRATE 11855670103092783 DYADIC.<= + EXEC.IF + 12 + ( TAXRATE 15979381443298969 DYADIC.<= + EXEC.IF + 13 + ( TAXRATE 22164948453608247 DYADIC.<= + EXEC.IF + 14 + ( TAXRATE 29381443298969072 DYADIC.<= + EXEC.IF + 15 + ( TAXRATE 38144329896907216 DYADIC.<= + EXEC.IF + 16 + ( TAXRATE 49484536082474226 DYADIC.<= + EXEC.IF + 17 + ( TAXRATE 63917525773195876 DYADIC.<= + EXEC.IF + 18 + ( TAXRATE 83505154639175257 DYADIC.<= + EXEC.IF + 19 + ( TAXRATE 109278350515463917 DYADIC.<= + EXEC.IF + 20 + ( TAXRATE 144329896907216494 DYADIC.<= + EXEC.IF + 21 + ( TAXRATE 185567010309278350 DYADIC.<= + EXEC.IF + 22 + ( TAXRATE 237113402061855670 DYADIC.<= + EXEC.IF + 23 + ( TAXRATE 309278350515463917 DYADIC.<= + EXEC.IF + 24 + ( TAXRATE 402061855670103092 DYADIC.<= + EXEC.IF + 25 + ( TAXRATE 520618556701030927 DYADIC.<= + EXEC.IF + 26 + ( TAXRATE 680412371134020618 DYADIC.<= + EXEC.IF + 27 + ( TAXRATE 886597938144329896 DYADIC.<= + EXEC.IF + 28 + 29 + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ;; Stack: [raw_idx (0-29)] + + ;; Apply effIdx shift: if raw_idx >= 14, effIdx = min(raw_idx + 1, 29) + DYADIC.DUP 14 DYADIC.>= + EXEC.IF + ( + 1 DYADIC.+ + DYADIC.DUP 29 DYADIC.> + EXEC.IF + ( DYADIC.POP 29 ) + ( ) + ) + ( ) + ;; Stack: [effIdx (0-29)] + + EFFIDX DYADIC.DEFINE + ;; Stack: [] + + ;; Compute penalty = deltaS^3 * effIdx / 20 + DELTAS DELTAS DYADIC.* + DELTAS DYADIC.* + EFFIDX DYADIC.* + 20 DYADIC./ + ;; Stack: [penalty] + + ;; Return penalty < 50 + 50 DYADIC.< + ;; bool_stack: [penalty < 50] + ) + + ;; FALSE branch: stakedPct <= 91 — always bear + ( + FALSE + ) +) diff --git a/tools/push3-transpiler/package-lock.json b/tools/push3-transpiler/package-lock.json new file mode 100644 index 0000000..1ce84e8 --- /dev/null +++ b/tools/push3-transpiler/package-lock.json @@ -0,0 +1,237 @@ +{ + "name": "push3-transpiler", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "push3-transpiler", + "version": "1.0.0", + "devDependencies": { + "@types/node": "^20.0.0", + "ts-node": "^10.0.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/tools/push3-transpiler/package.json b/tools/push3-transpiler/package.json new file mode 100644 index 0000000..9fd7f02 --- /dev/null +++ b/tools/push3-transpiler/package.json @@ -0,0 +1,16 @@ +{ + "name": "push3-transpiler", + "version": "1.0.0", + "description": "Push3 to Solidity transpiler for OptimizerV3", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "transpile": "ts-node src/index.ts" + }, + "dependencies": {}, + "devDependencies": { + "typescript": "^5.0.0", + "ts-node": "^10.0.0", + "@types/node": "^20.0.0" + } +} diff --git a/tools/push3-transpiler/src/index.ts b/tools/push3-transpiler/src/index.ts new file mode 100644 index 0000000..ce855ca --- /dev/null +++ b/tools/push3-transpiler/src/index.ts @@ -0,0 +1,70 @@ +/** + * Push3 → Solidity transpiler CLI entry point. + * + * Usage: ts-node src/index.ts + * + * Reads a Push3 program, transpiles isBullMarket logic, and emits a + * Solidity contract that can be compared against the hand-written OptimizerV3. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { parse } from './parser'; +import { transpile } from './transpiler'; + +function main(): void { + const args = process.argv.slice(2); + if (args.length < 2) { + console.error('Usage: ts-node src/index.ts '); + process.exit(1); + } + + const inputPath = args[0]; + const outputPath = args[1]; + + const src = fs.readFileSync(inputPath, 'utf8'); + console.log(`Parsing ${path.basename(inputPath)}...`); + + const ast = parse(src); + console.log('Transpiling...'); + + const { functionBody, resultVar } = transpile(ast); + + const solidityLines = [ + '// SPDX-License-Identifier: GPL-3.0-or-later', + 'pragma solidity ^0.8.19;', + '', + '/**', + ' * @title OptimizerV3Push3', + ' * @notice Auto-generated from optimizer_v3.push3 via Push3→Solidity transpiler.', + ' * Implements the same isBullMarket logic as OptimizerV3.', + ' */', + 'contract OptimizerV3Push3 {', + ' /**', + ' * @notice Determines if the market is in bull configuration.', + ' * @param percentageStaked Percentage of authorized stake in use (0 to 1e18).', + ' * @param averageTaxRate Normalized average tax rate from Stake contract (0 to 1e18).', + ' * @return bull True if bull config, false if bear.', + ' */', + ' function isBullMarket(', + ' uint256 percentageStaked,', + ' uint256 averageTaxRate', + ' ) public pure returns (bool bull) {', + ' require(percentageStaked <= 1e18, "Invalid percentage staked");', + ...functionBody, + ` bull = ${resultVar};`, + ' }', + '}', + '', + ]; + + const output = solidityLines.join('\n'); + fs.writeFileSync(outputPath, output, 'utf8'); + console.log(`Written: ${outputPath}`); + + // Print a summary + console.log(` Function body: ${functionBody.length} lines`); + console.log(` Result var: ${resultVar}`); +} + +main(); diff --git a/tools/push3-transpiler/src/parser.ts b/tools/push3-transpiler/src/parser.ts new file mode 100644 index 0000000..97f8012 --- /dev/null +++ b/tools/push3-transpiler/src/parser.ts @@ -0,0 +1,81 @@ +/** + * Push3 parser — converts Push3 source text into an AST. + * + * Node types: + * { kind: 'int', value: bigint } — integer literal (1e18 etc.) + * { kind: 'bool', value: boolean } — TRUE / FALSE + * { kind: 'instr', name: string } — DYADIC.+, EXEC.IF, etc. + * { kind: 'name', text: string } — unbound identifier (TAXRATE etc.) + * { kind: 'list', items: Node[] } — ( ... ) + */ + +export type Node = + | { kind: 'int'; value: bigint } + | { kind: 'bool'; value: boolean } + | { kind: 'instr'; name: string } + | { kind: 'name'; text: string } + | { kind: 'list'; items: Node[] }; + +// Known instruction prefixes / exact names +const KNOWN_INSTR_PREFIXES = [ + 'DYADIC.', 'EXEC.', 'BOOLEAN.', 'CODE.', 'NAME.', 'INDEX.', + 'INTVECTOR.', 'FLOATVECTOR.', 'BOOLVECTOR.', 'GRAPH.', +]; + +function isInstruction(token: string): boolean { + for (const pfx of KNOWN_INSTR_PREFIXES) { + if (token.startsWith(pfx)) return true; + } + return false; +} + +function tokenize(src: string): string[] { + // Strip comments (;; to end of line) + const noComments = src.replace(/;;[^\n]*/g, ' '); + // Split on whitespace, treating ( and ) as separate tokens + const spaced = noComments.replace(/\(/g, ' ( ').replace(/\)/g, ' ) '); + return spaced.trim().split(/\s+/).filter(t => t.length > 0); +} + +function parseTokens(tokens: string[], pos: number): [Node, number] { + const token = tokens[pos]; + if (token === undefined) throw new Error('Unexpected end of tokens'); + + if (token === '(') { + // Parse list until matching ')' + const items: Node[] = []; + let i = pos + 1; + while (i < tokens.length && tokens[i] !== ')') { + const [node, next] = parseTokens(tokens, i); + items.push(node); + i = next; + } + if (tokens[i] !== ')') throw new Error('Unmatched ('); + return [{ kind: 'list', items }, i + 1]; + } + + if (token === 'TRUE') return [{ kind: 'bool', value: true }, pos + 1]; + if (token === 'FALSE') return [{ kind: 'bool', value: false }, pos + 1]; + + // Integer literal — may be large (BigInt) + if (/^-?\d+$/.test(token)) { + return [{ kind: 'int', value: BigInt(token) }, pos + 1]; + } + + if (isInstruction(token)) { + return [{ kind: 'instr', name: token }, pos + 1]; + } + + // Otherwise: unbound name (e.g. TAXRATE, STAKED, DELTAS, EFFIDX) + return [{ kind: 'name', text: token }, pos + 1]; +} + +export function parse(src: string): Node { + const tokens = tokenize(src); + if (tokens.length === 0) throw new Error('Empty program'); + const [node, consumed] = parseTokens(tokens, 0); + if (consumed !== tokens.length) { + throw new Error(`Unexpected tokens after position ${consumed}: ${tokens[consumed]}`); + } + return node; +} diff --git a/tools/push3-transpiler/src/transpiler.ts b/tools/push3-transpiler/src/transpiler.ts new file mode 100644 index 0000000..326a315 --- /dev/null +++ b/tools/push3-transpiler/src/transpiler.ts @@ -0,0 +1,379 @@ +/** + * Push3 → Solidity transpiler. + * + * Uses symbolic stack simulation (SSA-style). Each stack holds Solidity expression strings. + * EXEC.IF runs both branches speculatively, then emits an if/else with result variables + * for any stack positions that differ between branches. + * + * Only the subset of instructions used in optimizer_v3.push3 is implemented. + */ + +import { Node } from './parser'; + +interface TranspilerState { + dStack: string[]; // DYADIC stack (Solidity expressions) + bStack: string[]; // BOOLEAN stack + nameStack: string[]; // NAME stack (unbound identifiers) + lines: string[]; // emitted Solidity statements + bindings: Map; // identifier → Solidity variable name + varCounter: number; // fresh variable counter (shared across branches) + indent: number; // current indentation level +} + +function freshVar(state: TranspilerState, prefix = 'v'): string { + return `${prefix}${state.varCounter++}`; +} + +function emit(state: TranspilerState, line: string): void { + const pad = ' '.repeat(state.indent); + state.lines.push(pad + line); +} + +function dpop(state: TranspilerState, ctx: string): string { + const v = state.dStack.pop(); + if (v === undefined) throw new Error(`DYADIC stack underflow at ${ctx}`); + return v; +} + +function bpop(state: TranspilerState, ctx: string): string { + const v = state.bStack.pop(); + if (v === undefined) throw new Error(`BOOLEAN stack underflow at ${ctx}`); + return v; +} + +function processNode(node: Node, state: TranspilerState): void { + switch (node.kind) { + case 'int': + state.dStack.push(node.value.toString()); + break; + + case 'bool': + state.bStack.push(node.value ? 'true' : 'false'); + break; + + case 'name': + // Unbound name goes to the NAME stack (separate from DYADIC stack) + state.nameStack.push(node.text); + break; + + case 'instr': + processInstruction(node.name, state); + break; + + case 'list': + for (const item of node.items) { + processNode(item, state); + } + break; + } +} + +function processInstruction(name: string, state: TranspilerState): void { + switch (name) { + // ---- DYADIC stack ops ---- + case 'DYADIC.SWAP': { + const a = dpop(state, 'DYADIC.SWAP'); + const b = dpop(state, 'DYADIC.SWAP'); + state.dStack.push(a); + state.dStack.push(b); + break; + } + case 'DYADIC.DUP': { + const a = dpop(state, 'DYADIC.DUP'); + // Materialise complex expressions before duplicating + const vname = freshVar(state, 'dup'); + emit(state, `uint256 ${vname} = uint256(${a});`); + state.dStack.push(vname); + state.dStack.push(vname); + break; + } + case 'DYADIC.POP': { + dpop(state, 'DYADIC.POP'); + break; + } + + // ---- DYADIC arithmetic ---- + case 'DYADIC.*': { + const b = dpop(state, 'DYADIC.*'); + const a = dpop(state, 'DYADIC.*'); + state.dStack.push(`(${a} * ${b})`); + break; + } + case 'DYADIC./': { + const b = dpop(state, 'DYADIC./'); + const a = dpop(state, 'DYADIC./'); + state.dStack.push(`(${a} / ${b})`); + break; + } + case 'DYADIC.+': { + const b = dpop(state, 'DYADIC.+'); + const a = dpop(state, 'DYADIC.+'); + state.dStack.push(`(${a} + ${b})`); + break; + } + case 'DYADIC.-': { + const b = dpop(state, 'DYADIC.-'); + const a = dpop(state, 'DYADIC.-'); + state.dStack.push(`(${a} - ${b})`); + break; + } + + // ---- DYADIC comparisons → BOOLEAN ---- + case 'DYADIC.>': { + const b = dpop(state, 'DYADIC.>'); + const a = dpop(state, 'DYADIC.>'); + state.bStack.push(`(${a} > ${b})`); + break; + } + case 'DYADIC.<': { + const b = dpop(state, 'DYADIC.<'); + const a = dpop(state, 'DYADIC.<'); + state.bStack.push(`(${a} < ${b})`); + break; + } + case 'DYADIC.>=': { + const b = dpop(state, 'DYADIC.>='); + const a = dpop(state, 'DYADIC.>='); + state.bStack.push(`(${a} >= ${b})`); + break; + } + case 'DYADIC.<=': { + const b = dpop(state, 'DYADIC.<='); + const a = dpop(state, 'DYADIC.<='); + state.bStack.push(`(${a} <= ${b})`); + break; + } + + // ---- Name binding ---- + case 'DYADIC.DEFINE': { + const val = dpop(state, 'DYADIC.DEFINE (value)'); + const id = state.nameStack.pop(); + if (id === undefined) throw new Error('DYADIC.DEFINE: NAME stack underflow'); + const varName = id.toLowerCase(); + emit(state, `uint256 ${varName} = uint256(${val});`); + state.bindings.set(id, varName); + break; + } + + // ---- BOOLEAN ---- + case 'BOOLEAN.NOT': { + const a = bpop(state, 'BOOLEAN.NOT'); + state.bStack.push(`!(${a})`); + break; + } + case 'BOOLEAN.AND': { + const b = bpop(state, 'BOOLEAN.AND'); + const a = bpop(state, 'BOOLEAN.AND'); + state.bStack.push(`(${a} && ${b})`); + break; + } + case 'BOOLEAN.OR': { + const b = bpop(state, 'BOOLEAN.OR'); + const a = bpop(state, 'BOOLEAN.OR'); + state.bStack.push(`(${a} || ${b})`); + break; + } + + case 'EXEC.IF': + throw new Error('EXEC.IF must be handled by processItems'); + + default: + throw new Error(`Unsupported instruction: ${name}`); + } +} + +/** + * Process a list of nodes sequentially, specially handling EXEC.IF by consuming + * the next two items as true/false branches. + */ +function processItems(items: Node[], state: TranspilerState): void { + let i = 0; + while (i < items.length) { + const item = items[i]; + + // Bound identifier → push its Solidity variable to dStack + if (item.kind === 'name' && state.bindings.has(item.text)) { + state.dStack.push(state.bindings.get(item.text)!); + i++; + continue; + } + + // EXEC.IF — consume it plus the next two items (branches) + if (item.kind === 'instr' && item.name === 'EXEC.IF') { + const trueBranch = items[i + 1]; + const falseBranch = items[i + 2]; + if (!trueBranch || !falseBranch) throw new Error('EXEC.IF: missing branches'); + i += 3; + processExecIf(trueBranch, falseBranch, state); + continue; + } + + processNode(item, state); + i++; + } +} + +function makeSubState(parent: TranspilerState, indentOffset = 1): TranspilerState { + return { + dStack: [...parent.dStack], + bStack: [...parent.bStack], + nameStack: [...parent.nameStack], + lines: [], + bindings: new Map(parent.bindings), + varCounter: parent.varCounter, + indent: parent.indent + indentOffset, + }; +} + +/** + * Emit an if/else block for EXEC.IF. + * + * Both branches are simulated speculatively. We compare final stacks elementwise: + * - Positions unchanged in both branches → keep as-is in parent + * - Positions that differ → emit a result variable, assign in each branch + * + * This correctly handles both "new value produced" and "existing value mutated" cases. + */ +function processExecIf( + trueBranch: Node, + falseBranch: Node, + state: TranspilerState, +): void { + const cond = bpop(state, 'EXEC.IF condition'); + + const dBefore = [...state.dStack]; + const bBefore = [...state.bStack]; + + // --- Simulate TRUE branch --- + const trueState = makeSubState(state); + processItems(toItems(trueBranch), trueState); + state.varCounter = trueState.varCounter; + + // --- Simulate FALSE branch --- + const falseState = makeSubState(state); + state.varCounter = falseState.varCounter; // will be updated after false branch too + processItems(toItems(falseBranch), falseState); + state.varCounter = falseState.varCounter; + + // --- Compare dStacks elementwise --- + const maxDLen = Math.max(trueState.dStack.length, falseState.dStack.length); + // result var for each position that changed or is new + const dResultMap = new Map(); // position → result var name + for (let k = 0; k < maxDLen; k++) { + const tv = trueState.dStack[k]; + const fv = falseState.dStack[k]; + const bv = dBefore[k]; // undefined for positions beyond dBefore + if (tv !== undefined && fv !== undefined && tv === fv && tv === bv) { + continue; // identical in both branches and unchanged from before — skip + } + const rv = freshVar(state, 'r'); + emit(state, `uint256 ${rv};`); + dResultMap.set(k, rv); + } + + // --- Compare bStacks elementwise --- + const maxBLen = Math.max(trueState.bStack.length, falseState.bStack.length); + const bResultMap = new Map(); + for (let k = 0; k < maxBLen; k++) { + const tv = trueState.bStack[k]; + const fv = falseState.bStack[k]; + const bv = bBefore[k]; + if (tv !== undefined && fv !== undefined && tv === fv && tv === bv) { + continue; + } + const rv = freshVar(state, 'b'); + emit(state, `bool ${rv};`); + bResultMap.set(k, rv); + } + + // --- Build branch assignments --- + const buildAssignments = ( + branchState: TranspilerState, + dMap: Map, + bMap: Map, + indentLevel: number, + ): string[] => { + const pad = ' '.repeat(indentLevel); + const assignments: string[] = []; + for (const [k, rv] of dMap) { + const val = branchState.dStack[k] ?? '0'; + assignments.push(`${pad}${rv} = uint256(${val});`); + } + for (const [k, rv] of bMap) { + const val = branchState.bStack[k] ?? 'false'; + assignments.push(`${pad}${rv} = ${val};`); + } + return assignments; + }; + + const trueAssign = buildAssignments(trueState, dResultMap, bResultMap, state.indent + 1); + const falseAssign = buildAssignments(falseState, dResultMap, bResultMap, state.indent + 1); + + // --- Emit if/else --- + const pad = ' '.repeat(state.indent); + state.lines.push(`${pad}if (${cond}) {`); + state.lines.push(...trueState.lines); + state.lines.push(...trueAssign); + + const falseBody = [...falseState.lines, ...falseAssign]; + if (falseBody.length > 0) { + state.lines.push(`${pad}} else {`); + state.lines.push(...falseBody); + } + state.lines.push(`${pad}}`); + + // --- Reconstruct parent stacks --- + const newDStack: string[] = []; + for (let k = 0; k < maxDLen; k++) { + const rv = dResultMap.get(k); + newDStack.push(rv ?? (dBefore[k] ?? trueState.dStack[k] ?? '0')); + } + const newBStack: string[] = []; + for (let k = 0; k < maxBLen; k++) { + const rv = bResultMap.get(k); + newBStack.push(rv ?? (bBefore[k] ?? trueState.bStack[k] ?? 'false')); + } + + state.dStack = newDStack; + state.bStack = newBStack; +} + +function toItems(node: Node): Node[] { + return node.kind === 'list' ? node.items : [node]; +} + +// ---- Public API ---- + +export interface TranspileResult { + functionBody: string[]; + resultVar: string; +} + +/** + * Transpile a Push3 program (top-level list) into Solidity function body lines. + * + * Inputs are primed on the DYADIC stack: + * bottom: averageTaxRate (0 to 1e18) + * top: percentageStaked (0 to 1e18) + * + * The Push3 program's first instruction is DYADIC.SWAP so it binds averageTaxRate + * first, then computes stakedPct. + */ +export function transpile(program: Node): TranspileResult { + if (program.kind !== 'list') throw new Error('Expected top-level list'); + + const state: TranspilerState = { + dStack: ['averageTaxRate', 'percentageStaked'], + bStack: [], + nameStack: [], + lines: [], + bindings: new Map(), + varCounter: 0, + indent: 2, + }; + + processItems(program.items, state); + + const resultVar = state.bStack[state.bStack.length - 1] ?? 'false'; + return { functionBody: state.lines, resultVar }; +} diff --git a/tools/push3-transpiler/tsconfig.json b/tools/push3-transpiler/tsconfig.json new file mode 100644 index 0000000..f63bd2d --- /dev/null +++ b/tools/push3-transpiler/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}