diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..cbaa428 Binary files /dev/null and b/bun.lockb differ diff --git a/contracts/script/PointTokenVault.s.sol b/contracts/script/PointTokenVault.s.sol index 57b7a9d..1d96b8a 100644 --- a/contracts/script/PointTokenVault.s.sol +++ b/contracts/script/PointTokenVault.s.sol @@ -124,10 +124,10 @@ contract PointTokenVaultScripts is BatchScript { bytes32 POINTS_ID_ETHENA_SATS_S2 = LibString.packTwo("Rumpel kPt: Ethena S2", "kpSATS-2"); ERC20 ENA = ERC20(0x57e114B691Db790C35207b2e685D4A43181e6061); - uint256 EXCHANGE_RATE = 2e18; + uint256 REWARDS_PER_P_TOKEN = 63381137368827226; bool REDEMPTION_RIGHTS = true; - pointTokenVault.setRedemption(POINTS_ID_ETHENA_SATS_S2, ENA, EXCHANGE_RATE, REDEMPTION_RIGHTS); + pointTokenVault.setRedemption(POINTS_ID_ETHENA_SATS_S2, ENA, REWARDS_PER_P_TOKEN, REDEMPTION_RIGHTS); // bytes32 MERKLE_ROOT_WIT_REDEMPTION_RIGHTS = 0x882aaf07b6b16e5f021a498e1a8c5de540e6ffe9345fdc48b51dd79dc894a059; diff --git a/js-scripts/generateRedemptionRights.ts b/js-scripts/generateRedemptionRights.ts new file mode 100644 index 0000000..0c1fb0e --- /dev/null +++ b/js-scripts/generateRedemptionRights.ts @@ -0,0 +1,171 @@ +import * as fs from "fs"; +import * as dotenv from "dotenv"; +import { + keccak256, + encodePacked, + createPublicClient, + http, + parseAbiItem, + Address, + zeroAddress, +} from "viem"; +import { mainnet } from "viem/chains"; +import { MerkleTree } from "merkletreejs"; + +function solidityKeccak256(types: string[], values: any[]) { + return keccak256(encodePacked(types, values)); +} + +dotenv.config({ path: "./scripts/.env" }); + +const MAINNET_RPC_URL = process.env.MAINNET_RPC_URL; +const PTOKEN_ADDRESSES = (process.env.PTOKEN_ADDRESSES as string).split( + "," +) as Address[]; +const POINTS_IDS = (process.env.POINTS_IDS as string).split( + "," +) as `0x${string}`[]; + +const REDEMPTION_RIGHTS_PREFIX = solidityKeccak256( + ["string"], + ["REDEMPTION_RIGHTS"] +); + +if ( + !MAINNET_RPC_URL || + PTOKEN_ADDRESSES.length === 0 || + POINTS_IDS.length === 0 +) { + console.error( + "Please set MAINNET_RPC_URL, PTOKEN_ADDRESSES, and POINTS_IDS in your .env file" + ); + process.exit(1); +} + +if (PTOKEN_ADDRESSES.length !== POINTS_IDS.length) { + console.error( + "The number of PTOKEN_ADDRESSES must match the number of POINTS_IDS" + ); + process.exit(1); +} + +async function generateMerkleTree() { + const publicClient = createPublicClient({ + chain: mainnet, + transport: http(MAINNET_RPC_URL), + }); + + const latestBlock = await publicClient.getBlockNumber(); + console.log(`latest block #: ${latestBlock}`); + + const allHolders = new Map>(); + + for (let i = 0; i < PTOKEN_ADDRESSES.length; i++) { + const pTokenAddress = PTOKEN_ADDRESSES[i]; + const pointsId = POINTS_IDS[i]; + + console.log( + `Processing pToken: ${pTokenAddress} with Points ID: ${pointsId}` + ); + + const logs = await publicClient.getLogs({ + address: pTokenAddress, + event: parseAbiItem( + "event Transfer(address indexed from, address indexed to, uint256)" + ), + fromBlock: 0n, + toBlock: "latest", + }); + + const holders = new Map(); + + for (const log of logs) { + const [from, to, value] = log.args as [Address, Address, bigint]; + if (!from || !to || value === undefined) { + throw new Error("Transfer event args are undefined"); + } + + if (from !== zeroAddress) { + const fromBalance = (holders.get(from) || 0n) - value; + holders.set(from, fromBalance); + } + + if (to !== zeroAddress) { + const toBalance = (holders.get(to) || 0n) + value; + holders.set(to, toBalance); + } + } + + for (const [address, balance] of holders.entries()) { + if (balance > 0n) { + if (!allHolders.has(address)) { + allHolders.set(address, new Map()); + } + allHolders.get(address)!.set(pointsId, balance); + } + } + } + + // TODO: for ptokens being LP'd in Uni, give the redemption rights to the LPs themselves rather than the pool address + + console.log("Generating Merkle tree..."); + + const leaves = Array.from(allHolders.entries()).flatMap( + ([address, balances]) => + Array.from(balances.entries()).map(([pointsId, balance]) => + solidityKeccak256( + ["bytes32", "address", "bytes32", "uint256"], + [REDEMPTION_RIGHTS_PREFIX, address, pointsId, balance.toString()] + ) + ) + ); + + const sortedLeaves = leaves.sort((a, b) => a.localeCompare(b)); + const tree = new MerkleTree(sortedLeaves, keccak256, { sortPairs: true }); + const root = tree.getHexRoot(); + + const merklizedPointsData = { + wallets: Object.fromEntries( + Array.from(allHolders.entries()).map(([userAddress, balances]) => [ + userAddress, + Object.fromEntries( + Array.from(balances.entries()).map(([pointsId, balance]) => { + const leaf = solidityKeccak256( + ["bytes32", "address", "bytes32", "uint256"], + [ + REDEMPTION_RIGHTS_PREFIX, + userAddress, + pointsId, + balance.toString(), + ] + ); + return [ + pointsId, + { + amount: balance.toString(), + proof: tree.getHexProof(leaf), + }, + ]; + }) + ), + ]) + ), + root, + }; + + console.log("Merkle tree generated successfully."); + console.log("Root:", root); + console.log( + "Total number of holders:", + Object.keys(merklizedPointsData.wallets).length + ); + + // Save the merklizedPointsData to a JSON file + fs.writeFileSync( + "merklizedPointsData.json", + JSON.stringify(merklizedPointsData, null, 2) + ); + console.log("Merkle data saved to merklizedPointsData.json"); +} + +generateMerkleTree().catch(console.error); diff --git a/package.json b/package.json new file mode 100644 index 0000000..82ff711 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "point-tokenization-vault-scripts", + "module": "index.ts", + "type": "module", + "scripts": { + "generate-merkle-tree": "bun run js-scripts/generateRedemptionRights.ts" + }, + "devDependencies": { + "bun-types": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "dotenv": "^16.3.1", + "merkletreejs": "^0.4.0", + "viem": "^2.21.25" + } +}