feat: Push3 → Solidity transpiler + OptimizerV3 port

This commit is contained in:
openhands 2026-02-21 07:48:43 +00:00
parent 9809e5e640
commit 5e8a94b7a9
9 changed files with 1364 additions and 0 deletions

View file

@ -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 Push3Solidity 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;
}
}

View file

@ -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);
}
}

View file

@ -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
)
)

237
tools/push3-transpiler/package-lock.json generated Normal file
View file

@ -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"
}
}
}
}

View file

@ -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"
}
}

View file

@ -0,0 +1,70 @@
/**
* Push3 Solidity transpiler CLI entry point.
*
* Usage: ts-node src/index.ts <input.push3> <output.sol>
*
* 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 <input.push3> <output.sol>');
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();

View file

@ -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;
}

View file

@ -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<string, string>; // 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<number, string>(); // 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<number, string>();
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<number, string>,
bMap: Map<number, string>,
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 };
}

View file

@ -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"]
}