diff --git a/.gitignore b/.gitignore index 855edf0..2f01c7d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ out/ !/broadcast /broadcast/*/31337/ /broadcast/*/5/ +/broadcast/*/18231/ /broadcast/**/dry-run/ # Docs diff --git a/README.md b/README.md index 57fd32c..6495496 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Sturgeon contracts -**Under development** - ## Usage +### Compile, test, etc + ```shell forge install forge build @@ -12,15 +12,46 @@ forge coverage forge fmt ``` +### Add liquidator routes + +Example of adding USDC-DAI route for Unreal testnet: + +```shell +cast send -i --legacy --rpc-url https://rpc.unreal.gelato.digital 0xE0D142466d1BF88FE23D5D265d76068077E4D6F0 'addLargestPools((address,address,address,address)[],bool)' '[("0x1933cB66cB5A2b47A93753773C556ab6CA825831","0x95b012C1D02c859dab6b302F4b72941Ba4E3C3C3","0xabAa4C39cf3dF55480292BBDd471E88de8Cc3C97","0x665D4921fe931C0eA1390Ca4e0C422ba34d26169")]' false +``` + +### View underlying share price + +```shell +cast call --rpc-url https://rpc.unreal.gelato.digital 0x35bf701C24357FD0C7F60376044323A2a830ad78 'getLiquidBoxSharePrice(address,address)(uint256)' 0x67048eA97Ca5DFDAe111A2304af1aED5115C7946 0xabAa4C39cf3dF55480292BBDd471E88de8Cc3C97 +``` + ## Deployments +### Unreal testnet + +* Controller 0x4F69329E8dE13aA7EAc664368C5858AF6371FA4c [Blockscout](https://unreal.blockscout.com/address/0x4F69329E8dE13aA7EAc664368C5858AF6371FA4c?tab=contract) +* IFO 0x3222eb4824cEb0E9CcfE11018C83429105dFE00F [Blockscout](https://unreal.blockscout.com/address/0x3222eb4824cEb0E9CcfE11018C83429105dFE00F?tab=contract) +* STGN 0x609e0d74fAB81085283df92B563750624054F8bE [Blockscout](https://unreal.blockscout.com/address/0x609e0d74fAB81085283df92B563750624054F8bE?tab=contract) +* veSTGN proxy 0x029Dfd1a79e0AD9305d773fb8F3c01D8eF9b913d [Blockscout](https://unreal.blockscout.com/address/0x029Dfd1a79e0AD9305d773fb8F3c01D8eF9b913d?tab=contract) +* Multigauge proxy 0x5B0Ad247bc0Fac75d76D1337932fc29b1eCb8eE6 [Blockscout](https://unreal.blockscout.com/address/0x5B0Ad247bc0Fac75d76D1337932fc29b1eCb8eE6?tab=contract) +* Factory proxy 0x045c8A060474874c5918717eCd55F07B62C59a90 [Blockscout](https://unreal.blockscout.com/address/0x045c8A060474874c5918717eCd55F07B62C59a90?tab=contract) +* VeDistributor proxy 0xAf95468B1a624605bbFb862B0FB6e9C73Ad847b8 [Blockscout](https://unreal.blockscout.com/address/0xAf95468B1a624605bbFb862B0FB6e9C73Ad847b8?tab=contract) +* Frontend 0xA38588970eD3c17C6De6A77D4E06C914B58A4F30 [Blockscout](https://unreal.blockscout.com/address/0xA38588970eD3c17C6De6A77D4E06C914B58A4F30?tab=contract) +* DepositHelper 0x7c8d0C7B63249A314df84707F8690F62CF625820 [Blockscout](https://unreal.blockscout.com/address/0x7c8d0C7B63249A314df84707F8690F62CF625820?tab=contract) +* Compounder proxy 0x89c06219C24ab4aBd762A49cdE97ce69B05f3EAF [Blockscout](https://unreal.blockscout.com/address/0x89c06219C24ab4aBd762A49cdE97ce69B05f3EAF?tab=contract) + ### Goerli testnet -* Controller 0xE3f1d1B8ea9721FF0399cF6c2990A4bE5e4fc023 -* IFO 0xE0D142466d1BF88FE23D5D265d76068077E4D6F0 -* STGN 0x95b012C1D02c859dab6b302F4b72941Ba4E3C3C3 -* veSTGN proxy 0x33222Ee7eAb1aBE6fC1724eAce207fA3Fa62C7C3 -* Multigauge 0xee7751bF946Da4cbb39A76fd8dD99a8872871a7F -* IFO Harvester 0x4e02AbD5Aa2731bdD1655D3a0a936912Ea5f0857 -* Harvester 0x9FeCf0827e7253F4dd3e9bF92cDd7A0ebC547D16 -* CompunderVault 0x0A45e97ACEBa96650F47DA979bde3A8642f26739 +* Controller 0x8216C9afFC982428aF33D1D9F165bAf9D75AebBa +* IFO 0x029Dfd1a79e0AD9305d773fb8F3c01D8eF9b913d +* STGN 0x5B0Ad247bc0Fac75d76D1337932fc29b1eCb8eE6 +* veSTGN proxy 0x87eDeA5aea52BA12Ebf4eBc253Ec3218C1090C70 +* Multigauge 0xAf95468B1a624605bbFb862B0FB6e9C73Ad847b8 +* Factory 0xBD5296DC2603942F116B375c8Ee373674be86f56 +* VeDistributor proxy 0x7c8d0C7B63249A314df84707F8690F62CF625820 +* MockGauge 0x54F22378E03BeA25a05A071b60357d31Ce535Bb9 +* MockLiquidator 0x97B56FEAdA7fb2D7A0A8576635f05314f184f0C2 +* MockA 0xBcA14CF8Cc2417a5B4ed242bA45aE4835aF4d5Df +* MockC 0x609e0d74fAB81085283df92B563750624054F8bE +* MockD 0x635B1F7dD7d0172533BA9fE5Cfe2D83D9848f701 diff --git a/chains/TestnetLib.sol b/chains/GoerliLib.sol similarity index 88% rename from chains/TestnetLib.sol rename to chains/GoerliLib.sol index c76fe7f..8afb5cf 100644 --- a/chains/TestnetLib.sol +++ b/chains/GoerliLib.sol @@ -5,10 +5,10 @@ import {console} from "forge-std/Test.sol"; import "../script/lib/DeployLib.sol"; import "../test/mock/MockTetuLiquidator.sol"; -library TestnetLib { +library GoerliLib { address public constant TOKEN_WETH = 0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9; - function runDeploy(bool showLog) internal returns(address) { + function runDeploy(address rewardToken, bool showLog) internal returns(address) { address governance = 0x3d0c177E035C30bb8681e5859EB98d114b48b935; // test deployer address[] memory vestingClaimant = new address[](3); uint[] memory vestingAmount = new uint[](3); @@ -17,7 +17,7 @@ library TestnetLib { vestingClaimant[2] = 0xcc16d636dD05b52FF1D8B9CE09B09BC62b11412B; // Tetu vestingAmount[0] = 375_000e18; vestingAmount[1] = 375_000e18; - vestingAmount[1] = 250_000e18; + vestingAmount[2] = 250_000e18; MockTetuLiquidator l = new MockTetuLiquidator(); @@ -30,7 +30,7 @@ library TestnetLib { vestingAmount: vestingAmount, vestingPeriod: 365 days, vestingCliff: 180 days, - rewardToken: TOKEN_WETH, + rewardToken: rewardToken, liquidator: address(l) }) ) diff --git a/chains/PolygonLib.sol b/chains/PolygonLib.sol index 8d1eab1..94b47b2 100644 --- a/chains/PolygonLib.sol +++ b/chains/PolygonLib.sol @@ -16,7 +16,7 @@ library PolygonLib { vestingClaimant[2] = 0xcc16d636dD05b52FF1D8B9CE09B09BC62b11412B; // Tetu vestingAmount[0] = 375_000e18; vestingAmount[1] = 375_000e18; - vestingAmount[1] = 250_000e18; + vestingAmount[2] = 250_000e18; Controller _c = Controller( DeployLib.deployPlatform( diff --git a/chains/UnrealLib.sol b/chains/UnrealLib.sol new file mode 100644 index 0000000..1b38e3b --- /dev/null +++ b/chains/UnrealLib.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.21; + +import {console} from "forge-std/Test.sol"; +import "../script/lib/DeployLib.sol"; + +library UnrealLib { + // Unreal tokens + address public constant TOKEN_USDC = 0xabAa4C39cf3dF55480292BBDd471E88de8Cc3C97; + address public constant TOKEN_DAI = 0x665D4921fe931C0eA1390Ca4e0C422ba34d26169; + address public constant TOKEN_PEARL = 0x1ef116600bBb2e99Ce6CE96B7E66A0df71AF5980; + address public constant TOKEN_CVR = 0xC716C749106B21aa4e8231b7ec891bdEab9bFB30; + + // Pearl DeX + address public constant POOL_PEARL_CVR_3000 = 0x6592E84E1903B990C5015F1Ff1A6cc27405EABfB; + address public constant POOL_DAI_USDC_1000 = 0x1933cB66cB5A2b47A93753773C556ab6CA825831; + address public constant POOL_DAI_PEARL_3000 = 0xeC491B6bC5554f76348FB40eEfbf0Ed60cd22Bd2; + address public constant POOL_PEARL_USDC_3000 = 0x76994b9683e17B0A9Ed344fCD432A8E3BF7E22C1; + // Trident TDT-DAI-USDC + address public constant LIQUID_BOX_DAI_USDC = 0x8B9184243B8a787eaff8C304b17ED23fFD6F8c23; + + // IGaugeV2ALM + address public constant ALM_GAUGE_DAI_USDC = 0x659f401aE8194e00673fe58367Ed77137542faA3; + + // Sturgeon infrastructure + address public constant LIQUIDATOR = 0xE0D142466d1BF88FE23D5D265d76068077E4D6F0; + + function runDeploy(bool showLog) internal returns(address) { + address governance = 0x3d0c177E035C30bb8681e5859EB98d114b48b935; // test deployer + address[] memory vestingClaimant = new address[](3); + uint[] memory vestingAmount = new uint[](3); + vestingClaimant[0] = 0x520Ab98a23100369E5280d214799b1E1c0123045; // Claw + vestingClaimant[1] = 0xe25e4df0432Ea55Fd76816fD8d4A21226dEE4bFF; // Minion + vestingClaimant[2] = 0xcc16d636dD05b52FF1D8B9CE09B09BC62b11412B; // Tetu + vestingAmount[0] = 375_000e18; + vestingAmount[1] = 375_000e18; + vestingAmount[2] = 250_000e18; + + Controller _c = Controller( + DeployLib.deployPlatform( + DeployLib.DeployParams({ + governance: governance, + ifoRate: 12e17, + vestingClaimant: vestingClaimant, + vestingAmount: vestingAmount, + vestingPeriod: 365 days, + vestingCliff: 180 days, + rewardToken: TOKEN_PEARL, + liquidator: LIQUIDATOR + }) + ) + ); + + if (showLog) { + console.log("Deployed. Controller:", address(_c)); + } + + return address(_c); + } + + function testA() public {} +} diff --git a/script/Deploy.Goerli.s.sol b/script/Deploy.Goerli.s.sol new file mode 100644 index 0000000..f9fd6f3 --- /dev/null +++ b/script/Deploy.Goerli.s.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.21; + +import "forge-std/Script.sol"; +import "../chains/GoerliLib.sol"; +import "../src/HarvesterVault.sol"; +import "../src/PearlStrategy.sol"; +import "../src/CompounderVault.sol"; +import "../test/mock/MockERC20.sol"; +import "../test/mock/MockPearlGaugeV2.sol"; + +contract DeployGoerli is Script { + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + console.log("deployer", vm.addr(deployerPrivateKey)); + + // deploy mocks + address tokenA = address(new MockERC20("Mock Token A", "MOCK_A", 18)); + address tokenC = address(new MockERC20("Mock Token C", "MOCK_C", 18)); + address tokenD = address(new MockERC20("Mock Token D", "MOCK_D", 18)); + IGaugeV2ALM pearlGauge = IGaugeV2ALM(address(new MockPearlGaugeV2(tokenA, tokenC))); + + IController controller = IController(GoerliLib.runDeploy(tokenC, true)); + Factory factory = Factory(controller.factory()); + + // deploy IFO harvester + factory.deployIfoHarvester(tokenA, address(pearlGauge), "IFO Harvester MOCK_A", "xTokenA"); + + // deploy compounder + harvester + CompounderVault compounderVault = + CompounderVault(factory.deployCompounder(tokenD, "Compounder vault for xTokenA", "xxTokenA")); + factory.deployHarvester(tokenA, address(pearlGauge), "Harvester MOCK_A", "xTokenA", address(compounderVault)); + + vm.stopBroadcast(); + } + + function testDeployPolygon() external {} +} diff --git a/script/Deploy.Testnet.s.sol b/script/Deploy.Testnet.s.sol deleted file mode 100644 index 46b3008..0000000 --- a/script/Deploy.Testnet.s.sol +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.21; - -import "forge-std/Script.sol"; -import "../chains/TestnetLib.sol"; -import "../src/HarvesterVault.sol"; -import "../src/PearlStrategy.sol"; -import "../src/CompounderVault.sol"; -import "../test/mock/MockERC20.sol"; -import "../test/mock/MockPearlGaugeV2.sol"; - -contract DeployTestnet is Script { - function run() external { - uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(deployerPrivateKey); - console.log("deployer", vm.addr(deployerPrivateKey)); - IController controller = IController(TestnetLib.runDeploy(true)); - - // deploy mocks - address tokenA = address(new MockERC20("Mock Token A", "MOCK_A", 18)); - address tokenC = address(new MockERC20("Mock Token C", "MOCK_C", 18)); - address tokenD = address(new MockERC20("Mock Token D", "MOCK_D", 18)); - IPearlGaugeV2 pearlGauge = IPearlGaugeV2(address(new MockPearlGaugeV2(tokenA, tokenC))); - - // deploy IFO harvester - HarvesterVault vault = - new HarvesterVault(address(controller), IERC20(tokenA), "IFO Harvester MOCK_A", "xTokenA", 4_000); - PearlStrategy strategy = new PearlStrategy(address(vault), address(pearlGauge), true, address(0)); - vault.setStrategy(address(strategy)); - IGauge(controller.multigauge()).addStakingToken(address(vault)); - - // deploy compounder + harvester - vault = new HarvesterVault(address(controller), IERC20(tokenA), "Harvester MOCK_A", "xTokenA", 4_000); - - CompounderVault compounderVault = - new CompounderVault(IERC20(tokenD), "Compounder vault for xTokenA", "xxTokenA"); - - strategy = new PearlStrategy(address(vault), address(pearlGauge), false, address(compounderVault)); - vault.setStrategy(address(strategy)); - - IGauge(controller.multigauge()).addStakingToken(address(vault)); - IMultiPool(controller.multigauge()).registerRewardToken(address(vault), address(compounderVault)); - - vm.stopBroadcast(); - } - - function testDeployPolygon() external {} -} diff --git a/script/Deploy.Unreal.s.sol b/script/Deploy.Unreal.s.sol new file mode 100644 index 0000000..00ed90e --- /dev/null +++ b/script/Deploy.Unreal.s.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.21; + +import "forge-std/Script.sol"; +import "../chains/UnrealLib.sol"; +import "../src/HarvesterVault.sol"; +import "../src/PearlStrategy.sol"; +import "../src/CompounderVault.sol"; +import "../test/mock/MockERC20.sol"; +import "../test/mock/MockPearlGaugeV2.sol"; + +contract DeployUnreal is Script { + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + console.log("deployer", vm.addr(deployerPrivateKey)); + + IController controller = IController(UnrealLib.runDeploy(true)); + + Factory factory = Factory(controller.factory()); + + // deploy IFO harvester + factory.deployIfoHarvester( + UnrealLib.LIQUID_BOX_DAI_USDC, UnrealLib.ALM_GAUGE_DAI_USDC, "IFO Harvester DAI-USDC", "ifoTDT-DAI-USDC" + ); + + // deploy compounder + harvester + CompounderVault compounderVault = + CompounderVault(factory.deployCompounder(UnrealLib.TOKEN_CVR, "Compounder CVR", "cCVR")); + factory.deployHarvester( + UnrealLib.LIQUID_BOX_DAI_USDC, + UnrealLib.ALM_GAUGE_DAI_USDC, + "Harvester DAI-USDC", + "xTDT-DAI-USDC", + address(compounderVault) + ); + + vm.stopBroadcast(); + } + + function testDeployPolygon() external {} +} diff --git a/script/DeployCompounder.Unreal.s.sol b/script/DeployCompounder.Unreal.s.sol new file mode 100644 index 0000000..0f695bd --- /dev/null +++ b/script/DeployCompounder.Unreal.s.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.21; + +import "forge-std/Script.sol"; +import "../src/Frontend.sol"; +import "../src/ControllableProxy.sol"; +import "../src/Compounder.sol"; + +contract DeployCompounderUnreal is Script { + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + ControllableProxy proxy = new ControllableProxy(); + address impl = address(new Compounder()); + proxy.initProxy(impl); + Compounder compounder = Compounder(address(proxy)); + compounder.init(0x4F69329E8dE13aA7EAc664368C5858AF6371FA4c); + vm.stopBroadcast(); + } + + function testDeployFrontendTestnet() external {} +} diff --git a/script/DeployDepositHelper.s.sol b/script/DeployDepositHelper.s.sol new file mode 100644 index 0000000..fad36d7 --- /dev/null +++ b/script/DeployDepositHelper.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.21; + +import "forge-std/Script.sol"; +import "../src/DepositHelper.sol"; + +contract DeployDepositHelper is Script { + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + new DepositHelper(); + vm.stopBroadcast(); + } + + function testDeploy_() external {} +} diff --git a/script/DeployFrontend.Goerli.s.sol b/script/DeployFrontend.Goerli.s.sol new file mode 100644 index 0000000..580ef25 --- /dev/null +++ b/script/DeployFrontend.Goerli.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.21; + +import "forge-std/Script.sol"; +import "../src/Frontend.sol"; + +contract DeployFrontendGoerli is Script { + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + new Frontend(0x8216C9afFC982428aF33D1D9F165bAf9D75AebBa); + vm.stopBroadcast(); + } + + function testDeployFrontendTestnet() external {} +} diff --git a/script/DeployFrontend.Unreal.s.sol b/script/DeployFrontend.Unreal.s.sol new file mode 100644 index 0000000..f969664 --- /dev/null +++ b/script/DeployFrontend.Unreal.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.21; + +import "forge-std/Script.sol"; +import "../src/Frontend.sol"; + +contract DeployFrontendUnreal is Script { + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + new Frontend(0x4F69329E8dE13aA7EAc664368C5858AF6371FA4c); + vm.stopBroadcast(); + } + + function testDeployFrontendTestnet() external {} +} diff --git a/src/DepositHelper.sol b/src/DepositHelper.sol new file mode 100644 index 0000000..11e863c --- /dev/null +++ b/src/DepositHelper.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.21; + +import "openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import "./interfaces/IVe.sol"; + +contract DepositHelper is ReentrancyGuard { + using SafeERC20 for IERC20; + + function createLock( + IVe ve, + address token, + uint value, + uint lockDuration + ) external nonReentrant returns (uint tokenId, uint lockedAmount, uint power, uint unlockDate) { + IERC20(token).safeTransferFrom(msg.sender, address(this), value); + _approveIfNeeds(token, value, address(ve)); + tokenId = ve.createLockFor(token, value, lockDuration, msg.sender); + + lockedAmount = ve.lockedAmounts(tokenId, token); + power = ve.balanceOfNFT(tokenId); + unlockDate = ve.lockedEnd(tokenId); + + _sendRemainingToken(token); + } + + function increaseAmount( + IVe ve, + address token, + uint tokenId, + uint value + ) external nonReentrant returns (uint lockedAmount, uint power, uint unlockDate) { + IERC20(token).safeTransferFrom(msg.sender, address(this), value); + _approveIfNeeds(token, value, address(ve)); + ve.increaseAmount(token, tokenId, value); + + lockedAmount = ve.lockedAmounts(tokenId, token); + power = ve.balanceOfNFT(tokenId); + unlockDate = ve.lockedEnd(tokenId); + + _sendRemainingToken(token); + } + + function _approveIfNeeds(address token, uint amount, address spender) internal { + if (IERC20(token).allowance(address(this), spender) < amount) { + IERC20(token).forceApprove(spender, type(uint).max); + } + } + + function _sendRemainingToken(address token) internal { + uint balance = IERC20(token).balanceOf(address(this)); + if (balance != 0) { + IERC20(token).safeTransfer(msg.sender, balance); + } + } +} diff --git a/src/Factory.sol b/src/Factory.sol index 67b0283..d7c5ec9 100644 --- a/src/Factory.sol +++ b/src/Factory.sol @@ -37,6 +37,7 @@ contract Factory is Controllable { string calldata vaultSymbol ) external onlyGovernance returns (address compounder) { compounder = address(new CompounderVault(IERC20(underlying), vaultName, vaultSymbol)); + IController(controller()).registerVault(compounder, false); } function deployHarvester( @@ -53,6 +54,7 @@ contract Factory is Controllable { address multigauge = IController(_controller).multigauge(); IGauge(multigauge).addStakingToken(vault); IMultiPool(multigauge).registerRewardToken(vault, compounderVault); + IController(_controller).registerVault(vault, true); } function _requireGovernance() internal view { diff --git a/src/Frontend.sol b/src/Frontend.sol new file mode 100644 index 0000000..2053121 --- /dev/null +++ b/src/Frontend.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.21; + +import "./interfaces/IController.sol"; +import "./interfaces/IVault.sol"; +import "./PearlStrategy.sol"; +import "./interfaces/IMultiPool.sol"; +import "./base/StakelessMultiPoolBase.sol"; +import "./interfaces/ILiquidBox.sol"; + +contract Frontend { + IController public controller; + + struct LocalVars { + address[] harvesters; + address[] compounders; + uint harvestersLen; + uint compoundersLen; + uint totalLen; + address stgn; + StakelessMultiPoolBase gauge; + } + + constructor(address controller_) { + controller = IController(controller_); + } + + function getLiquidBoxSharePrice(address alm, address priceToken) public view returns (uint price) { + ITetuLiquidator liquidator = ITetuLiquidator(controller.liquidator()); + ILiquidBox lb = ILiquidBox(alm); + address token0 = lb.token0(); + address token1 = lb.token1(); + (uint total0, uint total1,) = lb.getTotalAmounts(); + uint total0Priced; + uint total1Priced; + if (token0 != priceToken) { + uint decimals = IERC20Metadata(token0).decimals(); + total0Priced = liquidator.getPrice(token0, priceToken, 10 ** decimals) * total0 / 10 ** decimals; + } else { + total0Priced = total0; + } + if (token1 != priceToken) { + uint decimals = IERC20Metadata(token1).decimals(); + total1Priced = liquidator.getPrice(token1, priceToken, 10 ** decimals) * total1 / 10 ** decimals; + } else { + total1Priced = total0; + } + uint totalSupply = lb.totalSupply(); + if (totalSupply > 0) { + price = (total0Priced + total1Priced) * 1e18 / totalSupply; + } + } + + function allVaults( + address user, + address priceToken + ) + external + view + returns ( + address[] memory vaults, + address[] memory compounders, + address[] memory underlyings, + string[6][] memory strings, // vault name, vault symbol, compounder name, compounder symbol, underlying name, underlying symbol + address[] memory strategies, + uint[3][] memory nums, // totalAssets,decimals,sharePrice + uint[2][] memory balances, // vault user balance, underlying user balance + uint[3][] memory gaugeLeft // left, periodFinish, earned + ) + { + LocalVars memory v; + v.gauge = StakelessMultiPoolBase(controller.multigauge()); + v.stgn = controller.stgn(); + v.harvesters = controller.harvesterVaultsList(); + v.compounders = controller.compounderVaultsList(); + v.harvestersLen = v.harvesters.length; + v.compoundersLen = v.compounders.length; + v.totalLen = v.harvestersLen + v.compoundersLen; + vaults = new address[](v.totalLen); + compounders = new address[](v.totalLen); + underlyings = new address[](v.totalLen); + strategies = new address[](v.totalLen); + strings = new string[6][](v.totalLen); + nums = new uint[3][](v.totalLen); + balances = new uint[2][](v.totalLen); + gaugeLeft = new uint[3][](v.totalLen); + for (uint i; i < v.harvestersLen; ++i) { + vaults[i] = v.harvesters[i]; + underlyings[i] = IERC4626(vaults[i]).asset(); + strategies[i] = address(IVault(vaults[i]).strategy()); + compounders[i] = PearlStrategy(strategies[i]).compounder(); + nums[i][0] = IERC4626(vaults[i]).totalAssets(); + nums[i][1] = IERC20Metadata(vaults[i]).decimals(); + nums[i][2] = getLiquidBoxSharePrice(underlyings[i], priceToken); + + strings[i][0] = IERC20Metadata(vaults[i]).name(); + strings[i][1] = IERC20Metadata(vaults[i]).symbol(); + strings[i][4] = IERC20Metadata(underlyings[i]).name(); + strings[i][5] = IERC20Metadata(underlyings[i]).symbol(); + + if (compounders[i] != address(0)) { + strings[i][2] = IERC20Metadata(compounders[i]).name(); + strings[i][3] = IERC20Metadata(compounders[i]).symbol(); + gaugeLeft[i][0] = v.gauge.left(vaults[i], compounders[i]); + gaugeLeft[i][1] = v.gauge.periodFinish(vaults[i], compounders[i]); + if (user != address(0)) { + gaugeLeft[i][2] = v.gauge.earned(vaults[i], compounders[i], user); + } + } else { + gaugeLeft[i][0] = v.gauge.left(vaults[i], v.stgn); + gaugeLeft[i][1] = v.gauge.periodFinish(vaults[i], v.stgn); + if (user != address(0)) { + gaugeLeft[i][2] = v.gauge.earned(vaults[i], v.stgn, user); + } + } + } + + if (user != address(0)) { + for (uint i; i < v.harvestersLen; ++i) { + balances[i][0] = IERC20(vaults[i]).balanceOf(user); + balances[i][1] = IERC20(underlyings[i]).balanceOf(user); + } + } + + uint k; + for (uint i = v.harvestersLen; i < v.totalLen; ++i) { + vaults[i] = v.compounders[k]; + underlyings[i] = IERC4626(vaults[i]).asset(); + nums[i][0] = IERC4626(vaults[i]).totalAssets(); + nums[i][1] = IERC20Metadata(vaults[i]).decimals(); + strings[i][0] = IERC20Metadata(vaults[i]).name(); + strings[i][1] = IERC20Metadata(vaults[i]).symbol(); + strings[i][4] = IERC20Metadata(underlyings[i]).name(); + strings[i][5] = IERC20Metadata(underlyings[i]).symbol(); + ++k; + } + + if (user != address(0)) { + for (uint i = v.harvestersLen; i < v.totalLen; ++i) { + balances[i][0] = IERC20(vaults[i]).balanceOf(user); + balances[i][1] = IERC20(underlyings[i]).balanceOf(user); + } + } + } +} diff --git a/src/PearlStrategy.sol b/src/PearlStrategy.sol index 4189e86..4599519 100644 --- a/src/PearlStrategy.sol +++ b/src/PearlStrategy.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.21; import "./base/StrategyStrictBase.sol"; import "./interfaces/IVault.sol"; -import "./interfaces/IPearlGaugeV2.sol"; +import "./interfaces/IGaugeV2ALM.sol"; import "./interfaces/IController.sol"; import "./interfaces/IIFO.sol"; import "./interfaces/IGauge.sol"; @@ -41,7 +41,7 @@ contract PearlStrategy is StrategyStrictBase { compounder = compounder_; IERC20(asset).approve(gauge_, type(uint).max); IController controller = IController(IVault(vault).controller()); - address _gaugeRewardToken = IPearlGaugeV2(gauge_).rewardToken(); + address _gaugeRewardToken = IGaugeV2ALM(gauge_).rewardToken(); gaugeRewardToken = _gaugeRewardToken; if (ifo) { IERC20(_gaugeRewardToken).approve(controller.ifo(), type(uint).max); @@ -55,7 +55,7 @@ contract PearlStrategy is StrategyStrictBase { } function isReadyToHardWork() external view returns (bool) { - return IPearlGaugeV2(gauge).earned(address(this)) > 0; + return IGaugeV2ALM(gauge).earnedReward(address(this)) > 0; } function doHardWork() external /* returns (uint earned, uint lost)*/ { @@ -81,7 +81,7 @@ contract PearlStrategy is StrategyStrictBase { if (exchanged && got > 0) { multigauge.notifyRewardAmount(vault, controller.stgn(), got); } /* else { - multigauge.notifyRewardAmount(vault, IPearlGaugeV2(gauge).rewardToken(), rtReward); + multigauge.notifyRewardAmount(vault, IGaugeV2ALM(gauge).rewardToken(), rtReward); }*/ } else { address _compounder = compounder; @@ -98,18 +98,18 @@ contract PearlStrategy is StrategyStrictBase { } function investedAssets() public view override returns (uint) { - return IPearlGaugeV2(gauge).balanceOf(address(this)); + return IGaugeV2ALM(gauge).balanceOf(address(this)); } function _claim() internal override returns (uint rtReward) { IERC20 rt = IERC20(gaugeRewardToken); uint oldBal = rt.balanceOf(address(this)); - IPearlGaugeV2(gauge).getReward(); + IGaugeV2ALM(gauge).collectReward(); rtReward = rt.balanceOf(address(this)) - oldBal; } function _depositToPool(uint amount) internal override { - IPearlGaugeV2(gauge).deposit(amount); + IGaugeV2ALM(gauge).deposit(amount); } function _emergencyExitFromPool() internal override { @@ -118,11 +118,11 @@ contract PearlStrategy is StrategyStrictBase { } function _withdrawFromPool(uint amount) internal override { - IPearlGaugeV2(gauge).withdraw(amount); + IGaugeV2ALM(gauge).withdraw(amount); IERC20(asset).safeTransfer(vault, amount); } function _withdrawAllFromPool() internal override { - _withdrawFromPool(IPearlGaugeV2(gauge).balanceOf(address(this))); + _withdrawFromPool(IGaugeV2ALM(gauge).balanceOf(address(this))); } } diff --git a/src/interfaces/IPearlGaugeV2.sol b/src/interfaces/IGaugeV2ALM.sol similarity index 70% rename from src/interfaces/IPearlGaugeV2.sol rename to src/interfaces/IGaugeV2ALM.sol index 4d636dd..d646dec 100644 --- a/src/interfaces/IPearlGaugeV2.sol +++ b/src/interfaces/IGaugeV2ALM.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.21; -interface IPearlGaugeV2 { - function TOKEN() external view returns (address); +interface IGaugeV2ALM { + function box() external view returns (address); function rewardToken() external view returns (address); function balanceOf(address account) external view returns (uint); ///@notice see earned rewards for user - function earned(address account) external view returns (uint); + function earnedReward(address account) external view returns (uint); ///@notice deposit amount TOKEN function deposit(uint amount) external; @@ -18,5 +18,5 @@ interface IPearlGaugeV2 { function withdraw(uint amount) external; ///@notice User harvest function - function getReward() external; + function collectReward() external; } diff --git a/src/interfaces/ILiquidBox.sol b/src/interfaces/ILiquidBox.sol new file mode 100644 index 0000000..7c6a2f7 --- /dev/null +++ b/src/interfaces/ILiquidBox.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.21; + +interface ILiquidBox { + /** + * @notice Deposits tokens into the vault, distributing them + * in proportion to the current holdings. + * @dev Tokens deposited remain in the vault until the next + * rebalance and are not utilized for liquidity on Pearl. + * @param amount0Desired Maximum amount of token0 to deposit + * @param amount1Desired Maximum amount of token1 to deposit + * @param to Recipient of shares + * @param amount0Min Reverts if the resulting amount0 is less than this + * @param amount1Min Reverts if the resulting amount1 is less than this + * @return shares Number of shares minted + * @return amount0 Amount of token0 deposited + * @return amount1 Amount of token1 deposited + */ + function deposit( + uint amount0Desired, + uint amount1Desired, + address to, + uint amount0Min, + uint amount1Min + ) external returns (uint shares, uint amount0, uint amount1); + + /** + * @notice Withdraws tokens in proportion to the vault's holdings. + * @param shares Shares burned by sender + * @param amount0Min Revert if resulting `amount0` is smaller than this + * @param amount1Min Revert if resulting `amount1` is smaller than this + * @param to Recipient of tokens + * @return amount0 Amount of token0 sent to recipient + * @return amount1 Amount of token1 sent to recipient + */ + function withdraw( + uint shares, + address to, + uint amount0Min, + uint amount1Min + ) external returns (uint amount0, uint amount1); + + /** + * @notice Calculates the vault's total holdings of token0 and token1 - in + * other words, how much of each token the vault would hold if it withdrew + * all its liquidity from Uniswap. + */ + function getTotalAmounts() external view returns (uint total0, uint total1, uint128 liquidity); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint); + + function token0() external view returns (address); + + function token1() external view returns (address); +} diff --git a/src/lib/VeSTGNLib.sol b/src/lib/VeSTGNLib.sol index 6a36eb2..48e5042 100644 --- a/src/lib/VeSTGNLib.sol +++ b/src/lib/VeSTGNLib.sol @@ -375,11 +375,12 @@ library VeSTGNLib { uint _value ) public pure returns (string memory output) { output = - ''; + ''; + output = string( abi.encodePacked( output, - 'ID:', + 'ID:', _u2s(_tokenId), "" ) @@ -387,7 +388,7 @@ library VeSTGNLib { output = string( abi.encodePacked( output, - 'Balance:', + 'Balance:', _u2s(_balanceOf / 1e18), "" ) @@ -395,7 +396,7 @@ library VeSTGNLib { output = string( abi.encodePacked( output, - 'Until unlock:', + 'Locked end:', _u2s(untilEnd / 60 / 60 / 24), " days" ) @@ -403,9 +404,16 @@ library VeSTGNLib { output = string( abi.encodePacked( output, - 'Power:', + 'Value:', _u2s(_value / 1e18), - "" + "" + ) + ); + + output = string( + abi.encodePacked( + output, + ' ' ) ); diff --git a/test/PearlStrategy.t.sol b/test/PearlStrategy.t.sol index a4944bf..09bf5aa 100644 --- a/test/PearlStrategy.t.sol +++ b/test/PearlStrategy.t.sol @@ -23,8 +23,9 @@ contract PearlStrategyTest is MockSetup { vault.redeem(1e18, address(this), address(this)); assertEq(vault.balanceOf(address(this)), 0); - vault.deposit(1e18, address(this)); - assertEq(vault.balanceOf(address(this)), 1e18); + uint depositAmount = 212000; + vault.deposit(depositAmount, address(this)); + assertEq(vault.balanceOf(address(this)), depositAmount); skip(3600); @@ -59,8 +60,10 @@ contract PearlStrategyTest is MockSetup { assertEq(IMultiPool(controller.multigauge()).balanceOf(address(vault), address(this)), 1e18); vault.redeem(1e18, address(this), address(this)); assertEq(vault.balanceOf(address(this)), 0); - vault.deposit(1e18, address(this)); - assertEq(vault.balanceOf(address(this)), 1e18); + + uint depositAmount = 212000; + vault.deposit(depositAmount, address(this)); + assertEq(vault.balanceOf(address(this)), depositAmount); skip(3600); strategy.doHardWork(); deal(tokenC, address(pearlGauge), 1e18); diff --git a/test/mock/MockPearlGaugeV2.sol b/test/mock/MockPearlGaugeV2.sol index 193350d..b6f814b 100644 --- a/test/mock/MockPearlGaugeV2.sol +++ b/test/mock/MockPearlGaugeV2.sol @@ -2,42 +2,43 @@ pragma solidity ^0.8.21; import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import "../../src/interfaces/IPearlGaugeV2.sol"; +import "../../src/interfaces/IGaugeV2ALM.sol"; -contract MockPearlGaugeV2 { - address public TOKEN; +contract MockPearlGaugeV2 is IGaugeV2ALM { + address public box; address public rewardToken; mapping(address accuont => uint balance) public balanceOf; constructor(address TOKEN_, address rewardToken_) { - TOKEN = TOKEN_; + box = TOKEN_; rewardToken = rewardToken_; } ///@notice see earned rewards for user - function earned(address account) public view returns (uint) { - return balanceOf[account] / 1e10; + function earnedReward(address account) public view returns (uint) { + // todo changeable + return balanceOf[account] / 10; } ///@notice deposit amount TOKEN function deposit(uint amount) external { - IERC20(TOKEN).transferFrom(msg.sender, address(this), amount); + IERC20(box).transferFrom(msg.sender, address(this), amount); balanceOf[msg.sender] += amount; } ///@notice withdraw a certain amount of TOKEN function withdraw(uint amount) external { // require(amount <= balanceOf[msg.sender], "MockPearlGaugeV2: not enough balance"); - IERC20(TOKEN).transfer(msg.sender, amount); + IERC20(box).transfer(msg.sender, amount); balanceOf[msg.sender] -= amount; } ///@notice User harvest function - function getReward() external { + function collectReward() external { IERC20 rt = IERC20(rewardToken); - uint _earned = earned(msg.sender); + uint _earned = earnedReward(msg.sender); if (rt.balanceOf(address(this)) >= _earned) { - IERC20(rewardToken).transfer(msg.sender, earned(msg.sender)); + IERC20(rewardToken).transfer(msg.sender, _earned); } } } diff --git a/test/mock/MockTetuLiquidator.sol b/test/mock/MockTetuLiquidator.sol index a576c29..bd2df72 100644 --- a/test/mock/MockTetuLiquidator.sol +++ b/test/mock/MockTetuLiquidator.sol @@ -1,11 +1,24 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.21; +//import {console} from "forge-std/Test.sol"; import "../../src/interfaces/ITetuLiquidator.sol"; import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; contract MockTetuLiquidator is ITetuLiquidator { - function getPrice(address tokenIn, address tokenOut, uint amount) external view returns (uint) {} + mapping(address tokenIn => mapping(address tokenOut => uint price)) public prices; + + function setPrice(address tokenIn, address tokenOut, uint price) external { + prices[tokenIn][tokenOut] = price; + } + + function getPrice(address tokenIn, address tokenOut, uint amount) external view returns (uint) { + if (amount == 0) { + amount = 10 ** IERC20Metadata(tokenIn).decimals(); + } + return prices[tokenIn][tokenOut] * amount / 10 ** IERC20Metadata(tokenIn).decimals(); + } function getPriceForRoute(PoolData[] memory route, uint amount) external view returns (uint) {} @@ -17,7 +30,12 @@ contract MockTetuLiquidator is ITetuLiquidator { ) external view returns (PoolData[] memory route, string memory errorMessage) {} function liquidate(address tokenIn, address tokenOut, uint amount, uint /*priceImpactTolerance*/ ) external { - uint amountOut = amount / 10; + // console.log('liquidate tokenIn', IERC20Metadata(tokenIn).symbol()); + // console.log('liquidate tokenOut', IERC20Metadata(tokenOut).symbol()); + if (amount == 0) { + amount = 10 ** IERC20Metadata(tokenIn).decimals(); + } + uint amountOut = prices[tokenIn][tokenOut] * amount / 10 ** IERC20Metadata(tokenIn).decimals(); require(IERC20(tokenOut).balanceOf(address(this)) >= amountOut, "MockLiquidator: not enough balance"); IERC20(tokenIn).transferFrom(msg.sender, address(this), amount); IERC20(tokenOut).transfer(msg.sender, amountOut); diff --git a/test/setup/MockSetup.sol b/test/setup/MockSetup.sol index 6e80d2a..1e0bd03 100644 --- a/test/setup/MockSetup.sol +++ b/test/setup/MockSetup.sol @@ -12,7 +12,7 @@ import "../../src/VeDistributor.sol"; import "../../src/Vesting.sol"; import "../../script/lib/DeployLib.sol"; import "../mock/MockERC20.sol"; -import "../../src/interfaces/IPearlGaugeV2.sol"; +import "../../src/interfaces/IGaugeV2ALM.sol"; import "../mock/MockPearlGaugeV2.sol"; import "../mock/MockTetuLiquidator.sol"; @@ -32,7 +32,7 @@ abstract contract MockSetup is Test { // CVR (underlying of the first compounder vault) address public tokenD; - IPearlGaugeV2 public pearlGauge; + IGaugeV2ALM public pearlGauge; constructor() { tokenA = address(new MockERC20("Mock Token A", "MOCK_A", 18)); @@ -41,6 +41,8 @@ abstract contract MockSetup is Test { tokenD = address(new MockERC20("Mock Token D", "MOCK_D", 18)); controller = _init(); factory = Factory(controller.factory()); + MockTetuLiquidator l = MockTetuLiquidator(controller.liquidator()); + l.setPrice(tokenC, tokenD, 2e18); } function _init() public returns (Controller) { @@ -67,7 +69,7 @@ abstract contract MockSetup is Test { ); // mock gauge - pearlGauge = IPearlGaugeV2(address(new MockPearlGaugeV2(tokenA, tokenC))); + pearlGauge = IGaugeV2ALM(address(new MockPearlGaugeV2(tokenA, tokenC))); return _c; }