diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a632b75..e14c4a2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,3 +32,5 @@ jobs: run: | forge test -vvv id: test + env: + MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index a9cbde5..2c26010 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Compiler files cache/ -out/ +/out/ # Ignores development broadcast logs !/broadcast diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..13f08c7 Binary files /dev/null and b/bun.lockb differ diff --git a/contracts/script/PointTokenVault.s.sol b/contracts/script/PointTokenVault.s.sol index c6f6f8a..8e7fa4f 100644 --- a/contracts/script/PointTokenVault.s.sol +++ b/contracts/script/PointTokenVault.s.sol @@ -4,10 +4,8 @@ pragma solidity =0.8.24; import {BatchScript} from "forge-safe/src/BatchScript.sol"; import {PointTokenVault} from "../PointTokenVault.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; -import {CREATE3} from "solmate/utils/CREATE3.sol"; import {LibString} from "solady/utils/LibString.sol"; import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; @@ -97,11 +95,12 @@ contract PointTokenVaultScripts is BatchScript { } function deployPToken() public { - vm.startBroadcast(JIM_PRIVATE_KEY); + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); - PointTokenVault pointTokenVault = PointTokenVault(payable(0xbff7Fb79efC49504afc97e74F83EE618768e63E9)); + PointTokenVault pointTokenVault = PointTokenVault(payable(0xe47F9Dbbfe98d6930562017ee212C1A1Ae45ba61)); - pointTokenVault.deployPToken(LibString.packTwo("ETHERFI Points", "pEF")); + pointTokenVault.deployPToken(LibString.packTwo("Rumpel kPt: ETHERFI S4", "kpEF-4")); vm.stopBroadcast(); } @@ -118,6 +117,25 @@ contract PointTokenVaultScripts is BatchScript { vm.stopBroadcast(); } + function setRedemptionENA4Nov24() public { + // Core contract and token setup + PointTokenVault vaultV0_1_0 = PointTokenVault(payable(0x1EeEBa76f211C4Dce994b9c5A74BDF25DB649Fa1)); + bytes32 pointsId = LibString.packTwo("Rumpel kPoint: Ethena S2", "kpSATS"); + ERC20 senaToken = ERC20(0x8bE3460A480c80728a8C4D7a5D5303c85ba7B3b9); + uint256 rewardsPerPToken = 63381137368827226; + bool usesRedemptionRights = true; + + // Set redemption parameters + vm.startBroadcast(MAINNET_OPERATOR); + vaultV0_1_0.setRedemption(pointsId, senaToken, rewardsPerPToken, usesRedemptionRights); + vm.stopBroadcast(); + + // Update merkle root + vm.startBroadcast(MAINNET_MERKLE_UPDATER); + vaultV0_1_0.updateRoot(0xa1c76e2c6f7ac8300b288ff758b8a83c4a19e12780ca7ac5f61182f64ef8edf6); + vm.stopBroadcast(); + } + // Useful for emergencies, where we need to override both the current and previous root at once // For example, if minting for a specific pToken needs to be stopped, a root without any claim rights for the pToken would need to be pushed twice function doublePushRoot(address pointTokenVaultAddress, bytes32 newRoot, address merkleUpdaterSafe) public { diff --git a/contracts/test/SetRedemptionENA4Nov24.t.sol b/contracts/test/SetRedemptionENA4Nov24.t.sol new file mode 100644 index 0000000..8c0b285 --- /dev/null +++ b/contracts/test/SetRedemptionENA4Nov24.t.sol @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity =0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {LibString} from "solady/utils/LibString.sol"; +import {ERC20} from "solmate/test/utils/mocks/MockERC20.sol"; + +import {PointTokenVault} from "../PointTokenVault.sol"; +import {PointTokenVaultScripts} from "../script/PointTokenVault.s.sol"; +import {PToken} from "../PToken.sol"; + +contract SetRedemptionENA4Nov24Test is Test { + PointTokenVault vaultV0_1_0 = PointTokenVault(payable(0x1EeEBa76f211C4Dce994b9c5A74BDF25DB649Fa1)); + PToken kpSats = PToken(0xdFa21ceC8A46386F5d36F4b07E18BcCcA59f425B); + bytes32 pointsId = LibString.packTwo("Rumpel kPoint: Ethena S2", "kpSATS"); + + mapping(address => bool) userAccountedFor; // pToken holder accounted for + + function setUp() public { + string memory MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL"); + uint256 forkId = vm.createFork(MAINNET_RPC_URL, 21_112_610); // Block mined at Nov-04-2024 06:51:59 AM +UTC + vm.selectFork(forkId); + + PointTokenVaultScripts scripts = new PointTokenVaultScripts(); + scripts.setRedemptionENA4Nov24(); + } + + function test_RedemptionRights1() public { + bytes32[] memory proof = new bytes32[](5); + proof[0] = 0x4e0230e7a7546148f373efe52256490c04ea0019e199f2256a375e75cf2b6b96; + proof[1] = 0xfaca43636023a73bf06295aa693c262efe0bb3231357a68b8ad7eec7e901c8ef; + proof[2] = 0x03c4c7f992659d2c405eb95248d21dfd7f66c7bdef49d9d33cb97b0b5d35c37a; + proof[3] = 0x617d803c581341b5720c7932932ca45ebfb90e87a122f801b0d5fe53b747db51; + proof[4] = 0x620f05d3b25d35da47076de4dceb31f95567ff863138ae7d1fe416ea0bc2ff41; + + address USER = 0x25E426b153e74Ab36b2685c3A464272De60888Ae; + uint256 AMOUNT = 26396311093240867247; + + vm.prank(USER); + vaultV0_1_0.redeemRewards(PointTokenVault.Claim(pointsId, AMOUNT, AMOUNT, proof), USER); + } + + function test_FailedRedemptionRights1_BadProof() public { + bytes32[] memory proof = new bytes32[](5); + proof[0] = 0x000008976022911aeb40e40bcb5754f9529f2080710f7ec1db8ace85c4f7b7f8; + proof[1] = 0xc6f15a7cbd986873c6b761e81c98ee8ac4afd1c9885b7d8e0ae4de752040ab12; + proof[2] = 0xca808b743099c608cd9b81872c528c521d76505c116bedabfdcc6307c9c92bfb; + proof[3] = 0x703f21e968e8791afb70bcf780821f479ea90632b109016d8b24c8637771383c; + proof[4] = 0x0f76084b6c6777c64b0f591ee64d8c66c54c0bdeb5ce44142823c0f74b856267; + + address USER = 0x25E426b153e74Ab36b2685c3A464272De60888Ae; + uint256 AMOUNT = 52792622186481736164; + + vm.prank(USER); + vm.expectRevert(PointTokenVault.ProofInvalidOrExpired.selector); + vaultV0_1_0.redeemRewards(PointTokenVault.Claim(pointsId, AMOUNT, AMOUNT, proof), USER); + } + + function test_RedemptionRights1_ClaimTooMuch() public { + bytes32[] memory proof = new bytes32[](5); + proof[0] = 0x4e0230e7a7546148f373efe52256490c04ea0019e199f2256a375e75cf2b6b96; + proof[1] = 0xfaca43636023a73bf06295aa693c262efe0bb3231357a68b8ad7eec7e901c8ef; + proof[2] = 0x03c4c7f992659d2c405eb95248d21dfd7f66c7bdef49d9d33cb97b0b5d35c37a; + proof[3] = 0x617d803c581341b5720c7932932ca45ebfb90e87a122f801b0d5fe53b747db51; + proof[4] = 0x620f05d3b25d35da47076de4dceb31f95567ff863138ae7d1fe416ea0bc2ff41; + + address USER = 0x25E426b153e74Ab36b2685c3A464272De60888Ae; + uint256 TOTAL_CLAIMABLE = 26396311093240867247; + uint256 CLAIM_AMOUNT = 26396311093240867247 + 10; + + vm.prank(USER); + vm.expectRevert(PointTokenVault.ClaimTooLarge.selector); + vaultV0_1_0.redeemRewards(PointTokenVault.Claim(pointsId, TOTAL_CLAIMABLE, CLAIM_AMOUNT, proof), USER); + } + + struct RedemptionData { + uint256 pTokenBalance; + uint256 claimedPoints; + uint256 totalClaimablePoints; + uint256 unclaimedPoints; + uint256 totalRedeemableBalance; + uint256 currentRedeemableBalance; + } + + struct RedemptionFiles { + string root; + string path; + string alphaDistribution; + string merged; + string balances; + } + + function test_RedemptionRightsCalculatedAmount() public { + uint256 rewardsPerPToken = 63381137368827226; + + uint256 expectedRedemptionRights; + uint256 redemptionRightAmount; + + RedemptionFiles memory rf; + rf.root = vm.projectRoot(); + rf.path = string.concat(rf.root, "/js-scripts/generateRedemptionRights/last-alpha-distribution.json"); + rf.alphaDistribution = vm.readFile(rf.path); + rf.path = string.concat(rf.root, "/js-scripts/generateRedemptionRights/out/merged-distribution.json"); + rf.merged = vm.readFile(rf.path); + rf.path = string.concat(rf.root, "/js-scripts/generateRedemptionRights/out/ptoken-snapshot-kpsats.json"); + rf.balances = vm.readFile(rf.path); + + string[] memory users = vm.parseJsonKeys(rf.alphaDistribution,string.concat(".pTokens")); + for(uint256 i=0; i < users.length; i++) { + RedemptionData memory rd; + address user = stringToAddress(users[i]); + userAccountedFor[user] = true; + + rd.pTokenBalance = kpSats.balanceOf(user); + rd.claimedPoints = vaultV0_1_0.claimedPTokens(user, pointsId); + rd.totalClaimablePoints = vm.parseJsonUint( + rf.alphaDistribution, + string.concat(".pTokens.", vm.toString(user), ".", vm.toString(pointsId), ".accumulatingPoints") + ); + + rd.unclaimedPoints = rd. totalClaimablePoints - rd.claimedPoints; + rd.totalRedeemableBalance = rd.pTokenBalance + rd.unclaimedPoints; + + // uni overrides + if(user == 0x24C694d193B19119bcDea9D40a3b0bfaFb281E6D){ + rd.totalRedeemableBalance += 6487631537430741114; + } + if(user == 0x44Cb2d713BDa3858001f038645fD05E23E5DE03D){ + rd.totalRedeemableBalance += 27597767454066598826095; + } + + expectedRedemptionRights = rd.totalRedeemableBalance * rewardsPerPToken / 2e18; + try vm.parseJsonUint( + rf.merged, + string.concat(".redemptionRights.", vm.toString(user), ".", vm.toString(pointsId), ".amount") + ) returns (uint256 amount) { + redemptionRightAmount = amount; + } + catch { + redemptionRightAmount = 0; + } + + assertLe(redemptionRightAmount, expectedRedemptionRights); + assertApproxEqAbs(redemptionRightAmount, expectedRedemptionRights, 1e10); + + expectedRedemptionRights = 0; + redemptionRightAmount = 0; + } + + // account for users with pToken balances, but no claimable tokens + string[] memory balanceUsers = vm.parseJsonKeys(rf.balances,string.concat(".balances")); + for(uint256 i=0; i < balanceUsers.length; i++) { + address user = stringToAddress(balanceUsers[i]); + if(!userAccountedFor[user]){ + RedemptionData memory rd; + userAccountedFor[user] = true; + + rd.pTokenBalance = kpSats.balanceOf(user); + rd.totalRedeemableBalance = rd.pTokenBalance; + + // uni overrides + if(user == 0x24C694d193B19119bcDea9D40a3b0bfaFb281E6D){ + rd.totalRedeemableBalance += 6487631537430741114; + } + if(user == 0x44Cb2d713BDa3858001f038645fD05E23E5DE03D){ + rd.totalRedeemableBalance += 27597767454066598826095; + } + + expectedRedemptionRights = rd.totalRedeemableBalance * rewardsPerPToken / 2e18; + + redemptionRightAmount; + try vm.parseJsonUint( + rf.merged, + string.concat(".redemptionRights.", vm.toString(user), ".", vm.toString(pointsId), ".amount") + ) returns (uint256 amount) { + redemptionRightAmount = amount; + } + catch { + redemptionRightAmount = 0; + } + + assertLe(redemptionRightAmount, expectedRedemptionRights); + assertApproxEqAbs(redemptionRightAmount, expectedRedemptionRights, 1e10); + + expectedRedemptionRights = 0; + redemptionRightAmount = 0; + } + } + } + + function stringToAddress(string memory _address) internal returns (address) { + // Remove "0x" prefix if present + bytes memory _addressBytes = bytes(_address); + if (_addressBytes.length >= 2 && _addressBytes[0] == "0" && (_addressBytes[1] == "x" || _addressBytes[1] == "X")) { + string memory _cleanAddress = new string(_addressBytes.length - 2); + for(uint i = 0; i < _addressBytes.length - 2; i++) { + bytes(_cleanAddress)[i] = _addressBytes[i + 2]; + } + _address = _cleanAddress; + } + + // Check if the string length is correct (40 characters for address without 0x) + require(bytes(_address).length == 40, "Invalid address length"); + + // Convert string to bytes + bytes memory _hexBytes = bytes(_address); + uint160 _parsedAddress = 0; + + // Convert each character to its hex value + for(uint i = 0; i < 40; i++) { + bytes1 char = _hexBytes[i]; + uint8 digit; + + if (uint8(char) >= 48 && uint8(char) <= 57) { + // 0-9 + digit = uint8(char) - 48; + } else if (uint8(char) >= 65 && uint8(char) <= 70) { + // A-F + digit = uint8(char) - 55; + } else if (uint8(char) >= 97 && uint8(char) <= 102) { + // a-f + digit = uint8(char) - 87; + } else { + revert("Invalid character in address string"); + } + + _parsedAddress = _parsedAddress * 16 + digit; + } + + return address(_parsedAddress); + } +} + +interface OldVault { + function claimPTokens(PointTokenVault.Claim calldata claim, address account) external; +} diff --git a/foundry.toml b/foundry.toml index 6d69766..797fa61 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,6 +10,10 @@ ffi = true ast = true build_info = true extra_output = ["storageLayout"] +fs_permissions = [{ access = "read", path = "./js-scripts/generateRedemptionRights/last-alpha-distribution.json" }, +{ access = "read", path = "./js-scripts/generateRedemptionRights/out/merged-distribution.json" }, +{ access = "read", path = "./js-scripts/generateRedemptionRights/out/ptoken-snapshot-kpsats.json" }, +{ access = "read", path = "./out/PointTokenVault.sol/PointTokenVault.json" }] [rpc_endpoints] sepolia = "${SEPOLIA_RPC_URL}" diff --git a/js-scripts/generateRedemptionRights/abis/point-token-vault.ts b/js-scripts/generateRedemptionRights/abis/point-token-vault.ts new file mode 100644 index 0000000..197a192 --- /dev/null +++ b/js-scripts/generateRedemptionRights/abis/point-token-vault.ts @@ -0,0 +1,914 @@ +export const pointTokenVaultABI = [ + { type: "constructor", inputs: [], stateMutability: "nonpayable" }, + { type: "receive", stateMutability: "payable" }, + { + type: "function", + name: "DEFAULT_ADMIN_ROLE", + inputs: [], + outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + stateMutability: "view", + }, + { + type: "function", + name: "MERKLE_UPDATER_ROLE", + inputs: [], + outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + stateMutability: "view", + }, + { + type: "function", + name: "OPERATOR_ROLE", + inputs: [], + outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + stateMutability: "view", + }, + { + type: "function", + name: "REDEMPTION_RIGHTS_PREFIX", + inputs: [], + outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + stateMutability: "view", + }, + { + type: "function", + name: "UPGRADE_INTERFACE_VERSION", + inputs: [], + outputs: [{ name: "", type: "string", internalType: "string" }], + stateMutability: "view", + }, + { + type: "function", + name: "balances", + inputs: [ + { name: "", type: "address", internalType: "address" }, + { name: "", type: "address", internalType: "contract ERC20" }, + ], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "caps", + inputs: [{ name: "", type: "address", internalType: "address" }], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "claimPTokens", + inputs: [ + { + name: "_claim", + type: "tuple", + internalType: "struct PointTokenVault.Claim", + components: [ + { name: "pointsId", type: "bytes32", internalType: "bytes32" }, + { name: "totalClaimable", type: "uint256", internalType: "uint256" }, + { name: "amountToClaim", type: "uint256", internalType: "uint256" }, + { name: "proof", type: "bytes32[]", internalType: "bytes32[]" }, + ], + }, + { name: "_account", type: "address", internalType: "address" }, + { name: "_receiver", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "claimedPTokens", + inputs: [ + { name: "", type: "address", internalType: "address" }, + { name: "", type: "bytes32", internalType: "bytes32" }, + ], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "claimedRedemptionRights", + inputs: [ + { name: "", type: "address", internalType: "address" }, + { name: "", type: "bytes32", internalType: "bytes32" }, + ], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "collectFees", + inputs: [{ name: "_pointsId", type: "bytes32", internalType: "bytes32" }], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "convertRewardsToPTokens", + inputs: [ + { name: "_receiver", type: "address", internalType: "address" }, + { name: "_pointsId", type: "bytes32", internalType: "bytes32" }, + { name: "_amountToConvert", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "currRoot", + inputs: [], + outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + stateMutability: "view", + }, + { + type: "function", + name: "deployPToken", + inputs: [{ name: "_pointsId", type: "bytes32", internalType: "bytes32" }], + outputs: [ + { name: "pToken", type: "address", internalType: "contract PToken" }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "deposit", + inputs: [ + { name: "_token", type: "address", internalType: "contract ERC20" }, + { name: "_amount", type: "uint256", internalType: "uint256" }, + { name: "_receiver", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "execute", + inputs: [ + { name: "_to", type: "address", internalType: "address" }, + { name: "_data", type: "bytes", internalType: "bytes" }, + { name: "_txGas", type: "uint256", internalType: "uint256" }, + ], + outputs: [{ name: "success", type: "bool", internalType: "bool" }], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "feeCollector", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "feelesslyRedeemedPTokens", + inputs: [ + { name: "", type: "address", internalType: "address" }, + { name: "", type: "bytes32", internalType: "bytes32" }, + ], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "getRoleAdmin", + inputs: [{ name: "role", type: "bytes32", internalType: "bytes32" }], + outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + stateMutability: "view", + }, + { + type: "function", + name: "grantRole", + inputs: [ + { name: "role", type: "bytes32", internalType: "bytes32" }, + { name: "account", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "hasRole", + inputs: [ + { name: "role", type: "bytes32", internalType: "bytes32" }, + { name: "account", type: "address", internalType: "address" }, + ], + outputs: [{ name: "", type: "bool", internalType: "bool" }], + stateMutability: "view", + }, + { + type: "function", + name: "initialize", + inputs: [ + { name: "_admin", type: "address", internalType: "address" }, + { name: "_feeCollector", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "mintFee", + inputs: [], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "multicall", + inputs: [{ name: "data", type: "bytes[]", internalType: "bytes[]" }], + outputs: [{ name: "results", type: "bytes[]", internalType: "bytes[]" }], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "pTokenFeeAcc", + inputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "pTokens", + inputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + outputs: [{ name: "", type: "address", internalType: "contract PToken" }], + stateMutability: "view", + }, + { + type: "function", + name: "pausePToken", + inputs: [{ name: "_pointsId", type: "bytes32", internalType: "bytes32" }], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "prevRoot", + inputs: [], + outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + stateMutability: "view", + }, + { + type: "function", + name: "proxiableUUID", + inputs: [], + outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + stateMutability: "view", + }, + { + type: "function", + name: "redeemRewards", + inputs: [ + { + name: "_claim", + type: "tuple", + internalType: "struct PointTokenVault.Claim", + components: [ + { name: "pointsId", type: "bytes32", internalType: "bytes32" }, + { name: "totalClaimable", type: "uint256", internalType: "uint256" }, + { name: "amountToClaim", type: "uint256", internalType: "uint256" }, + { name: "proof", type: "bytes32[]", internalType: "bytes32[]" }, + ], + }, + { name: "_receiver", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "redemptionFee", + inputs: [], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "redemptions", + inputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + outputs: [ + { name: "rewardToken", type: "address", internalType: "contract ERC20" }, + { name: "rewardsPerPToken", type: "uint256", internalType: "uint256" }, + { name: "isMerkleBased", type: "bool", internalType: "bool" }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "renouncePauseRole", + inputs: [{ name: "_pointsId", type: "bytes32", internalType: "bytes32" }], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "renounceRole", + inputs: [ + { name: "role", type: "bytes32", internalType: "bytes32" }, + { name: "callerConfirmation", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "revokeRole", + inputs: [ + { name: "role", type: "bytes32", internalType: "bytes32" }, + { name: "account", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "rewardTokenFeeAcc", + inputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "setCap", + inputs: [ + { name: "_token", type: "address", internalType: "address" }, + { name: "_cap", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "setFeeCollector", + inputs: [ + { name: "_feeCollector", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "setMintFee", + inputs: [{ name: "_mintFee", type: "uint256", internalType: "uint256" }], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "setRedemption", + inputs: [ + { name: "_pointsId", type: "bytes32", internalType: "bytes32" }, + { name: "_rewardToken", type: "address", internalType: "contract ERC20" }, + { name: "_rewardsPerPToken", type: "uint256", internalType: "uint256" }, + { name: "_isMerkleBased", type: "bool", internalType: "bool" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "setRedemptionFee", + inputs: [ + { name: "_redemptionFee", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "supportsInterface", + inputs: [{ name: "interfaceId", type: "bytes4", internalType: "bytes4" }], + outputs: [{ name: "", type: "bool", internalType: "bool" }], + stateMutability: "view", + }, + { + type: "function", + name: "totalDeposited", + inputs: [{ name: "", type: "address", internalType: "address" }], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "trustReceiver", + inputs: [ + { name: "_account", type: "address", internalType: "address" }, + { name: "_isTrusted", type: "bool", internalType: "bool" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "trustedReceivers", + inputs: [ + { name: "", type: "address", internalType: "address" }, + { name: "", type: "address", internalType: "address" }, + ], + outputs: [{ name: "", type: "bool", internalType: "bool" }], + stateMutability: "view", + }, + { + type: "function", + name: "unpausePToken", + inputs: [{ name: "_pointsId", type: "bytes32", internalType: "bytes32" }], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "updateRoot", + inputs: [{ name: "_newRoot", type: "bytes32", internalType: "bytes32" }], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "upgradeToAndCall", + inputs: [ + { name: "newImplementation", type: "address", internalType: "address" }, + { name: "data", type: "bytes", internalType: "bytes" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "withdraw", + inputs: [ + { name: "_token", type: "address", internalType: "contract ERC20" }, + { name: "_amount", type: "uint256", internalType: "uint256" }, + { name: "_receiver", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "event", + name: "CapSet", + inputs: [ + { + name: "token", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "prevCap", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + { name: "cap", type: "uint256", indexed: false, internalType: "uint256" }, + ], + anonymous: false, + }, + { + type: "event", + name: "Deposit", + inputs: [ + { + name: "depositor", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "receiver", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "token", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "amount", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "FeeCollectorSet", + inputs: [ + { + name: "feeCollector", + type: "address", + indexed: false, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "FeesCollected", + inputs: [ + { + name: "pointsId", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "feeCollector", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "pTokenFee", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + { + name: "rewardTokenFee", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "Initialized", + inputs: [ + { + name: "version", + type: "uint64", + indexed: false, + internalType: "uint64", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "MintFeeSet", + inputs: [ + { + name: "mintFee", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "PTokenDeployed", + inputs: [ + { + name: "pointsId", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "pToken", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "PTokensClaimed", + inputs: [ + { + name: "account", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "receiver", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "pointsId", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "amount", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + { name: "fee", type: "uint256", indexed: false, internalType: "uint256" }, + ], + anonymous: false, + }, + { + type: "event", + name: "RedemptionFeeSet", + inputs: [ + { + name: "redemptionFee", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "RewardRedemptionSet", + inputs: [ + { + name: "pointsId", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "rewardToken", + type: "address", + indexed: false, + internalType: "contract ERC20", + }, + { + name: "rewardsPerPToken", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + { + name: "isMerkleBased", + type: "bool", + indexed: false, + internalType: "bool", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "RewardsClaimed", + inputs: [ + { + name: "owner", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "receiver", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "pointsId", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "amount", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + { name: "fee", type: "uint256", indexed: false, internalType: "uint256" }, + ], + anonymous: false, + }, + { + type: "event", + name: "RewardsConverted", + inputs: [ + { + name: "owner", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "receiver", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "pointsId", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "amount", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "RoleAdminChanged", + inputs: [ + { name: "role", type: "bytes32", indexed: true, internalType: "bytes32" }, + { + name: "previousAdminRole", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "newAdminRole", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "RoleGranted", + inputs: [ + { name: "role", type: "bytes32", indexed: true, internalType: "bytes32" }, + { + name: "account", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "sender", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "RoleRevoked", + inputs: [ + { name: "role", type: "bytes32", indexed: true, internalType: "bytes32" }, + { + name: "account", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "sender", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "RootUpdated", + inputs: [ + { + name: "prevRoot", + type: "bytes32", + indexed: false, + internalType: "bytes32", + }, + { + name: "newRoot", + type: "bytes32", + indexed: false, + internalType: "bytes32", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "TrustReceiver", + inputs: [ + { + name: "owner", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "delegate", + type: "address", + indexed: true, + internalType: "address", + }, + { name: "isTrusted", type: "bool", indexed: false, internalType: "bool" }, + ], + anonymous: false, + }, + { + type: "event", + name: "Upgraded", + inputs: [ + { + name: "implementation", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "Withdraw", + inputs: [ + { + name: "withdrawer", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "receiver", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "token", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "amount", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, + { type: "error", name: "AccessControlBadConfirmation", inputs: [] }, + { + type: "error", + name: "AccessControlUnauthorizedAccount", + inputs: [ + { name: "account", type: "address", internalType: "address" }, + { name: "neededRole", type: "bytes32", internalType: "bytes32" }, + ], + }, + { + type: "error", + name: "AddressEmptyCode", + inputs: [{ name: "target", type: "address", internalType: "address" }], + }, + { type: "error", name: "AmountTooSmall", inputs: [] }, + { type: "error", name: "CantConvertMerkleRedemption", inputs: [] }, + { type: "error", name: "ClaimTooLarge", inputs: [] }, + { type: "error", name: "DepositExceedsCap", inputs: [] }, + { + type: "error", + name: "ERC1967InvalidImplementation", + inputs: [ + { name: "implementation", type: "address", internalType: "address" }, + ], + }, + { type: "error", name: "ERC1967NonPayable", inputs: [] }, + { + type: "error", + name: "ExecutionFailed", + inputs: [ + { name: "to", type: "address", internalType: "address" }, + { name: "data", type: "bytes", internalType: "bytes" }, + ], + }, + { type: "error", name: "FailedCall", inputs: [] }, + { type: "error", name: "InvalidInitialization", inputs: [] }, + { type: "error", name: "NotInitializing", inputs: [] }, + { type: "error", name: "NotTrustedReceiver", inputs: [] }, + { type: "error", name: "PTokenAlreadyDeployed", inputs: [] }, + { type: "error", name: "PTokenNotDeployed", inputs: [] }, + { type: "error", name: "ProofInvalidOrExpired", inputs: [] }, + { type: "error", name: "RewardsNotLive", inputs: [] }, + { type: "error", name: "UUPSUnauthorizedCallContext", inputs: [] }, + { + type: "error", + name: "UUPSUnsupportedProxiableUUID", + inputs: [{ name: "slot", type: "bytes32", internalType: "bytes32" }], + }, +]; diff --git a/js-scripts/generateRedemptionRights/countpoints.js b/js-scripts/generateRedemptionRights/countpoints.js new file mode 100644 index 0000000..772ba72 --- /dev/null +++ b/js-scripts/generateRedemptionRights/countpoints.js @@ -0,0 +1,24 @@ +import { readFileSync } from "fs"; + +const rewards = 132431200000000000000000 / 1e18; + +const data = JSON.parse( + readFileSync( + `${process.cwd()}/js-scripts/generateRedemptionRights/last-alpha-distribution.json` + ) +); + +const kpSatId = + "0x1852756d70656c206b506f696e743a20457468656e61205332066b7053415453"; + +const pTokens = data.pTokens; + +let total = 0; +for (const user in pTokens) { + console.log(pTokens[user][kpSatId]); + total += pTokens[user][kpSatId].accumulatingPoints; +} +console.log(total); +const totalPTokens = total / 1e18; +const rewardsPerPToken = rewards / totalPTokens; +console.log(rewardsPerPToken * 2); diff --git a/js-scripts/generateRedemptionRights/index.ts b/js-scripts/generateRedemptionRights/index.ts new file mode 100644 index 0000000..2ff8d10 --- /dev/null +++ b/js-scripts/generateRedemptionRights/index.ts @@ -0,0 +1,396 @@ +import * as fs from "fs"; +import * as dotenv from "dotenv"; +import { + keccak256, + encodePacked, + createPublicClient, + http, + parseAbiItem, + Address, + zeroAddress, + PublicClient, + getContract, + erc20Abi, +} from "viem"; +import { mainnet } from "viem/chains"; +import { MerkleTree } from "merkletreejs"; +import { pointTokenVaultABI } from "./abis/point-token-vault.ts"; +import { LosslessNumber, parse, stringify } from "lossless-json"; + +// Types +type PointsBalance = Map<`0x${string}`, bigint>; +type RedemptionRightsMap = Map
; + +interface AlphaDistributionData { + pTokens: { + [address: Address]: { + [pointsId: `0x${string}`]: { + accumulatingPoints: LosslessNumber; + }; + }; + }; +} + +interface MerklizedData { + root: `0x${string}`; + redemptionRights: { + [address: Address]: { + [pointsId: `0x${string}`]: { + amount: string; + proof: `0x${string}`[]; + }; + }; + }; + pTokens: { + [address: Address]: { + [pointsId: `0x${string}`]: { + amount: string; + proof: `0x${string}`[]; + }; + }; + }; +} + +interface PTokenSnapshot { + address: Address; + blockNumber: string; + balances: { + [address: string]: string; + }; +} + +// Overrides +const UNI_POOL_OVERRIDES = { + "0x597a1b0515bbeEe6796B89a6f403c3fD41BB626C": { + "0x24C694d193B19119bcDea9D40a3b0bfaFb281E6D": "6487631537430741114", + "0x44Cb2d713BDa3858001f038645fD05E23E5DE03D": "27597767454066598826095", + }, +}; + +// Config +dotenv.config({ path: "./js-scripts/generateRedemptionRights/.env" }); +const config = { + rpcUrl: process.env.MAINNET_RPC_URL, + pTokenAddresses: (process.env.PTOKEN_ADDRESSES as string)?.split( + "," + ) as Address[], + pointsIds: (process.env.POINTS_IDS as string)?.split(",") as `0x${string}`[], + rewardsPerPToken: (process.env.REWARDS_PER_P_TOKEN as string)?.split( + "," + ) as string[], + vestSizes: (process.env.VEST_SIZE as string)?.split(",") as string[], + pointTokenVaultAddress: process.env.POINT_TOKEN_VAULT_ADDRESS as Address, +}; + +// Core functions +async function calculateRedemptionRights( + client: PublicClient, + iteration: number, + previousDistribution: AlphaDistributionData +): Promise<[Map, PTokenSnapshot]> { + const pTokenAddress = config.pTokenAddresses[iteration]; + const rewardsMultiplier = config.rewardsPerPToken[iteration]; + const pointsId = config.pointsIds[iteration]; + const vestSize = config.vestSizes[iteration]; + + const redemptionRights = new Map(); + const rewardsMultiplierBigInt = BigInt( + Number(rewardsMultiplier) * Number(vestSize) + ); + const blockNumber = await client.getBlockNumber(); + + const logs = await client.getLogs({ + address: pTokenAddress, + event: parseAbiItem( + "event Transfer(address indexed from, address indexed to, uint256)" + ), + fromBlock: 0n, + toBlock: "latest", + }); + + // Track raw pToken balances separately + const pTokenBalances = new Map(); + + for (const log of logs) { + const [from, to, value] = log.args as [Address, Address, bigint]; + + // Update redemption rights + if (from !== zeroAddress) { + redemptionRights.set( + from, + (redemptionRights.get(from) || 0n) - + (value * rewardsMultiplierBigInt) / BigInt(1e18) + ); + pTokenBalances.set(from, (pTokenBalances.get(from) || 0n) - value); + } + if (to !== zeroAddress) { + redemptionRights.set( + to, + (redemptionRights.get(to) || 0n) + + (value * rewardsMultiplierBigInt) / BigInt(1e18) + ); + pTokenBalances.set(to, (pTokenBalances.get(to) || 0n) + value); + } + } + + // Apply UNI pool overrides after processing all transfers + for (const [poolAddress, redistributions] of Object.entries( + UNI_POOL_OVERRIDES + )) { + const poolBalance = pTokenBalances.get(poolAddress as Address) || 0n; + if (poolBalance > 0n) { + // Remove balance from pool + pTokenBalances.set(poolAddress as Address, 0n); + redemptionRights.set(poolAddress as Address, 0n); + + // Redistribute to specified addresses + for (const [recipient, amount] of Object.entries(redistributions)) { + const recipientAddress = recipient as Address; + const overrideAmount = BigInt(amount); + + pTokenBalances.set( + recipientAddress, + (pTokenBalances.get(recipientAddress) || 0n) + overrideAmount + ); + + redemptionRights.set( + recipientAddress, + (redemptionRights.get(recipientAddress) || 0n) + + (overrideAmount * rewardsMultiplierBigInt) / BigInt(1e18) + ); + } + } + } + + // Add unclaimed pToken balances + const pointTokenVault = getContract({ + address: config.pointTokenVaultAddress, + abi: pointTokenVaultABI, + client, + }); + for (const [userAddress, addressPointsData] of Object.entries( + previousDistribution.pTokens + )) { + const { accumulatingPoints } = addressPointsData[pointsId]; + + const claimedPtokens = (await pointTokenVault.read.claimedPTokens([ + userAddress, + pointsId, + ])) as bigint; + + const unclaimedPtokens = + BigInt(accumulatingPoints.toString()) - claimedPtokens; + + if (unclaimedPtokens > 0n) { + pTokenBalances.set( + userAddress as Address, + (pTokenBalances.get(userAddress as Address) || 0n) + unclaimedPtokens + ); + + redemptionRights.set( + userAddress as Address, + (redemptionRights.get(userAddress as Address) || 0n) + + (unclaimedPtokens * rewardsMultiplierBigInt) / BigInt(1e18) + ); + } + } + + // Create snapshot object + const snapshot: PTokenSnapshot = { + address: pTokenAddress, + blockNumber: blockNumber.toString(), + balances: Object.fromEntries( + Array.from(pTokenBalances.entries()) + .filter(([_, balance]) => balance > 0n) + .map(([addr, balance]) => [addr, balance.toString()]) + ), + }; + + return [ + new Map( + Array.from(redemptionRights.entries()).filter( + ([_, balance]) => balance > 0n + ) + ), + snapshot, + ]; +} + +function generateMerkleData( + allRedemptionRights: RedemptionRightsMap, + previousDistribution: AlphaDistributionData +): MerklizedData { + const prefix = keccak256(encodePacked(["string"], ["REDEMPTION_RIGHTS"])); + + // Generate redemption rights leaves + const rightsLeaves = Array.from(allRedemptionRights.entries()).flatMap( + ([address, balances]) => + Array.from(balances.entries()).map(([pointsId, balance]) => + keccak256( + encodePacked( + ["bytes32", "address", "bytes32", "uint256"], + [prefix, address, pointsId, balance] + ) + ) + ) + ); + + // Generate pTokens leaves + const pTokenLeaves = Object.entries(previousDistribution.pTokens).flatMap( + ([address, pointsData]) => + Object.entries(pointsData).map(([pointsId, data]) => + keccak256( + encodePacked( + ["address", "bytes32", "uint256"], + [ + address as Address, + pointsId as `0x${string}`, + BigInt(data.accumulatingPoints.toString()), + ] + ) + ) + ) + ); + + // Build tree + const tree = new MerkleTree( + [...rightsLeaves, ...pTokenLeaves].sort(), + keccak256, + { sortPairs: true } + ); + + return { + root: tree.getHexRoot() as `0x${string}`, + redemptionRights: Object.fromEntries( + Array.from(allRedemptionRights.entries()).map(([address, balances]) => [ + address, + Object.fromEntries( + Array.from(balances.entries()).map(([pointsId, balance]) => [ + pointsId, + { + amount: balance.toString(), + proof: tree.getHexProof( + keccak256( + encodePacked( + ["bytes32", "address", "bytes32", "uint256"], + [prefix, address, pointsId, balance] + ) + ) + ) as `0x${string}`[], + }, + ]) + ), + ]) + ), + pTokens: Object.fromEntries( + Object.entries(previousDistribution.pTokens).map( + ([address, pointsData]) => [ + address, + Object.fromEntries( + Object.entries(pointsData).map(([pointsId, data]) => [ + pointsId, + { + amount: data.accumulatingPoints.toString(), + proof: tree.getHexProof( + keccak256( + encodePacked( + ["address", "bytes32", "uint256"], + [ + address as Address, + pointsId as `0x${string}`, + BigInt(data.accumulatingPoints.toString()), + ] + ) + ) + ) as `0x${string}`[], + }, + ]) + ), + ] + ) + ), + }; +} + +// Main execution +async function generateMerkleTree(): Promise