From 520bd852b57175ee059414046045ec23a3289c6a Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Thu, 7 Dec 2023 03:30:36 +0100 Subject: [PATCH 01/12] feat: Livepeer fork integration test --- foundry.toml | 1 + src/adapters/LivepeerAdapter.sol | 59 ++-- src/adapters/interfaces/ILivepeer.sol | 2 + src/adapters/interfaces/IUniswap_Quoter.sol | 54 ++++ src/tenderizer/Tenderizer.sol | 2 + src/unlocks/Unlocks.sol | 12 +- src/utils/ERC721Receiver.sol | 11 + test/factory/Factory.t.sol | 2 +- test/fork-tests/Fixture.sol | 62 ++++ test/fork-tests/Livepeer.arbitrum.t.sol | 332 ++++++++++++++++++++ test/tenderizer/Tenderizer.t.sol | 3 +- 11 files changed, 506 insertions(+), 34 deletions(-) create mode 100644 src/adapters/interfaces/IUniswap_Quoter.sol create mode 100644 src/utils/ERC721Receiver.sol create mode 100644 test/fork-tests/Fixture.sol create mode 100644 test/fork-tests/Livepeer.arbitrum.t.sol diff --git a/foundry.toml b/foundry.toml index 6aa14af..7b7485d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -32,3 +32,4 @@ depth = 100 [rpc_endpoints] # Uncomment to enable the RPC server arbitrum_goerli = "${ARBITRUM_GOERLI_RPC}" +arbitrum = "${ARBITRUM_RPC}" diff --git a/src/adapters/LivepeerAdapter.sol b/src/adapters/LivepeerAdapter.sol index db7f5e4..3199282 100644 --- a/src/adapters/LivepeerAdapter.sol +++ b/src/adapters/LivepeerAdapter.sol @@ -21,6 +21,16 @@ import { IWETH9 } from "core/adapters/interfaces/IWETH9.sol"; import { IERC165 } from "core/interfaces/IERC165.sol"; import { TWAP } from "core/utils/TWAP.sol"; +ILivepeerBondingManager constant LIVEPEER_BONDING = ILivepeerBondingManager(0x35Bcf3c30594191d53231E4FF333E8A770453e40); +ILivepeerRoundsManager constant LIVEPEER_ROUNDS = ILivepeerRoundsManager(0xdd6f56DcC28D3F5f27084381fE8Df634985cc39f); +ERC20 constant LPT = ERC20(0x289ba1701C2F088cf0faf8B3705246331cB8A839); +IWETH9 constant WETH = IWETH9(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1); +ISwapRouter constant UNISWAP_ROUTER = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); +address constant UNI_POOL = 0x4fD47e5102DFBF95541F64ED6FE13d4eD26D2546; +uint24 constant UNISWAP_POOL_FEE = 3000; +uint256 constant ETH_THRESHOLD = 1e16; // 0.01 ETH +uint32 constant TWAP_INTERVAL = 36_000; + contract LivepeerAdapter is Adapter { using SafeTransferLib for ERC20; @@ -39,16 +49,6 @@ contract LivepeerAdapter is Adapter { } } - ILivepeerBondingManager private constant LIVEPEER = ILivepeerBondingManager(0x35Bcf3c30594191d53231E4FF333E8A770453e40); - ILivepeerRoundsManager private constant LIVEPEER_ROUNDS = ILivepeerRoundsManager(0xdd6f56DcC28D3F5f27084381fE8Df634985cc39f); - ERC20 private constant LPT = ERC20(0x289ba1701C2F088cf0faf8B3705246331cB8A839); - IWETH9 private constant WETH = IWETH9(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1); - ISwapRouter private constant UNISWAP_ROUTER = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); - address private constant UNI_POOL = 0x4fD47e5102DFBF95541F64ED6FE13d4eD26D2546; - uint24 private constant UNISWAP_POOL_FEE = 10_000; - uint256 private constant ETH_THRESHOLD = 1e16; // 0.01 ETH - uint32 private constant TWAP_INTERVAL = 36_000; - function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { return interfaceId == type(Adapter).interfaceId || interfaceId == type(IERC165).interfaceId; } @@ -58,7 +58,7 @@ contract LivepeerAdapter is Adapter { } function previewWithdraw(uint256 unlockID) external view returns (uint256 amount) { - (amount,) = LIVEPEER.getDelegatorUnbondingLock(address(this), unlockID); + (amount,) = LIVEPEER_BONDING.getDelegatorUnbondingLock(address(this), unlockID); } function unlockMaturity(uint256 unlockID) external view returns (uint256 maturity) { @@ -67,20 +67,20 @@ contract LivepeerAdapter is Adapter { // roundLength = n // currentRound = r // withdrawRound = w - // blockRemainingInCurrentRound = b = roundLength - (block.number - currentRoundStartBlock) + // blocksRemainingInCurrentRound = b = roundLength - (block.number - currentRoundStartBlock) // maturity = n*(w - r - 1) + b - (, uint256 withdrawRound) = LIVEPEER.getDelegatorUnbondingLock(address(this), unlockID); + (, uint256 withdrawRound) = LIVEPEER_BONDING.getDelegatorUnbondingLock(address(this), unlockID); uint256 currentRound = LIVEPEER_ROUNDS.currentRound(); uint256 roundLength = LIVEPEER_ROUNDS.roundLength(); uint256 currentRoundStartBlock = LIVEPEER_ROUNDS.currentRoundStartBlock(); - uint256 blockRemainingInCurrentRound = roundLength - (block.number - currentRoundStartBlock); + uint256 blocksRemainingInCurrentRound = roundLength - (block.number - currentRoundStartBlock); if (withdrawRound > currentRound) { - maturity = roundLength * (withdrawRound - currentRound - 1) + blockRemainingInCurrentRound; + maturity = block.number + roundLength * (withdrawRound - currentRound - 1) + blocksRemainingInCurrentRound; } } function unlockTime() external view override returns (uint256) { - return LIVEPEER_ROUNDS.roundLength() * LIVEPEER.unbondingPeriod(); + return LIVEPEER_ROUNDS.roundLength() * LIVEPEER_BONDING.unbondingPeriod(); } function currentTime() external view override returns (uint256) { @@ -88,27 +88,27 @@ contract LivepeerAdapter is Adapter { } function stake(address validator, uint256 amount) public { - LPT.approve(address(LIVEPEER), amount); - LIVEPEER.bond(amount, validator); + LPT.approve(address(LIVEPEER_BONDING), amount); + LIVEPEER_BONDING.bond(amount, validator); } function unstake(address, /*validator*/ uint256 amount) external returns (uint256 unlockID) { // returns the *next* Livepeer unbonding lock ID for the delegator // this will be the `unlockID` after calling unbond - (,,,,,, unlockID) = LIVEPEER.getDelegator(address(this)); - LIVEPEER.unbond(amount); + (,,,,,, unlockID) = LIVEPEER_BONDING.getDelegator(address(this)); + LIVEPEER_BONDING.unbond(amount); } function withdraw(address, /*validator*/ uint256 unlockID) external returns (uint256 amount) { - (amount,) = LIVEPEER.getDelegatorUnbondingLock(address(this), unlockID); - LIVEPEER.withdrawStake(unlockID); + (amount,) = LIVEPEER_BONDING.getDelegatorUnbondingLock(address(this), unlockID); + LIVEPEER_BONDING.withdrawStake(unlockID); } function rebase(address validator, uint256 currentStake) external returns (uint256 newStake) { uint256 currentRound = LIVEPEER_ROUNDS.currentRound(); Storage storage $ = _loadStorage(); - if ($.lastRebaseRound < currentRound) { + if ($.lastRebaseRound == currentRound) { return currentStake; } @@ -123,28 +123,31 @@ contract LivepeerAdapter is Adapter { } // Read new stake - newStake = LIVEPEER.pendingStake(address(this), 0); + newStake = LIVEPEER_BONDING.pendingStake(address(this), 0); } function isValidator(address validator) public view override returns (bool) { - return LIVEPEER.isRegisteredTranscoder(validator); + return LIVEPEER_BONDING.isRegisteredTranscoder(validator); } /// @notice function for swapping ETH fees to LPT function _livepeerClaimFees() internal { // get pending fees uint256 pendingFees; - if ((pendingFees = LIVEPEER.pendingFees(address(this), 0)) < ETH_THRESHOLD) return; + if ((pendingFees = LIVEPEER_BONDING.pendingFees(address(this), 0)) < ETH_THRESHOLD) return; + + if (!LIVEPEER_ROUNDS.currentRoundInitialized()) return; // withdraw fees - LIVEPEER.withdrawFees(payable(address(this)), pendingFees); + LIVEPEER_BONDING.withdrawFees(payable(address(this)), pendingFees); // get ETH balance uint256 ethBalance = address(this).balance; // convert fees to WETH WETH.deposit{ value: ethBalance }(); ERC20(address(WETH)).safeApprove(address(UNISWAP_ROUTER), ethBalance); // Calculate Slippage Threshold - uint256 twapPrice = TWAP.getInversePriceX96(TWAP.getPriceX96(TWAP.getSqrtTwapX96(UNI_POOL, TWAP_INTERVAL))); + uint160 sqrtPriceLimitX96 = TWAP.getSqrtTwapX96(UNI_POOL, TWAP_INTERVAL); + uint256 twapPrice = TWAP.getInversePriceX96(TWAP.getPriceX96(sqrtPriceLimitX96)); uint256 amountOut = ethBalance * twapPrice >> 96; // Create initial params for swap ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ diff --git a/src/adapters/interfaces/ILivepeer.sol b/src/adapters/interfaces/ILivepeer.sol index 06ea70a..901eb62 100644 --- a/src/adapters/interfaces/ILivepeer.sol +++ b/src/adapters/interfaces/ILivepeer.sol @@ -53,6 +53,8 @@ interface ILivepeerBondingManager { interface ILivepeerRoundsManager { function currentRound() external view returns (uint256); + function currentRoundInitialized() external view returns (bool); + function currentRoundStartBlock() external view returns (uint256); function roundLength() external view returns (uint256); diff --git a/src/adapters/interfaces/IUniswap_Quoter.sol b/src/adapters/interfaces/IUniswap_Quoter.sol new file mode 100644 index 0000000..b695b09 --- /dev/null +++ b/src/adapters/interfaces/IUniswap_Quoter.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.8.19; + +/// @title Quoter Interface +/// @notice Supports quoting the calculated amounts from exact input or exact output swaps +/// @dev These functions are not marked view because they rely on calling non-view functions and reverting +/// to compute the result. They are also not gas efficient and should not be called on-chain. +interface IQuoter { + /// @notice Returns the amount out received for a given exact input swap without executing the swap + /// @param path The path of the swap, i.e. each token pair and the pool fee + /// @param amountIn The amount of the first token to swap + /// @return amountOut The amount of the last token that would be received + function quoteExactInput(bytes memory path, uint256 amountIn) external returns (uint256 amountOut); + + /// @notice Returns the amount out received for a given exact input but for a swap of a single pool + /// @param tokenIn The token being swapped in + /// @param tokenOut The token being swapped out + /// @param fee The fee of the token pool to consider for the pair + /// @param amountIn The desired input amount + /// @param sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap + /// @return amountOut The amount of `tokenOut` that would be received + function quoteExactInputSingle( + address tokenIn, + address tokenOut, + uint24 fee, + uint256 amountIn, + uint160 sqrtPriceLimitX96 + ) + external + returns (uint256 amountOut); + + /// @notice Returns the amount in required for a given exact output swap without executing the swap + /// @param path The path of the swap, i.e. each token pair and the pool fee + /// @param amountOut The amount of the last token to receive + /// @return amountIn The amount of first token required to be paid + function quoteExactOutput(bytes memory path, uint256 amountOut) external returns (uint256 amountIn); + + /// @notice Returns the amount in required to receive the given exact output amount but for a swap of a single pool + /// @param tokenIn The token being swapped in + /// @param tokenOut The token being swapped out + /// @param fee The fee of the token pool to consider for the pair + /// @param amountOut The desired output amount + /// @param sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap + /// @return amountIn The amount required as the input for the swap in order to receive `amountOut` + function quoteExactOutputSingle( + address tokenIn, + address tokenOut, + uint24 fee, + uint256 amountOut, + uint160 sqrtPriceLimitX96 + ) + external + returns (uint256 amountIn); +} diff --git a/src/tenderizer/Tenderizer.sol b/src/tenderizer/Tenderizer.sol index 2163ea6..eaaa469 100644 --- a/src/tenderizer/Tenderizer.sol +++ b/src/tenderizer/Tenderizer.sol @@ -43,6 +43,8 @@ contract Tenderizer is TenderizerImmutableArgs, TenderizerEvents, TToken, Multic // solhint-disable-next-line no-empty-blocks constructor(address _registry, address _unlocks) TenderizerImmutableArgs(_registry, _unlocks) { } + receive() external payable { } + fallback() external payable { } // @inheritdoc TToken function name() external view override returns (string memory) { diff --git a/src/unlocks/Unlocks.sol b/src/unlocks/Unlocks.sol index c2fa9ab..77641b5 100644 --- a/src/unlocks/Unlocks.sol +++ b/src/unlocks/Unlocks.sol @@ -17,6 +17,8 @@ import { Tenderizer } from "core/tenderizer/Tenderizer.sol"; import { Registry } from "core/registry/Registry.sol"; import { Renderer } from "core/unlocks/Renderer.sol"; +import { FixedPointMathLib } from "solmate/utils/FixedPointMathLib.sol"; + pragma solidity >=0.8.19; // solhint-disable quotes @@ -103,7 +105,7 @@ contract Unlocks is ERC721 { * @return metadata of the unlock token */ function getMetadata(uint256 tokenId) external view returns (Metadata memory metadata) { - (address tenderizer, uint256 unlockId) = _decodeTokenId(tokenId); + (address payable tenderizer, uint256 unlockId) = _decodeTokenId(tokenId); address asset = Tenderizer(tenderizer).asset(); Adapter adapter = Tenderizer(tenderizer).adapter(); @@ -113,7 +115,9 @@ contract Unlocks is ERC721 { return Metadata({ amount: Tenderizer(tenderizer).previewWithdraw(unlockId), maturity: maturity, - progress: maturity > currentTime ? 100 - (maturity - currentTime) * 100 / adapter.unlockTime() : 100, + progress: maturity > currentTime + ? 100 - FixedPointMathLib.mulDivUp((maturity - currentTime), 100, adapter.unlockTime()) + : 100, unlockId: unlockId, symbol: ERC20(asset).symbol(), name: ERC20(asset).name(), @@ -129,8 +133,8 @@ contract Unlocks is ERC721 { return uint256(bytes32(abi.encodePacked(tenderizer, unlockId))); } - function _decodeTokenId(uint256 tokenId) internal pure virtual returns (address tenderizer, uint96 unlockId) { + function _decodeTokenId(uint256 tokenId) internal pure virtual returns (address payable tenderizer, uint96 unlockId) { bytes32 a = bytes32(tokenId); - (tenderizer, unlockId) = (address(bytes20(a)), uint96(bytes12(a << 160))); + (tenderizer, unlockId) = (payable(address(bytes20(a))), uint96(bytes12(a << 160))); } } diff --git a/src/utils/ERC721Receiver.sol b/src/utils/ERC721Receiver.sol new file mode 100644 index 0000000..9d09d50 --- /dev/null +++ b/src/utils/ERC721Receiver.sol @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2021 Tenderize + +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.19; + +abstract contract ERC721Receiver { + function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { + return ERC721Receiver.onERC721Received.selector; + } +} diff --git a/test/factory/Factory.t.sol b/test/factory/Factory.t.sol index 3308538..905e5ad 100644 --- a/test/factory/Factory.t.sol +++ b/test/factory/Factory.t.sol @@ -47,7 +47,7 @@ contract FactoryTest is Test { vm.mockCall(registry, abi.encodeCall(Registry.registerTenderizer, (asset, validator, tenderizer)), ""); vm.expectCall(registry, abi.encodeCall(Registry.adapter, (asset))); - address newTenderizer = factory.newTenderizer(asset, validator); + address payable newTenderizer = payable(factory.newTenderizer(asset, validator)); assertEq(newTenderizer, 0xffD4505B3452Dc22f8473616d50503bA9E1710Ac, "tenderizer not created with correct address"); assertEq(Tenderizer(newTenderizer).asset(), asset, "asset not set"); assertEq(Tenderizer(newTenderizer).validator(), validator, "validator not set"); diff --git a/test/fork-tests/Fixture.sol b/test/fork-tests/Fixture.sol new file mode 100644 index 0000000..5b20408 --- /dev/null +++ b/test/fork-tests/Fixture.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +// +// _____ _ _ +// |_ _| | | (_) +// | | ___ _ __ __| | ___ _ __ _ _______ +// | |/ _ \ '_ \ / _` |/ _ \ '__| |_ / _ \ +// | | __/ | | | (_| | __/ | | |/ / __/ +// \_/\___|_| |_|\__,_|\___|_| |_/___\___| +// +// Copyright (c) Tenderize Labs Ltd + +// solhint-disable no-console + +pragma solidity >=0.8.19; + +import { ERC1967Proxy } from "openzeppelin-contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import { Tenderizer } from "core/tenderizer/Tenderizer.sol"; +import { Registry } from "core/registry/Registry.sol"; +import { FACTORY_ROLE } from "core/registry/Roles.sol"; +import { Renderer } from "core/unlocks/Renderer.sol"; +import { Unlocks } from "core/unlocks/Unlocks.sol"; +import { Factory } from "core/factory/Factory.sol"; + +struct TenderizerFixture { + Registry registry_impl; + Registry registry; + Renderer renderer_impl; + Renderer renderer; + Unlocks unlocks; + Tenderizer tenderizer; + Factory factory; +} + +function tenderizerFixture() returns (TenderizerFixture memory) { + bytes32 salt = bytes32(uint256(1)); + + Registry registry = new Registry{salt: salt}(); + address registryProxy = address(new ERC1967Proxy{salt: salt}(address(registry), "")); + + Renderer renderer = new Renderer{salt: salt}(); + ERC1967Proxy rendererProxy = new ERC1967Proxy{salt: salt}(address(renderer), abi.encodeCall(renderer.initialize, ())); + Unlocks unlocks = new Unlocks{salt: salt}(address(registryProxy), address(rendererProxy)); + + Tenderizer tenderizer = new Tenderizer{salt: salt}(registryProxy, address(unlocks)); + + Registry(registryProxy).initialize(address(tenderizer), address(unlocks)); + + Factory factory = new Factory{salt: salt}(address(registryProxy)); + + Registry(registryProxy).grantRole(FACTORY_ROLE, address(factory)); + + return TenderizerFixture({ + registry_impl: registry, + registry: Registry(registryProxy), + renderer_impl: renderer, + renderer: Renderer(address(rendererProxy)), + unlocks: unlocks, + tenderizer: tenderizer, + factory: factory + }); +} diff --git a/test/fork-tests/Livepeer.arbitrum.t.sol b/test/fork-tests/Livepeer.arbitrum.t.sol new file mode 100644 index 0000000..511bd4f --- /dev/null +++ b/test/fork-tests/Livepeer.arbitrum.t.sol @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: MIT +// +// _____ _ _ +// |_ _| | | (_) +// | | ___ _ __ __| | ___ _ __ _ _______ +// | |/ _ \ '_ \ / _` |/ _ \ '__| |_ / _ \ +// | | __/ | | | (_| | __/ | | |/ / __/ +// \_/\___|_| |_|\__,_|\___|_| |_/___\___| +// +// Copyright (c) Tenderize Labs Ltd + +pragma solidity >=0.8.19; + +import { Test, console } from "forge-std/Test.sol"; +import { MockERC20 } from "solmate/test/utils/mocks/MockERC20.sol"; + +import { Factory } from "core/factory/Factory.sol"; +import { + LivepeerAdapter, + LPT, + LIVEPEER_BONDING, + LIVEPEER_ROUNDS, + UNI_POOL, + TWAP_INTERVAL, + WETH +} from "core/adapters/LivepeerAdapter.sol"; +import { Tenderizer, TenderizerEvents } from "core/tenderizer/Tenderizer.sol"; +import { Unlocks, Metadata } from "core/unlocks/Unlocks.sol"; +import { TWAP } from "core/utils/TWAP.sol"; +import { ILivepeerBondingManager, ILivepeerRoundsManager } from "core/adapters/interfaces/ILivepeer.sol"; +import { ERC721Receiver } from "core/utils/ERC721Receiver.sol"; +import { IQuoter } from "core/adapters/interfaces/IUniswap_Quoter.sol"; +import { TenderizerFixture, tenderizerFixture } from "./Fixture.sol"; + +ILivepeerBonding constant BONDING = ILivepeerBonding(address(LIVEPEER_BONDING)); +ILivepeerRounds constant ROUNDS = ILivepeerRounds(address(LIVEPEER_ROUNDS)); +address constant MINTER = 0xc20DE37170B45774e6CD3d2304017fc962f27252; +address constant TICKET_BROKER = 0xa8bB618B1520E284046F3dFc448851A1Ff26e41B; + +address constant UNI_QUOTER = 0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6; + +address constant ORCHESTRATOR_1 = 0xf4e8Ef0763BCB2B1aF693F5970a00050a6aC7E1B; + +uint256 constant roundLength = 6377; // round length in blocks +uint256 constant unlockRounds = 7; // unlock time in rounds + +interface IMinter { + function depositETH() external payable returns (bool); + function currentMintableTokens() external view returns (uint256); +} + +interface ILivepeerBonding is ILivepeerBondingManager { + function updateTranscoderWithFees(address transcoder, uint256 amount, uint256 round) external; + + function getTranscoder(address _transcoder) + external + view + returns ( + uint256 lastRewardRound, + uint256 rewardCut, + uint256 feeShare, + uint256 lastActiveStakeUpdateRound, + uint256 activationRound, + uint256 deactivationRound, + uint256 activeCumulativeRewards, + uint256 cumulativeRewards, + uint256 cumulativeFees, + uint256 lastFeeRound + ); + + function transcoderTotalStake(address _transcoder) external view returns (uint256); + + function currentRoundTotalActiveStake() external view returns (uint256); + + function reward() external; + + function getTranscoderEarningsPoolForRound( + address _transcoder, + uint256 _round + ) + external + view + returns (uint256, uint256, uint256, uint256, uint256); +} + +interface ILivepeerRounds is ILivepeerRoundsManager { + function initializeRound() external; +} + +contract LivepeerForkTest is Test, TenderizerEvents, ERC721Receiver { + TenderizerFixture fixture; + LivepeerAdapter adapter; + + event NewTenderizer(address indexed asset, address indexed validator, address tenderizer); + + function mintLPT(address account, uint256 amount) public { + vm.prank(MINTER); + MockERC20(address(LPT)).mint(account, amount); + } + + function setUp() public { + bytes32 salt = bytes32(uint256(1)); + vm.createSelectFork(vm.envString("ARBITRUM_RPC")); + fixture = tenderizerFixture(); + adapter = new LivepeerAdapter{salt: salt}(); + fixture.registry.registerAdapter(address(LPT), address(adapter)); + } + + function test_registry_AdapterRegistered() public { + assertEq(fixture.registry.adapter(address(LPT)), address(adapter), "adapter not registered"); + } + + function test_adapter_unlockTime() public { + assertEq(adapter.unlockTime(), roundLength * unlockRounds, "unlock time incorrect"); + } + + function test_adapter_currentTime() public { + assertEq(adapter.currentTime(), block.number, "current time incorrect"); + } + + function test_adapter_isValidator() public { + assertTrue(adapter.isValidator(ORCHESTRATOR_1), "isValidator true incorrect"); + assertFalse(adapter.isValidator(makeAddr("NOT_ORCHESTRATOR")), "isValidator false incorrect"); + } + + function test_adapter_previewDeposit() public { + assertEq(adapter.previewDeposit(10 ether), 10 ether, "previewDeposit incorrect"); + } + + 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(LPT), inactiveOrchestrator); + + // Deploy tenderizer + vm.expectEmit({ checkTopic1: true, checkTopic2: true, checkTopic3: false, checkData: false }); + emit NewTenderizer(address(LPT), ORCHESTRATOR_1, address(0x0)); + fixture.factory.newTenderizer(address(LPT), ORCHESTRATOR_1); + } + + function test_deposit() public { + uint256 depositAmount = 10 ether; + + // Deploy tenderizer + Tenderizer tenderizer = Tenderizer(payable(fixture.factory.newTenderizer(address(LPT), ORCHESTRATOR_1))); + + uint256 currentStake = BONDING.pendingStake(address(this), type(uint256).max); + + // Deposit + mintLPT(address(this), depositAmount); + LPT.approve(address(tenderizer), depositAmount); + + vm.expectEmit({ checkTopic1: true, checkTopic2: true, checkTopic3: false, checkData: true }); + emit Deposit(address(this), address(this), depositAmount, depositAmount); + tenderizer.deposit(address(this), depositAmount); + + (uint256 bondedAmount,,,,,,) = BONDING.getDelegator(address(tenderizer)); + + assertEq(tenderizer.totalSupply(), depositAmount, "total supply"); + assertEq(tenderizer.balanceOf(address(this)), depositAmount, "balance of"); + assertEq(bondedAmount, currentStake + depositAmount, "Bonded amount"); + assertEq(BONDING.pendingStake(address(tenderizer), type(uint256).max), currentStake + depositAmount, "pending stake"); + } + + function test_unstake_withdraw() public { + uint256 depositAmount = 10 ether; + uint256 unstakeAmount = 5 ether; + Tenderizer tenderizer = Tenderizer(payable(fixture.factory.newTenderizer(address(LPT), ORCHESTRATOR_1))); + mintLPT(address(this), depositAmount); + LPT.approve(address(tenderizer), depositAmount); + tenderizer.deposit(address(this), depositAmount); + + // Livepeer only allows unbonding from when the next round starts + // That's when added stake becomes active + // So we have to roll `roundLength` and initialize the round + // (roll to exact round start) + uint256 currentRoundStartBlock = ROUNDS.currentRoundStartBlock(); + vm.roll(currentRoundStartBlock + roundLength); + ROUNDS.initializeRound(); + + vm.expectEmit(); + emit Unlock(address(this), unstakeAmount, 0); + uint256 unlockID = tenderizer.unlock(unstakeAmount); + (uint256 bondedAmount,,,,,,) = BONDING.getDelegator(address(tenderizer)); + assertEq(bondedAmount, depositAmount - unstakeAmount, "Bonded amount"); + assertEq(BONDING.pendingStake(address(tenderizer), type(uint256).max), depositAmount - unstakeAmount, "pending stake"); + + { + (uint256 amount,) = BONDING.getDelegatorUnbondingLock(address(tenderizer), unlockID); + assertEq(amount, unstakeAmount, "unstake amount"); + } + + assertEq(unlockID, 0, "unlock ID"); + currentRoundStartBlock = ROUNDS.currentRoundStartBlock(); + uint256 blocksRemainingInCurrentRound = roundLength - (block.number - currentRoundStartBlock); + + assertEq( + tenderizer.unlockMaturity(unlockID), + block.number + roundLength * (unlockRounds - 1) + blocksRemainingInCurrentRound, + "unlock maturity" + ); + + uint256 tokenId = uint256(bytes32(abi.encodePacked(address(tenderizer), unlockID))); + Metadata memory metadata = fixture.unlocks.getMetadata(tokenId); + + assertEq(metadata.amount, unstakeAmount, "metadata amount"); + assertEq(metadata.progress, 0, "metadata progress"); + assertEq( + metadata.maturity, block.number + roundLength * (unlockRounds - 1) + blocksRemainingInCurrentRound, "metadata maturity" + ); + assertEq(metadata.unlockId, unlockID, "metadata unlock ID"); + assertEq(metadata.validator, ORCHESTRATOR_1, "metadata validator"); + + // Roll to 50% unlock progress + vm.roll(currentRoundStartBlock + roundLength * (unlockRounds / 2) + roundLength / 2); + ROUNDS.initializeRound(); + metadata = fixture.unlocks.getMetadata(tokenId); + // rounding error + assertEq(metadata.progress, 49, "metadata progress 50%"); + + // Roll to 100% progress and withdraw + uint256 lptBalBeforeWithdraw = LPT.balanceOf(address(this)); + vm.roll(block.number + unlockRounds * roundLength); + ROUNDS.initializeRound(); + + vm.expectEmit(); + emit Withdraw(address(this), unstakeAmount, unlockID); + uint256 withdrawn = tenderizer.withdraw(address(this), unlockID); + + assertEq(withdrawn, unstakeAmount); + // Check Livepeer's unbonding lock is deleted + { + (uint256 amount, uint256 withdrawRound) = BONDING.getDelegatorUnbondingLock(address(tenderizer), unlockID); + assertEq(amount, 0, "unstake amount zero"); + assertEq(withdrawRound, 0, "withdraw round zero"); + } + // Check Tenderize Unlock is deleted + vm.expectRevert("NOT_MINTED"); + fixture.unlocks.ownerOf(tokenId); + // Check LPT balance + assertEq(LPT.balanceOf(address(this)), lptBalBeforeWithdraw + unstakeAmount, "LPT balance"); + } + + function test_rebase() public { + uint256 depositAmount = 100_000 ether; + Tenderizer tenderizer = Tenderizer(payable(fixture.factory.newTenderizer(address(LPT), ORCHESTRATOR_1))); + mintLPT(address(this), depositAmount); + LPT.approve(address(tenderizer), depositAmount); + tenderizer.deposit(address(this), depositAmount); + + // Initialize next round to make stake active + uint256 currentRoundStartBlock = ROUNDS.currentRoundStartBlock(); + vm.roll(currentRoundStartBlock + roundLength); + ROUNDS.initializeRound(); + + // Add fees - check eth fees only rebase + uint256 fees = 0.1 ether + 1; + updateTenderizerFees(address(tenderizer), fees); + // account for rounding error of 1 wei + fees = BONDING.pendingFees(address(tenderizer), 0); + assertEq(fees, 0.1 ether, "pending fees"); + uint256 quotedOut = IQuoter(UNI_QUOTER).quoteExactInputSingle(address(WETH), address(LPT), 3000, fees, 0); + // vm.expectEmit(); + // emit Rebase(100_000 ether, 100_000 ether + quotedOut); + currentRoundStartBlock = ROUNDS.currentRoundStartBlock(); + vm.roll(currentRoundStartBlock + roundLength); + ROUNDS.initializeRound(); + Tenderizer(tenderizer).rebase(); + assertEq(tenderizer.totalSupply(), depositAmount + quotedOut, "total supply"); + (uint256 bondedAmount,,,,,,) = BONDING.getDelegator(address(tenderizer)); + assertEq(bondedAmount, depositAmount + quotedOut, "Bonded amount"); + + // Initialize next round + // Check LPT rewards only rebase + currentRoundStartBlock = ROUNDS.currentRoundStartBlock(); + vm.roll(currentRoundStartBlock + roundLength); + ROUNDS.initializeRound(); + + (uint256 lastRewardRound,,,,,,,,,) = BONDING.getTranscoder(ORCHESTRATOR_1); + (,,, uint256 crfBefore,) = BONDING.getTranscoderEarningsPoolForRound(ORCHESTRATOR_1, lastRewardRound); + + // call reward + vm.prank(ORCHESTRATOR_1); + BONDING.reward(); + Tenderizer(tenderizer).rebase(); + uint256 round = ROUNDS.currentRound(); + (,,, uint256 crfAfter,) = BONDING.getTranscoderEarningsPoolForRound(ORCHESTRATOR_1, round); + uint256 expStake = bondedAmount * crfAfter / crfBefore; + assertEq(tenderizer.totalSupply(), expStake, "total supply"); + + // Initialize next round + currentRoundStartBlock = ROUNDS.currentRoundStartBlock(); + vm.roll(currentRoundStartBlock + roundLength); + ROUNDS.initializeRound(); + crfBefore = crfAfter; + + // Add Eth Fees + fees = 0.1 ether + 1; + updateTenderizerFees(address(tenderizer), fees); + fees = BONDING.pendingFees(address(tenderizer), 0); + quotedOut = IQuoter(UNI_QUOTER).quoteExactInputSingle(address(WETH), address(LPT), 3000, fees, 0); + + // Call reward + vm.prank(ORCHESTRATOR_1); + BONDING.reward(); + round = ROUNDS.currentRound(); + (,,, crfAfter,) = BONDING.getTranscoderEarningsPoolForRound(ORCHESTRATOR_1, round); + expStake = expStake * crfAfter / crfBefore + quotedOut; + Tenderizer(tenderizer).rebase(); + assertEq(tenderizer.totalSupply(), expStake, "total supply"); + } + + function updateTenderizerFees(address tenderizer, uint256 amount) internal { + address orchestrator = Tenderizer(payable(tenderizer)).validator(); + uint256 round = ROUNDS.currentRound(); + + // get tenderizer stake share of delegation pool + // and orchestrator's fee cut + (,, uint256 feeShare,,,,,,,) = BONDING.getTranscoder(orchestrator); + uint256 orchStake = BONDING.transcoderTotalStake(orchestrator); + uint256 tenderizerStake = BONDING.pendingStake(tenderizer, round); + + // amount = fees * feeShare / 1e6 * tenderizerStake / orchStake + // fees = amount * orchStake *1e6 / feeShare / tenderizerStake + uint256 fees = amount * orchStake * 1e6 / feeShare / tenderizerStake; + vm.prank(TICKET_BROKER); + BONDING.updateTranscoderWithFees(orchestrator, fees, round); + vm.prank(MINTER); + IMinter(MINTER).depositETH{ value: fees }(); + } +} diff --git a/test/tenderizer/Tenderizer.t.sol b/test/tenderizer/Tenderizer.t.sol index 5001232..1eaad7e 100644 --- a/test/tenderizer/Tenderizer.t.sol +++ b/test/tenderizer/Tenderizer.t.sol @@ -58,7 +58,8 @@ contract TenderizerSetup is Test, TestHelpers { vm.mockCall(registry, abi.encodeCall(Registry.treasury, ()), abi.encode(treasury)); vm.mockCall(asset, abi.encodeCall(IERC20Metadata.symbol, ()), abi.encode(symbol)); - tenderizer = TenderizerHarness(address(new TenderizerHarness(registry, unlocks)).clone(abi.encodePacked(asset, validator))); + tenderizer = + TenderizerHarness(payable(address(new TenderizerHarness(registry, unlocks)).clone(abi.encodePacked(asset, validator)))); } } From 348e693ac007566e4a5f122293b202b466be5e87 Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Fri, 8 Dec 2023 13:03:57 +0100 Subject: [PATCH 02/12] feat: GraphAdapter code improvements --- src/adapters/GraphAdapter.sol | 42 +++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/adapters/GraphAdapter.sol b/src/adapters/GraphAdapter.sol index 5055b59..61b9eeb 100644 --- a/src/adapters/GraphAdapter.sol +++ b/src/adapters/GraphAdapter.sol @@ -17,15 +17,15 @@ import { Adapter } from "core/adapters/Adapter.sol"; import { IGraphStaking, IEpochManager } from "core/adapters/interfaces/IGraph.sol"; import { IERC165 } from "core/interfaces/IERC165.sol"; +IEpochManager constant GRAPH_EPOCHS = IEpochManager(0x5A843145c43d328B9bB7a4401d94918f131bB281); +IGraphStaking constant GRAPH_STAKING = IGraphStaking(0x00669A4CF01450B64E8A2A20E9b1FCB71E61eF03); +ERC20 constant GRT = ERC20(0x9623063377AD1B27544C965cCd7342f7EA7e88C7); +uint256 constant MAX_PPM = 1e6; + contract GraphAdapter is Adapter { using SafeTransferLib for ERC20; - IGraphStaking private constant GRAPH = IGraphStaking(0xF55041E37E12cD407ad00CE2910B8269B01263b9); - IEpochManager private constant GRAPH_EPOCHS = IEpochManager(0x03541c5cd35953CD447261122F93A5E7b812D697); - ERC20 private constant GRT = ERC20(0xc944E90C64B2c07662A292be6244BDf05Cda44a7); - uint256 private constant MAX_PPM = 1e6; - - uint256 private constant STORAGE = uint256(keccak256("xyz.tenderize.graph.withdrawals.storage.location")) - 1; + uint256 private constant STORAGE = uint256(keccak256("xyz.tenderize.graph.adapter.storage.location")) - 1; error WithdrawPending(); @@ -62,7 +62,7 @@ contract GraphAdapter is Adapter { } function previewDeposit(uint256 assets) external view override returns (uint256) { - return assets - assets * GRAPH.delegationTaxPercentage() / MAX_PPM; + return assets - assets * GRAPH_STAKING.delegationTaxPercentage() / MAX_PPM; } function previewWithdraw(uint256 unlockID) external view override returns (uint256) { @@ -75,7 +75,7 @@ contract GraphAdapter is Adapter { function unlockMaturity(uint256 unlockID) external view override returns (uint256) { Storage storage $ = _loadStorage(); Unlock memory unlock = $.unlocks[unlockID]; - uint256 THAWING_PERIOD = GRAPH.thawingPeriod(); + uint256 THAWING_PERIOD = GRAPH_STAKING.thawingPeriod(); // if userEpoch == currentEpoch, it is yet to unlock // => unlockBlock + thawingPeriod // if userEpoch == currentEpoch - 1, it is processing @@ -93,16 +93,20 @@ contract GraphAdapter is Adapter { } function unlockTime() external view override returns (uint256) { - return GRAPH.thawingPeriod(); + return GRAPH_STAKING.thawingPeriod(); } function currentTime() external view override returns (uint256) { return block.number; } + function isValidator(address validator) public view override returns (bool) { + return GRAPH_STAKING.hasStake(validator); + } + function stake(address validator, uint256 amount) external override { - GRT.safeApprove(address(GRAPH), amount); - GRAPH.delegate(validator, amount); + GRT.safeApprove(address(GRAPH_STAKING), amount); + GRAPH_STAKING.delegate(validator, amount); } function unstake(address validator, uint256 amount) external override returns (uint256 unlockID) { @@ -145,7 +149,7 @@ contract GraphAdapter is Adapter { function rebase(address validator, uint256 currentStake) external override returns (uint256 newStake) { Storage storage $ = _loadStorage(); Epoch memory currentEpoch = $.epochs[$.currentEpoch]; - IGraphStaking.DelegationPool memory delPool = GRAPH.delegationPools(validator); + IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(validator); uint256 _tokensPerShare = delPool.shares != 0 ? delPool.tokens * 1 ether / delPool.shares : 1 ether; newStake = currentStake; @@ -160,7 +164,7 @@ contract GraphAdapter is Adapter { return newStake; } - IGraphStaking.Delegation memory delegation = GRAPH.getDelegation(validator, address(this)); + IGraphStaking.Delegation memory delegation = GRAPH_STAKING.getDelegation(validator, address(this)); uint256 staked = delegation.shares * _tokensPerShare / 1 ether; // account for stake still to unstake @@ -188,10 +192,6 @@ contract GraphAdapter is Adapter { newStake = staked - currentEpoch.amount; } - function isValidator(address validator) public view override returns (bool) { - return GRAPH.hasStake(validator); - } - function _processWithdrawals(address validator) internal { // process possible withdrawals before unstakes _processWithdraw(validator); @@ -199,7 +199,7 @@ contract GraphAdapter is Adapter { } function _processUnstake(address validator) internal { - IGraphStaking.Delegation memory del = GRAPH.getDelegation(validator, address(this)); + IGraphStaking.Delegation memory del = GRAPH_STAKING.getDelegation(validator, address(this)); // undelegation already ungoing: no-op if (del.tokensLockedUntil != 0) return; @@ -225,7 +225,7 @@ contract GraphAdapter is Adapter { undelegationShares = del.shares < undelegationShares ? del.shares : undelegationShares; // undelegate - GRAPH.undelegate(validator, undelegationShares); + GRAPH_STAKING.undelegate(validator, undelegationShares); } else if ($.epochs[$.currentEpoch - 1].amount != 0) { ++$.currentEpoch; $.lastEpochUnlockedAt = block.number; @@ -234,7 +234,7 @@ contract GraphAdapter is Adapter { function _processWithdraw(address validator) internal { // withdrawal isn't ready: no-op - uint256 tokensLockedUntil = GRAPH.getDelegation(validator, address(this)).tokensLockedUntil; + uint256 tokensLockedUntil = GRAPH_STAKING.getDelegation(validator, address(this)).tokensLockedUntil; if (tokensLockedUntil == 0 || tokensLockedUntil > GRAPH_EPOCHS.currentEpoch()) return; Storage storage $ = _loadStorage(); @@ -244,7 +244,7 @@ contract GraphAdapter is Adapter { // $.currentEpoch - 1 is safe as we only call this function after at least 1 _processUnstake // which increments $.currentEpoch, otherwise del.tokensLockedUntil would still be 0 and we would // not reach this branch - $.epochs[$.currentEpoch - 1].amount = GRAPH.withdrawDelegated(validator, address(0)); + $.epochs[$.currentEpoch - 1].amount = GRAPH_STAKING.withdrawDelegated(validator, address(0)); } } } From cdf9efb9ce862ddfb368aa403bb3daa056ca78d0 Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Fri, 8 Dec 2023 13:24:02 +0100 Subject: [PATCH 03/12] [Graph] feat: GraphAdapter::rebase code improvements --- src/adapters/GraphAdapter.sol | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/adapters/GraphAdapter.sol b/src/adapters/GraphAdapter.sol index 61b9eeb..fc2a1d2 100644 --- a/src/adapters/GraphAdapter.sol +++ b/src/adapters/GraphAdapter.sol @@ -148,22 +148,27 @@ contract GraphAdapter is Adapter { function rebase(address validator, uint256 currentStake) external override returns (uint256 newStake) { Storage storage $ = _loadStorage(); - Epoch memory currentEpoch = $.epochs[$.currentEpoch]; + uint256 currentEpochNum = $.currentEpoch; + Epoch memory currentEpoch = $.epochs[currentEpochNum]; IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(validator); + uint256 oldTokensPerShare = $.tokensPerShare; uint256 _tokensPerShare = delPool.shares != 0 ? delPool.tokens * 1 ether / delPool.shares : 1 ether; + newStake = currentStake; // Account for rounding error of -1 or +1 // This occurs due to a slight change in ratio because of new delegations or withdrawals, // rather than an effective reward or loss if ( - (_tokensPerShare >= $.tokensPerShare && _tokensPerShare - $.tokensPerShare <= 1) - || (_tokensPerShare < $.tokensPerShare && $.tokensPerShare - _tokensPerShare <= 1) + (_tokensPerShare >= oldTokensPerShare && _tokensPerShare - oldTokensPerShare <= 1) + || (_tokensPerShare < oldTokensPerShare && oldTokensPerShare - _tokensPerShare <= 1) ) { return newStake; } + $.tokensPerShare = _tokensPerShare; + IGraphStaking.Delegation memory delegation = GRAPH_STAKING.getDelegation(validator, address(this)); uint256 staked = delegation.shares * _tokensPerShare / 1 ether; @@ -171,7 +176,7 @@ contract GraphAdapter is Adapter { uint256 oldStake = currentStake + currentEpoch.amount; // Last epoch amount should be synced with Delegation.tokensLocked - if ($.currentEpoch > 0) $.epochs[$.currentEpoch - 1].amount = delegation.tokensLocked; + if (currentEpochNum > 0) $.epochs[currentEpochNum - 1].amount = delegation.tokensLocked; if (staked > oldStake) { // handle rewards @@ -179,16 +184,14 @@ contract GraphAdapter is Adapter { // for which their stake is still to be unlocked // because technically it is not unlocked from the Graph either // We do this by adding the rewards to the current epoch - uint256 currentEpochAmount = (staked - oldStake) * currentEpoch.amount / oldStake; - currentEpoch.amount += currentEpochAmount; + currentEpoch.amount += (staked - oldStake) * currentEpoch.amount / oldStake; } else { return newStake; } - $.epochs[$.currentEpoch] = currentEpoch; - $.tokensPerShare = _tokensPerShare; + $.epochs[currentEpochNum] = currentEpoch; - // slash/rewards is already accounted for in $.epochs[$.currentEpoch].amount + // rewards is already accounted for in $.epochs[$.currentEpoch].amount newStake = staked - currentEpoch.amount; } From a0b2841460b6194848a4e2f378054d73f2a12d67 Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Fri, 8 Dec 2023 13:25:49 +0100 Subject: [PATCH 04/12] [Unlocks] fix: Unlocks::getMetadata tokenId decoding type explicit uint96 --- src/unlocks/Unlocks.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unlocks/Unlocks.sol b/src/unlocks/Unlocks.sol index 77641b5..20a3b43 100644 --- a/src/unlocks/Unlocks.sol +++ b/src/unlocks/Unlocks.sol @@ -105,7 +105,7 @@ contract Unlocks is ERC721 { * @return metadata of the unlock token */ function getMetadata(uint256 tokenId) external view returns (Metadata memory metadata) { - (address payable tenderizer, uint256 unlockId) = _decodeTokenId(tokenId); + (address payable tenderizer, uint96 unlockId) = _decodeTokenId(tokenId); address asset = Tenderizer(tenderizer).asset(); Adapter adapter = Tenderizer(tenderizer).adapter(); From e4f2b1fb408420e5d3e9729510c765c01afda5c6 Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Fri, 8 Dec 2023 13:28:07 +0100 Subject: [PATCH 05/12] [Adapter,Tenderizer] feat: update Adapter::stake and Adapter::previewDeposit API for better accuracy and cleaner code in Tenderizer::deposit --- src/adapters/Adapter.sol | 4 ++-- src/adapters/GraphAdapter.sol | 14 ++++++++++---- src/adapters/LivepeerAdapter.sol | 7 ++++--- src/adapters/PolygonAdapter.sol | 5 +++-- src/tenderizer/Tenderizer.sol | 14 +++++--------- test/adapters/GraphAdapter.t.sol | 2 +- test/adapters/PolygonAdapter.t.sol | 2 +- test/fork-tests/Livepeer.arbitrum.t.sol | 2 +- test/helpers/XYZAdapter.sol | 5 +++-- test/tenderizer/Tenderizer.t.sol | 18 +++++++++--------- 10 files changed, 39 insertions(+), 34 deletions(-) diff --git a/src/adapters/Adapter.sol b/src/adapters/Adapter.sol index 39ea70a..132804a 100644 --- a/src/adapters/Adapter.sol +++ b/src/adapters/Adapter.sol @@ -14,7 +14,7 @@ import { IERC165 } from "core/interfaces/IERC165.sol"; pragma solidity >=0.8.19; interface Adapter is IERC165 { - function previewDeposit(uint256 assets) external view returns (uint256); + function previewDeposit(address validator, uint256 assets) external view returns (uint256); function previewWithdraw(uint256 unlockID) external view returns (uint256); @@ -24,7 +24,7 @@ interface Adapter is IERC165 { function currentTime() external view returns (uint256); - function stake(address validator, uint256 amount) external; + function stake(address validator, uint256 amount) external returns (uint256 staked); function unstake(address validator, uint256 amount) external returns (uint256 unlockID); diff --git a/src/adapters/GraphAdapter.sol b/src/adapters/GraphAdapter.sol index fc2a1d2..6566d2f 100644 --- a/src/adapters/GraphAdapter.sol +++ b/src/adapters/GraphAdapter.sol @@ -61,8 +61,12 @@ contract GraphAdapter is Adapter { return interfaceId == type(Adapter).interfaceId || interfaceId == type(IERC165).interfaceId; } - function previewDeposit(uint256 assets) external view override returns (uint256) { - return assets - assets * GRAPH_STAKING.delegationTaxPercentage() / MAX_PPM; + function previewDeposit(address validator, uint256 assets) external view override returns (uint256) { + assets -= assets * GRAPH_STAKING.delegationTaxPercentage() / MAX_PPM; + IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(validator); + + uint256 shares = delPool.tokens != 0 ? assets * delPool.shares / delPool.tokens : assets; + return shares * (delPool.tokens + assets) / (delPool.shares + shares); } function previewWithdraw(uint256 unlockID) external view override returns (uint256) { @@ -104,9 +108,11 @@ contract GraphAdapter is Adapter { return GRAPH_STAKING.hasStake(validator); } - function stake(address validator, uint256 amount) external override { + function stake(address validator, uint256 amount) external override returns (uint256) { GRT.safeApprove(address(GRAPH_STAKING), amount); - GRAPH_STAKING.delegate(validator, amount); + uint256 delShares = GRAPH_STAKING.delegate(validator, amount); + IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(validator); + return delShares * delPool.tokens / delPool.shares; } function unstake(address validator, uint256 amount) external override returns (uint256 unlockID) { diff --git a/src/adapters/LivepeerAdapter.sol b/src/adapters/LivepeerAdapter.sol index 3199282..0d13f52 100644 --- a/src/adapters/LivepeerAdapter.sol +++ b/src/adapters/LivepeerAdapter.sol @@ -53,7 +53,7 @@ contract LivepeerAdapter is Adapter { return interfaceId == type(Adapter).interfaceId || interfaceId == type(IERC165).interfaceId; } - function previewDeposit(uint256 assets) external pure returns (uint256) { + function previewDeposit(address, /*validator*/ uint256 assets) external pure returns (uint256) { return assets; } @@ -87,9 +87,10 @@ contract LivepeerAdapter is Adapter { return block.number; } - function stake(address validator, uint256 amount) public { - LPT.approve(address(LIVEPEER_BONDING), amount); + function stake(address validator, uint256 amount) public returns (uint256) { + LPT.safeApprove(address(LIVEPEER_BONDING), amount); LIVEPEER_BONDING.bond(amount, validator); + return amount; } function unstake(address, /*validator*/ uint256 amount) external returns (uint256 unlockID) { diff --git a/src/adapters/PolygonAdapter.sol b/src/adapters/PolygonAdapter.sol index 8cdc339..c4f4758 100644 --- a/src/adapters/PolygonAdapter.sol +++ b/src/adapters/PolygonAdapter.sol @@ -50,7 +50,7 @@ contract PolygonAdapter is Adapter { return address(_getValidatorSharesContract(_getValidatorId(validator))) != address(0); } - function previewDeposit(uint256 assets) external pure returns (uint256) { + function previewDeposit(address, /*validator*/ uint256 assets) external pure returns (uint256) { return assets; } @@ -85,7 +85,7 @@ contract PolygonAdapter is Adapter { return MATIC_STAKE_MANAGER.epoch(); } - function stake(address validator, uint256 amount) external override { + function stake(address validator, uint256 amount) external override returns (uint256) { // approve tokens POLY.safeApprove(address(MATIC_STAKE_MANAGER), amount); @@ -100,6 +100,7 @@ contract PolygonAdapter is Adapter { // Mint voucher shares validatorShares.buyVoucher(amount, min); + return amount; } function unstake(address validator, uint256 amount) external override returns (uint256 unlockID) { diff --git a/src/tenderizer/Tenderizer.sol b/src/tenderizer/Tenderizer.sol index eaaa469..a89210b 100644 --- a/src/tenderizer/Tenderizer.sol +++ b/src/tenderizer/Tenderizer.sol @@ -79,16 +79,12 @@ contract Tenderizer is TenderizerImmutableArgs, TenderizerEvents, TToken, Multic // transfer tokens before minting (or ERC777's could re-enter) ERC20(asset()).safeTransferFrom(msg.sender, address(this), assets); - // preview deposit to get actual assets to mint for - // deducts any possible third-party protocol taxes or fees - uint256 actualAssets = _previewDeposit(assets); - // stake assets - _stake(validator(), assets); + uint256 staked = _stake(validator(), assets); // mint tokens to receiver uint256 shares; - if ((shares = _mint(receiver, actualAssets)) == 0) revert InsufficientAssets(); + if ((shares = _mint(receiver, staked)) == 0) revert InsufficientAssets(); uint256 tTokenOut = convertToAssets(shares); emit Deposit(msg.sender, receiver, assets, tTokenOut); @@ -200,7 +196,7 @@ contract Tenderizer is TenderizerImmutableArgs, TenderizerEvents, TToken, Multic // using a `staticcall` to `this`. // This is a hacky workaround while better solidity features are being developed. function _previewDeposit(uint256 assets) public returns (uint256) { - return abi.decode(adapter()._delegatecall(abi.encodeCall(adapter().previewDeposit, (assets))), (uint256)); + return abi.decode(adapter()._delegatecall(abi.encodeCall(adapter().previewDeposit, (validator(), assets))), (uint256)); } function _previewWithdraw(uint256 unlockID) public returns (uint256) { @@ -216,8 +212,8 @@ contract Tenderizer is TenderizerImmutableArgs, TenderizerEvents, TToken, Multic newStake = abi.decode(adapter()._delegatecall(abi.encodeCall(adapter().rebase, (validator, currentStake))), (uint256)); } - function _stake(address validator, uint256 amount) internal { - adapter()._delegatecall(abi.encodeCall(adapter().stake, (validator, amount))); + function _stake(address validator, uint256 amount) internal returns (uint256 staked) { + staked = abi.decode(adapter()._delegatecall(abi.encodeCall(adapter().stake, (validator, amount))), (uint256)); } function _unstake(address validator, uint256 amount) internal returns (uint256 unlockID) { diff --git a/test/adapters/GraphAdapter.t.sol b/test/adapters/GraphAdapter.t.sol index db6116f..be20862 100644 --- a/test/adapters/GraphAdapter.t.sol +++ b/test/adapters/GraphAdapter.t.sol @@ -41,7 +41,7 @@ contract GraphAdapterTest is Test, GraphAdapter, TestHelpers { function testFuzz_PreviewDeposit(uint256 amount) public { amount = bound(amount, 0, MAX_UINT / DELEGATION_TAX); vm.expectCall(staking, abi.encodeCall(IGraphStaking.delegationTaxPercentage, ())); - assertEq(this.previewDeposit(amount), amount - amount * DELEGATION_TAX / MAX_PPM); + assertEq(this.previewDeposit(validator, amount), amount - amount * DELEGATION_TAX / MAX_PPM); } function testFuzz_UnlockMaturity(uint256 lastEpochUnlockedAt, uint256 userEpoch) public { diff --git a/test/adapters/PolygonAdapter.t.sol b/test/adapters/PolygonAdapter.t.sol index 5d2b8a5..312931f 100644 --- a/test/adapters/PolygonAdapter.t.sol +++ b/test/adapters/PolygonAdapter.t.sol @@ -53,7 +53,7 @@ contract PolygonAdapterTest is Test { function test_previewDeposit() public { uint256 assets = 100; uint256 expected = assets; - uint256 actual = adapter.previewDeposit(assets); + uint256 actual = adapter.previewDeposit(validatorShares, assets); assertEq(actual, expected); } diff --git a/test/fork-tests/Livepeer.arbitrum.t.sol b/test/fork-tests/Livepeer.arbitrum.t.sol index 511bd4f..ccdece4 100644 --- a/test/fork-tests/Livepeer.arbitrum.t.sol +++ b/test/fork-tests/Livepeer.arbitrum.t.sol @@ -124,7 +124,7 @@ contract LivepeerForkTest is Test, TenderizerEvents, ERC721Receiver { } function test_adapter_previewDeposit() public { - assertEq(adapter.previewDeposit(10 ether), 10 ether, "previewDeposit incorrect"); + assertEq(adapter.previewDeposit(ORCHESTRATOR_1, 10 ether), 10 ether, "previewDeposit incorrect"); } function test_factory_newTenderizer() public { diff --git a/test/helpers/XYZAdapter.sol b/test/helpers/XYZAdapter.sol index 0b610b1..d0b4328 100644 --- a/test/helpers/XYZAdapter.sol +++ b/test/helpers/XYZAdapter.sol @@ -31,7 +31,7 @@ contract XYZAdapter is Adapter { return interfaceId == type(Adapter).interfaceId || interfaceId == type(IERC165).interfaceId; } - function previewDeposit(uint256 assets) external pure returns (uint256) { + function previewDeposit(address, /*validator*/ uint256 assets) external pure returns (uint256) { return assets; } @@ -51,9 +51,10 @@ contract XYZAdapter is Adapter { return block.timestamp; } - function stake(address, uint256 amount) external { + function stake(address, uint256 amount) external returns (uint256) { ERC20(XYZ_TOKEN).approve(STAKINGXYZ, amount); StakingXYZ(STAKINGXYZ).stake(amount); + return amount; } function unstake(address, uint256 amount) external returns (uint256 unlockID) { diff --git a/test/tenderizer/Tenderizer.t.sol b/test/tenderizer/Tenderizer.t.sol index 1eaad7e..12324f5 100644 --- a/test/tenderizer/Tenderizer.t.sol +++ b/test/tenderizer/Tenderizer.t.sol @@ -86,13 +86,13 @@ contract TenderizerTest is TenderizerSetup, TenderizerEvents { function test_PreviewDeposit() public { uint256 amountIn = 100 ether; uint256 amountOut = 99.5 ether; - vm.mockCall(adapter, abi.encodeCall(Adapter.previewDeposit, (amountIn)), abi.encode(amountOut)); - vm.expectCall(adapter, abi.encodeCall(Adapter.previewDeposit, (amountIn))); + vm.mockCall(adapter, abi.encodeCall(Adapter.previewDeposit, (validator, amountIn)), abi.encode(amountOut)); + vm.expectCall(adapter, abi.encodeCall(Adapter.previewDeposit, (validator, amountIn))); assertEq(tenderizer.previewDeposit(amountIn), amountOut); } function test_PreviewDeposit_RevertIfAdapterReverts() public { - vm.mockCallRevert(adapter, abi.encodeCall(Adapter.previewDeposit, (1 ether)), ERROR_MESSAGE); + vm.mockCallRevert(adapter, abi.encodeCall(Adapter.previewDeposit, (validator, 1 ether)), ERROR_MESSAGE); vm.expectRevert( abi.encodeWithSelector( StaticCallFailed.selector, address(tenderizer), abi.encodeCall(tenderizer._previewDeposit, (1 ether)), "" @@ -146,12 +146,12 @@ contract TenderizerTest is TenderizerSetup, TenderizerEvents { amountOut = bound(amountOut, 1, MAX_UINT_SQRT); vm.mockCall(adapter, abi.encodeCall(Adapter.rebase, (validator, 0)), abi.encode(0)); - vm.mockCall(adapter, abi.encodeCall(Adapter.previewDeposit, (amountIn)), abi.encode(amountOut)); + vm.mockCall(adapter, abi.encodeCall(Adapter.previewDeposit, (validator, amountIn)), abi.encode(amountOut)); vm.mockCall(asset, abi.encodeCall(IERC20.transferFrom, (account1, address(tenderizer), amountIn)), abi.encode(true)); vm.expectCall(adapter, abi.encodeCall(Adapter.rebase, (validator, 0))); vm.expectCall(asset, abi.encodeCall(IERC20.transferFrom, (account1, address(tenderizer), amountIn))); - vm.expectCall(adapter, abi.encodeCall(Adapter.previewDeposit, (amountIn))); + vm.expectCall(adapter, abi.encodeCall(Adapter.previewDeposit, (validator, amountIn))); vm.expectCall(adapter, abi.encodeCall(Adapter.stake, (validator, amountIn))); vm.expectEmit(true, true, true, true); emit Deposit(account1, account2, amountIn, amountOut); @@ -166,7 +166,7 @@ contract TenderizerTest is TenderizerSetup, TenderizerEvents { function test_Deposit_RevertIfStakeReverts() public { uint256 depositAmount = 100 ether; vm.mockCall(adapter, abi.encodeCall(Adapter.rebase, (validator, 0)), abi.encode(0)); - vm.mockCall(adapter, abi.encodeCall(Adapter.previewDeposit, (depositAmount)), abi.encode(depositAmount)); + vm.mockCall(adapter, abi.encodeCall(Adapter.previewDeposit, (validator, depositAmount)), abi.encode(depositAmount)); vm.mockCallRevert( adapter, abi.encodeCall(Adapter.stake, (validator, depositAmount)), @@ -178,7 +178,7 @@ contract TenderizerTest is TenderizerSetup, TenderizerEvents { function test_Deposit_RevertIfZeroAmount() public { vm.mockCall(adapter, abi.encodeCall(Adapter.rebase, (validator, 0)), abi.encode(0)); - vm.mockCall(adapter, abi.encodeCall(Adapter.previewDeposit, (0)), abi.encode(0)); + vm.mockCall(adapter, abi.encodeCall(Adapter.previewDeposit, (validator, 0)), abi.encode(0)); vm.expectRevert(TToken.ZeroAmount.selector); tenderizer.deposit(account1, 0); } @@ -186,7 +186,7 @@ contract TenderizerTest is TenderizerSetup, TenderizerEvents { function test_Deposit_RevertIfAssetTransferFails() public { uint256 depositAmount = 100 ether; vm.mockCall(adapter, abi.encodeCall(Adapter.rebase, (validator, 0)), abi.encode(0)); - vm.mockCall(adapter, abi.encodeCall(Adapter.previewDeposit, (depositAmount)), abi.encode(depositAmount)); + vm.mockCall(adapter, abi.encodeCall(Adapter.previewDeposit, (validator, depositAmount)), abi.encode(depositAmount)); vm.mockCall(asset, abi.encodeCall(IERC20.transferFrom, (account1, address(tenderizer), depositAmount)), abi.encode(false)); vm.prank(account1); @@ -389,7 +389,7 @@ contract TenderizerTest is TenderizerSetup, TenderizerEvents { } function _deposit(address account, uint256 amount, uint256 totalPreviousDeposits) internal { - vm.mockCall(adapter, abi.encodeCall(Adapter.previewDeposit, (amount)), abi.encode(amount)); + vm.mockCall(adapter, abi.encodeCall(Adapter.previewDeposit, (validator, amount)), abi.encode(amount)); vm.mockCall(adapter, abi.encodeCall(Adapter.rebase, (validator, totalPreviousDeposits)), abi.encode(totalPreviousDeposits)); vm.prank(account); tenderizer.deposit(account, amount); From 1c89325c0343ed13d7f821e366e446348573d531 Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Fri, 8 Dec 2023 13:28:50 +0100 Subject: [PATCH 06/12] [Graph] feat: Arbitrum fork test for The Graph --- test/fork-tests/Graph.arbitrum.t.sol | 271 +++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 test/fork-tests/Graph.arbitrum.t.sol diff --git a/test/fork-tests/Graph.arbitrum.t.sol b/test/fork-tests/Graph.arbitrum.t.sol new file mode 100644 index 0000000..64ad2d5 --- /dev/null +++ b/test/fork-tests/Graph.arbitrum.t.sol @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: MIT +// +// _____ _ _ +// |_ _| | | (_) +// | | ___ _ __ __| | ___ _ __ _ _______ +// | |/ _ \ '_ \ / _` |/ _ \ '__| |_ / _ \ +// | | __/ | | | (_| | __/ | | |/ / __/ +// \_/\___|_| |_|\__,_|\___|_| |_/___\___| +// +// Copyright (c) Tenderize Labs Ltd + +pragma solidity >=0.8.19; + +import { Test, console } from "forge-std/Test.sol"; +import { VmSafe } from "forge-std/Vm.sol"; +import { MockERC20 } from "solmate/test/utils/mocks/MockERC20.sol"; + +import { + GraphAdapter, + IGraphStaking, + IGraphEpochManager, + GRT, + GRAPH_EPOCHS, + GRAPH_STAKING, + MAX_PPM +} from "core/adapters/GraphAdapter.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 INDEXER_1 = 0x4e5c87772C29381bCaBC58C3f182B6633B5a274a; +address constant GOVERNOR = 0x8C6de8F8D562f3382417340A6994601eE08D3809; +address constant CURATION = 0x22d78fb4bc72e191C765807f8891B5e1785C8014; + +interface IGraphStakingTest is IGraphStaking { + function stake(uint256 amount) external; + function allocate( + bytes32 _subgraphDeploymentID, + uint256 _tokens, + address _allocationID, + bytes32 _metadata, + bytes calldata _proof + ) + external; + function closeAllocation(address _allocationID, bytes32 _poi) external; +} + +interface IGraphEpochsTest is IGraphEpochManager { + function epochLength() external view returns (uint256); + function currentEpochBlocksSinceStart() external view returns (uint256); +} + +interface IGraphCurationTest { + function mint(bytes32 _subgraphDeploymentID, uint256 _tokensIn, uint256 _signalOutMin) external returns (uint256, uint256); +} + +contract GraphForkTest is Test, TenderizerEvents, ERC721Receiver { + TenderizerFixture fixture; + GraphAdapter adapter; + address immutable MINTER_ROLE = makeAddr("MINTER_ROLE"); + + event NewTenderizer(address indexed asset, address indexed validator, address tenderizer); + + function mintGRT(address to, uint256 amount) public { + vm.prank(MINTER_ROLE); + MockERC20(address(GRT)).mint(to, amount); + } + + function setUp() public { + bytes32 salt = bytes32(uint256(1)); + vm.createSelectFork(vm.envString("ARBITRUM_RPC")); + fixture = tenderizerFixture(); + adapter = new GraphAdapter{ salt: salt }(); + fixture.registry.registerAdapter(address(GRT), address(adapter)); + + // Add MINTER_ROLE + vm.prank(GOVERNOR); + // solhint-disable-next-line avoid-low-level-calls + (bool success,) = address(GRT).call(abi.encodeWithSignature("addMinter(address)", (address(MINTER_ROLE)))); + assertTrue(success, "assigning minter role failed"); + } + + function test_registry_AdapterRegistered() public { + assertEq(fixture.registry.adapter(address(GRT)), address(adapter), "adapter not registered"); + } + + function test_adapter_unlockTime() public { + assertEq(adapter.unlockTime(), 201_600, "unlock time not set"); + } + + function test_currentTime() public { + assertEq(adapter.currentTime(), block.number, "current time not set"); + } + + function test_isValidator() public { + assertEq(adapter.isValidator(INDEXER_1), true, "isValidator true incorrect"); + assertEq(adapter.isValidator(makeAddr("NOT_INDEXER")), false, "isValidator false incorrect"); + } + + function testFuzz_previewDeposit(uint256 amount) public { + amount = bound(amount, 1, 10e28); + uint256 preview = adapter.previewDeposit(INDEXER_1, amount); + + uint256 delTax = GRAPH_STAKING.delegationTaxPercentage(); + amount -= amount * delTax / MAX_PPM; + IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(INDEXER_1); + uint256 shares = delPool.tokens != 0 ? amount * delPool.shares / delPool.tokens : amount; + uint256 expected = shares * (delPool.tokens + amount) / (delPool.shares + shares); + assertEq(preview, expected, "previewDeposit incorrect"); + } + + 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); + + // Deploy tenderizer + vm.expectEmit({ checkTopic1: true, checkTopic2: true, checkTopic3: false, checkData: false }); + emit NewTenderizer(address(GRT), INDEXER_1, address(0x0)); + fixture.factory.newTenderizer(address(GRT), INDEXER_1); + } + + function test_deposit() public { + uint256 depositAmount = 100_000 ether; + + Tenderizer tenderizer = Tenderizer(payable(fixture.factory.newTenderizer(address(GRT), INDEXER_1))); + + mintGRT(address(this), depositAmount); + GRT.approve(address(tenderizer), depositAmount); + + uint256 delTax = GRAPH_STAKING.delegationTaxPercentage(); + IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(INDEXER_1); + IGraphStaking.Delegation memory delegation = GRAPH_STAKING.getDelegation(INDEXER_1, address(tenderizer)); + + uint256 actualDeposit = depositAmount - (depositAmount * delTax / MAX_PPM); + uint256 sharesOut = delPool.tokens != 0 ? actualDeposit * delPool.shares / delPool.tokens : actualDeposit; + uint256 expected = sharesOut * (delPool.tokens + actualDeposit) / (delPool.shares + sharesOut); + uint256 preview = tenderizer.previewDeposit(depositAmount); + + // TODO: this assertion might not hold with multiple deposits + // and rounding error upon share calculation + assertEq(preview, expected, "previewDeposit incorrect"); + vm.expectEmit({ checkTopic1: true, checkTopic2: true, checkTopic3: false, checkData: true }); + emit Deposit(address(this), address(this), depositAmount, expected); + tenderizer.deposit(address(this), depositAmount); + + assertEq(tenderizer.totalSupply(), expected, "total supply incorrect"); + assertEq(tenderizer.balanceOf(address(this)), expected, "balance incorrect"); + + delPool = GRAPH_STAKING.delegationPools(INDEXER_1); + delegation = GRAPH_STAKING.getDelegation(INDEXER_1, address(tenderizer)); + uint256 staked = delegation.shares * delPool.tokens / delPool.shares; + assertEq(tenderizer.totalSupply(), staked, "total staked incorrect"); + } + + function test_unlock_withdraw_simple() public { + uint256 depositAmount = 100_000 ether; + uint256 unstakeAmount = 50_000 ether; + Tenderizer tenderizer = Tenderizer(payable(fixture.factory.newTenderizer(address(GRT), INDEXER_1))); + mintGRT(address(this), depositAmount); + GRT.approve(address(tenderizer), depositAmount); + uint256 tTokenOut = tenderizer.deposit(address(this), depositAmount); + + vm.expectEmit(); + emit Unlock(address(this), unstakeAmount, 1); + IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(INDEXER_1); + + uint256 unlockID = tenderizer.unlock(unstakeAmount); + assertEq(unlockID, 1, "unlockID incorrect"); + assertEq(tenderizer.totalSupply(), tTokenOut - unstakeAmount, "total supply incorrect"); + assertEq(tenderizer.balanceOf(address(this)), tTokenOut - unstakeAmount, "balance incorrect"); + IGraphStaking.Delegation memory delegation = GRAPH_STAKING.getDelegation(INDEXER_1, address(tenderizer)); + uint256 actualUnstakedAmount = unstakeAmount * delPool.shares / delPool.tokens * delPool.tokens / delPool.shares; + assertEq(delegation.tokensLocked, actualUnstakedAmount, "tokens locked incorrect"); + + assertEq(tenderizer.unlockMaturity(unlockID), block.number + adapter.unlockTime(), "maturity incorrect"); + + uint256 tokenId = uint256(bytes32(abi.encodePacked(tenderizer, uint96(unlockID)))); + + Metadata memory metadata = fixture.unlocks.getMetadata(tokenId); + assertEq(metadata.amount, unstakeAmount, "metadata amount incorrect"); + assertEq(metadata.maturity, block.number + adapter.unlockTime(), "metadata maturity incorrect"); + assertEq(metadata.progress, 0, "metadata progress incorrect"); + assertEq(metadata.unlockId, unlockID, "metadata unlockId incorrect"); + assertEq(metadata.validator, INDEXER_1, "metadata validator incorrect"); + + // roll to 50% progress + vm.roll(block.number + adapter.unlockTime() / 2); + metadata = fixture.unlocks.getMetadata(tokenId); + assertEq(metadata.progress, 50, "metadata progress incorrect"); + + // roll to 100% progress and withdraw + vm.roll(block.number + adapter.unlockTime() / 2 + 1); + + uint256 grtBalBeforeWithdraw = GRT.balanceOf(address(this)); + vm.expectEmit(); + emit Withdraw(address(this), actualUnstakedAmount, unlockID); + uint256 withdrawn = Tenderizer(tenderizer).withdraw(address(this), unlockID); + assertEq(withdrawn, actualUnstakedAmount, "withdrawn amount incorrect"); + delegation = GRAPH_STAKING.getDelegation(INDEXER_1, address(tenderizer)); + assertEq(delegation.tokensLocked, 0, "tokens locked not 0"); + vm.expectRevert("NOT_MINTED"); + fixture.unlocks.ownerOf(tokenId); + assertEq(GRT.balanceOf(address(this)), grtBalBeforeWithdraw + actualUnstakedAmount, "GRT balance incorrect"); + } + + function test_rebase() public { + Tenderizer tenderizer = Tenderizer(payable(fixture.factory.newTenderizer(address(GRT), INDEXER_1))); + + uint256 epochLength = IGraphEpochsTest(address(GRAPH_EPOCHS)).epochLength(); + // ====================================== + // Deposit & Process epoch + uint256 depositAmount = 100_000 ether; + + mintGRT(address(this), depositAmount); + GRT.approve(address(tenderizer), depositAmount); + uint256 tTokenOut = tenderizer.deposit(address(this), depositAmount); + vm.roll(block.number + epochLength); + // ====================================== + + // Allocate Rewards + VmSafe.Wallet memory wallet = vm.createWallet("ALLOCATION_SIGNER"); + + { + bytes32 subgraphID = bytes32("SUBGRAPH"); + bytes32 metadata = bytes32("METADATA"); + + // Curate subgraph + mintGRT(address(this), 100_000 ether); + GRT.approve(CURATION, 100_000 ether); + IGraphCurationTest(CURATION).mint(subgraphID, 100_000 ether, 0); + + uint256 allocationAmount = 10_000 ether; + mintGRT(INDEXER_1, 100_000 ether); + vm.startPrank(INDEXER_1); + GRT.approve(address(GRAPH_STAKING), 50_000 ether); + IGraphStakingTest(address(GRAPH_STAKING)).stake(50_000 ether); + bytes32 msgHash = keccak256(abi.encodePacked(INDEXER_1, wallet.addr)); + bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", msgHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet, digest); + IGraphStakingTest(address(GRAPH_STAKING)).allocate( + subgraphID, allocationAmount, wallet.addr, metadata, abi.encodePacked(r, s, v) + ); + vm.roll(block.number + epochLength); + } + + // Close Allocation + { + bytes32 poi = bytes32("foo"); + + IGraphStakingTest(address(GRAPH_STAKING)).closeAllocation(wallet.addr, poi); + } + vm.stopPrank(); + // ====================================== + { + IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(INDEXER_1); + + tenderizer.rebase(); + uint256 delShares = GRAPH_STAKING.getDelegation(INDEXER_1, address(tenderizer)).shares; + uint256 totalStaked = delShares * delPool.tokens / delPool.shares; + assertEq(tenderizer.totalSupply(), totalStaked, "total supply incorrect"); + assertEq(tenderizer.balanceOf(address(this)), totalStaked, "balance incorrect"); + assertTrue(tenderizer.balanceOf(address(this)) > tTokenOut, "balance not greater than before"); + } + } + + // TODO Rebase when there are pending unstakes in the current epoch +} From 53625c71c5d5ed586206f77a8e0fb6709fafe32f Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Fri, 8 Dec 2023 16:06:31 +0100 Subject: [PATCH 07/12] [Foundry] upgrade: forge-std --- .gitmodules | 7 +++---- lib/forge-std | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.gitmodules b/.gitmodules index 6e308a5..ed5585b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,3 @@ -[submodule "lib/forge-std"] - branch = "master" - path = lib/forge-std - url = https://github.com/foundry-rs/forge-std [submodule "lib/prb-test"] branch = "0.3.1" path = "lib/prb-test" @@ -26,3 +22,6 @@ [submodule "lib/uniswap-v3-core"] path = lib/uniswap-v3-core url = https://github.com/tenderize/uniswap-v3-core +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/lib/forge-std b/lib/forge-std index 8d0971b..2f11269 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 8d0971b66e204e20782e2d01855deb02736b0e48 +Subproject commit 2f112697506eab12d433a65fdc31a639548fe365 From 0838e5b3da058c8f8974d70dd6776e118d179025 Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Fri, 8 Dec 2023 16:08:07 +0100 Subject: [PATCH 08/12] [Graph] fix: rename IEpochManager to IGraphEpochManager --- src/adapters/GraphAdapter.sol | 4 ++-- src/adapters/interfaces/IGraph.sol | 2 +- test/adapters/GraphAdapter.t.sol | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/adapters/GraphAdapter.sol b/src/adapters/GraphAdapter.sol index 6566d2f..74c136e 100644 --- a/src/adapters/GraphAdapter.sol +++ b/src/adapters/GraphAdapter.sol @@ -14,10 +14,10 @@ pragma solidity >=0.8.19; import { ERC20 } from "solmate/tokens/ERC20.sol"; import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol"; import { Adapter } from "core/adapters/Adapter.sol"; -import { IGraphStaking, IEpochManager } from "core/adapters/interfaces/IGraph.sol"; +import { IGraphStaking, IGraphEpochManager } from "core/adapters/interfaces/IGraph.sol"; import { IERC165 } from "core/interfaces/IERC165.sol"; -IEpochManager constant GRAPH_EPOCHS = IEpochManager(0x5A843145c43d328B9bB7a4401d94918f131bB281); +IGraphEpochManager constant GRAPH_EPOCHS = IGraphEpochManager(0x5A843145c43d328B9bB7a4401d94918f131bB281); IGraphStaking constant GRAPH_STAKING = IGraphStaking(0x00669A4CF01450B64E8A2A20E9b1FCB71E61eF03); ERC20 constant GRT = ERC20(0x9623063377AD1B27544C965cCd7342f7EA7e88C7); uint256 constant MAX_PPM = 1e6; diff --git a/src/adapters/interfaces/IGraph.sol b/src/adapters/interfaces/IGraph.sol index c03330a..5034324 100644 --- a/src/adapters/interfaces/IGraph.sol +++ b/src/adapters/interfaces/IGraph.sol @@ -55,6 +55,6 @@ interface IGraphStaking { function hasStake(address _indexer) external view returns (bool); } -interface IEpochManager { +interface IGraphEpochManager { function currentEpoch() external view returns (uint256); } diff --git a/test/adapters/GraphAdapter.t.sol b/test/adapters/GraphAdapter.t.sol index be20862..960b937 100644 --- a/test/adapters/GraphAdapter.t.sol +++ b/test/adapters/GraphAdapter.t.sol @@ -14,7 +14,7 @@ pragma solidity >=0.8.19; import { Test, stdError } from "forge-std/Test.sol"; import { GraphAdapter } from "core/adapters/GraphAdapter.sol"; import { IERC20 } from "core/interfaces/IERC20.sol"; -import { IGraphStaking, IEpochManager } from "core/adapters/interfaces/IGraph.sol"; +import { IGraphStaking, IGraphEpochManager } from "core/adapters/interfaces/IGraph.sol"; import { TestHelpers } from "test/helpers/Helpers.sol"; // solhint-disable func-name-mixedcase @@ -114,7 +114,7 @@ contract GraphAdapterTest is Test, GraphAdapter, TestHelpers { abi.encode(10 ether, 1 ether, block.number + 1) ); - vm.mockCall(epochs, abi.encodeCall(IEpochManager.currentEpoch, ()), abi.encode(epoch)); + vm.mockCall(epochs, abi.encodeCall(IGraphEpochManager.currentEpoch, ()), abi.encode(epoch)); Storage storage $ = _loadStorage(); $.currentEpoch = epoch; @@ -154,7 +154,7 @@ contract GraphAdapterTest is Test, GraphAdapter, TestHelpers { staking, abi.encodeCall(IGraphStaking.getDelegation, (validator, address(this))), abi.encode(stakedShares, 0, 0) ); - vm.mockCall(epochs, abi.encodeCall(IEpochManager.currentEpoch, ()), abi.encode(epoch)); + vm.mockCall(epochs, abi.encodeCall(IGraphEpochManager.currentEpoch, ()), abi.encode(epoch)); uint256 expShares = (currentEpochAmount + amount) * 1 ether / $.tokensPerShare; expShares = expShares > stakedShares ? stakedShares : expShares; @@ -224,7 +224,7 @@ contract GraphAdapterTest is Test, GraphAdapter, TestHelpers { abi.encodeCall(IGraphStaking.getDelegation, (validator, address(this))), abi.encode(10 ether, tokensLocked, tokensLockedUntil) ); - vm.mockCall(epochs, abi.encodeCall(IEpochManager.currentEpoch, ()), abi.encode(tokensLockedUntil)); + vm.mockCall(epochs, abi.encodeCall(IGraphEpochManager.currentEpoch, ()), abi.encode(tokensLockedUntil)); vm.mockCall(staking, abi.encodeCall(IGraphStaking.delegationPools, validator), abi.encode(0, 0, 0, 0, 10 ether, 10 ether)); vm.mockCall(staking, abi.encodeCall(IGraphStaking.undelegate, (validator, amount)), abi.encode(0)); @@ -260,7 +260,7 @@ contract GraphAdapterTest is Test, GraphAdapter, TestHelpers { abi.encode(10 ether, 1 ether, unlockEpoch) ); vm.mockCall(staking, abi.encodeCall(IGraphStaking.delegationPools, (validator)), abi.encode(0, 0, 0, 0, 10 ether, 10 ether)); - vm.mockCall(epochs, abi.encodeCall(IEpochManager.currentEpoch, ()), abi.encode(unlockEpoch - 1)); + vm.mockCall(epochs, abi.encodeCall(IGraphEpochManager.currentEpoch, ()), abi.encode(unlockEpoch - 1)); Storage storage $ = _loadStorage(); $.unlocks[unlockID].epoch = unlockEpoch; $.currentEpoch = unlockEpoch + 2; @@ -293,7 +293,7 @@ contract GraphAdapterTest is Test, GraphAdapter, TestHelpers { abi.encodeCall(IGraphStaking.getDelegation, (validator, address(this))), abi.encode(10 ether, 1 ether, unlockEpoch) ); - vm.mockCall(epochs, abi.encodeCall(IEpochManager.currentEpoch, ()), abi.encode(currentEpoch)); + vm.mockCall(epochs, abi.encodeCall(IGraphEpochManager.currentEpoch, ()), abi.encode(currentEpoch)); vm.mockCall(staking, abi.encodeCall(IGraphStaking.delegationPools, validator), abi.encode(0, 0, 0, 0, 10 ether, 10 ether)); Storage storage $ = _loadStorage(); From 860699b17d151140c9162664b7b29f939b1765f1 Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Fri, 8 Dec 2023 16:08:39 +0100 Subject: [PATCH 09/12] [Graph] fix: improve reward and unstake amount accuracy --- src/adapters/GraphAdapter.sol | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/src/adapters/GraphAdapter.sol b/src/adapters/GraphAdapter.sol index 74c136e..cae0432 100644 --- a/src/adapters/GraphAdapter.sol +++ b/src/adapters/GraphAdapter.sol @@ -158,25 +158,10 @@ contract GraphAdapter is Adapter { Epoch memory currentEpoch = $.epochs[currentEpochNum]; IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(validator); - uint256 oldTokensPerShare = $.tokensPerShare; - uint256 _tokensPerShare = delPool.shares != 0 ? delPool.tokens * 1 ether / delPool.shares : 1 ether; - newStake = currentStake; - // Account for rounding error of -1 or +1 - // This occurs due to a slight change in ratio because of new delegations or withdrawals, - // rather than an effective reward or loss - if ( - (_tokensPerShare >= oldTokensPerShare && _tokensPerShare - oldTokensPerShare <= 1) - || (_tokensPerShare < oldTokensPerShare && oldTokensPerShare - _tokensPerShare <= 1) - ) { - return newStake; - } - - $.tokensPerShare = _tokensPerShare; - IGraphStaking.Delegation memory delegation = GRAPH_STAKING.getDelegation(validator, address(this)); - uint256 staked = delegation.shares * _tokensPerShare / 1 ether; + uint256 staked = delegation.shares * delPool.tokens / delPool.shares; // account for stake still to unstake uint256 oldStake = currentStake + currentEpoch.amount; @@ -191,12 +176,9 @@ contract GraphAdapter is Adapter { // because technically it is not unlocked from the Graph either // We do this by adding the rewards to the current epoch currentEpoch.amount += (staked - oldStake) * currentEpoch.amount / oldStake; - } else { - return newStake; + $.epochs[currentEpochNum].amount = currentEpoch.amount; } - $.epochs[currentEpochNum] = currentEpoch; - // rewards is already accounted for in $.epochs[$.currentEpoch].amount newStake = staked - currentEpoch.amount; } @@ -228,7 +210,8 @@ contract GraphAdapter is Adapter { $.lastEpochUnlockedAt = block.number; // calculate shares to undelegate from The Graph - uint256 undelegationShares = currentEpochAmount * 1 ether / $.tokensPerShare; + IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(validator); + uint256 undelegationShares = currentEpochAmount * delPool.shares / delPool.tokens; // account for possible rounding error undelegationShares = del.shares < undelegationShares ? del.shares : undelegationShares; From f93412e634adb4a8e8e54d21fd463f6191d33f2b Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Mon, 11 Dec 2023 13:57:12 +0100 Subject: [PATCH 10/12] [tests] fix: fixup GraphAdapter and Tenderizer tests --- src/adapters/GraphAdapter.sol | 2 - test/adapters/GraphAdapter.t.sol | 74 +++++++++++++++++++------------- test/tenderizer/Tenderizer.t.sol | 18 +++++--- 3 files changed, 54 insertions(+), 40 deletions(-) diff --git a/src/adapters/GraphAdapter.sol b/src/adapters/GraphAdapter.sol index cae0432..38b72ae 100644 --- a/src/adapters/GraphAdapter.sol +++ b/src/adapters/GraphAdapter.sol @@ -45,7 +45,6 @@ contract GraphAdapter is Adapter { uint256 lastEpochUnlockedAt; mapping(uint256 => Epoch) epochs; mapping(uint256 => Unlock) unlocks; - uint256 tokensPerShare; } function _loadStorage() internal pure returns (Storage storage $) { @@ -212,7 +211,6 @@ contract GraphAdapter is Adapter { // calculate shares to undelegate from The Graph IGraphStaking.DelegationPool memory delPool = GRAPH_STAKING.delegationPools(validator); uint256 undelegationShares = currentEpochAmount * delPool.shares / delPool.tokens; - // account for possible rounding error undelegationShares = del.shares < undelegationShares ? del.shares : undelegationShares; diff --git a/test/adapters/GraphAdapter.t.sol b/test/adapters/GraphAdapter.t.sol index 960b937..85c0af5 100644 --- a/test/adapters/GraphAdapter.t.sol +++ b/test/adapters/GraphAdapter.t.sol @@ -11,8 +11,8 @@ pragma solidity >=0.8.19; -import { Test, stdError } from "forge-std/Test.sol"; -import { GraphAdapter } from "core/adapters/GraphAdapter.sol"; +import { Test } from "forge-std/Test.sol"; +import { GraphAdapter, GRAPH_STAKING, GRAPH_EPOCHS, GRT } from "core/adapters/GraphAdapter.sol"; import { IERC20 } from "core/interfaces/IERC20.sol"; import { IGraphStaking, IGraphEpochManager } from "core/adapters/interfaces/IGraph.sol"; import { TestHelpers } from "test/helpers/Helpers.sol"; @@ -20,9 +20,9 @@ import { TestHelpers } from "test/helpers/Helpers.sol"; // solhint-disable func-name-mixedcase contract GraphAdapterTest is Test, GraphAdapter, TestHelpers { - address private staking = 0xF55041E37E12cD407ad00CE2910B8269B01263b9; - address private epochs = 0x03541c5cd35953CD447261122F93A5E7b812D697; - address private token = 0xc944E90C64B2c07662A292be6244BDf05Cda44a7; + address private staking = address(GRAPH_STAKING); + address private epochs = address(GRAPH_EPOCHS); + address private token = address(GRT); address private validator = vm.addr(1); @@ -34,14 +34,19 @@ contract GraphAdapterTest is Test, GraphAdapter, TestHelpers { uint256 private MAX_UINT_SQRT = sqrt(MAX_UINT - 1); function setUp() public { + vm.etch(staking, bytes("code")); vm.mockCall(staking, abi.encodeCall(IGraphStaking.delegationTaxPercentage, ()), abi.encode(DELEGATION_TAX)); vm.mockCall(staking, abi.encodeCall(IGraphStaking.thawingPeriod, ()), abi.encode(THAWING_PERIOD)); } function testFuzz_PreviewDeposit(uint256 amount) public { - amount = bound(amount, 0, MAX_UINT / DELEGATION_TAX); - vm.expectCall(staking, abi.encodeCall(IGraphStaking.delegationTaxPercentage, ())); - assertEq(this.previewDeposit(validator, amount), amount - amount * DELEGATION_TAX / MAX_PPM); + amount = bound(amount, 1, 10e32); + + vm.mockCall(staking, abi.encodeCall(IGraphStaking.delegationPools, (validator)), abi.encode(0, 0, 0, 0, 1 ether, 1 ether)); + uint256 am = amount - amount * DELEGATION_TAX / MAX_PPM; + uint256 shares = am * 1 ether / 1 ether; + uint256 ev = shares * (1 ether + am) / (1 ether + shares); + assertEq(this.previewDeposit(validator, amount), ev); } function testFuzz_UnlockMaturity(uint256 lastEpochUnlockedAt, uint256 userEpoch) public { @@ -82,13 +87,18 @@ contract GraphAdapterTest is Test, GraphAdapter, TestHelpers { assertEq(this.previewWithdraw(unlockID), unlockShares * epochAmount / epochTotalShares); } - function test_Stake() public { - uint256 amount = 1 ether; + function testFuzz_Stake(uint256 amount) public { + amount = bound(amount, 1, 10e32); vm.mockCall(token, abi.encodeCall(IERC20.approve, (staking, amount)), abi.encode(true)); - vm.mockCall(staking, abi.encodeCall(IGraphStaking.delegate, (validator, amount)), abi.encode(amount)); + uint256 am = amount - amount * DELEGATION_TAX / MAX_PPM; + uint256 shares = am * 1 ether / 1 ether; + uint256 ev = shares * (1 ether + am) / (1 ether + shares); + vm.mockCall(staking, abi.encodeCall(IGraphStaking.delegate, (validator, amount)), abi.encode(shares)); + vm.mockCall(staking, abi.encodeCall(IGraphStaking.delegationPools, (validator)), abi.encode(0, 0, 0, 0, 1 ether, 1 ether)); vm.expectCall(token, abi.encodeCall(IERC20.approve, (staking, amount))); vm.expectCall(staking, abi.encodeCall(IGraphStaking.delegate, (validator, amount))); - this.stake(validator, amount); + uint256 staked = this.stake(validator, amount); + assertEq(staked, ev, "invalid staked amount"); } function test_Stake_RevertIfApproveFails() public { @@ -134,40 +144,42 @@ contract GraphAdapterTest is Test, GraphAdapter, TestHelpers { assertEq($.currentEpoch, epoch, "invalid epoch"); } - function testFuzz_Unstake(uint256 currentEpochAmount, uint256 stakedAmount, uint256 stakedShares) public { - uint256 amount = 1 ether; - currentEpochAmount = bound(currentEpochAmount, amount, MAX_UINT_SQRT - amount); - stakedAmount = bound(stakedAmount, 1, MAX_UINT_SQRT); - stakedShares = bound(stakedShares, stakedAmount, MAX_UINT_SQRT); + function testFuzz_Unstake(uint256 amount, uint256 stakedAmount, uint256 stakedShares) public { + stakedAmount = bound(stakedAmount, 1 ether, 10e32); + amount = bound(amount, 1, stakedAmount); + stakedShares = bound(stakedShares, 1 ether, 10e32); uint256 epoch = 1; uint256 lastUnlockID = 1; Storage storage $ = _loadStorage(); $.currentEpoch = epoch; - $.epochs[epoch].amount = currentEpochAmount; - $.epochs[epoch].totalShares = currentEpochAmount; $.lastUnlockID = lastUnlockID; - $.tokensPerShare = stakedAmount * 1 ether / stakedShares; - $.tokensPerShare = $.tokensPerShare == 0 ? 1 ether : $.tokensPerShare; + + // No unlock processing (del.tokensLockedUntil = 0) + // and currentEpoch.amount != 0 since we undelegate `amount` + // => undelegate currentEpoch.amount + // For this test epoch shares are 1:1 to epoch amounts vm.mockCall( - staking, abi.encodeCall(IGraphStaking.getDelegation, (validator, address(this))), abi.encode(stakedShares, 0, 0) + staking, abi.encodeCall(IGraphStaking.delegationPools, (validator)), abi.encode(0, 0, 0, 0, stakedAmount, stakedShares) ); + vm.mockCall( + staking, abi.encodeCall(IGraphStaking.getDelegation, (validator, address(this))), abi.encode(stakedShares, 0, 0) + ); vm.mockCall(epochs, abi.encodeCall(IGraphEpochManager.currentEpoch, ()), abi.encode(epoch)); - - uint256 expShares = (currentEpochAmount + amount) * 1 ether / $.tokensPerShare; - expShares = expShares > stakedShares ? stakedShares : expShares; - vm.mockCall(staking, abi.encodeCall(IGraphStaking.undelegate, (validator, expShares)), abi.encode(expShares)); - - vm.expectCall(staking, abi.encodeCall(IGraphStaking.undelegate, (validator, expShares))); + uint256 expUndelegateShares = amount * stakedShares / stakedAmount; + vm.mockCall( + staking, abi.encodeCall(IGraphStaking.undelegate, (validator, expUndelegateShares)), abi.encode(expUndelegateShares) + ); + vm.expectCall(staking, abi.encodeCall(IGraphStaking.undelegate, (validator, expUndelegateShares))); this.unstake(validator, amount); assertEq($.currentEpoch, epoch + 1, "invalid epoch"); assertEq($.lastEpochUnlockedAt, block.number, "invalid lastEpochUnlockedAt"); assertEq($.unlocks[lastUnlockID + 1].shares, amount, "invalid unlock shares"); assertEq($.unlocks[lastUnlockID + 1].epoch, epoch, "invalid unlock epoch"); - assertEq($.epochs[epoch].amount, currentEpochAmount + amount, "invalid epoch amount"); - assertEq($.epochs[epoch].totalShares, currentEpochAmount + amount, "invalid epoch shares"); + assertEq($.epochs[epoch].amount, amount, "invalid epoch amount"); + assertEq($.epochs[epoch].totalShares, amount, "invalid epoch shares"); } function test_Withdraw_LastTwoEpochsEmpty() public { @@ -186,6 +198,7 @@ contract GraphAdapterTest is Test, GraphAdapter, TestHelpers { function test_Withdraw_PreviousEpochEmpty() public { uint256 amount = 1 ether; + vm.mockCall(staking, abi.encodeCall(IGraphStaking.delegationPools, (validator)), abi.encode(0, 0, 0, 0, amount, amount)); // should call `undelegate` but not `withdrawDelegation` vm.mockCall(staking, abi.encodeCall(IGraphStaking.getDelegation, (validator, address(this))), abi.encode(amount, 0, 0)); @@ -196,7 +209,6 @@ contract GraphAdapterTest is Test, GraphAdapter, TestHelpers { $.unlocks[0].shares = amount; $.epochs[0].totalShares = amount; $.epochs[0].amount = amount; - $.tokensPerShare = 1 ether; uint256 epoch = 2; $.currentEpoch = epoch; diff --git a/test/tenderizer/Tenderizer.t.sol b/test/tenderizer/Tenderizer.t.sol index 12324f5..4155a1b 100644 --- a/test/tenderizer/Tenderizer.t.sol +++ b/test/tenderizer/Tenderizer.t.sol @@ -52,6 +52,11 @@ contract TenderizerSetup is Test, TestHelpers { string internal symbol = "FOO"; function setUp() public { + vm.etch(registry, bytes("code")); + vm.etch(adapter, bytes("code")); + vm.etch(asset, bytes("code")); + vm.etch(staking, bytes("code")); + vm.etch(unlocks, bytes("code")); // Setup global mock responses vm.mockCall(registry, abi.encodeCall(Registry.adapter, (asset)), abi.encode(adapter)); vm.mockCall(registry, abi.encodeCall(Registry.fee, (asset)), abi.encode(0.05 ether)); @@ -146,17 +151,15 @@ contract TenderizerTest is TenderizerSetup, TenderizerEvents { amountOut = bound(amountOut, 1, MAX_UINT_SQRT); vm.mockCall(adapter, abi.encodeCall(Adapter.rebase, (validator, 0)), abi.encode(0)); - vm.mockCall(adapter, abi.encodeCall(Adapter.previewDeposit, (validator, amountIn)), abi.encode(amountOut)); vm.mockCall(asset, abi.encodeCall(IERC20.transferFrom, (account1, address(tenderizer), amountIn)), abi.encode(true)); - + vm.mockCall(adapter, abi.encodeCall(Adapter.stake, (validator, amountIn)), abi.encode(amountOut)); vm.expectCall(adapter, abi.encodeCall(Adapter.rebase, (validator, 0))); vm.expectCall(asset, abi.encodeCall(IERC20.transferFrom, (account1, address(tenderizer), amountIn))); - vm.expectCall(adapter, abi.encodeCall(Adapter.previewDeposit, (validator, amountIn))); vm.expectCall(adapter, abi.encodeCall(Adapter.stake, (validator, amountIn))); + vm.prank(account1); + vm.expectEmit(true, true, true, true); emit Deposit(account1, account2, amountIn, amountOut); - - vm.prank(account1); uint256 actualAssets = tenderizer.deposit(account2, amountIn); assertEq(actualAssets, amountOut, "invalid return value"); @@ -178,7 +181,7 @@ contract TenderizerTest is TenderizerSetup, TenderizerEvents { function test_Deposit_RevertIfZeroAmount() public { vm.mockCall(adapter, abi.encodeCall(Adapter.rebase, (validator, 0)), abi.encode(0)); - vm.mockCall(adapter, abi.encodeCall(Adapter.previewDeposit, (validator, 0)), abi.encode(0)); + vm.mockCall(adapter, abi.encodeCall(Adapter.stake, (validator, 0)), abi.encode(0)); vm.expectRevert(TToken.ZeroAmount.selector); tenderizer.deposit(account1, 0); } @@ -326,7 +329,6 @@ contract TenderizerTest is TenderizerSetup, TenderizerEvents { _deposit(account2, deposit2, deposit1); vm.mockCall(adapter, abi.encodeCall(Adapter.rebase, (validator, totalDeposit)), abi.encode(newStake)); - vm.mockCall(registry, abi.encodeCall(Registry.fee, (asset)), abi.encode(feeRate)); uint256 cappedFeeRate = feeRate > MAX_FEE ? MAX_FEE : feeRate; @@ -391,6 +393,8 @@ contract TenderizerTest is TenderizerSetup, TenderizerEvents { function _deposit(address account, uint256 amount, uint256 totalPreviousDeposits) internal { vm.mockCall(adapter, abi.encodeCall(Adapter.previewDeposit, (validator, amount)), abi.encode(amount)); vm.mockCall(adapter, abi.encodeCall(Adapter.rebase, (validator, totalPreviousDeposits)), abi.encode(totalPreviousDeposits)); + vm.mockCall(adapter, abi.encodeCall(Adapter.stake, (validator, amount)), abi.encode(amount)); + vm.prank(account); tenderizer.deposit(account, amount); } From d9816a6f981a63df5941b738b5a3fae0d6dd0431 Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Thu, 14 Dec 2023 14:21:11 +0100 Subject: [PATCH 11/12] [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" + ); + } +} From e0b49a60ceb0099876de485130b667550763db35 Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Thu, 14 Dec 2023 14:54:12 +0100 Subject: [PATCH 12/12] fixup! [Polygon] feat: mainnet fork tests --- test/adapters/PolygonAdapter.t.sol | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/adapters/PolygonAdapter.t.sol b/test/adapters/PolygonAdapter.t.sol index d07f997..fd71808 100644 --- a/test/adapters/PolygonAdapter.t.sol +++ b/test/adapters/PolygonAdapter.t.sol @@ -31,6 +31,7 @@ contract PolygonAdapterTest is Test { function setUp() public { adapter = new PolygonAdapter(); + vm.etch(validatorShares, bytes("code")); vm.etch(MATIC_STAKE_MANAGER, bytes("code")); // Set default mock calls @@ -55,7 +56,12 @@ contract PolygonAdapterTest is Test { function test_previewDeposit() public { uint256 assets = 100; uint256 expected = assets; - uint256 actual = adapter.previewDeposit(validatorShares, assets); + vm.mockCall(MATIC_STAKE_MANAGER, abi.encodeCall(IPolygonStakeManager.delegatedAmount, (validatorId)), abi.encode(1 ether)); + vm.mockCall( + validatorShares, abi.encodeCall(IPolygonValidatorShares.exchangeRate, ()), abi.encode(EXCHANGE_RATE_PRECISION_HIGH) + ); + vm.mockCall(validatorShares, abi.encodeCall(IPolygonValidatorShares.totalSupply, ()), abi.encode(1 ether)); + uint256 actual = adapter.previewDeposit(address(this), assets); assertEq(actual, expected); }