From d9816a6f981a63df5941b738b5a3fae0d6dd0431 Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Thu, 14 Dec 2023 14:21:11 +0100 Subject: [PATCH] [Polygon] feat: mainnet fork tests --- foundry.toml | 1 + src/adapters/PolygonAdapter.sol | 54 +++--- src/adapters/interfaces/IPolygon.sol | 9 +- test/adapters/PolygonAdapter.t.sol | 28 +-- test/fork-tests/Graph.arbitrum.t.sol | 10 +- test/fork-tests/Polygon.mainnet.t.sol | 250 ++++++++++++++++++++++++++ 6 files changed, 312 insertions(+), 40 deletions(-) create mode 100644 test/fork-tests/Polygon.mainnet.t.sol diff --git a/foundry.toml b/foundry.toml index 7b7485d..f755528 100644 --- a/foundry.toml +++ b/foundry.toml @@ -33,3 +33,4 @@ depth = 100 # Uncomment to enable the RPC server arbitrum_goerli = "${ARBITRUM_GOERLI_RPC}" arbitrum = "${ARBITRUM_RPC}" +mainnet = "${MAINNET_RPC}" diff --git a/src/adapters/PolygonAdapter.sol b/src/adapters/PolygonAdapter.sol index c4f4758..3bb9105 100644 --- a/src/adapters/PolygonAdapter.sol +++ b/src/adapters/PolygonAdapter.sol @@ -16,13 +16,16 @@ import { ERC20 } from "solmate/tokens/ERC20.sol"; import { Adapter } from "core/adapters/Adapter.sol"; import { IERC165 } from "core/interfaces/IERC165.sol"; import { ITenderizer } from "core/tenderizer/ITenderizer.sol"; -import { IMaticStakeManager, IValidatorShares, DelegatorUnbond } from "core/adapters/interfaces/IPolygon.sol"; +import { IPolygonStakeManager, IPolygonValidatorShares, DelegatorUnbond } from "core/adapters/interfaces/IPolygon.sol"; // Matic exchange rate precision uint256 constant EXCHANGE_RATE_PRECISION = 100; // For Validator ID < 8 uint256 constant EXCHANGE_RATE_PRECISION_HIGH = 10 ** 29; // For Validator ID >= 8 uint256 constant WITHDRAW_DELAY = 80; // 80 epochs, epoch length can vary on average between 200-300 Ethereum L1 blocks +IPolygonStakeManager constant POLYGON_STAKEMANAGER = IPolygonStakeManager(address(0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908)); +ERC20 constant POL = ERC20(address(0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0)); + // Polygon validators with a `validatorId` less than 8 are foundation validators // These are special case validators that don't have slashing enabled and still operate // On the old precision for the ValidatorShares contract. @@ -37,21 +40,28 @@ function getExchangePrecision(uint256 validatorId) pure returns (uint256) { contract PolygonAdapter is Adapter { using SafeTransferLib for ERC20; - IMaticStakeManager private constant MATIC_STAKE_MANAGER = - IMaticStakeManager(address(0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908)); - ERC20 private constant POLY = ERC20(address(0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0)); - function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { return interfaceId == type(Adapter).interfaceId || interfaceId == type(IERC165).interfaceId; } function isValidator(address validator) public view returns (bool) { // Validator must have a validator shares contract + // This will revert if the address does not own its StakeNFT + // Which could lead to unexpected behaviour if used by external contracts return address(_getValidatorSharesContract(_getValidatorId(validator))) != address(0); } - function previewDeposit(address, /*validator*/ uint256 assets) external pure returns (uint256) { - return assets; + function previewDeposit(address validator, uint256 assets) external view returns (uint256) { + uint256 validatorId = _getValidatorId(validator); + uint256 delegatedAmount = IPolygonStakeManager(POLYGON_STAKEMANAGER).delegatedAmount(validatorId); + IPolygonValidatorShares validatorShares = _getValidatorSharesContract(validatorId); + uint256 totalShares = validatorShares.totalSupply(); + uint256 prec = getExchangePrecision(_getValidatorId(validator)); + uint256 fxRate_0 = prec * delegatedAmount / totalShares; + uint256 sharesToMint = assets * prec / fxRate_0; + uint256 amountToTransfer = sharesToMint * fxRate_0 / prec; + uint256 fxRate_1 = prec * (delegatedAmount + amountToTransfer) / (totalShares + sharesToMint); + return sharesToMint * fxRate_1 / prec; } function previewWithdraw(uint256 unlockID) external view returns (uint256 amount) { @@ -59,7 +69,7 @@ contract PolygonAdapter is Adapter { address validator = _getValidatorAddress(); // get the validator shares contract for validator uint256 validatorId = _getValidatorId(validator); - IValidatorShares validatorShares = _getValidatorSharesContract(validatorId); + IPolygonValidatorShares validatorShares = _getValidatorSharesContract(validatorId); DelegatorUnbond memory unbond = validatorShares.unbonds_new(address(this), unlockID); // calculate amount of tokens to withdraw by converting shares back into amount @@ -82,15 +92,15 @@ contract PolygonAdapter is Adapter { } function currentTime() external view override returns (uint256) { - return MATIC_STAKE_MANAGER.epoch(); + return POLYGON_STAKEMANAGER.epoch(); } function stake(address validator, uint256 amount) external override returns (uint256) { // approve tokens - POLY.safeApprove(address(MATIC_STAKE_MANAGER), amount); + POL.safeApprove(address(POLYGON_STAKEMANAGER), amount); uint256 validatorId = _getValidatorId(validator); - IValidatorShares validatorShares = _getValidatorSharesContract(validatorId); + IPolygonValidatorShares validatorShares = _getValidatorSharesContract(validatorId); // calculate minimum amount of voucher shares to mint // adjust for integer truncation upon division @@ -99,13 +109,12 @@ contract PolygonAdapter is Adapter { uint256 min = amount * precision / fxRate - 1; // Mint voucher shares - validatorShares.buyVoucher(amount, min); - return amount; + return validatorShares.buyVoucher(amount, min); } function unstake(address validator, uint256 amount) external override returns (uint256 unlockID) { uint256 validatorId = _getValidatorId(validator); - IValidatorShares validatorShares = _getValidatorSharesContract(validatorId); + IPolygonValidatorShares validatorShares = _getValidatorSharesContract(validatorId); uint256 precision = getExchangePrecision(validatorId); uint256 fxRate = validatorShares.exchangeRate(); @@ -120,7 +129,7 @@ contract PolygonAdapter is Adapter { function withdraw(address validator, uint256 unlockID) external override returns (uint256 amount) { uint256 validatorId = _getValidatorId(validator); - IValidatorShares validatorShares = _getValidatorSharesContract(validatorId); + IPolygonValidatorShares validatorShares = _getValidatorSharesContract(validatorId); DelegatorUnbond memory unbond = validatorShares.unbonds_new(address(this), unlockID); // foundation validators (id < 8) don't have slashing enabled @@ -133,7 +142,7 @@ contract PolygonAdapter is Adapter { function rebase(address validator, uint256 currentStake) external returns (uint256 newStake) { uint256 validatorId = _getValidatorId(validator); - IValidatorShares validatorShares = _getValidatorSharesContract(validatorId); + IPolygonValidatorShares validatorShares = _getValidatorSharesContract(validatorId); // This call will revert if there are no rewards // In which case we don't throw, just return the current staked amount. @@ -153,12 +162,13 @@ contract PolygonAdapter is Adapter { function _getValidatorAddress() internal view returns (address) { return ITenderizer(address(this)).validator(); } +} - function _getValidatorId(address validator) internal view returns (uint256) { - return MATIC_STAKE_MANAGER.getValidatorId(validator); - } +function _getValidatorId(address validator) view returns (uint256) { + // This will revert if validator is not valid + return POLYGON_STAKEMANAGER.getValidatorId(validator); +} - function _getValidatorSharesContract(uint256 validatorId) internal view returns (IValidatorShares) { - return IValidatorShares(MATIC_STAKE_MANAGER.getValidatorContract(validatorId)); - } +function _getValidatorSharesContract(uint256 validatorId) view returns (IPolygonValidatorShares) { + return IPolygonValidatorShares(POLYGON_STAKEMANAGER.getValidatorContract(validatorId)); } diff --git a/src/adapters/interfaces/IPolygon.sol b/src/adapters/interfaces/IPolygon.sol index e1ec203..4896fd5 100644 --- a/src/adapters/interfaces/IPolygon.sol +++ b/src/adapters/interfaces/IPolygon.sol @@ -11,18 +11,19 @@ struct DelegatorUnbond { uint256 withdrawEpoch; } -interface IMaticStakeManager { +interface IPolygonStakeManager { function getValidatorId(address user) external view returns (uint256); function getValidatorContract(uint256 validatorId) external view returns (address); function epoch() external view returns (uint256); + function delegatedAmount(uint256 validatorId) external view returns (uint256); } -interface IValidatorShares { +interface IPolygonValidatorShares { function owner() external view returns (address); function restake() external; - function buyVoucher(uint256 _amount, uint256 _minSharesToMint) external; + function buyVoucher(uint256 _amount, uint256 _minSharesToMint) external returns (uint256 amount); function sellVoucher_new(uint256 claimAmount, uint256 maximumSharesToBurn) external; @@ -39,4 +40,6 @@ interface IValidatorShares { function withdrawExchangeRate() external view returns (uint256); function unbonds_new(address, uint256) external view returns (DelegatorUnbond memory); + + function totalSupply() external view returns (uint256); } diff --git a/test/adapters/PolygonAdapter.t.sol b/test/adapters/PolygonAdapter.t.sol index 312931f..d07f997 100644 --- a/test/adapters/PolygonAdapter.t.sol +++ b/test/adapters/PolygonAdapter.t.sol @@ -17,7 +17,7 @@ pragma solidity >=0.8.19; import { Test, stdError } from "forge-std/Test.sol"; import { PolygonAdapter, EXCHANGE_RATE_PRECISION_HIGH, WITHDRAW_DELAY } from "core/adapters/PolygonAdapter.sol"; import { ITenderizer } from "core/tenderizer/ITenderizer.sol"; -import { IMaticStakeManager, IValidatorShares, DelegatorUnbond } from "core/adapters/interfaces/IPolygon.sol"; +import { IPolygonStakeManager, IPolygonValidatorShares, DelegatorUnbond } from "core/adapters/interfaces/IPolygon.sol"; import { AdapterDelegateCall } from "core/adapters/Adapter.sol"; contract PolygonAdapterTest is Test { @@ -38,11 +38,13 @@ contract PolygonAdapterTest is Test { vm.mockCall(address(this), abi.encodeCall(ITenderizer.validator, ()), abi.encode(address(this))); // set validator id for `address(this)` to 8 (not a foundation validator) vm.mockCall( - MATIC_STAKE_MANAGER, abi.encodeCall(IMaticStakeManager.getValidatorId, (address(this))), abi.encode(validatorId) + MATIC_STAKE_MANAGER, abi.encodeCall(IPolygonStakeManager.getValidatorId, (address(this))), abi.encode(validatorId) ); // set validator shares contract for `address(this)` to `validatorShares` vm.mockCall( - MATIC_STAKE_MANAGER, abi.encodeCall(IMaticStakeManager.getValidatorContract, (validatorId)), abi.encode(validatorShares) + MATIC_STAKE_MANAGER, + abi.encodeCall(IPolygonStakeManager.getValidatorContract, (validatorId)), + abi.encode(validatorShares) ); } @@ -65,8 +67,10 @@ contract PolygonAdapterTest is Test { DelegatorUnbond memory unbond = DelegatorUnbond({ shares: shares, withdrawEpoch: 0 }); - vm.mockCall(validatorShares, abi.encodeCall(IValidatorShares.unbonds_new, (address(this), unlockID)), abi.encode(unbond)); - vm.mockCall(validatorShares, abi.encodeCall(IValidatorShares.withdrawExchangeRate, ()), abi.encode(fxRate)); + vm.mockCall( + validatorShares, abi.encodeCall(IPolygonValidatorShares.unbonds_new, (address(this), unlockID)), abi.encode(unbond) + ); + vm.mockCall(validatorShares, abi.encodeCall(IPolygonValidatorShares.withdrawExchangeRate, ()), abi.encode(fxRate)); uint256 actual = abi.decode(adapter._delegatecall(abi.encodeCall(PolygonAdapter.previewWithdraw, (unlockID))), (uint256)); assertEq(actual, expected); } @@ -76,7 +80,9 @@ contract PolygonAdapterTest is Test { uint256 unlockID = 1; DelegatorUnbond memory unbond = DelegatorUnbond({ shares: 0, withdrawEpoch: epoch }); - vm.mockCall(validatorShares, abi.encodeCall(IValidatorShares.unbonds_new, (address(this), unlockID)), abi.encode(unbond)); + vm.mockCall( + validatorShares, abi.encodeCall(IPolygonValidatorShares.unbonds_new, (address(this), unlockID)), abi.encode(unbond) + ); uint256 actual = abi.decode(adapter._delegatecall(abi.encodeCall(PolygonAdapter.unlockMaturity, (unlockID))), (uint256)); assertEq(actual, epoch + WITHDRAW_DELAY); } @@ -84,14 +90,16 @@ contract PolygonAdapterTest is Test { function test_rebase() public { uint256 currentStake = 100; uint256 newStake = 200; - vm.mockCall(validatorShares, abi.encodeCall(IValidatorShares.exchangeRate, ()), abi.encode(EXCHANGE_RATE_PRECISION_HIGH)); - vm.mockCall(validatorShares, abi.encodeCall(IValidatorShares.balanceOf, (address(this))), abi.encode(newStake)); - vm.mockCallRevert(validatorShares, abi.encodeCall(IValidatorShares.restake, ()), ""); + vm.mockCall( + validatorShares, abi.encodeCall(IPolygonValidatorShares.exchangeRate, ()), abi.encode(EXCHANGE_RATE_PRECISION_HIGH) + ); + vm.mockCall(validatorShares, abi.encodeCall(IPolygonValidatorShares.balanceOf, (address(this))), abi.encode(newStake)); + vm.mockCallRevert(validatorShares, abi.encodeCall(IPolygonValidatorShares.restake, ()), ""); uint256 actual = abi.decode(adapter._delegatecall(abi.encodeCall(PolygonAdapter.rebase, (address(this), currentStake))), (uint256)); assertEq(actual, currentStake); - vm.mockCall(validatorShares, abi.encodeCall(IValidatorShares.restake, ()), abi.encode(true)); + vm.mockCall(validatorShares, abi.encodeCall(IPolygonValidatorShares.restake, ()), abi.encode(true)); actual = abi.decode(adapter._delegatecall(abi.encodeCall(PolygonAdapter.rebase, (address(this), currentStake))), (uint256)); assertEq(actual, newStake); } diff --git a/test/fork-tests/Graph.arbitrum.t.sol b/test/fork-tests/Graph.arbitrum.t.sol index 64ad2d5..c24d5fb 100644 --- a/test/fork-tests/Graph.arbitrum.t.sol +++ b/test/fork-tests/Graph.arbitrum.t.sol @@ -11,7 +11,7 @@ pragma solidity >=0.8.19; -import { Test, console } from "forge-std/Test.sol"; +import { Test } from "forge-std/Test.sol"; import { VmSafe } from "forge-std/Vm.sol"; import { MockERC20 } from "solmate/test/utils/mocks/MockERC20.sol"; @@ -112,10 +112,10 @@ contract GraphForkTest is Test, TenderizerEvents, ERC721Receiver { } function test_factory_newTenderizer() public { - // Revert with inactive orchestrator - address inactiveOrchestrator = makeAddr("INACTIVE_ORCHESTRATOR"); - vm.expectRevert(abi.encodeWithSelector(Factory.NotValidator.selector, (inactiveOrchestrator))); - fixture.factory.newTenderizer(address(GRT), inactiveOrchestrator); + // Revert with inactive indexer + address inactiveIndexer = makeAddr("INACTIVE_INDEXER"); + vm.expectRevert(abi.encodeWithSelector(Factory.NotValidator.selector, (inactiveIndexer))); + fixture.factory.newTenderizer(address(GRT), inactiveIndexer); // Deploy tenderizer vm.expectEmit({ checkTopic1: true, checkTopic2: true, checkTopic3: false, checkData: false }); diff --git a/test/fork-tests/Polygon.mainnet.t.sol b/test/fork-tests/Polygon.mainnet.t.sol new file mode 100644 index 0000000..9027c21 --- /dev/null +++ b/test/fork-tests/Polygon.mainnet.t.sol @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: MIT +// +// _____ _ _ +// |_ _| | | (_) +// | | ___ _ __ __| | ___ _ __ _ _______ +// | |/ _ \ '_ \ / _` |/ _ \ '__| |_ / _ \ +// | | __/ | | | (_| | __/ | | |/ / __/ +// \_/\___|_| |_|\__,_|\___|_| |_/___\___| +// +// Copyright (c) Tenderize Labs Ltd + +pragma solidity >=0.8.19; + +import { Test } from "forge-std/Test.sol"; +import { VmSafe } from "forge-std/Vm.sol"; +import { MockERC20 } from "solmate/test/utils/mocks/MockERC20.sol"; + +import { + PolygonAdapter, + POLYGON_STAKEMANAGER, + POL, + WITHDRAW_DELAY, + EXCHANGE_RATE_PRECISION_HIGH, + _getValidatorSharesContract, + _getValidatorId +} from "core/adapters/PolygonAdapter.sol"; +import { IPolygonStakeManager, IPolygonValidatorShares } from "core/adapters/interfaces/IPolygon.sol"; +import { Tenderizer, TenderizerEvents } from "core/tenderizer/Tenderizer.sol"; +import { Unlocks, Metadata } from "core/unlocks/Unlocks.sol"; +import { ERC721Receiver } from "core/utils/ERC721Receiver.sol"; +import { Factory } from "core/factory/Factory.sol"; +import { TenderizerFixture, tenderizerFixture } from "./Fixture.sol"; + +address constant VALIDATOR_1 = 0xe7DB0D2384587956ef9d47304E96236022cCE3Af; // 0xeA105Ab4e3F01f7f8DA09Cb84AB501Aeb02E9FC7; +address constant TOKEN_HOLDER = 0xF977814e90dA44bFA03b6295A0616a897441aceC; +address constant GOVERNANCE = 0x6e7a5820baD6cebA8Ef5ea69c0C92EbbDAc9CE48; +uint256 constant REWARD_PRECISION = 1e25; + +interface IPolygonStakeManagerTest is IPolygonStakeManager { + function setCurrentEpoch(uint256 epoch) external; +} + +interface IPolygonValidatorSharesTest is IPolygonValidatorShares { + function initalRewardPerShare(address user) external view returns (uint256); +} + +contract PolygonForkTest is Test, TenderizerEvents, ERC721Receiver { + TenderizerFixture fixture; + PolygonAdapter adapter; + + uint256 balance; + + event NewTenderizer(address indexed asset, address indexed validator, address tenderizer); + + function setRewards(uint256 amount, uint256 initialRewardPerShare) internal returns (uint256 rewardPerShare) { + IPolygonValidatorShares valShares = _getValidatorSharesContract(_getValidatorId(VALIDATOR_1)); + uint256 totalShares = valShares.totalSupply(); + rewardPerShare = initialRewardPerShare + amount * REWARD_PRECISION / totalShares; + // We have to update the `Validator.delegatorsRewards` for our validator + // in the StakingManager contract. + // for the current hardcoded validator this storage slot can be found at + // '0x511480fe2fa645166a40382828f5ab06983719d0fe9ae7a53d61f4612e299e33' + vm.store(address(POLYGON_STAKEMANAGER), 0x511480fe2fa645166a40382828f5ab06983719d0fe9ae7a53d61f4612e299e33, bytes32(amount)); + } + + function setUp() public { + bytes32 salt = bytes32(uint256(1)); + vm.createSelectFork(vm.envString("MAINNET_RPC")); + fixture = tenderizerFixture(); + adapter = new PolygonAdapter{ salt: salt }(); + fixture.registry.registerAdapter(address(POL), address(adapter)); + balance = POL.balanceOf(TOKEN_HOLDER); + vm.prank(TOKEN_HOLDER); + POL.transfer(address(this), balance); + } + + function test_registry_AdapterRegistered() public { + assertEq(fixture.registry.adapter(address(POL)), address(adapter), "adapter not registered"); + } + + function test_adapter_unlockTime() public { + assertEq(adapter.unlockTime(), WITHDRAW_DELAY, "unlock time not set"); + } + + function test_currentTime() public { + assertEq(adapter.currentTime(), POLYGON_STAKEMANAGER.epoch(), "current time not set"); + } + + function test_isValidator() public { + assertEq(adapter.isValidator(VALIDATOR_1), true, "isValidator true incorrect"); + vm.expectRevert(); + adapter.isValidator(makeAddr("NOT VALIDATOR")); + } + + function test_factory_newTenderizer() public { + // Revert with inactive validator + address inactiveValidator = makeAddr("INACTIVE_VALIDATOR"); + vm.expectRevert(); + fixture.factory.newTenderizer(address(POL), inactiveValidator); + + // Deploy tenderizer + vm.expectEmit({ checkTopic1: true, checkTopic2: true, checkTopic3: false, checkData: false }); + emit NewTenderizer(address(POL), VALIDATOR_1, address(0x0)); + fixture.factory.newTenderizer(address(POL), VALIDATOR_1); + } + + function testFuzz_previewDeposit(uint256 amount) public { + amount = bound(amount, 1, 10e28); + IPolygonValidatorShares valShares = _getValidatorSharesContract(_getValidatorId(VALIDATOR_1)); + uint256 totalShares = valShares.totalSupply(); + uint256 delegatedAmount = POLYGON_STAKEMANAGER.delegatedAmount(_getValidatorId(VALIDATOR_1)); + uint256 preview = adapter.previewDeposit(VALIDATOR_1, amount); + uint256 mintedPolShares = + amount * EXCHANGE_RATE_PRECISION_HIGH / (delegatedAmount * EXCHANGE_RATE_PRECISION_HIGH / totalShares); + uint256 amountToTransfer = + mintedPolShares * (delegatedAmount * EXCHANGE_RATE_PRECISION_HIGH / totalShares) / EXCHANGE_RATE_PRECISION_HIGH; + + uint256 exp = mintedPolShares + * ((delegatedAmount + amountToTransfer) * EXCHANGE_RATE_PRECISION_HIGH / (totalShares + mintedPolShares)) + / EXCHANGE_RATE_PRECISION_HIGH; + assertEq(preview, exp, "previewDeposit incorrect"); + } + + function testFuzz_deposit(uint256 amount) public { + amount = bound(amount, 1, balance); + + Tenderizer tenderizer = Tenderizer(payable(fixture.factory.newTenderizer(address(POL), VALIDATOR_1))); + + IPolygonValidatorShares valShares = _getValidatorSharesContract(_getValidatorId(VALIDATOR_1)); + uint256 totalShares = valShares.totalSupply(); + uint256 delegatedAmount = POLYGON_STAKEMANAGER.delegatedAmount(_getValidatorId(VALIDATOR_1)); + uint256 preview = tenderizer.previewDeposit(amount); + + uint256 fxRateBefore = delegatedAmount * EXCHANGE_RATE_PRECISION_HIGH / totalShares; + assertEq(fxRateBefore, valShares.exchangeRate()); + + uint256 mintedPolShares = amount * EXCHANGE_RATE_PRECISION_HIGH / fxRateBefore; + uint256 amountToTransfer = mintedPolShares * fxRateBefore / EXCHANGE_RATE_PRECISION_HIGH; + uint256 expectedOut = mintedPolShares + * ((delegatedAmount + amountToTransfer) * EXCHANGE_RATE_PRECISION_HIGH / (totalShares + mintedPolShares)) + / EXCHANGE_RATE_PRECISION_HIGH; + POL.approve(address(tenderizer), amount); + vm.expectEmit({ checkTopic1: true, checkTopic2: true, checkTopic3: false, checkData: true }); + emit Deposit(address(this), address(this), amount, expectedOut); + uint256 tTokenOut = tenderizer.deposit(address(this), amount); + assertEq(preview, tTokenOut, "previewDeposit incorrect"); + uint256 fxRateAfter = (delegatedAmount + amountToTransfer) * EXCHANGE_RATE_PRECISION_HIGH / (totalShares + mintedPolShares); + assertEq(fxRateAfter, valShares.exchangeRate()); + + assertEq(tenderizer.totalSupply(), expectedOut, "total supply incorrect"); + assertEq(tenderizer.balanceOf(address(this)), expectedOut, "balance incorrect"); + } + + function test_unlock_withdraw_simple() public { + uint256 depositAmount = 100_000 ether; + uint256 unstakeAmount = 25_000 ether; + Tenderizer tenderizer = Tenderizer(payable(fixture.factory.newTenderizer(address(POL), VALIDATOR_1))); + POL.approve(address(tenderizer), depositAmount); + tenderizer.deposit(address(this), depositAmount); + + vm.expectEmit(); + emit Unlock(address(this), unstakeAmount, 1); + uint256 unlockID = tenderizer.unlock(unstakeAmount); + assertEq(unlockID, 1, "unlockID incorrect"); + + assertEq(tenderizer.unlockMaturity(unlockID), POLYGON_STAKEMANAGER.epoch() + WITHDRAW_DELAY, "unlockMaturity incorrect"); + + uint256 tokenId = uint256(bytes32(abi.encodePacked(address(tenderizer), uint96(unlockID)))); + Metadata memory metadata = fixture.unlocks.getMetadata(tokenId); + assertEq(metadata.amount, unstakeAmount, "amount incorrect"); + assertEq(metadata.unlockId, unlockID, "unlockID incorrect"); + assertEq(metadata.validator, VALIDATOR_1, "validator incorrect"); + assertEq(metadata.maturity, POLYGON_STAKEMANAGER.epoch() + WITHDRAW_DELAY, "maturity incorrect"); + assertEq(metadata.progress, 0, "progress incorrect"); + + // Process epochs to 50% + uint256 newEpoch = POLYGON_STAKEMANAGER.epoch() + WITHDRAW_DELAY / 2; + vm.prank(GOVERNANCE); + IPolygonStakeManagerTest(address(POLYGON_STAKEMANAGER)).setCurrentEpoch(newEpoch); + metadata = fixture.unlocks.getMetadata(tokenId); + assertEq(metadata.progress, 50, "metadata progress incorrect"); + + newEpoch = POLYGON_STAKEMANAGER.epoch() + WITHDRAW_DELAY; + vm.prank(GOVERNANCE); + IPolygonStakeManagerTest(address(POLYGON_STAKEMANAGER)).setCurrentEpoch(newEpoch); + + uint256 polBalBefore = POL.balanceOf(address(this)); + vm.expectEmit(); + emit Withdraw(address(this), unstakeAmount, unlockID); + uint256 withdrawn = tenderizer.withdraw(address(this), unlockID); + assertEq(withdrawn, unstakeAmount, "withdrawn incorrect"); + assertEq(POL.balanceOf(address(this)), polBalBefore + unstakeAmount, "balance incorrect"); + assertEq(tenderizer.totalSupply(), depositAmount - unstakeAmount, "total supply incorrect"); + vm.expectRevert("NOT_MINTED"); + fixture.unlocks.ownerOf(tokenId); + } + + // TODO: test slash while undelegating + + // TODO: make fuzz test + function test_rebase() public { + uint256 rewardAmount = 100_000 ether; + + address HOLDER_1 = makeAddr("HOLDER_1"); + address HOLDER_2 = makeAddr("HOLDER_2"); + uint256 HOLDER_1_DEPOSIT = 25_000 ether; + uint256 HOLDER_2_DEPOSIT = 12_500 ether; + POL.transfer(HOLDER_1, HOLDER_1_DEPOSIT); + POL.transfer(HOLDER_2, HOLDER_2_DEPOSIT); + + Tenderizer tenderizer = Tenderizer(payable(fixture.factory.newTenderizer(address(POL), VALIDATOR_1))); + vm.startPrank(HOLDER_1); + POL.approve(address(tenderizer), HOLDER_1_DEPOSIT); + uint256 tTokenOut_1 = tenderizer.deposit(HOLDER_1, HOLDER_1_DEPOSIT); + vm.stopPrank(); + + vm.startPrank(HOLDER_2); + POL.approve(address(tenderizer), HOLDER_2_DEPOSIT); + uint256 tTokenOut_2 = tenderizer.deposit(HOLDER_2, HOLDER_2_DEPOSIT); + vm.stopPrank(); + IPolygonValidatorShares valShares = _getValidatorSharesContract(_getValidatorId(VALIDATOR_1)); + + uint256 tenderizerValShares = valShares.balanceOf(address(tenderizer)); + uint256 initialRewardPerShare = IPolygonValidatorSharesTest(address(valShares)).initalRewardPerShare(address(tenderizer)); + uint256 rewardPerShare = setRewards(rewardAmount, initialRewardPerShare); + // Due to logic in the Polygon contracts the actual reward amount will be rewardAmount -1 + // uint256 tenderizerRewardAfterFee = rewardsForTenderizer - rewardsForTenderizer * 5e3 / 1e6; + uint256 tenderizerRewards = tenderizerValShares * (rewardPerShare - initialRewardPerShare) / REWARD_PRECISION; + vm.expectEmit(); + emit Rebase(tTokenOut_1 + tTokenOut_2, tTokenOut_1 + tTokenOut_2 + tenderizerRewards); + tenderizer.rebase(); + + assertEq( + tenderizer.totalSupply(), + valShares.balanceOf(address(tenderizer)) * valShares.exchangeRate() / EXCHANGE_RATE_PRECISION_HIGH, + "total supply incorrect vs total staked incorrect" + ); + assertEq(tenderizer.totalSupply(), tTokenOut_1 + tTokenOut_2 + tenderizerRewards, "total supply incorrect"); + assertEq( + tenderizer.balanceOf(HOLDER_1), + tTokenOut_1 + tenderizerRewards * tTokenOut_1 / (tTokenOut_1 + tTokenOut_2), + "balance 1 incorrect" + ); + assertEq( + tenderizer.balanceOf(HOLDER_2), + tTokenOut_2 + tenderizerRewards * tTokenOut_2 / (tTokenOut_1 + tTokenOut_2), + "balance 2 incorrect" + ); + } +}