diff --git a/contracts/script/PointTokenVault.s.sol b/contracts/script/PointTokenVault.s.sol index 9fc0e9e..8e7fa4f 100644 --- a/contracts/script/PointTokenVault.s.sol +++ b/contracts/script/PointTokenVault.s.sol @@ -132,7 +132,7 @@ contract PointTokenVaultScripts is BatchScript { // Update merkle root vm.startBroadcast(MAINNET_MERKLE_UPDATER); - vaultV0_1_0.updateRoot(0x8add61929f9038b9b001852126409672b6c4c12161be6caee4c4ce44fe59538c); + vaultV0_1_0.updateRoot(0xa1c76e2c6f7ac8300b288ff758b8a83c4a19e12780ca7ac5f61182f64ef8edf6); vm.stopBroadcast(); } diff --git a/contracts/test/SetRedemptionENA4Nov24.t.sol b/contracts/test/SetRedemptionENA4Nov24.t.sol index 0006871..8c0b285 100644 --- a/contracts/test/SetRedemptionENA4Nov24.t.sol +++ b/contracts/test/SetRedemptionENA4Nov24.t.sol @@ -7,11 +7,15 @@ 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 @@ -23,11 +27,11 @@ contract SetRedemptionENA4Nov24Test is Test { function test_RedemptionRights1() public { bytes32[] memory proof = new bytes32[](5); - proof[0] = 0x6258fb3ee01fe8edb76a1ee2cb317cd32b8464c63e31903826acd02863982f31; - proof[1] = 0xd9d5035f478e73b0e33ae1677a34fb215dd9f25d858d17265608acd57f066e48; - proof[2] = 0x65c2bc0496edc30a605d1618f867da252e88358e98d08d80ad0d485df4439055; - proof[3] = 0x9c4c736ac69fbbc0f510cbee646cfcb0a5186004dbb3c577d7c3ab658ddcf209; - proof[4] = 0x2129557061359b11c571b9a63f6363b144b6ae40058d9731d0e165463fc18438; + proof[0] = 0x4e0230e7a7546148f373efe52256490c04ea0019e199f2256a375e75cf2b6b96; + proof[1] = 0xfaca43636023a73bf06295aa693c262efe0bb3231357a68b8ad7eec7e901c8ef; + proof[2] = 0x03c4c7f992659d2c405eb95248d21dfd7f66c7bdef49d9d33cb97b0b5d35c37a; + proof[3] = 0x617d803c581341b5720c7932932ca45ebfb90e87a122f801b0d5fe53b747db51; + proof[4] = 0x620f05d3b25d35da47076de4dceb31f95567ff863138ae7d1fe416ea0bc2ff41; address USER = 0x25E426b153e74Ab36b2685c3A464272De60888Ae; uint256 AMOUNT = 26396311093240867247; @@ -54,11 +58,11 @@ contract SetRedemptionENA4Nov24Test is Test { function test_RedemptionRights1_ClaimTooMuch() public { bytes32[] memory proof = new bytes32[](5); - proof[0] = 0x6258fb3ee01fe8edb76a1ee2cb317cd32b8464c63e31903826acd02863982f31; - proof[1] = 0xd9d5035f478e73b0e33ae1677a34fb215dd9f25d858d17265608acd57f066e48; - proof[2] = 0x65c2bc0496edc30a605d1618f867da252e88358e98d08d80ad0d485df4439055; - proof[3] = 0x9c4c736ac69fbbc0f510cbee646cfcb0a5186004dbb3c577d7c3ab658ddcf209; - proof[4] = 0x2129557061359b11c571b9a63f6363b144b6ae40058d9731d0e165463fc18438; + proof[0] = 0x4e0230e7a7546148f373efe52256490c04ea0019e199f2256a375e75cf2b6b96; + proof[1] = 0xfaca43636023a73bf06295aa693c262efe0bb3231357a68b8ad7eec7e901c8ef; + proof[2] = 0x03c4c7f992659d2c405eb95248d21dfd7f66c7bdef49d9d33cb97b0b5d35c37a; + proof[3] = 0x617d803c581341b5720c7932932ca45ebfb90e87a122f801b0d5fe53b747db51; + proof[4] = 0x620f05d3b25d35da47076de4dceb31f95567ff863138ae7d1fe416ea0bc2ff41; address USER = 0x25E426b153e74Ab36b2685c3A464272De60888Ae; uint256 TOTAL_CLAIMABLE = 26396311093240867247; @@ -69,21 +73,161 @@ contract SetRedemptionENA4Nov24Test is Test { vaultV0_1_0.redeemRewards(PointTokenVault.Claim(pointsId, TOTAL_CLAIMABLE, CLAIM_AMOUNT, proof), USER); } - function test_NormalPTokenClaim() public { - bytes32[] memory proof = new bytes32[](5); - proof[0] = 0x6a69b863aea1e9a735f460a6fb449a28b7398f958c4792872d7764700936b79d; - proof[1] = 0xf88a43494a509c28f8321582f54b7964fc2232a7f109d49a44631cbcfa5d30a9; - proof[2] = 0x65c2bc0496edc30a605d1618f867da252e88358e98d08d80ad0d485df4439055; - proof[3] = 0x9c4c736ac69fbbc0f510cbee646cfcb0a5186004dbb3c577d7c3ab658ddcf209; - proof[4] = 0x2129557061359b11c571b9a63f6363b144b6ae40058d9731d0e165463fc18438; + struct RedemptionData { + uint256 pTokenBalance; + uint256 claimedPoints; + uint256 totalClaimablePoints; + uint256 unclaimedPoints; + uint256 totalRedeemableBalance; + uint256 currentRedeemableBalance; + } - address USER = 0x24C694d193B19119bcDea9D40a3b0bfaFb281E6D; - uint256 AMOUNT = 152407798291890457882; + struct RedemptionFiles { + string root; + string path; + string alphaDistribution; + string merged; + string balances; + } - vm.prank(USER); - OldVault(address(vaultV0_1_0)).claimPTokens(PointTokenVault.Claim(pointsId, AMOUNT, AMOUNT, proof), USER); + 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; + } + } + } - assertEq(ERC20(address(vaultV0_1_0.pTokens(pointsId))).balanceOf(USER), AMOUNT); + 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); } } 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/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);