diff --git a/config/riskParameter.json b/config/riskParameter.json index 8f223e2..57690e6 100644 --- a/config/riskParameter.json +++ b/config/riskParameter.json @@ -80,6 +80,15 @@ "WBETH_borrowingEnabled": "true", "WBETH_flashLoanEnabled": "true", + "SMART_LP_HAY_baseLTV": "0", + "SMART_LP_HAY_liquidationThreshold": "0", + "SMART_LP_HAY_liquidationBonus": "0", + "SMART_LP_HAY_reserveFactor": "1500", + "SMART_LP_HAY_borrowCap": "1", + "SMART_LP_HAY_supplyCap": "1000", + "SMART_LP_HAY_stableBorrowingEnabled": "false", + "SMART_LP_HAY_borrowingEnabled": "false", + "SMART_LP_HAY_flashLoanEnabled": "false" "PUSDC_baseLTV": "7500", "PUSDC_liquidationThreshold": "8000", "PUSDC_liquidationBonus": "10800", diff --git a/deployment/mainnet.json b/deployment/mainnet.json index cc4326b..715131c 100644 --- a/deployment/mainnet.json +++ b/deployment/mainnet.json @@ -16,7 +16,7 @@ "sdTokenImpl": "0xc3752D2ce05CD638523CcCaA090EF5e25A2B87B4", "vdTokenImpl": "0x00170FbBC27793837f1b7fb073F91F5ED8dBAEe8", "ReservesSetupHelper": "0xD9C5bdF9C17934d480Dfa47c3c1276458f788f57", - "SupplyLogic": "0x763B2a4EbA91c3667a74Ba87A7142be3282FC1C2", + "SupplyLogic": "0x15e0e9810D2EDa58f63336C397ACdF745D775091", "BorrowLogic": "0xa6265a8cE6F89610d3f97851abcf7F8203B006FB", "LiquidationLogic": "0x54C33c8669D52Bb0cd6682dB419483a1e86D8e67", "EModeLogic": "0xC8CB15Bc73B7f5f95Af33AEce738bC9B62Cb28D9", diff --git a/deployment/testnet.json b/deployment/testnet.json index 1b7076c..7936057 100644 --- a/deployment/testnet.json +++ b/deployment/testnet.json @@ -1,16 +1,19 @@ { "Treasury": "0xa445BC34f142808c5dCB0A68679159CC7Ac58329", "Faucet": "0x02Aabca391725De4F6e9Ea44b0A3C1C7D853253E", + "Treasury": "0xa445BC34f142808c5dCB0A68679159CC7Ac58329", "PoolAddressesProviderRegistry": "0x69Fb711e9362327856587bEa96F1E2d429727C4D", "PoolAddressesProvider": "0x5Add4de8a8577bA3B5fb50Dc89571130a20FaCD8", "PoolDataProvider": "0x8118E91E7B986435c6f0c64aa5e0Ce9c63567A5C", - "PoolImpl":"0x6Cac33DBF58c583653d61A14EBdF83fa4020F4f8", + "PoolImpl": "0x6Cac33DBF58c583653d61A14EBdF83fa4020F4f8", + "Pool2Impl": "0x25Ca395cAB876257993F02f1859754620DE3DCc2", "PoolConfiguratorImpl": "0x13E58BDAFa53184283190b1587F4c7956C1ADC76", "ACLManager": "0x6f2Bb5d7Ff621163159e7cE5F111e85e8108030e", "Oracle": "0xBee779e5998295B514E0A92A1449a83090a06F3F", "EmissionManager": "0x433C25C973812DdA394eD5D17B9cE2330A158E71", "RewardsController": "0xd642291EBBD364679F9387E0C41Cf30444A661e4", "ATokenImpl": "0xACeF8022Ae19723ee9e749EefC5665bA7570f195", + "ATokenWombatStakerImpl": "0x9d574FDe0Fd40447524F04F14c6a896eeb89FE09", "sdTokenImpl": "0x1a637c6282E0B86CB138020747506164C9aCd38F", "vdTokenImpl": "0x67e8C47B383923ac495d3D282BB61a248Ec0a35E", "ReservesSetupHelper": "0x3Fd1eEbB0610D91d2091989730ff46619f15bb66", @@ -35,6 +38,7 @@ "WBNB": "0xae13d989daC2f0dEbFf460aC112a837C89BAa7cd", "WBETH": "0x69Ae9744459A4b717eA8b9839c9B1EEC1067b8b9", "HAY": "0x7adC9A28Fab850586dB99E7234EA2Eb7014950fA", + "SMART_LP_HAY": "0x99D8A5FC39ea381dc340f6C03608A258cb7Cc51D", "WBNB_AGGREGATOR": "0x2514895c72f50D8bd4B4F9b1110F0D6bD2c97526", "BTCB_AGGREGATOR": "0x5741306c21795FdCBb9b265Ea0255F499DFe515C", "ETH_AGGREGATOR": "0x143db3CEEfbdfe5631aDD3E50f7614B6ba708BA7", @@ -45,11 +49,17 @@ "WBETH_AGGREGATOR": "0x84070fa93793Fc26CDc0D783da367713C97Ea786", "Binance_WBETH_AGGREGATOR": "0xc461C3b766Ce39222abdb39FF0eBb5a568a09E68", "Binance_HAY_AGGREGATOR": "0xe866271fcb12c806be73f071fbf9acb5d906b32e", + "Custom_SMART_LP_HAY_AGGREGATOR": "0x153F4694C070fB8E3a904530b45320268bCcf70E", "PUSDT": "0x994B71462Ed45472553fF27B28C9B64Af65c6008", "PBUSD": "0xf67c8D1c43051509568e42D08CBe728192b78301", "PWBNB": "0xaD9570eb4CC4e4F73E36A98E6cE662ed08D89337", "PUSDC" :"0x01ed60D4fF5B4E6042468aFd6145E7F5D09691C1", "PTUSD": "0xA17f4B8BE877fe7eD6257517C28EF6530E2E6cF4", "PTokenGateway": "0xEFc25A66f4Cc5FB053076833cd6279c456bEB85F", - "PTokenNativeGateway": "0x2929138f8d1d327f87442d045aa34527375f5e06" + "PTokenNativeGateway": "0x2929138f8d1d327f87442d045aa34527375f5e06", + "ZeroInterestRateStrategy": "0x303Fe73216250874D1F5B4602de25843A8Addc01", + "EmissionAdmin": "0x1af483123E7C952d533b5BB850c5f533F7E1e149", + "MasterWombat": "0xBE2db4c4a712823CA601612041C19b95d57210Bd", + "WOM": "0x83037C47a4Ba74e18A133610cEDbb4c6fCB57789", + "WomOracle": "0xa3334A9762090E827413A7495AfeCE76F41dFc06" } \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 6fa7bdd..3dfe470 100644 --- a/foundry.toml +++ b/foundry.toml @@ -12,7 +12,7 @@ remappings = [ solc-version = "0.8.10" # See more config options https://github.com/foundry-rs/foundry/tree/master/config libraries = [ - "src/core/protocol/libraries/logic/SupplyLogic.sol:SupplyLogic:0x763B2a4EbA91c3667a74Ba87A7142be3282FC1C2", + "src/core/protocol/libraries/logic/SupplyLogic.sol:SupplyLogic:0x15e0e9810D2EDa58f63336C397ACdF745D775091", "src/core/protocol/libraries/logic/BorrowLogic.sol:BorrowLogic:0xa6265a8cE6F89610d3f97851abcf7F8203B006FB", "src/core/protocol/libraries/logic/LiquidationLogic.sol:LiquidationLogic:0x54C33c8669D52Bb0cd6682dB419483a1e86D8e67", "src/core/protocol/libraries/logic/EModeLogic.sol:EModeLogic:0xC8CB15Bc73B7f5f95Af33AEce738bC9B62Cb28D9", @@ -28,4 +28,10 @@ chapel = { key = "${CHAPEL_ETHERSCAN_API_KEY}", chain=97, url="https://api-testn [default.rpc_endpoints] bsc = "https://bsc-dataseed1.binance.org/" -chapel = "https://data-seed-prebsc-1-s1.binance.org:8545/" \ No newline at end of file +chapel = "https://data-seed-prebsc-1-s1.binance.org:8545/" + +[profile.wombat] +test = 'test/ATokenStaker/WombatStaker' + +[profile.magpie] +test = 'test/ATokenStaker/MagpieStaker' \ No newline at end of file diff --git a/remappings.txt b/remappings.txt index fc0ef33..2480d6e 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1 +1,3 @@ -@openzeppelin/=lib/openzeppelin-contracts/contracts/ \ No newline at end of file +forge-std/=lib/forge-std/src/ +test/=test/ +@openzeppelin/=lib/openzeppelin-contracts/contracts/ diff --git a/script/13-addLPMarket/13.0-test-createMockLP.s.sol b/script/13-addLPMarket/13.0-test-createMockLP.s.sol new file mode 100644 index 0000000..078cd82 --- /dev/null +++ b/script/13-addLPMarket/13.0-test-createMockLP.s.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +import "forge-std/Script.sol"; +import "../../src/periphery/mocks/testnet-helpers/TestnetERC20.sol"; +import "../../src/periphery/mocks/testnet-helpers/Faucet.sol"; + +contract DeployMockTokens is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address f = vm.envAddress("Faucet"); + vm.startBroadcast(deployerPrivateKey); + // the first one is a test aggregator deployed on mumbai + // LP-USDT + new TestnetERC20("Wombat Tether Stablecoin Asset", "LP-USDT", 18, f); + // LP-USDC + new TestnetERC20("Wombat USDC Coin Asset", "LP-USDC", 18, f); + // LP-HAY + new TestnetERC20("Wombat Hay Stablecoin Asset", "LP-HAY", 18, f); + + vm.stopBroadcast(); + } +} diff --git a/script/13-addLPMarket/13.0.5-deployZeroReserveInterestRateStrategy.s.sol b/script/13-addLPMarket/13.0.5-deployZeroReserveInterestRateStrategy.s.sol new file mode 100644 index 0000000..b33f9de --- /dev/null +++ b/script/13-addLPMarket/13.0.5-deployZeroReserveInterestRateStrategy.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +import "forge-std/Script.sol"; +import "../../src/core/misc/ZeroReserveInterestRateStrategy.sol"; +import "../../src/core/interfaces/IPoolAddressesProvider.sol"; +contract deployZeroInterestStrategy is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address provider = vm.envAddress("PoolAddressesProvider"); + vm.startBroadcast(deployerPrivateKey); + new ZeroReserveInterestRateStrategy(IPoolAddressesProvider(provider)); + vm.stopBroadcast(); + + } +} \ No newline at end of file diff --git a/script/13-addLPMarket/13.0.6-deployATokenStakerImpl.s.sol b/script/13-addLPMarket/13.0.6-deployATokenStakerImpl.s.sol new file mode 100644 index 0000000..ce8dcb5 --- /dev/null +++ b/script/13-addLPMarket/13.0.6-deployATokenStakerImpl.s.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +import "forge-std/Script.sol"; +import "../../src/core/protocol/tokenization/ATokenWombatStaker.sol"; +import "../../src/core/interfaces/IPoolAddressesProvider.sol"; +import "../../src/core/interfaces/IPool.sol"; +import "../../src/core/interfaces/IAaveIncentivesController.sol"; + +contract DeployATokensImpl is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address provider = vm.envAddress("PoolAddressesProvider"); + vm.startBroadcast(deployerPrivateKey); + + IPool pool = IPool(IPoolAddressesProvider(provider).getPool()); + AToken atoken = new ATokenWombatStaker(pool); + atoken.initialize( + pool, + address(0), // treasury + address(0), // underlyingAsset + IAaveIncentivesController(address(0)), // incentivesController + 0, // aTokenDecimals + "ATOKEN_IMPL", // aTokenName + "ATOKEN_IMPL", // aTokenSymbol + "0x00" // param + ); + + vm.stopBroadcast(); + } +} diff --git a/script/13-addLPMarket/13.1-addMarketWithATokenStaker.s.sol b/script/13-addLPMarket/13.1-addMarketWithATokenStaker.s.sol new file mode 100644 index 0000000..ae0d1b3 --- /dev/null +++ b/script/13-addLPMarket/13.1-addMarketWithATokenStaker.s.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +import "forge-std/Script.sol"; +import "../../src/core/dependencies/openzeppelin/contracts/IERC20Detailed.sol"; +import "../../src/core/protocol/pool/DefaultReserveInterestRateStrategy.sol"; +import "../../src/core/interfaces/IPoolConfigurator.sol"; +import "../../src/core/interfaces/IPoolAddressesProvider.sol"; +import "../../src/core/protocol/libraries/types/ConfiguratorInputTypes.sol"; +contract InitReserve is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address provider = vm.envAddress("PoolAddressesProvider"); + address treasury = vm.envAddress("Treasury"); + address aTokenImpl = vm.envAddress("ATokenWombatStakerImpl"); + address sdTokenImpl = vm.envAddress("sdTokenImpl"); + address vdTokenImpl = vm.envAddress("vdTokenImpl"); + address interestRateStrategyAddress = vm.envAddress("ZeroInterestRateStrategy"); + vm.startBroadcast(deployerPrivateKey); + IPoolConfigurator configurator = IPoolConfigurator(IPoolAddressesProvider(provider).getPoolConfigurator()); + bytes32 incentivesControllerId = 0x703c2c8634bed68d98c029c18f310e7f7ec0e5d6342c590190b3cb8b3ba54532; + address incentivesController = IPoolAddressesProvider(provider).getAddress(incentivesControllerId); + + string[] memory tokens = new string[](1); + tokens[0] = "SMART_LP_HAY"; + + + ConfiguratorInputTypes.InitReserveInput[] memory inputs = new ConfiguratorInputTypes.InitReserveInput[](1); + + for (uint i; i < tokens.length; ++i) { + address token = vm.envAddress(tokens[i]); + uint8 decimals = IERC20Detailed(token).decimals(); + inputs[i] = ConfiguratorInputTypes.InitReserveInput( + aTokenImpl, + sdTokenImpl, + vdTokenImpl, + decimals, + interestRateStrategyAddress, + token, + treasury, + incentivesController, + string(abi.encodePacked("Kinza ", tokens[i])), + string(abi.encodePacked("k", tokens[i])), + string(abi.encodePacked("Kinza Variable Debt ", tokens[i])), + string(abi.encodePacked("vDebt", tokens[i])), + string(abi.encodePacked("Kinza Stable Debt ", tokens[i])), + string(abi.encodePacked("sDebt", tokens[i])), + abi.encodePacked("0x10") + ); + + } + configurator.initReserves(inputs); + + + + } +} diff --git a/script/13-addLPMarket/13.10-setUpEmissionManager.s.sol b/script/13-addLPMarket/13.10-setUpEmissionManager.s.sol new file mode 100644 index 0000000..67b9cbb --- /dev/null +++ b/script/13-addLPMarket/13.10-setUpEmissionManager.s.sol @@ -0,0 +1,37 @@ + +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +import "forge-std/Script.sol"; +import "../../src/periphery/rewards/interfaces/IEmissionManager.sol"; +import "../../src/periphery/rewards/interfaces/ITransferStrategyBase.sol"; +import "../../src/periphery/misc/interfaces/IEACAggregatorProxy.sol"; +import "../../src/core/interfaces/IPoolDataProvider.sol"; + +contract DeployATokensImpl is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address emissionManagerOwner = vm.envAddress("EmissionManagerOwner"); + address dataProvider = vm.envAddress("PoolDataProvider"); + address underlying = vm.envAddress("SMART_LP_HAY"); + address emissionManager = vm.envAddress("EmissionManager"); + address emissionAdmin = vm.envAddress("EmissionAdmin"); + address rewardToken = vm.envAddress("WOM"); + address rewardOracle = vm.envAddress("WomOracle"); + vm.startBroadcast(deployerPrivateKey); + + (address ATokenProxyAddress,,) = IPoolDataProvider(dataProvider).getReserveTokensAddresses(underlying); + RewardsDataTypes.RewardsConfigInput[] memory config = new RewardsDataTypes.RewardsConfigInput[](1); + config[0].asset = ATokenProxyAddress; + config[0].reward = rewardToken; + config[0].transferStrategy = ITransferStrategyBase(emissionAdmin); + config[0].rewardOracle = IEACAggregatorProxy(rewardOracle); + // set admin itself as the reward emission admin, which later would be replaced by the contract + IEmissionManager(emissionManager).setEmissionAdmin(rewardToken, emissionManagerOwner); + IEmissionManager(emissionManager).configureAssets(config); + IEmissionManager(emissionManager).setEmissionAdmin(rewardToken, emissionAdmin); + vm.stopBroadcast(); + } +} + + diff --git a/script/13-addLPMarket/13.2-setupRiskParameter.s.sol b/script/13-addLPMarket/13.2-setupRiskParameter.s.sol new file mode 100644 index 0000000..2dbe232 --- /dev/null +++ b/script/13-addLPMarket/13.2-setupRiskParameter.s.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +import "forge-std/Script.sol"; +import "../../src/core/deployments/ReservesSetupHelper.sol"; +import "../../src/core/protocol/configuration/ACLManager.sol"; +import "../../src/core/protocol/pool/PoolConfigurator.sol"; +import "../../src/core/interfaces/IPoolAddressesProvider.sol"; + +contract setupRiskParameter is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address provider = vm.envAddress("PoolAddressesProvider"); + address aclAddress = vm.envAddress("ACLManager"); + address helperAddr = vm.envAddress("ReservesSetupHelper"); + vm.startBroadcast(deployerPrivateKey); + + ReservesSetupHelper helper = ReservesSetupHelper(helperAddr); + //add helper to pool admin + ACLManager acl = ACLManager(aclAddress); + acl.addPoolAdmin(address(helper)); + + PoolConfigurator configurator = PoolConfigurator(IPoolAddressesProvider(provider).getPoolConfigurator()); + ReservesSetupHelper.ConfigureReserveInput[] memory inputs = new ReservesSetupHelper.ConfigureReserveInput[](1); + string[] memory tokens = new string[](1); + tokens[0] = "SMART_LP_HAY"; + // tokens[1] = "SMART_LP_USDC"; + // tokens[2] = "SMART_LP_USDT"; + address token; + for (uint256 i; i < tokens.length; i++) { + token = vm.envAddress(tokens[i]); + + inputs[i] = ReservesSetupHelper.ConfigureReserveInput( + token, + vm.envUint(string(abi.encodePacked(tokens[i], "_baseLTV"))), + vm.envUint(string(abi.encodePacked(tokens[i], "_liquidationThreshold"))), + vm.envUint(string(abi.encodePacked(tokens[i], "_liquidationBonus"))), + vm.envUint(string(abi.encodePacked(tokens[i], "_reserveFactor"))), + vm.envUint(string(abi.encodePacked(tokens[i], "_borrowCap"))), + vm.envUint(string(abi.encodePacked(tokens[i], "_supplyCap"))), + vm.envBool(string(abi.encodePacked(tokens[i], "_stableBorrowingEnabled"))), + vm.envBool(string(abi.encodePacked(tokens[i], "_borrowingEnabled"))), + vm.envBool(string(abi.encodePacked(tokens[i], "_flashLoanEnabled"))) + ); + } + + helper.configureReserves(configurator, inputs); + + //remove helper from pool admin + acl.removePoolAdmin(address(helper)); + vm.stopBroadcast(); + } +} diff --git a/script/13-addLPMarket/13.3-deployLPOracle.s.sol b/script/13-addLPMarket/13.3-deployLPOracle.s.sol new file mode 100644 index 0000000..55c6da3 --- /dev/null +++ b/script/13-addLPMarket/13.3-deployLPOracle.s.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +import "forge-std/Script.sol"; +import "../../src/core/misc/AaveOracle.sol"; +import "../../src/core/misc/WombatOracle/SmartHayPoolOracle.sol"; +import "../../src/core/dependencies/openzeppelin/contracts/IERC20Detailed.sol"; +contract DeployLPOracle is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address oracle = vm.envAddress("Oracle"); + address _lp = vm.envAddress("SMART_LP_HAY"); + address _hay = vm.envAddress("HAY"); + address _usdc = vm.envAddress("USDC"); + address _usdt = vm.envAddress("USDT"); + address _usdcAggregator = vm.envAddress("USDC_AGGREGATOR"); + address _hayAggregator = vm.envAddress("Binance_HAY_AGGREGATOR"); + address _usdtAggregator = vm.envAddress("USDT_AGGREGATOR"); + + + //address underlying_aggregator = vm.envAddress("HAY_AGGREGATOR"); + vm.startBroadcast(deployerPrivateKey); + //string memory description = string(abi.encodePacked(IERC20Detailed(underlying).symbol(), "-LP/USD")); + SmartHayPoolOracle hay_pool_oracle = new SmartHayPoolOracle(_usdcAggregator, _usdtAggregator, _hayAggregator, + _usdc, _usdt, _hay); + + address[] memory assets = new address[](1); + address[] memory sources = new address[](1); + assets[0] = _lp; + sources[0] = address(hay_pool_oracle); + AaveOracle(oracle).setAssetSources(assets, sources); + vm.stopBroadcast(); + } +} \ No newline at end of file diff --git a/script/13-addLPMarket/13.4-addEmode.s.sol b/script/13-addLPMarket/13.4-addEmode.s.sol new file mode 100644 index 0000000..2c292bf --- /dev/null +++ b/script/13-addLPMarket/13.4-addEmode.s.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +import "forge-std/Script.sol"; +import "../../src/core/dependencies/openzeppelin/contracts/IERC20Detailed.sol"; +import "../../src/core/interfaces/IPoolConfigurator.sol"; +import "../../src/core/interfaces/IPoolAddressesProvider.sol"; +import "../../src/core/interfaces/IPoolDataProvider.sol"; + +contract setUpEmode is Script { + function run() external { + + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address provider = vm.envAddress("PoolAddressesProvider"); + address dataprovider = vm.envAddress("PoolDataProvider"); + string memory token = "HAY"; + vm.startBroadcast(deployerPrivateKey); + IPoolConfigurator configurator = IPoolConfigurator(IPoolAddressesProvider(provider).getPoolConfigurator()); + uint8 categoryId = 2; + uint16 ltv = 9700; + uint16 liquidationThreshold = 9750; + uint16 liquidationBonus = 10100; + // no custom eMode feed + address eModeAssetToFetchPrice = address(0); + string memory label = string(abi.encodePacked("LP-HAY", token)); + configurator.setEModeCategory(categoryId, ltv, liquidationThreshold, liquidationBonus, eModeAssetToFetchPrice, label); + string[] memory addressToAdd = new string[](2); + addressToAdd[0] = token; + addressToAdd[1] = string(abi.encodePacked("SMART_LP_", token)); + for (uint256 i; i < addressToAdd.length; i++) { + address tokenAddress = vm.envAddress(string(abi.encodePacked(addressToAdd[i]))); + configurator.setAssetEModeCategory(tokenAddress, categoryId); + } + vm.stopBroadcast(); + } +} diff --git a/script/13-addLPMarket/13.5-deploySupplyLogic.sh b/script/13-addLPMarket/13.5-deploySupplyLogic.sh new file mode 100644 index 0000000..e9c8bd5 --- /dev/null +++ b/script/13-addLPMarket/13.5-deploySupplyLogic.sh @@ -0,0 +1,4 @@ +# deploy supplyLogic and put it into foundry.toml +forge create src/core/protocol/libraries/logic/SupplyLogic.sol:SupplyLogic --private-key $PRIVATE_KEY --rpc-url $RPC_URL --verify --verifier-url $VERIFIER_URL --etherscan-api-key $ETHERSCAN_API_KEY > log/SupplyLogic.txt +SupplyLogic=$(grep 'Deployed to: ' log/SupplyLogic.txt | awk '{print $3}') +echo "SupplyLogic=$SupplyLogic" >> ".env" \ No newline at end of file diff --git a/script/13-addLPMarket/13.5.5-deployAndInitialNewPoolImpl.s.sol b/script/13-addLPMarket/13.5.5-deployAndInitialNewPoolImpl.s.sol new file mode 100644 index 0000000..16015ca --- /dev/null +++ b/script/13-addLPMarket/13.5.5-deployAndInitialNewPoolImpl.s.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +import "forge-std/Script.sol"; +import "../../src/core/interfaces/IPoolAddressesProvider.sol"; +import "../../src/core/protocol/pool/Pool.sol"; +contract deployPoolImpl is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address provider = vm.envAddress("PoolAddressesProvider"); + vm.startBroadcast(deployerPrivateKey); + Pool pool = new Pool(IPoolAddressesProvider(provider)); + pool.initialize(IPoolAddressesProvider(provider)); + vm.stopBroadcast(); + + } +} \ No newline at end of file diff --git a/script/13-addLPMarket/13.6-upgradePoolImpl.s.sol b/script/13-addLPMarket/13.6-upgradePoolImpl.s.sol new file mode 100644 index 0000000..ec5fab5 --- /dev/null +++ b/script/13-addLPMarket/13.6-upgradePoolImpl.s.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +import "forge-std/Script.sol"; +import "../../src/core/interfaces/IPoolAddressesProvider.sol"; +import "../../src/core/protocol/pool/Pool.sol"; +contract upgradePoolImpl is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address provider = vm.envAddress("PoolAddressesProvider"); + address newPoolImpl = vm.envAddress("Pool2"); + vm.startBroadcast(deployerPrivateKey); + IPoolAddressesProvider(provider).setPoolImpl(address(newPoolImpl)); + vm.stopBroadcast(); + + } +} \ No newline at end of file diff --git a/script/13-addLPMarket/13.7-deployEmissionAdmin.s.sol b/script/13-addLPMarket/13.7-deployEmissionAdmin.s.sol new file mode 100644 index 0000000..79f487e --- /dev/null +++ b/script/13-addLPMarket/13.7-deployEmissionAdmin.s.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +import "forge-std/Script.sol"; +import "../../src/core/interfaces/IPoolAddressesProvider.sol"; +import "../../src/core/interfaces/IPoolDataProvider.sol"; +import "../../src/core/interfaces/IPool.sol"; +import "../../src/core/protocol/tokenization/EmissionAdminAndDirectTransferStrategy.sol"; +import {IEmissionManager} from '../../src/periphery/rewards/interfaces/IEmissionManager.sol'; +contract upgradePoolImpl is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address provider = vm.envAddress("PoolAddressesProvider"); + address dataProvider = vm.envAddress("PoolDataProvider"); + address emissionManager = vm.envAddress("EmissionManager"); + address underlying = vm.envAddress("SMART_LP_HAY"); + vm.startBroadcast(deployerPrivateKey); + address pool = IPoolAddressesProvider(provider).getPool(); + EmissionAdminAndDirectTransferStrategy t = new EmissionAdminAndDirectTransferStrategy( + IPool(pool), IEmissionManager(emissionManager) + ); + (address ATokenProxyAddress,,) = IPoolDataProvider(dataProvider).getReserveTokensAddresses(underlying); + t.toggleATokenWhitelist(IAToken(ATokenProxyAddress)); + vm.stopBroadcast(); + + } +} \ No newline at end of file diff --git a/script/13-addLPMarket/13.8-setupATokenStakerProxy.s.sol b/script/13-addLPMarket/13.8-setupATokenStakerProxy.s.sol new file mode 100644 index 0000000..b1c8f04 --- /dev/null +++ b/script/13-addLPMarket/13.8-setupATokenStakerProxy.s.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +import "forge-std/Script.sol"; +import "../../src/core/protocol/tokenization/ATokenWombatStaker.sol"; +import "../../src/core/interfaces/IPoolDataProvider.sol"; + +contract DeployATokensImpl is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address dataProvider = vm.envAddress("PoolDataProvider"); + address underlying = vm.envAddress("SMART_LP_HAY"); + address masterWombat = vm.envAddress("MasterWombat"); + address emissionAdmin = vm.envAddress("EmissionAdmin"); + vm.startBroadcast(deployerPrivateKey); + + (address ATokenProxyAddress,,) = IPoolDataProvider(dataProvider).getReserveTokensAddresses(underlying); + ATokenWombatStaker(ATokenProxyAddress).updateEmissionAdmin(emissionAdmin); + ATokenWombatStaker(ATokenProxyAddress).updateMasterWombat(masterWombat); + vm.stopBroadcast(); + } +} diff --git a/script/13-addLPMarket/13.9-deployLeverageHelper.s.sol b/script/13-addLPMarket/13.9-deployLeverageHelper.s.sol new file mode 100644 index 0000000..98cfbfc --- /dev/null +++ b/script/13-addLPMarket/13.9-deployLeverageHelper.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +import "forge-std/Script.sol"; +import "../../src/core/interfaces/IPoolConfigurator.sol"; +import "../../src/periphery/misc/WombatLeverageHelper.sol"; + +contract deployLeverageHelper is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address provider = vm.envAddress("PoolAddressesProvider"); + + vm.startBroadcast(deployerPrivateKey); + + new WombatLeverageHelper(provider); + + vm.stopBroadcast(); + } +} diff --git a/script/13-addLPMarket/deploy.sh b/script/13-addLPMarket/deploy.sh new file mode 100644 index 0000000..011fd41 --- /dev/null +++ b/script/13-addLPMarket/deploy.sh @@ -0,0 +1,28 @@ +#13.0.5 +forge script script/13-addLPMarket/13.0.5-deployZeroReserveInterestRateStrategy.s.sol --rpc-url $RPC_URL --broadcast --verify --verifier-url $VERIFIER_URL --etherscan-api-key $ETHERSCAN_API_KEY -vvvv +#13.0.6 +forge script script/13-addLPMarket/13.0.6-deployATokenStakerImpl.s.sol --rpc-url $RPC_URL --broadcast --verify --verifier-url $VERIFIER_URL --etherscan-api-key $ETHERSCAN_API_KEY -vvvv +#13.1 +forge script script/13-addLPMarket/13.1-addMarketWithATokenStaker.s.sol --rpc-url $RPC_URL --broadcast --verify --verifier-url $VERIFIER_URL --etherscan-api-key $ETHERSCAN_API_KEY -vvvv +#13.2 +forge script script/13-addLPMarket/13.2-setupRiskParameter.s.sol --rpc-url $RPC_URL --broadcast --verify --verifier-url $VERIFIER_URL --etherscan-api-key $ETHERSCAN_API_KEY -vvvv +#13.3 +forge script script/13-addLPMarket/13.3-deployLPOracle.s.sol --rpc-url $RPC_URL --broadcast --verify --verifier-url $VERIFIER_URL --etherscan-api-key $ETHERSCAN_API_KEY -vvvv +# 13.4 +forge script script/13-addLPMarket/13.4-addEmode.s.sol --rpc-url $RPC_URL --broadcast --verify --verifier-url $VERIFIER_URL --etherscan-api-key $ETHERSCAN_API_KEY -vvvv +# 13.5 +forge create src/core/protocol/libraries/logic/SupplyLogic.sol:SupplyLogic --private-key $PRIVATE_KEY --rpc-url $RPC_URL --verify --verifier-url $VERIFIER_URL --etherscan-api-key $ETHERSCAN_API_KEY > log/SupplyLogic.txt +SupplyLogic=$(grep 'Deployed to: ' log/SupplyLogic.txt | awk '{print $3}') +echo "SupplyLogic=$SupplyLogic" >> ".env" +#13.5.5 put the new SupplyLogic into foundry.toml, ensure POOL_REVISION is pumped to 0x2 +forge script script/13-addLPMarket/13.5.5-deployAndInitialNewPoolImpl.s.sol --rpc-url $RPC_URL --broadcast --verify --verifier-url $VERIFIER_URL --etherscan-api-key $ETHERSCAN_API_KEY -vvvv +PoolV2=($(jq -r '.transactions[0].contractAddress' broadcast/13.5.5-deployAndInitialNewPoolImpl.s.sol/${chainId}/run-latest.json)) +echo "PoolV2=$PoolV2" >> ".env" + +#13.6 upgrade +forge script script/13-addLPMarket/13.6-upgradePoolImpl.s.sol --rpc-url $RPC_URL --broadcast --verify --verifier-url $VERIFIER_URL --etherscan-api-key $ETHERSCAN_API_KEY -vvvv + +forge script script/13-addLPMarket/13.7-deployEmissionAdmin.s.sol --rpc-url $RPC_URL --broadcast --verify --verifier-url $VERIFIER_URL --etherscan-api-key $ETHERSCAN_API_KEY -vvvv +forge script script/13-addLPMarket/13.8-setupATokenStakerProxy.s.sol --rpc-url $RPC_URL --broadcast --verify --verifier-url $VERIFIER_URL --etherscan-api-key $ETHERSCAN_API_KEY -vvvv +forge script script/13-addLPMarket/13.9-deployLeverageHelper.s.sol --rpc-url $RPC_URL --broadcast --verify --verifier-url $VERIFIER_URL --etherscan-api-key $ETHERSCAN_API_KEY -vvvv +forge script script/13-addLPMarket/13.10-setupEmissionManager.s.sol --rpc-url $RPC_URL --broadcast --verify --verifier-url $VERIFIER_URL --etherscan-api-key $ETHERSCAN_API_KEY -vvvv \ No newline at end of file diff --git a/src/core/interfaces/IEmissionAdmin.sol b/src/core/interfaces/IEmissionAdmin.sol new file mode 100644 index 0000000..17c796b --- /dev/null +++ b/src/core/interfaces/IEmissionAdmin.sol @@ -0,0 +1,4 @@ +interface IEmissionAdmin { + function notify(address[] memory rewards, uint256[] memory amounts) external; + +} \ No newline at end of file diff --git a/src/core/interfaces/IMasterMagpie.sol b/src/core/interfaces/IMasterMagpie.sol new file mode 100644 index 0000000..5b0328e --- /dev/null +++ b/src/core/interfaces/IMasterMagpie.sol @@ -0,0 +1,5 @@ +interface IMasterMagpie { + function multiClaim(address[] memory stakingTokens) external; + function withdraw(address stakingToken, uint256 amount) external; + +} \ No newline at end of file diff --git a/src/core/interfaces/IMasterWombat.sol b/src/core/interfaces/IMasterWombat.sol new file mode 100644 index 0000000..db931b9 --- /dev/null +++ b/src/core/interfaces/IMasterWombat.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +interface IMasterWombat { + function multiClaim(uint256[] calldata pids) external; + function deposit(uint256 pid, uint256 amount) external; + function withdraw(uint256 pid, uint256 amount) external; + function getAssetPid(address asset) external view returns (uint256); + function poolInfo(uint256 pid) external view returns (address, address, uint40, uint128, uint128, uint104, uint104, uint40); + function userInfo(uint256 pid, address user) external view returns (uint128, uint128, uint128, uint128); + function pendingTokens( + uint256 _pid, + address _user + ) + external + view + returns ( + uint256 pendingRewards, + address[] memory bonusTokenAddresses, + string[] memory bonusTokenSymbols, + uint256[] memory pendingBonusRewards + ); +} \ No newline at end of file diff --git a/src/core/interfaces/IWombatHelper.sol b/src/core/interfaces/IWombatHelper.sol new file mode 100644 index 0000000..6180c86 --- /dev/null +++ b/src/core/interfaces/IWombatHelper.sol @@ -0,0 +1,5 @@ +interface IWombatHelper { + function depositLP(uint256 amonut) external; + function wombatStaking() external returns(address); + +} \ No newline at end of file diff --git a/src/core/misc/WombatOracle/SmartHayPoolOracle.sol b/src/core/misc/WombatOracle/SmartHayPoolOracle.sol new file mode 100644 index 0000000..7e1b8ab --- /dev/null +++ b/src/core/misc/WombatOracle/SmartHayPoolOracle.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.10; + +import {AggregatorInterface} from "../../dependencies/chainlink/AggregatorInterface.sol"; + +// immutable, just create another contract if any underlying aggregators need to be updated +contract SmartHayPoolOracle { + // for information; refer to https://oracle.binance.com/docs/price-feeds/contract-addresses/bnb-mainnet + AggregatorInterface public usdcAggregator; + AggregatorInterface public usdtAggregator; + AggregatorInterface public hayAggregator; + address public usdc; + address public usdt; + address public hay; + + // dependent on other internal aggregator. + constructor(address _usdcAggregator, address _usdtAggregator, address _hayAggregator, + address _usdc, address _usdt, address _hay) { + usdcAggregator = AggregatorInterface(_usdcAggregator); + usdtAggregator = AggregatorInterface(_usdtAggregator); + hayAggregator = AggregatorInterface(_hayAggregator); + usdc = _usdc; + usdt = _usdt; + hay = _hay; + } + + + function latestAnswer() external view returns (int256) { + int256 usdcPrice = usdcAggregator.latestAnswer(); + int256 usdtPrice = usdtAggregator.latestAnswer(); + int256 hayPrice = hayAggregator.latestAnswer(); + // return the minimum of three + return usdcPrice > usdtPrice + ? usdtPrice > hayPrice ? hayPrice + : usdtPrice + : usdcPrice > hayPrice ? hayPrice + : usdcPrice; + } + + function latestTimestamp() external view returns (uint256) { + uint256 usdcTime = usdcAggregator.latestTimestamp(); + uint256 usdtTime = usdtAggregator.latestTimestamp(); + uint256 hayTime = hayAggregator.latestTimestamp(); + // return the minimum of three time (the most stale) + return usdcTime > usdtTime + ? usdtTime > hayTime ? hayTime + : usdtTime + : usdcTime > hayTime ? hayTime + : usdcTime; + } + + // this is quite not useful since each round might be different on each aggreagator + function latestRound() external view returns (uint256) { + uint256 usdcRound = usdcAggregator.latestRound(); + uint256 usdtRound = usdtAggregator.latestRound(); + uint256 hayRound = hayAggregator.latestRound(); + // return the minimum of three round (the most stale) + return usdcRound > usdtRound + ? usdtRound > hayRound ? hayRound + : usdtRound + : usdcRound > hayRound ? hayRound + : usdcRound; + } + + function getSubTokens() external view returns (address[] memory) { + address[] memory _dependentAssets = new address[](3); + _dependentAssets[0] = usdc; + _dependentAssets[1] = usdt; + _dependentAssets[2] = hay; + return _dependentAssets; + } + + function getTokenType() public pure returns(uint256) { + // for subgraph purpose + return 2; + } +} diff --git a/src/core/protocol/pool/Pool.sol b/src/core/protocol/pool/Pool.sol index 1ea8f49..b4f49b2 100644 --- a/src/core/protocol/pool/Pool.sol +++ b/src/core/protocol/pool/Pool.sol @@ -39,7 +39,7 @@ import {PoolStorage} from './PoolStorage.sol'; contract Pool is VersionedInitializable, PoolStorage, IPool { using ReserveLogic for DataTypes.ReserveData; - uint256 public constant POOL_REVISION = 0x1; + uint256 public constant POOL_REVISION = 0x2; IPoolAddressesProvider public immutable ADDRESSES_PROVIDER; /** diff --git a/src/core/protocol/tokenization/ATokenMagpieStaker.sol b/src/core/protocol/tokenization/ATokenMagpieStaker.sol new file mode 100644 index 0000000..ce5057c --- /dev/null +++ b/src/core/protocol/tokenization/ATokenMagpieStaker.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.10; + +import {IERC20} from '../../dependencies/openzeppelin/contracts/IERC20.sol'; +import {GPv2SafeERC20} from '../../dependencies/gnosis/contracts/GPv2SafeERC20.sol'; +import {IMasterMagpie} from '../../interfaces/IMasterMagpie.sol'; +import {IWombatHelper} from '../../interfaces/IWombatHelper.sol'; +import {IEmissionAdmin} from '../../interfaces/IEmissionAdmin.sol'; +import {IPool} from '../../interfaces/IPool.sol'; +import {AToken} from './AToken.sol'; + +/** + * @notice Implementation of the AToken with the underlying LP staked + * stake on an external gauge; rewards are claimed to the Atoken proxy + * and distributed upon the discretion of an admin + */ +contract ATokenMagpieStaker is AToken { + using GPv2SafeERC20 for IERC20; + IMasterMagpie public _masterMagpie; + IWombatHelper public _wombatHelper; + // manage emission on internal EmissionManager + address public _emissionAdmin; + + event StakingRewardClaimed(); + /** + * @dev Constructor. + * @param pool The address of the Pool contract + */ + constructor( + IPool pool + ) AToken(pool) { + // Intentionally left blank + } + + function updateMasterMagpie(address masterMagpie) external onlyPoolAdmin { + // this is fine since it's a proxy + require(address(_masterMagpie) == address(0), "masterMagpie can only be set once"); + _masterMagpie = IMasterMagpie(masterMagpie); + } + function updateWombatHelper(address wombatHelper) external onlyPoolAdmin { + // poolHelper can be updated + // require(address(_wombatHelper) == address(0), "wombatHelper can only be set once"); + address wombatStaking = IWombatHelper(wombatHelper).wombatStaking(); + // wombatHerlp::depositLP would trigger wombatStaking which pull LP token from this contract + IERC20(_underlyingAsset).approve(wombatStaking, type(uint256).max); + _wombatHelper = IWombatHelper(wombatHelper); + } + + function updateEmissionAdmin(address emissionAdmin) external onlyPoolAdmin { + _emissionAdmin = emissionAdmin; + } + + function multiClaim() public onlyPoolAdmin { + address[] memory stakingTokens = new address[](1); + stakingTokens[0] = _underlyingAsset; + /// @dev that is no return data from multiclaim; also masterMagpie is a proxy. + _masterMagpie.multiClaim(stakingTokens); + emit StakingRewardClaimed(); + } + + function sendEmission(address[] memory rewards) external onlyPoolAdmin { + // claim + multiClaim(); + uint256[] memory amounts = new uint256[](rewards.length); + for (uint i; i < rewards.length;) { + require(rewards[i] != _underlyingAsset, "underlying token cannot be sent as rewards"); + amounts[i] = IERC20(rewards[i]).balanceOf(address(this)); + // avoid unnecessary allowance + // reward are sent to the corresponding vault + IERC20(rewards[i]).transfer(_emissionAdmin, amounts[i]); + unchecked { + i++; + } + } + // emissionAdmin define the distribution details regarding the reward token + IEmissionAdmin(_emissionAdmin).notify(rewards, amounts); + } + + function mint( + address caller, + address onBehalfOf, + uint256 amount, + uint256 index + ) external virtual override onlyPool returns (bool) { + // helper takes our LP, call wombat staking, + // then stake the wombat stakingToken on magpie itself + _wombatHelper.depositLP(amount); + return _mintScaled(caller, onBehalfOf, amount, index); + } + + function burn( + address from, + address receiverOfUnderlying, + uint256 amount, + uint256 index + ) external virtual override onlyPool { + address stakingToken = _underlyingAsset; + _masterMagpie.withdraw(stakingToken, amount); + _burnScaled(from, receiverOfUnderlying, amount, index); + if (receiverOfUnderlying != address(this)) { + IERC20(_underlyingAsset).safeTransfer(receiverOfUnderlying, amount); + } + } + + /// * @dev Used by the Pool to transfer assets in borrow(() and flashLoan() --- not used in withdraw + function transferUnderlyingTo(address target, uint256 amount) external virtual override onlyPool { + revert("ATokenStaker does not allow flashloan or borrow"); + } + +} diff --git a/src/core/protocol/tokenization/ATokenWombatStaker.sol b/src/core/protocol/tokenization/ATokenWombatStaker.sol new file mode 100644 index 0000000..3cdfae0 --- /dev/null +++ b/src/core/protocol/tokenization/ATokenWombatStaker.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.10; + +import {IERC20} from '../../dependencies/openzeppelin/contracts/IERC20.sol'; +import {GPv2SafeERC20} from '../../dependencies/gnosis/contracts/GPv2SafeERC20.sol'; +import {IMasterWombat} from '../../interfaces/IMasterWombat.sol'; +import {IEmissionAdmin} from '../../interfaces/IEmissionAdmin.sol'; +import {IPool} from '../../interfaces/IPool.sol'; +import {AToken} from './AToken.sol'; + +/** + * @notice Implementation of the AToken with the underlying LP staked + * stake on an external gauge; rewards are claimed to the Atoken proxy + * and distributed upon the discretion of an admin + */ +contract ATokenWombatStaker is AToken { + using GPv2SafeERC20 for IERC20; + uint256 public _pid; + IMasterWombat public _masterWombat; + // manage emission on internal EmissionManager + address public _emissionAdmin; + mapping(address => bool) public whitelist; + bool public isOpenForEveryone; + bool public isInEmergency; + event StakingRewardClaimed(); + event OpenForEveryoneChanged(bool newValue); + event WhitelistChanged(address dest, bool newValue); + event EmergencyStateChanged(bool newValue); + /** + * @dev Constructor. + * @param pool The address of the Pool contract + */ + constructor( + IPool pool + ) AToken(pool) { + // Intentionally left blank + } + + modifier isWhitelistedOrOpen(address caller) { + require (isOpenForEveryone || whitelist[caller], "access controlled"); + _; + } + + // withdraw all LP from the masterWombat + function toggleEmergencyWithdraw() external onlyPoolAdmin { + if (isInEmergency) { + uint256 amount = IERC20(_underlyingAsset).balanceOf(address(this)); + _masterWombat.deposit(_pid, amount); + isInEmergency = false; + emit EmergencyStateChanged(false); + } else { + (uint128 amount,,,) = _masterWombat.userInfo(_pid, address(this)); + _masterWombat.withdraw(_pid, uint256(amount)); + isInEmergency = true; + emit EmergencyStateChanged(true); + } + + } + + function toogleOpenForEveryone(bool newOpenForEveryone) external onlyPoolAdmin { + isOpenForEveryone = newOpenForEveryone; + emit OpenForEveryoneChanged(newOpenForEveryone); + } + + function toggleWhitelist(address dest, bool newValue) external onlyPoolAdmin { + whitelist[dest] = newValue; + emit WhitelistChanged(dest, newValue); + } + function updateMasterWombat(address masterWombat) external onlyPoolAdmin { + // this is fine since it's a proxy + require(address(_masterWombat) == address(0), "master can only be set once"); + IERC20(_underlyingAsset).approve(masterWombat, type(uint256).max); + _masterWombat = IMasterWombat(masterWombat); + _pid = _masterWombat.getAssetPid(_underlyingAsset); + } + + function updateEmissionAdmin(address emissionAdmin) external onlyPoolAdmin { + _emissionAdmin = emissionAdmin; + } + + function multiClaim() public onlyPoolAdmin { + uint256[] memory pids = new uint256[](1); + pids[0] = _pid; + /// @dev that is no return data from multiclaim; also masterMagpie is a proxy. + _masterWombat.multiClaim(pids); + emit StakingRewardClaimed(); + } + + function sendEmission(address[] memory rewards) external onlyPoolAdmin { + // claim + multiClaim(); + uint256[] memory amounts = new uint256[](rewards.length); + for (uint i; i < rewards.length;) { + require(rewards[i] != _underlyingAsset, "underlying token cannot be sent as rewards"); + amounts[i] = IERC20(rewards[i]).balanceOf(address(this)); + // avoid unnecessary allowance + // reward are sent to the corresponding vault + IERC20(rewards[i]).transfer(_emissionAdmin, amounts[i]); + unchecked { + i++; + } + } + // emissionAdmin define the distribution details regarding the reward token + IEmissionAdmin(_emissionAdmin).notify(rewards, amounts); + } + + function mint( + address caller, + address onBehalfOf, + uint256 amount, + uint256 index + ) external virtual override onlyPool isWhitelistedOrOpen(caller) returns (bool) { + // helper takes our LP, call wombat staking, + // then stake the wombat stakingToken on magpie itself + require(!isInEmergency, "deposit is paused due to emergency"); + _masterWombat.deposit(_pid, amount); + return _mintScaled(caller, onBehalfOf, amount, index); + } + + function burn( + address from, + address receiverOfUnderlying, + uint256 amount, + uint256 index + ) external virtual override onlyPool { + address stakingToken = _underlyingAsset; + // in normal operation, user need to withdraw from masterWombat + // if in emergency, LP should already be withdrawn from the masterWombat + if (!isInEmergency) { + _masterWombat.withdraw(_pid, amount); + } + _burnScaled(from, receiverOfUnderlying, amount, index); + if (receiverOfUnderlying != address(this)) { + IERC20(_underlyingAsset).safeTransfer(receiverOfUnderlying, amount); + } + } + + /// * @dev Used by the Pool to transfer assets in borrow(() and flashLoan() --- not used in withdraw + function transferUnderlyingTo(address target, uint256 amount) external virtual override onlyPool { + revert("ATokenStaker does not allow flashloan or borrow"); + } + +} diff --git a/src/core/protocol/tokenization/EmissionAdminAndDirectTransferStrategy.sol b/src/core/protocol/tokenization/EmissionAdminAndDirectTransferStrategy.sol new file mode 100644 index 0000000..9ba0ed6 --- /dev/null +++ b/src/core/protocol/tokenization/EmissionAdminAndDirectTransferStrategy.sol @@ -0,0 +1,90 @@ +import {IERC20} from '../../dependencies/openzeppelin/contracts/IERC20.sol'; +import {IAToken} from '../../interfaces/IAToken.sol'; +import {IPool} from '../../interfaces/IPool.sol'; +import {DataTypes} from '../libraries/types/DataTypes.sol'; +import {IEmissionManager} from '../../../periphery/rewards/interfaces/IEmissionManager.sol'; +import {Ownable} from '../../../core/dependencies/openzeppelin/contracts/Ownable.sol'; +// this contract exists to consolidate the reward emission +// that many ATokens need to send for the same rewardToken +// plus being the transferStrategy on rewardsDisbutor:: performTransfer +contract EmissionAdminAndDirectTransferStrategy is Ownable { + IEmissionManager public immutable emissionManager; + IPool public immutable pool; + + mapping(IAToken => bool) public ATokenWhitelist; + // the duration of distribution + uint256 public REWARD_PERIOD = 3 days; + // _token => reward => lastdistributionTimestamp + mapping(address => mapping(address => uint256)) public lastRewardDistributionEnd; + event ATokenWhitelistChanged(bool current); + constructor(IPool _pool, IEmissionManager _emissionManager) { + pool = _pool; + emissionManager = _emissionManager; + } + + modifier onlyATokenWithWhitelist() { + require(_isATokenProxy()); + require(ATokenWhitelist[IAToken(msg.sender)]); + /// @TODO confirm it's an AToken indeed from the pool + _; + } + + modifier onlyRewardsController() { + require(msg.sender == address(emissionManager.getRewardsController())); + _; + } + + // assume money is already sent to the corresponding vault for claims, + // this contract is not responsible for reward backing + function notify(address[] memory rewards, uint256[] memory amounts) external onlyATokenWithWhitelist { + require(rewards.length == amounts.length); + address token = msg.sender; + _updateEmissionManager(token, rewards, amounts); + } + + + function performTransfer(address to, address reward, uint256 amount) external onlyRewardsController { + IERC20(reward).transfer(to, amount); + } + + function toggleATokenWhitelist(IAToken aToken) external onlyOwner { + /// @TODO confirm it's an AToken indeed from the pool + bool current = ATokenWhitelist[aToken]; + emit ATokenWhitelistChanged(current); + ATokenWhitelist[aToken] = !current; + } + + function updateRewardPeriod(uint256 newRewardPeriod) external onlyOwner { + require(newRewardPeriod > 0); + REWARD_PERIOD = newRewardPeriod; + } + + function _updateEmissionManager(address token, address[] memory rewards, uint256[] memory amounts) internal { + uint88[] memory rates = new uint88[](amounts.length); + for (uint i; i < rewards.length;) { + require(lastRewardDistributionEnd[token][rewards[i]] <= block.timestamp); + uint256 rate = amounts[i] / REWARD_PERIOD; + rates[i] = toUint88(rate); + emissionManager.setDistributionEnd(token, rewards[i], uint32(block.timestamp + REWARD_PERIOD)); + lastRewardDistributionEnd[token][rewards[i]] = block.timestamp + REWARD_PERIOD; + unchecked { + i++; + } + } + emissionManager.setEmissionPerSecond(token, rewards, rates); + } + function _isATokenProxy() view private returns (bool) { + // mutual confirmation + address underlying = IAToken(msg.sender).UNDERLYING_ASSET_ADDRESS(); + DataTypes.ReserveData memory d = pool.getReserveData(underlying); + return d.aTokenAddress == msg.sender; + } + + function toUint88(uint256 value) internal pure returns (uint88) { + if (value > type(uint88).max) { + revert("SafeCastOverflowedUintDowncast(88, value)"); + } + return uint88(value); + } + +} \ No newline at end of file diff --git a/src/periphery/misc/WombatLeverageHelper.sol b/src/periphery/misc/WombatLeverageHelper.sol new file mode 100644 index 0000000..73f530b --- /dev/null +++ b/src/periphery/misc/WombatLeverageHelper.sol @@ -0,0 +1,161 @@ +import '../../core/interfaces/IPoolAddressesProvider.sol'; +import '../../core/interfaces/IPool.sol'; +import '../../core/interfaces/IAaveOracle.sol'; +import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol'; + + +// just need one function rather use it here +interface IBorrowableProvider { + function getUserMaxBorrowable(address user, address asset) external view returns(uint256); +} + +interface IWombatPool { + function deposit(address underlying, uint256 amount, uint256 minLiquidity, address receiver, uint256 expiry, bool isStaked) external; +} + +// just need one function for wombat LP Asset +interface IAsset { + function underlyingToken() external view returns(address); + +} + +contract WombatLeverageHelper { + // return from borrowable provider is 10 ** 8; + uint256 constant public BORROWABLE_MULTIPLIER = 10 ** 10; + IPoolAddressesProvider immutable public provider; + IPool immutable public pool; + constructor(address _provider) { + provider = IPoolAddressesProvider(_provider); + pool = IPool(IPoolAddressesProvider(_provider).getPool()); + } + + // LP and underlying is assumed to be 1:1 + // in reality it depends on the conversion ratio on wombatPool + function calculateUnderlyingBorrow(uint256 targetHF, address lpAddr, uint256 depositAmount, uint8 emodeCategory) public view returns(uint256) { + address underlying = IAsset(lpAddr).underlyingToken(); + (uint256 underlyingPrice, uint256 lpPrice) = getPrice(underlying, lpAddr); + // assume a person without any other position + uint256 emodeLiqT = uint256(pool.getEModeCategoryData(emodeCategory).liquidationThreshold); + // targetHF = depositTotal * emodeLiqT / borrowedUnderlyingTotal + // targetHF = (deposit + borrow) * emodeLiqT * lpPrice / borrow * underlyingPrice + // targetHF = (deposit * LiqT * lpPrice) / borrow * underlyingPrice + LiqT * emodeLiqT / underlyingPrice + // after refactor: the constant part => depositRatio. + uint256 depositRatio = targetHF - 1e18 * lpPrice * emodeLiqT / underlyingPrice / 10000; + return 1e18 * depositAmount * lpPrice * emodeLiqT / (depositRatio * underlyingPrice) / 10000; + } + + function calculateTarsgetedHF(uint256 targetedBorrow, address lpAddr, uint256 depositAmount, uint8 emodeCategory) public view returns(uint256) { + address underlying = IAsset(lpAddr).underlyingToken(); + (uint256 underlyingPrice, uint256 lpPrice) = getPrice(underlying, lpAddr); + uint256 emodeLiqT = uint256(pool.getEModeCategoryData(emodeCategory).liquidationThreshold); + return 1e18 * (targetedBorrow + depositAmount) * emodeLiqT * lpPrice / (targetedBorrow * underlyingPrice); + } + + function depositUnderlyingAndLoop(address borrowableProvider, address wombatPool, uint256 targetHF, address lpAddr, uint256 amount, uint8 emodeCategory, uint256 slippage) external returns (uint256) { + address underlying = IAsset(lpAddr).underlyingToken(); + (uint256 underlyingPrice, uint256 lpPrice) = getPrice(underlying, lpAddr); + IERC20(underlying).transferFrom(msg.sender, address(this), amount); + // initial approval + _checkWombatAllowance(underlying, wombatPool); + _checkPoolAllowance(lpAddr); + require(slippage <= 10000, "slippage too big"); + IWombatPool(wombatPool).deposit( + underlying, + amount, + // apply 10 bp slippage for minLiq, + // with ref to relative lpPrice and underlyingPrice + amount * lpPrice * (10000 - slippage) / underlyingPrice / 10000, + address(this), + block.timestamp, + false + ); + uint256 lpCollected = IERC20(lpAddr).balanceOf(address(this)); + pool.deposit(lpAddr, lpCollected, msg.sender, 0); + return _loop(borrowableProvider, wombatPool, targetHF, lpAddr, amount, emodeCategory, slippage); + } + + function depositLpAndLoop(address borrowableProvider, address wombatPool, uint256 targetHF, address lpAddr, uint256 amount, uint8 emodeCategory, uint256 slippage) external returns (uint256) { + require(slippage <= 10000, "slippage too big"); + address underlying = IAsset(lpAddr).underlyingToken(); + IERC20(lpAddr).transferFrom(msg.sender, address(this), amount); + _checkWombatAllowance(underlying, wombatPool); + _checkPoolAllowance(lpAddr); + pool.deposit(lpAddr, amount, msg.sender, 0); + return _loop(borrowableProvider, wombatPool, targetHF, lpAddr, amount, emodeCategory, slippage); + } + + // assume the user already deposited the LP or have some collateral power + function Loop(address borrowableProvider, address wombatPool, uint256 targetHF, address lpAddr, uint256 amount, uint8 emodeCategory, uint256 slippage) external returns (uint256) { + require(slippage <= 10000, "slippage too big"); + address underlying = IAsset(lpAddr).underlyingToken(); + _checkWombatAllowance(underlying, wombatPool); + _checkPoolAllowance(lpAddr); + return _loop(borrowableProvider, wombatPool, targetHF, lpAddr, amount, emodeCategory, slippage); + } + // this function just leverage by borrowing underlying for the user + // and loop the needed borrowedAmount by re-depositing back the lp + // the final health factor may vary from the targetHF + // since the deposited LP might be different depends on wombat + function _loop(address borrowableProvider, address wombatPool, uint256 targetHF, address lpAddr, uint256 amount, uint8 emodeCategory, uint256 slippage) internal returns(uint256) { + + require(targetHF > 1e18 && amount > 0, "targetHF or deposit amount invalid"); + address underlying = IAsset(lpAddr).underlyingToken(); + (uint256 underlyingPrice, uint256 lpPrice) = getPrice(underlying, lpAddr); + uint256 borrowTotal = calculateUnderlyingBorrow(targetHF, lpAddr, amount, emodeCategory); + uint256 borrowed; + while(borrowed < borrowTotal) { + // start borrow + uint256 diff = borrowTotal - borrowed; + // the return is 10 * 8 from borrowableProvider + uint256 toBorrow = BORROWABLE_MULTIPLIER * IBorrowableProvider(borrowableProvider).getUserMaxBorrowable(msg.sender, underlying); + if (toBorrow == 0) { + // somehow the user can not borrow anymore + // can be due to other existing borrow, or debt ceiling/available in the protocol + break; + } + if (toBorrow > diff) { + toBorrow = diff; + } + borrowed += toBorrow; + // 2 = variable interest rate, 0 = referral code + pool.borrow(underlying, toBorrow, 2, 0, msg.sender); + // convert to LP on wombat + IWombatPool(wombatPool).deposit( + underlying, + toBorrow, + // apply 10 bp slippage for minLiq, + // with ref to relative lpPrice and underlyingPrice + toBorrow * lpPrice * (10000 - slippage) / underlyingPrice / 10000, + address(this), + block.timestamp, + false + ); + // deposit the LP into pool + uint256 amount = IERC20(lpAddr).balanceOf(address(this)); + pool.deposit(lpAddr, amount, msg.sender, 0); + } + return borrowed; + } + + function getPrice(address underlying, address lp) public view returns(uint256, uint256) { + IAaveOracle oracle = IAaveOracle(provider.getPriceOracle()); + address[] memory assets = new address[](2); + assets[0] = underlying; + assets[1] = lp; + uint256[] memory prices = oracle.getAssetsPrices(assets); + return (prices[0], prices[1]); + } + + function _checkPoolAllowance(address lpAddr) internal { + if (IERC20(lpAddr).allowance(address(pool), address(this)) == 0) { + IERC20(lpAddr).approve(address(pool), type(uint256).max); + } + } + + function _checkWombatAllowance(address underlying, address wombatPool) internal { + if (IERC20(underlying).allowance(wombatPool, address(this)) == 0) { + IERC20(underlying).approve(wombatPool, type(uint256).max); + } + } + +} \ No newline at end of file diff --git a/test/ATokenStaker/MagpieStaker/ATokenMagpieStakerBaseTest.t.sol b/test/ATokenStaker/MagpieStaker/ATokenMagpieStakerBaseTest.t.sol new file mode 100644 index 0000000..2d6d6f7 --- /dev/null +++ b/test/ATokenStaker/MagpieStaker/ATokenMagpieStakerBaseTest.t.sol @@ -0,0 +1,116 @@ + +import {BaseTest} from "test/BaseTest.t.sol"; + +import "../../../src/core/dependencies/openzeppelin/contracts/IERC20Detailed.sol"; +import {IPoolAddressesProvider} from "../../../src/core/interfaces/IPoolAddressesProvider.sol"; +import {IPool} from "../../../src/core/interfaces/IPool.sol"; +import {IACLManager} from '../../../src/core/interfaces/IACLManager.sol'; +import {IAaveIncentivesController} from '../../../src/core/interfaces/IAaveIncentivesController.sol'; +import {AToken} from "../../../src/core/protocol/tokenization/AToken.sol"; +import {ATokenMagpieStaker} from "../../../src/core/protocol/tokenization/ATokenMagpieStaker.sol"; +import {ZeroReserveInterestRateStrategy} from "../../../src/core/misc/ZeroReserveInterestRateStrategy.sol"; +import {PoolConfigurator} from "../../../src/core/protocol/pool/PoolConfigurator.sol"; +import {EmissionAdminAndDirectTransferStrategy} from "../../../src/core/protocol/tokenization/EmissionAdminAndDirectTransferStrategy.sol"; +import {ConfiguratorInputTypes} from '../../../src/core/protocol/libraries/types/ConfiguratorInputTypes.sol'; +import {ReservesSetupHelper} from "../../../src/core/deployments/ReservesSetupHelper.sol"; + +import {ADDRESSES_PROVIDER, POOLDATA_PROVIDER, ACL_MANAGER, POOL, POOL_CONFIGURATOR, EMISSION_MANAGER, + ATOKENIMPL, SDTOKENIMPL, VDTOKENIMPL, TREASURY, POOL_ADMIN, + MASTER_MAGPIE, SMART_HAY_LP, WOMBAT_HELPER_SMART_HAY_LP} from "test/utils/Addresses.sol"; + +contract ATokenMagpieStakerBaseTest is BaseTest { + address internal underlying = SMART_HAY_LP; + ATokenMagpieStaker internal ATokenProxyStaker; + EmissionAdminAndDirectTransferStrategy internal emissionAdmin; + function setUp() public virtual override(BaseTest) { + BaseTest.setUp(); + emissionAdmin = new EmissionAdminAndDirectTransferStrategy(pool, emissionManager); + // deploy reserve, get ATokenProxy + address aTokenProxy = deployReserveForATokenStaker(); + ATokenProxyStaker = ATokenMagpieStaker(aTokenProxy); + // configure riskParameter + configuraRiskParameterForReserve(underlying); + vm.startPrank(POOL_ADMIN); + // the magpie stakerAToken require setting up 1.) wombat helper for deposit + // 2.) master magpir for withdrawal + // 3.) emissionAdmin for sending over reward + ATokenProxyStaker.updateWombatHelper(WOMBAT_HELPER_SMART_HAY_LP); + ATokenProxyStaker.updateMasterMagpie(MASTER_MAGPIE); + // ATokenProxyStaker.updateEmissionAdmin(); + + } + + // return aTokenProxy + function deployReserveForATokenStaker() public returns (address) { + vm.startPrank(POOL_ADMIN); + bytes32 incentivesControllerId = 0x703c2c8634bed68d98c029c18f310e7f7ec0e5d6342c590190b3cb8b3ba54532; + address incentivesController = provider.getAddress(incentivesControllerId); + address interestStrategyAddress = deployStrategy(); + address atokenImpl = deployATokenStakerImpl(); + ConfiguratorInputTypes.InitReserveInput[] memory inputs = new ConfiguratorInputTypes.InitReserveInput[](1); + inputs[0] = ConfiguratorInputTypes.InitReserveInput( + atokenImpl, + SDTOKENIMPL, + VDTOKENIMPL, + IERC20Detailed(underlying).decimals(), + interestStrategyAddress, + underlying, + TREASURY, + incentivesController, + string(abi.encodePacked("Kinza", "LP-HAY")), + string(abi.encodePacked("k", "LP-HAY")), + string(abi.encodePacked("Kinza Variable Debt ", "LP-HAY")), + string(abi.encodePacked("vDebt", "LP-HAY")), + string(abi.encodePacked("Kinza Stable Debt ", "LP-HAY")), + string(abi.encodePacked("sDebt", "LP-HAY")), + abi.encodePacked("0x10") + ); + configurator.initReserves(inputs); + + (address ATokenProxyAddress,,) = dataProvider.getReserveTokensAddresses(underlying); + require(ATokenProxyAddress != address(0), "AToken not set"); + return ATokenProxyAddress; + } + + function deployATokenStakerImpl() internal returns(address) { + ATokenMagpieStaker aTokenImpl = new ATokenMagpieStaker(pool); + aTokenImpl.initialize( + pool, + address(0), // treasury + address(0), // underlyingAsset + IAaveIncentivesController(address(0)), // incentivesController + 0, // aTokenDecimals + "ATOKEN_IMPL", // aTokenName + "ATOKEN_IMPL", // aTokenSymbol + "0x00" // param + ); + return address(aTokenImpl); + } + function deployStrategy() internal returns (address) { + ZeroReserveInterestRateStrategy interestRateStrategy = new ZeroReserveInterestRateStrategy( + IPoolAddressesProvider(ADDRESSES_PROVIDER) + ); + return address(interestRateStrategy); + } + + function configuraRiskParameterForReserve(address underlying) internal { + aclManager.addPoolAdmin(address(helper)); + ReservesSetupHelper.ConfigureReserveInput[] memory inputs = new ReservesSetupHelper.ConfigureReserveInput[](1); + inputs[0] = ReservesSetupHelper.ConfigureReserveInput( + underlying, + 8000, // baseLTV + 9000, // liquidationThreshold + 10800, // liquidationBonus + 1500, // reserveFactor + 1000000, //borrowCap + 2000000, //supplyCap + false, //stableBorrowingEnabled + false, //borrowingEnabled + false //flashLoanEnabled + ); + helper.configureReserves(PoolConfigurator(address(configurator)), inputs); + //remove helper from pool admin + aclManager.removePoolAdmin(address(helper)); + } + +} \ No newline at end of file diff --git a/test/ATokenStaker/MagpieStaker/unit.t.sol b/test/ATokenStaker/MagpieStaker/unit.t.sol new file mode 100644 index 0000000..3c1e16e --- /dev/null +++ b/test/ATokenStaker/MagpieStaker/unit.t.sol @@ -0,0 +1,48 @@ + +import {ATokenMagpieStakerBaseTest} from "./ATokenMagpieStakerBaseTest.t.sol"; +import {IERC20} from "../../../src/core/dependencies/openzeppelin/contracts/IERC20.sol"; +import {ADDRESSES_PROVIDER, POOLDATA_PROVIDER, ACL_MANAGER, POOL, POOL_CONFIGURATOR, EMISSION_MANAGER, + ATOKENIMPL, SDTOKENIMPL, VDTOKENIMPL, TREASURY, POOL_ADMIN, + MASTER_MAGPIE, SMART_HAY_LP, WOMBAT_HELPER_SMART_HAY_LP} from "test/utils/Addresses.sol"; + +contract unitTest is ATokenMagpieStakerBaseTest { + + function setUp() public virtual override(ATokenMagpieStakerBaseTest) { + ATokenMagpieStakerBaseTest.setUp(); + } + + function test_deposit() public { + address bob = address(1); + uint256 amount = 1 ether; + deal(underlying, bob, amount); + deposit(bob, amount); + } + + // function test_withdraw() public { + // address bob = address(1); + // uint256 amount = 1 ether; + // deal(underlying, bob, amount); + // deposit(bob, amount); + // withdraw(bob, amount); + // } + + function deposit(address user, uint256 amount) public { + vm.startPrank(user); + uint256 before_aToken = ATokenProxyStaker.balanceOf(user); + uint256 before_underlying = IERC20(underlying).balanceOf(user); + IERC20(underlying).approve(address(pool), amount); + pool.deposit(underlying, amount, user, 0); + assertEq(ATokenProxyStaker.balanceOf(user), before_aToken + amount); + assertEq(IERC20(underlying).balanceOf(user), before_underlying - amount); + } + + // function withdraw(address user, uint256 amount) public { + // vm.startPrank(user); + // uint256 before_aToken = ATokenProxyStaker.balanceOf(user); + // uint256 before_underlying = IERC20(underlying).balanceOf(user); + // pool.withdraw(underlying, amount, user); + // assertEq(ATokenProxyStaker.balanceOf(user), before_aToken - amount); + // assertEq(IERC20(underlying).balanceOf(user), before_underlying + amount); + + // } +} \ No newline at end of file diff --git a/test/ATokenStaker/WombatStaker/ATokenWombatStakerBaseTest.t.sol b/test/ATokenStaker/WombatStaker/ATokenWombatStakerBaseTest.t.sol new file mode 100644 index 0000000..5a879db --- /dev/null +++ b/test/ATokenStaker/WombatStaker/ATokenWombatStakerBaseTest.t.sol @@ -0,0 +1,331 @@ + +import {BaseTest} from "test/BaseTest.t.sol"; + +import "../../../src/core/dependencies/openzeppelin/contracts/IERC20Detailed.sol"; +import {IPoolAddressesProvider} from "../../../src/core/interfaces/IPoolAddressesProvider.sol"; +import {IPool} from "../../../src/core/interfaces/IPool.sol"; +import {IACLManager} from '../../../src/core/interfaces/IACLManager.sol'; +import {IAaveOracle} from '../../../src/core/interfaces/IAaveOracle.sol'; +import {IAaveIncentivesController} from '../../../src/core/interfaces/IAaveIncentivesController.sol'; +import {IMasterWombat} from '../../../src/core/interfaces/IMasterWombat.sol'; +import {AToken} from "../../../src/core/protocol/tokenization/AToken.sol"; +import {SmartHayPoolOracle} from "../../../src/core/misc/wombatOracle/SmartHayPoolOracle.sol"; +import {ATokenWombatStaker} from "../../../src/core/protocol/tokenization/ATokenWombatStaker.sol"; +import {ZeroReserveInterestRateStrategy} from "../../../src/core/misc/ZeroReserveInterestRateStrategy.sol"; +import {PoolConfigurator} from "../../../src/core/protocol/pool/PoolConfigurator.sol"; +import {EmissionAdminAndDirectTransferStrategy} from "../../../src/core/protocol/tokenization/EmissionAdminAndDirectTransferStrategy.sol"; +import {ConfiguratorInputTypes} from '../../../src/core/protocol/libraries/types/ConfiguratorInputTypes.sol'; +import {ReservesSetupHelper} from "../../../src/core/deployments/ReservesSetupHelper.sol"; + +import {USDC, ADDRESSES_PROVIDER, POOLDATA_PROVIDER, ACL_MANAGER, POOL, POOL_CONFIGURATOR, EMISSION_MANAGER, + ATOKENIMPL, SDTOKENIMPL, VDTOKENIMPL, TREASURY, POOL_ADMIN, ORACLE, HAY_AGGREGATOR, HAY, + MASTER_WOMBAT, SMART_HAY_LP, LIQUIDATION_ADAPTOR, RANDOM, + USDC_AGGREGATOR, USDT_AGGREGATOR, USDC, USDT} from "test/utils/Addresses.sol"; + +contract ATokenWombatStakerBaseTest is BaseTest { + address internal underlying = SMART_HAY_LP; + ATokenWombatStaker internal ATokenProxyStaker; + EmissionAdminAndDirectTransferStrategy internal emissionAdmin; + uint8 internal eModeCategoryId = 2; + uint16 internal ltv = 9700; + uint16 internal liquidationThreshold = 9750; + uint16 internal liquidationBonus = 10100; + // each eMode category may or may not have a custom oracle to override the individual assets price oracles + // use HAY oracle for now + address internal LpOracle; + string internal label = "wombat LP Emode"; + function setUp() public virtual override(BaseTest) { + BaseTest.setUp(); + // deploy reserve, get ATokenProxy + address aTokenProxy = deployReserveForATokenStaker(); + ATokenProxyStaker = ATokenWombatStaker(aTokenProxy); + // configure riskParameter + configuraRiskParameterForReserve(underlying); + vm.startPrank(POOL_ADMIN); + // thewombat stakerAToken require setting up + // 1.) wombat master + // 2.) emissionAdmin for sending over reward + ATokenProxyStaker.updateMasterWombat(MASTER_WOMBAT); + emissionAdmin = new EmissionAdminAndDirectTransferStrategy(pool, emissionManager); + ATokenProxyStaker.updateEmissionAdmin(address(emissionAdmin)); + // add emode, setup oracle specific to the emode + LpOracle = address(new SmartHayPoolOracle( + USDC_AGGREGATOR, USDT_AGGREGATOR, HAY_AGGREGATOR, USDC, USDT, HAY + )); + setUpEmodeAndOracle(); + addAssetIntoEmode(); + turnOnOpenForEveryone(); + } + + // return aTokenProxy + function deployReserveForATokenStaker() public returns (address) { + vm.startPrank(POOL_ADMIN); + bytes32 incentivesControllerId = 0x703c2c8634bed68d98c029c18f310e7f7ec0e5d6342c590190b3cb8b3ba54532; + address incentivesController = provider.getAddress(incentivesControllerId); + address interestStrategyAddress = deployZeroRateStrategy(); + address atokenImpl = deployATokenStakerImpl(); + ConfiguratorInputTypes.InitReserveInput[] memory inputs = new ConfiguratorInputTypes.InitReserveInput[](1); + inputs[0] = ConfiguratorInputTypes.InitReserveInput( + atokenImpl, + SDTOKENIMPL, + VDTOKENIMPL, + IERC20Detailed(underlying).decimals(), + interestStrategyAddress, + underlying, + TREASURY, + incentivesController, + string(abi.encodePacked("Kinza", "LP-HAY")), + string(abi.encodePacked("k", "LP-HAY")), + string(abi.encodePacked("Kinza Variable Debt ", "LP-HAY")), + string(abi.encodePacked("vDebt", "LP-HAY")), + string(abi.encodePacked("Kinza Stable Debt ", "LP-HAY")), + string(abi.encodePacked("sDebt", "LP-HAY")), + abi.encodePacked("0x10") + ); + configurator.initReserves(inputs); + + (address ATokenProxyAddress,,) = dataProvider.getReserveTokensAddresses(underlying); + require(ATokenProxyAddress != address(0), "AToken not set"); + return ATokenProxyAddress; + } + + function deployATokenStakerImpl() internal returns(address) { + ATokenWombatStaker aTokenImpl = new ATokenWombatStaker(pool); + aTokenImpl.initialize( + pool, + address(0), // treasury + address(0), // underlyingAsset + IAaveIncentivesController(address(0)), // incentivesController + 0, // aTokenDecimals + "ATOKEN_IMPL", // aTokenName + "ATOKEN_IMPL", // aTokenSymbol + "0x00" // param + ); + return address(aTokenImpl); + } + function deployZeroRateStrategy() internal returns (address) { + ZeroReserveInterestRateStrategy interestRateStrategy = new ZeroReserveInterestRateStrategy( + IPoolAddressesProvider(ADDRESSES_PROVIDER) + ); + return address(interestRateStrategy); + } + + function configuraRiskParameterForReserve(address underlying) internal { + aclManager.addPoolAdmin(address(helper)); + ReservesSetupHelper.ConfigureReserveInput[] memory inputs = new ReservesSetupHelper.ConfigureReserveInput[](1); + inputs[0] = ReservesSetupHelper.ConfigureReserveInput( + underlying, + 0, // baseLTV + 0, // liquidationThreshold + 0, // liquidationBonus + 1500, // reserveFactor + 1, //borrowCap + 2000000, //supplyCap + false, //stableBorrowingEnabled + false, //borrowingEnabled + false //flashLoanEnabled + ); + helper.configureReserves(PoolConfigurator(address(configurator)), inputs); + //remove helper from pool admin + aclManager.removePoolAdmin(address(helper)); + } + + // function setUpOracleThroughFallback() internal { + // GenericLPFallbackOracle LPFallbackOracle = new GenericLPFallbackOracle(); + // vm.startPrank(POOL_ADMIN); + // IAaveOracle(oracle).setFallbackOracle(address(LPFallbackOracle)); + // } + + function setUpEmodeAndOracle() internal { + vm.startPrank(POOL_ADMIN); + configurator.setEModeCategory(eModeCategoryId, ltv, liquidationThreshold, liquidationBonus, address(0), label); + // then set oracle + address[] memory assets = new address[](1); + address[] memory sources = new address[](1); + + assets[0] = underlying; + sources[0] = LpOracle; + IAaveOracle(oracle).setAssetSources(assets, sources); + uint256[] memory price = new uint256[](1); + price = IAaveOracle(oracle).getAssetsPrices(assets); + assertGt(price[0], 0); + } + + function addAssetIntoEmode() internal { + vm.startPrank(POOL_ADMIN); + configurator.setAssetEModeCategory(underlying, eModeCategoryId); + configurator.setAssetEModeCategory(HAY, eModeCategoryId); + } + + function setUpOracle(address source, address asset) internal { + address[] memory assets = new address[](1); + address[] memory sources = new address[](1); + assets[0] = asset; + sources[0] = source; + vm.startPrank(POOL_ADMIN); + IAaveOracle(oracle).setAssetSources(assets, sources); + } + + function deposit(address user, uint256 amount, address underlying) internal { + vm.startPrank(user); + deal(underlying, user, amount); + (address ATokenProxyAddress,,) = dataProvider.getReserveTokensAddresses(underlying); + uint256 before_aToken = IERC20(ATokenProxyAddress).balanceOf(user); + uint256 before_underlying = IERC20(underlying).balanceOf(user); + IERC20(underlying).approve(address(pool), amount); + pool.deposit(underlying, amount, user, 0); + assertEq(IERC20(ATokenProxyAddress).balanceOf(user), before_aToken + amount); + assertEq(IERC20(underlying).balanceOf(user), before_underlying - amount); + } + + function withdraw(address user, uint256 amount, address underlying) internal { + vm.startPrank(user); + (address ATokenProxyAddress,,) = dataProvider.getReserveTokensAddresses(underlying); + uint256 before_aToken = IERC20(ATokenProxyAddress).balanceOf(user); + uint256 before_underlying = IERC20(underlying).balanceOf(user); + pool.withdraw(underlying, amount, user); + assertEq(IERC20(ATokenProxyAddress).balanceOf(user), before_aToken - amount); + assertEq(IERC20(underlying).balanceOf(user), before_underlying + amount); + } + + + function borrow(address user, uint256 amount, address underlying) internal { + vm.startPrank(user); + // 2 = variable mode, 0 = no referral + pool.borrow(underlying, amount, 2, 0, user); + } + + function borrowExpectFail(address user, uint256 amount, address underlying, string memory errorMsg) internal { + vm.startPrank(user); + vm.expectRevert(abi.encodePacked(errorMsg)); + // 2 = variable mode, 0 = no referral + pool.borrow(underlying, amount, 2, 0, user); + } + + function flashloan(address user, address dest, uint256 amount, address underlying) internal { + vm.startPrank(user); + // 2 = variable mode, 0 = no referral + // no param, 0 = no referral + // receiver needs to be a contract + pool.flashLoanSimple(dest, underlying, amount, "", 0); + } + function flashloanRevert(address user, uint256 amount, address underlying, string memory errorMsg) internal { + vm.startPrank(user); + vm.expectRevert(abi.encodePacked(errorMsg)); + // 2 = variable mode, 0 = no referral + // no param, 0 = no referral + // receiver needs to be a contract + pool.flashLoanSimple(LIQUIDATION_ADAPTOR, underlying, amount, "", 0); + } + + function prepUSDC(address user, uint256 collateral_amount) internal { + // deposit USDC to have some collateral power + deposit(user, collateral_amount, USDC); + } + + function setUpPositiveLTV() internal { + vm.startPrank(POOL_ADMIN); + configurator.configureReserveAsCollateral(underlying, 70, 80, 10100); + } + function turnOnBorrow() internal { + vm.startPrank(POOL_ADMIN); + configurator.setReserveBorrowing(underlying, true); + } + + function turnOnFlashloan() internal { + vm.startPrank(POOL_ADMIN); + configurator.setReserveFlashLoaning(underlying, true); + } + + + function turnOnCollateral(address user, address collateral) internal { + vm.startPrank(user); + pool.setUserUseReserveAsCollateral(collateral, true); + } + + function turnOnCollateralExpectRevert(address user, address collateral, string memory errorMsg) internal { + vm.startPrank(user); + vm.expectRevert(abi.encodePacked(errorMsg)); + pool.setUserUseReserveAsCollateral(collateral, true); + } + + function turnOffCollateral(address user, address collateral) internal { + vm.startPrank(user); + pool.setUserUseReserveAsCollateral(collateral, false); + } + + function turnOnEmode(address user) internal { + vm.startPrank(user); + pool.setUserEMode(eModeCategoryId); + } + + function turnOffEmode(address user) internal { + vm.startPrank(user); + pool.setUserEMode(0); + } + + function liquidate(address user, address debtAsset, address collateralAsset, uint256 debtToCover) internal { + address liuqidator = RANDOM; + deal(debtAsset, liuqidator, debtToCover); + IERC20(debtAsset).approve(address(pool), debtToCover); + bool receiveAToken = false; + pool.liquidationCall(collateralAsset, debtAsset, user, debtToCover, receiveAToken); + } + + function liquidateRevert(address user, address debtAsset, address collateralAsset, uint256 debtToCover) internal { + address liuqidator = RANDOM; + deal(debtAsset, liuqidator, debtToCover); + IERC20(debtAsset).approve(address(pool), debtToCover); + bool receiveAToken = false; + vm.expectRevert(); + pool.liquidationCall(collateralAsset, debtAsset, user, debtToCover, receiveAToken); + } + + function liquidateRevertWith46(address user, address debtAsset, address collateralAsset, uint256 debtToCover) internal { + address liuqidator = RANDOM; + deal(debtAsset, liuqidator, debtToCover); + IERC20(debtAsset).approve(address(pool), debtToCover); + bool receiveAToken = false; + vm.expectRevert(abi.encodePacked("46")); + pool.liquidationCall(collateralAsset, debtAsset, user, debtToCover, receiveAToken); + } + + + function transferAToken(address from, address to, uint256 amount, address ATokenProxy) internal { + uint256 before = IERC20(ATokenProxy).balanceOf(to); + vm.startPrank(from); + IERC20(ATokenProxy).transfer(to, amount); + assertEq(before + amount, IERC20(ATokenProxy).balanceOf(to)); + } + + function transferATokenRevert(address from, address to, uint256 amount, address ATokenProxy, string memory errorMsg) internal { + uint256 before = IERC20(ATokenProxy).balanceOf(to); + vm.startPrank(from); + vm.expectRevert(); + IERC20(ATokenProxy).transfer(to, amount); + } + + function turnOnOpenForEveryone() internal { + vm.startPrank(POOL_ADMIN); + ATokenProxyStaker.toogleOpenForEveryone(true); + } + + function toggleEmergency() internal { + vm.startPrank(POOL_ADMIN); + ATokenProxyStaker.toggleEmergencyWithdraw(); + } + + function setReserveAsZeroLTV() internal { + vm.startPrank(POOL_ADMIN); + configurator.configureReserveAsCollateral(underlying, 0, 0, 0); + } + + function getPrice(address underlying, address lp) public view returns(uint256, uint256) { + IAaveOracle oracle = IAaveOracle(provider.getPriceOracle()); + address[] memory assets = new address[](2); + assets[0] = underlying; + assets[1] = lp; + uint256[] memory prices = oracle.getAssetsPrices(assets); + return (prices[0], prices[1]); + } +} \ No newline at end of file diff --git a/test/ATokenStaker/WombatStaker/fuzzing.t.sol b/test/ATokenStaker/WombatStaker/fuzzing.t.sol new file mode 100644 index 0000000..dcd894b --- /dev/null +++ b/test/ATokenStaker/WombatStaker/fuzzing.t.sol @@ -0,0 +1,51 @@ + +import {ATokenWombatStakerBaseTest} from "./ATokenWombatStakerBaseTest.t.sol"; +import {IERC20} from "../../../src/core/dependencies/openzeppelin/contracts/IERC20.sol"; +import {USDC, ADDRESSES_PROVIDER, POOLDATA_PROVIDER, ACL_MANAGER, POOL, POOL_CONFIGURATOR, EMISSION_MANAGER, + ATOKENIMPL, SDTOKENIMPL, VDTOKENIMPL, TREASURY, POOL_ADMIN, HAY_AGGREGATOR, + MASTER_WOMBAT, SMART_HAY_LP, LIQUIDATION_ADAPTOR} from "test/utils/Addresses.sol"; + +contract fuzzingTest is ATokenWombatStakerBaseTest { + function setUp() public virtual override(ATokenWombatStakerBaseTest) { + ATokenWombatStakerBaseTest.setUp(); + } + + function testFuzz_deposit( + address user, + uint256 amount + ) external { + (address ATokenProxyAddress,,) = dataProvider.getReserveTokensAddresses(underlying); + if (user == address(0) || user == ATokenProxyAddress) { + user = address(1); + } + ( ,uint256 supplyCap) = dataProvider.getReserveCaps(underlying); + amount = bound(amount, 1, supplyCap * 1e18); + deal(address(underlying), user, amount); + vm.startPrank(user); + IERC20(underlying).approve(address(pool), amount); + pool.deposit(underlying, amount, user, 0); + assertEq(IERC20(ATokenProxyAddress).balanceOf(user), amount); + } + + function testFuzz_withdraw( + address user, + uint256 amount + ) external { + (address ATokenProxyAddress,,) = dataProvider.getReserveTokensAddresses(underlying); + if (user == address(0) || user == ATokenProxyAddress) { + user = address(1); + } + // we are fuzzing withdraw here so just max deposit for a user + ( ,uint256 supplyCap) = dataProvider.getReserveCaps(underlying); + uint256 maxDposit = supplyCap * 1e18; + deal(address(underlying), user, maxDposit); + vm.startPrank(user); + IERC20(underlying).approve(address(pool), maxDposit); + pool.deposit(underlying, maxDposit, user, 0); + // here is the withdraw + amount = bound(amount, 1, maxDposit); + uint256 before = IERC20(underlying).balanceOf(user); + pool.withdraw(underlying, amount, user); + assertEq(IERC20(underlying).balanceOf(user), before + amount); + } +} \ No newline at end of file diff --git a/test/ATokenStaker/WombatStaker/leverage.t.sol b/test/ATokenStaker/WombatStaker/leverage.t.sol new file mode 100644 index 0000000..194a5d1 --- /dev/null +++ b/test/ATokenStaker/WombatStaker/leverage.t.sol @@ -0,0 +1,100 @@ + +import "forge-std/console2.sol"; +import {ATokenWombatStakerBaseTest} from "./ATokenWombatStakerBaseTest.t.sol"; +import {IERC20} from "../../../src/core/dependencies/openzeppelin/contracts/IERC20.sol"; +import {IPoolDataProvider} from "../../../src/core/interfaces/IPoolDataProvider.sol"; +import {ICreditDelegationToken} from "../../../src/core/interfaces/ICreditDelegationToken.sol"; + + +import {BorrowableDataProvider} from "../../../src/periphery/misc/BorrowableDataProvider.sol"; +import {WombatLeverageHelper} from "../../../src/periphery/misc/WombatLeverageHelper.sol"; +import {ADDRESSES_PROVIDER, POOLDATA_PROVIDER, ACL_MANAGER, POOL, POOL_CONFIGURATOR, EMISSION_MANAGER, + ATOKENIMPL, SDTOKENIMPL, VDTOKENIMPL, TREASURY, POOL_ADMIN, HAY_AGGREGATOR, HAY, + SMART_HAY_POOL, SMART_HAY_LP, LIQUIDATION_ADAPTOR, BORROWABLE_DATA_PROVIDER} from "test/utils/Addresses.sol"; + +contract leverageTest is ATokenWombatStakerBaseTest { + uint256 internal slippage = 10; + WombatLeverageHelper internal levHelper; + ICreditDelegationToken internal vDebtUnderlyingToken; + function setUp() public virtual override(ATokenWombatStakerBaseTest) { + ATokenWombatStakerBaseTest.setUp(); + levHelper = new WombatLeverageHelper( + ADDRESSES_PROVIDER + ); + (,,address vDebtProxy) = dataProvider.getReserveTokensAddresses(HAY); + vDebtUnderlyingToken = ICreditDelegationToken(vDebtProxy); + } + + function test_leverageSetUp() public { + + } + + function test_borrowTotal() public { + uint256 targetHF = 2 * 1e18; + uint256 depositAmount = 100 * 1e18; + address bob = address(1); + (uint256 underlyingPrice, uint256 lpPrice) = getPrice(HAY, underlying); + vm.startPrank(bob); + uint256 toBorrow = levHelper.calculateUnderlyingBorrow( + targetHF, underlying, depositAmount, eModeCategoryId + ); + console2.log(toBorrow); + } + function test_maxBorrowable() public { + address bob = address(1); + turnOnEmode(bob); + uint256 depositAmount = 100 * 1e18; + deal(underlying, bob, depositAmount); + vm.startPrank(bob); + IERC20(underlying).approve(address(pool), depositAmount); + deposit(bob, depositAmount, underlying); + uint256 borrowable = BorrowableDataProvider(BORROWABLE_DATA_PROVIDER).calculateLTVBorrowable(bob, HAY); + assertGt(borrowable, 0); + } + function test_loopWithLP() public { + uint256 targetHF = 1.2 * 1e18; + uint256 depositAmount = 100 * 1e18; + address bob = address(1); + turnOnEmode(bob); + deal(underlying, bob, depositAmount); + vm.startPrank(bob); + IERC20(underlying).approve(address(levHelper), depositAmount); + // approve borrowing allowance or levHelper + vDebtUnderlyingToken.approveDelegation(address(levHelper), type(uint256).max); + //assertEq(vDebtUnderlyingToken.borrowAllowance(bob, address(levHelper)), type(uint256).max); + bool isUnderlying = false; + loop(bob, targetHF, depositAmount, isUnderlying); + (,,,,, uint256 healthFactor) = pool.getUserAccountData(bob); + console2.log("HF after loop: ",healthFactor); + } + + function test_loopWithUnderlying() public { + uint256 targetHF = 1.2 * 1e18; + uint256 depositAmount = 100 * 1e18; + address bob = address(1); + turnOnEmode(bob); + deal(HAY, bob, depositAmount); + vm.startPrank(bob); + IERC20(HAY).approve(address(levHelper), depositAmount); + vDebtUnderlyingToken.approveDelegation(address(levHelper), type(uint256).max); + bool isUnderlying = true; + loop(bob, targetHF, depositAmount, isUnderlying); + (,,,,, uint256 healthFactor) = pool.getUserAccountData(bob); + console2.log("HF after loop: ",healthFactor); + + } + + function loop(address user, uint256 targetHF, uint256 depositAmount, bool isUnderlying) public { + vm.startPrank(user); + // check variableDebt of the underlying asset is indeed the borrowed. + (,,address vDebtProxy) = IPoolDataProvider(POOLDATA_PROVIDER).getReserveTokensAddresses(HAY); + uint256 debtBefore = IERC20(vDebtProxy).balanceOf(user); + uint256 borrowed; + if (isUnderlying) { + borrowed = levHelper.depositUnderlyingAndLoop(BORROWABLE_DATA_PROVIDER, SMART_HAY_POOL, targetHF, underlying, depositAmount, eModeCategoryId, slippage); + } else { + borrowed = levHelper.depositLpAndLoop(BORROWABLE_DATA_PROVIDER, SMART_HAY_POOL, targetHF, underlying, depositAmount, eModeCategoryId, slippage); + } + assertEq(debtBefore + borrowed, IERC20(vDebtProxy).balanceOf(user)); + } +} \ No newline at end of file diff --git a/test/ATokenStaker/WombatStaker/reward-fuzz.t.sol b/test/ATokenStaker/WombatStaker/reward-fuzz.t.sol new file mode 100644 index 0000000..929194b --- /dev/null +++ b/test/ATokenStaker/WombatStaker/reward-fuzz.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import './reward.t.sol'; +// import { console2 } from "forge-std/console2.sol"; + +contract rewardFuzzTest is rewardTest { + function setUp() public virtual override(rewardTest) { + rewardTest.setUp(); + } + + function testFuzz_claimWOM(uint16 TimeToPass) public { + vm.assume(TimeToPass > 1); + address bob = address(1); + deposit(bob, 1e18, address(underlying)); + vm.warp(TimeToPass + block.timestamp); + claimRewardFromMasterWombat(); + } + + function testFuzz_distribute(uint32 _TimeToPass, uint32 _TimeToPassForRewardToAccrue) public { + uint256 timeToPass = bound(_TimeToPass, 1, 2**31); + // console2.log('REWARD_PERIOD', emissionAdmin.REWARD_PERIOD()); + uint256 timeToPassForRewardToAccrue = bound(_TimeToPassForRewardToAccrue, 1, emissionAdmin.REWARD_PERIOD()); + // set up a depositor + address bob = address(1); + uint256 amount = 1e18; + deposit(bob, amount, address(underlying)); + // passage of time + vm.warp(timeToPass + block.timestamp); + + (address ATokenProxyAddress,,) = dataProvider.getReserveTokensAddresses(underlying); + + uint256 beforeReward = IERC20(rewardToken).balanceOf(ATokenProxyAddress); + IMasterWombat masterWombat = ATokenProxyStaker._masterWombat(); + (uint256 pendingReward,,,) = masterWombat.pendingTokens(ATokenProxyStaker._pid(), ATokenProxyAddress); + assertGt(pendingReward, 0); + uint256 totalReward = pendingReward + beforeReward; + // console2.log("totalReward", totalReward); + + address[] memory assets = new address[](1); + assets[0] = address(ATokenProxyStaker); + uint256 claimableBefore = emissionManager.getRewardsController().getUserRewards(assets, bob, address(rewardToken)); + assertEq(claimableBefore, 0); + + // distribute + sendToEmissionManager(); + // assert the user has non-zero claimable + vm.warp(timeToPassForRewardToAccrue + block.timestamp); + uint256 claimable = emissionManager.getRewardsController().getUserRewards(assets, bob, address(rewardToken)); + assertEq(claimable, totalReward / emissionAdmin.REWARD_PERIOD() * timeToPassForRewardToAccrue); + } +} diff --git a/test/ATokenStaker/WombatStaker/reward.t.sol b/test/ATokenStaker/WombatStaker/reward.t.sol new file mode 100644 index 0000000..d62e6f8 --- /dev/null +++ b/test/ATokenStaker/WombatStaker/reward.t.sol @@ -0,0 +1,121 @@ + +import {ATokenWombatStakerBaseTest} from "./ATokenWombatStakerBaseTest.t.sol"; +import {IAToken} from "../../../src/core/interfaces/IAToken.sol"; +import {IERC20} from "../../../src/core/dependencies/openzeppelin/contracts/IERC20.sol"; +import {IERC20} from "../../../src/core/dependencies/openzeppelin/contracts/IERC20Detailed.sol"; +import {RewardsDataTypes} from '../../../src/periphery/rewards/libraries/RewardsDataTypes.sol'; +import {ITransferStrategyBase} from '../../../src/periphery/rewards/interfaces/ITransferStrategyBase.sol'; +import {IEACAggregatorProxy} from '../../../src/periphery/misc/interfaces/IEACAggregatorProxy.sol'; +import {MOCK_WOM_ORACLE, WOM, POOL_ADMIN, EMISSION_MANAGER_ADMIN} from "test/utils/Addresses.sol"; +import {IMasterWombat} from '../../../src/core/interfaces/IMasterWombat.sol'; + +contract rewardTest is ATokenWombatStakerBaseTest { + uint256 internal _forkBlock = 31_400_000; + + IERC20 internal rewardToken = IERC20(WOM); + function setUp() public virtual override(ATokenWombatStakerBaseTest) { + // this varaible is taken by the parent setUp + // uint256 forkBlock = _forkBlock; + ATokenWombatStakerBaseTest.setUp(); + // configure WOM as a reward token sendable by emissionAdmin + configReward(); + vm.startPrank(EMISSION_MANAGER_ADMIN); + emissionManager.setEmissionAdmin(address(rewardToken), address(emissionAdmin)); + vm.startPrank(POOL_ADMIN); + emissionAdmin.toggleATokenWhitelist(IAToken(ATokenProxyStaker)); + } + + function test_claim() public { + address bob = address(1); + uint256 amount = 1e18; + deposit(bob, amount, address(underlying)); + uint256 TimeToPass = 1 days; + vm.warp(TimeToPass + block.timestamp); + claimRewardFromMasterWombat(); + } + + function test_distribute() public { + // set up a depositor + address bob = address(1); + uint256 amount = 1e18; + deposit(bob, amount, address(underlying)); + // passage of time + uint256 TimeToPass = 1 days; + vm.warp(TimeToPass + block.timestamp); + + (address ATokenProxyAddress,,) = dataProvider.getReserveTokensAddresses(underlying); + + uint256 beforeReward = rewardToken.balanceOf(ATokenProxyAddress); + IMasterWombat masterWombat = ATokenProxyStaker._masterWombat(); + (uint256 pendingReward,,,) = masterWombat.pendingTokens(ATokenProxyStaker._pid(), ATokenProxyAddress); + uint256 totalReward = pendingReward + beforeReward; + // console2.log("totalReward", totalReward); + + address[] memory assets = new address[](1); + assets[0] = address(ATokenProxyStaker); + uint256 claimableBefore = emissionManager.getRewardsController().getUserRewards(assets, bob, address(rewardToken)); + assertEq(claimableBefore, 0); + + // distribute + sendToEmissionManager(); + // console2.log('REWARD_PERIOD', emissionAdmin.REWARD_PERIOD()); + // assert the user has non-zero claimable + uint256 TimeToPassForRewardToAccrue = 1 days; + vm.warp(TimeToPassForRewardToAccrue + block.timestamp); + uint256 claimable = emissionManager.getRewardsController().getUserRewards(assets, bob, address(rewardToken)); + assertEq(claimable, totalReward / emissionAdmin.REWARD_PERIOD() * TimeToPassForRewardToAccrue); + } + + // test if reward are split fairly + function test_splitReward() public { + // set up a depositor + address bob = address(1); + uint256 bob_amount = 1e18; + address alice = address(2); + uint256 alice_amount = 2e18; + deposit(bob, bob_amount, address(underlying)); + deposit(alice, alice_amount, address(underlying)); + // passage of time + uint256 TimeToPass = 1 days; + vm.warp(TimeToPass + block.timestamp); + // distribute + sendToEmissionManager(); + // assert the user has non-zero claimable + uint256 TimeToPassForRewardToAccrue = 1 days; + vm.warp(TimeToPassForRewardToAccrue + block.timestamp); + address[] memory assets = new address[](1); + assets[0] = address(ATokenProxyStaker); + uint256 bob_claimable = emissionManager.getRewardsController().getUserRewards(assets, bob, address(rewardToken)); + uint256 alice_claimable = emissionManager.getRewardsController().getUserRewards(assets, alice, address(rewardToken)); + assertEq(bob_claimable * alice_amount / bob_amount, alice_claimable); + + } + + function configReward() internal { + RewardsDataTypes.RewardsConfigInput[] memory config = new RewardsDataTypes.RewardsConfigInput[](1); + config[0].asset = address(ATokenProxyStaker); + config[0].reward = address(rewardToken); + config[0].transferStrategy = ITransferStrategyBase(address(emissionAdmin)); + config[0].rewardOracle = IEACAggregatorProxy(MOCK_WOM_ORACLE); + vm.startPrank(EMISSION_MANAGER_ADMIN); + // set admin itself as the reward emission admin, which later would be replaced by the contract + emissionManager.setEmissionAdmin(address(rewardToken), EMISSION_MANAGER_ADMIN); + emissionManager.configureAssets(config); + } + + function claimRewardFromMasterWombat() internal { + address aToken = address(ATokenProxyStaker); + vm.startPrank(POOL_ADMIN); + uint256 beforeReward = rewardToken.balanceOf(aToken); + ATokenProxyStaker.multiClaim(); + uint256 afterReward = rewardToken.balanceOf(aToken); + assertGt(afterReward, beforeReward); + } + + function sendToEmissionManager() internal { + address[] memory rewards = new address[](1); + rewards[0] = address(rewardToken); + vm.startPrank(POOL_ADMIN); + ATokenProxyStaker.sendEmission(rewards); + } +} \ No newline at end of file diff --git a/test/ATokenStaker/WombatStaker/unit.t.sol b/test/ATokenStaker/WombatStaker/unit.t.sol new file mode 100644 index 0000000..004a64f --- /dev/null +++ b/test/ATokenStaker/WombatStaker/unit.t.sol @@ -0,0 +1,285 @@ + +import {ATokenWombatStakerBaseTest} from "./ATokenWombatStakerBaseTest.t.sol"; +import {IERC20} from "../../../src/core/dependencies/openzeppelin/contracts/IERC20.sol"; +import {ValidationLogic} from "../../../src/core/protocol/libraries/logic/ValidationLogic.sol"; +import {IPoolAddressesProvider} from "../../../src/core/interfaces/IPoolAddressesProvider.sol"; +import {Pool} from "../../../src/core/protocol/pool/Pool.sol"; + +import {TIMELOCK, ADDRESSES_PROVIDER, POOLDATA_PROVIDER, ACL_MANAGER, POOL, POOL_CONFIGURATOR, EMISSION_MANAGER, + ATOKENIMPL, SDTOKENIMPL, VDTOKENIMPL, TREASURY, POOL_ADMIN, HAY_AGGREGATOR, HAY, + MASTER_WOMBAT, SMART_HAY_LP, LIQUIDATION_ADAPTOR, BORROWABLE_DATA_PROVIDER} from "test/utils/Addresses.sol"; + +// @dev disable linked lib in foundry.toml, since forge test would inherit those setting +// https://book.getfoundry.sh/reference/forge/forge-build?highlight=link#linker-options +contract poolUpgradeUnitTest is ATokenWombatStakerBaseTest { + + function setUp() public virtual override(ATokenWombatStakerBaseTest) { + ATokenWombatStakerBaseTest.setUp(); + // deploy new pool impl with new validationLogic + + Pool poolV2 = new Pool(IPoolAddressesProvider(ADDRESSES_PROVIDER)); + poolV2.initialize(IPoolAddressesProvider(ADDRESSES_PROVIDER)); + // upgrade + vm.startPrank(TIMELOCK); + provider.setPoolImpl(address(poolV2)); + assertEq(Pool(address(pool)).POOL_REVISION(), 0x2); + } + + function test_enableCollateralWithZeroLTV() public { + setReserveAsZeroLTV(); + address bob = address(1); + turnOnEmode(bob); + deposit(bob, 1e18, underlying); + turnOnCollateral(bob, underlying); + } + + function test_liquidateRevertOutsideEmodeAfterProxyUpgrade() public { + setReserveAsZeroLTV(); + address bob = address(1); + uint256 collateralAmount = 100 * 1e18; + deposit(bob, collateralAmount, underlying); + turnOnCollateral(bob, underlying); + // now setup a bad debt + prepUSDC(bob, 1e18); + address debtAsset = HAY; + borrow(bob, 6e17, debtAsset); + // pass 100y + vm.warp(36500 days); + // verify health factor < 1; + (,,,,, uint256 healthFactor) = pool.getUserAccountData(bob); + assertLt(healthFactor, 1e18); + // attempt to liquidate half of original debt + liquidateRevertWith46(bob, debtAsset, underlying, 3e17); + + } + + function test_deposit() public { + address bob = address(1); + uint256 amount = 1 ether; + deposit(bob, amount, underlying); + } + + function test_withdraw() public { + address bob = address(1); + uint256 amount = 1 ether; + deposit(bob, amount, underlying); + withdraw(bob, amount, underlying); + } + + function test_transfer() public { + address bob = address(1); + address alice = address(2); + uint256 amount = 1 ether; + deposit(bob, amount, underlying); + transferAToken(bob, alice, amount, address(ATokenProxyStaker)); + } + + function test_borrowWhenBorrowDisabled() public { + address bob = address(1); + uint256 collateralAmount = 100_000; + uint256 borrow_amount = 100; + prepUSDC(bob, collateralAmount); + //when borrow is disabled + borrowExpectFail(bob, borrow_amount, underlying, '30'); + } + function test_borrowWhenBorrowEnabled() public { + address bob = address(1); + uint256 collateralAmount = 100_000 * 1e18; + uint256 borrow_amount = 100 * 1e18; + // have a deposit first, so there is reserve available + deposit(bob, borrow_amount, underlying); + prepUSDC(bob, collateralAmount); + turnOnBorrow(); + borrowExpectFail(bob, borrow_amount, underlying, 'ATokenStaker does not allow flashloan or borrow'); + } + + + function test_flashloanWhenDisabled() public { + address bob = address(1); + uint256 collateralAmount = 100 * 1e18; + deposit(bob, collateralAmount, underlying); + flashloanRevert(bob, collateralAmount, underlying, '91'); + } + + function test_flashloanWhenEnabled() public { + address bob = address(1); + uint256 collateralAmount = 100 * 1e18; + turnOnFlashloan(); + deposit(bob, collateralAmount, underlying); + flashloanRevert(bob, collateralAmount, underlying, 'ATokenStaker does not allow flashloan or borrow'); + } + + function test_enableCollateral() public { + address bob = address(1); + uint256 collateralAmount = 100 * 1e18; + deposit(bob, collateralAmount, underlying); + turnOnCollateral(bob, underlying); + } + + function test_disableAsCollateral() public { + address bob = address(1); + uint256 collateralAmount = 100 * 1e18; + deposit(bob, collateralAmount, underlying); + turnOffCollateral(bob, underlying); + } + + // liquidate + function test_liquidateRevertOutsideEmode() public { + address bob = address(1); + uint256 collateralAmount = 100 * 1e18; + deposit(bob, collateralAmount, underlying); + turnOnCollateral(bob, underlying); + // now setup a bad debt + prepUSDC(bob, 1e18); + address debtAsset = HAY; + borrow(bob, 6e17, debtAsset); + // pass 100y + vm.warp(36500 days); + // verify health factor < 1; + (,,,,, uint256 healthFactor) = pool.getUserAccountData(bob); + assertLt(healthFactor, 1e18); + // attempt to liquidate half of original debt + liquidateRevert(bob, debtAsset, underlying, 3e17); + + } + function test_liquidateInsideEmode() public { + address bob = address(1); + uint256 collateralAmount = 100 * 1e18; + deposit(bob, collateralAmount, underlying); + turnOnEmode(bob); + turnOnCollateral(bob, underlying); + address debtAsset = HAY; + uint256 debtAmount = collateralAmount / 2; + borrow(bob, debtAmount, debtAsset); + // pass 100y + vm.warp(36500 days); + // verify health factor < 1; + (,,,,, uint256 healthFactor) = pool.getUserAccountData(bob); + assertLt(healthFactor, 1e18); + // attempt to liquidate half of original debt + liquidate(bob, debtAsset, underlying, debtAmount / 2); + // assert some collateral are seize + assertLt(IERC20(ATokenProxyStaker).balanceOf(bob), collateralAmount); + } + + function test_transferInsideEmode() public { + address bob = address(1); + address alice = address(2); + uint256 amount = 1 ether; + deposit(bob, amount, underlying); + turnOnEmode(bob); + transferAToken(bob, alice, amount, address(ATokenProxyStaker)); + } + + function test_transferInsideEmodeRevert() public { + address bob = address(1); + address alice = address(2); + uint256 collateralAmount = 1 ether; + deposit(bob, collateralAmount, underlying); + turnOnEmode(bob); + address debtAsset = HAY; + uint256 debtAmount = collateralAmount / 2; + borrow(bob, debtAmount, debtAsset); + transferATokenRevert(bob, alice, collateralAmount, address(ATokenProxyStaker), ""); + } + + // testEmode + function test_enableEmode() public { + address bob = address(1); + uint256 collateralAmount = 100 * 1e18; + deposit(bob, collateralAmount, underlying); + turnOnEmode(bob); + } + + function test_disableEmode() public { + address bob = address(1); + uint256 collateralAmount = 100 * 1e18; + deposit(bob, collateralAmount, underlying); + turnOnEmode(bob); + turnOffEmode(bob); + } + + function test_borrowOther() public { + address bob = address(1); + uint256 collateralAmount = 100 * 1e18; + deposit(bob, collateralAmount, underlying); + turnOnCollateral(bob, underlying); + // borrow + uint256 borrowAmount = 100; + // borrow(bob, borrowAmount, HAY); + borrowExpectFail(bob, borrowAmount, HAY, ''); + } + function test_borrowWithEmode() public { + address bob = address(1); + uint256 collateralAmount = 100 * 1e18; + deposit(bob, collateralAmount, underlying); + turnOnEmode(bob); + turnOnCollateral(bob, underlying); + + } + function test_borrowWithEmode() public { + address bob = address(1); + uint256 collateralAmount = 100 * 1e18; + deposit(bob, collateralAmount, underlying); + turnOnEmode(bob); + turnOnCollateral(bob, underlying); + // borrow + uint256 borrowAmount = collateralAmount / 2; + borrow(bob, borrowAmount, HAY); + + // same prices, emode liquidationThreshold = 9750 + uint256 calcHealthFactor = collateralAmount * liquidationThreshold * 1e18 / 10000 / borrowAmount; + // console2.log('calcHealthFactor', calcHealthFactor); + (,,,,, uint256 healthFactor) = pool.getUserAccountData(bob); + assertEq(healthFactor, calcHealthFactor); + } + + function test_flashLoanRevertWithEmode() public { + address bob = address(1); + uint256 collateralAmount = 100 * 1e18; + deposit(bob, collateralAmount, underlying); + turnOnEmode(bob); + turnOnCollateral(bob, underlying); + flashloanRevert(bob, collateralAmount, underlying, '91'); + } + + function test_toggleEmergencyAround() public { + address bob = address(1); + uint256 collateralAmount = 100 * 1e18; + deposit(bob, collateralAmount, underlying); + toggleEmergency(); + toggleEmergency(); + withdraw(bob, collateralAmount, underlying); + } + + function test_toggleEmergencyAround2() public { + toggleEmergency(); + toggleEmergency(); + address bob = address(1); + uint256 collateralAmount = 100 * 1e18; + deposit(bob, collateralAmount, underlying); + withdraw(bob, collateralAmount, underlying); + } + function test_depositRevertWhenEmergency() public { + toggleEmergency(); + address bob = address(1); + uint256 collateralAmount = 100 * 1e18; + vm.startPrank(bob); + deal(underlying, bob, collateralAmount); + (address ATokenProxyAddress,,) = dataProvider.getReserveTokensAddresses(underlying); + uint256 before_aToken = IERC20(ATokenProxyAddress).balanceOf(bob); + uint256 before_underlying = IERC20(underlying).balanceOf(bob); + IERC20(underlying).approve(address(pool), collateralAmount); + vm.expectRevert(abi.encodePacked("deposit is paused due to emergency")); + pool.deposit(underlying, collateralAmount, bob, 0); + } + + function test_withdrawWhenEmergency() public {address bob = address(1); + uint256 collateralAmount = 100 * 1e18; + deposit(bob, collateralAmount, underlying); + toggleEmergency(); + withdraw(bob, collateralAmount, underlying); + } + + +} \ No newline at end of file diff --git a/test/BaseTest.t.sol b/test/BaseTest.t.sol new file mode 100644 index 0000000..d923704 --- /dev/null +++ b/test/BaseTest.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: UNLICENSED +// Copyright (c) 2023 Tokemak Foundation. All rights reserved. +pragma solidity > 0.8.0; + +import { Test } from "forge-std/Test.sol"; +import {IPoolAddressesProvider} from "../../src/core/interfaces/IPoolAddressesProvider.sol"; +import {IPool} from "../../src/core/interfaces/IPool.sol"; +import {IAaveOracle} from "../../src/core/interfaces/IAaveOracle.sol"; +import {IACLManager} from '../../src/core/interfaces/IACLManager.sol'; +import {IAaveIncentivesController} from '../../src/core/interfaces/IAaveIncentivesController.sol'; +import {IEmissionManager} from "../../src/periphery/rewards/interfaces/IEmissionManager.sol"; + +import {IPoolConfigurator} from "../../src/core/interfaces/IPoolConfigurator.sol"; +import {IPoolDataProvider} from '../../src/core/interfaces/IPoolDataProvider.sol'; +import {ReservesSetupHelper} from "../../src/core/deployments/ReservesSetupHelper.sol"; + +import {ADDRESSES_PROVIDER, POOLDATA_PROVIDER, ACL_MANAGER, POOL, POOL_CONFIGURATOR, EMISSION_MANAGER, + ATOKENIMPL, SDTOKENIMPL, VDTOKENIMPL, TREASURY, POOL_ADMIN, RESERVES_SETUP_HELPER, ORACLE, + MASTER_MAGPIE, SMART_HAY_LP} from "test/utils/Addresses.sol"; + +contract BaseTest is Test { + // if forking is required at specific block, set this in sub-contract's setup before calling parent + uint256 internal forkBlock; + // the fork id to roll if needed + uint256 internal mainnetFork; + IPoolConfigurator internal configurator = IPoolConfigurator(POOL_CONFIGURATOR); + IPool internal pool = IPool(POOL); + IPoolAddressesProvider internal provider = IPoolAddressesProvider(ADDRESSES_PROVIDER); + IPoolDataProvider internal dataProvider = IPoolDataProvider(POOLDATA_PROVIDER); + IEmissionManager internal emissionManager = IEmissionManager(EMISSION_MANAGER); + ReservesSetupHelper internal helper = ReservesSetupHelper(RESERVES_SETUP_HELPER); + IACLManager internal aclManager = IACLManager(ACL_MANAGER); + IAaveOracle internal oracle = IAaveOracle(ORACLE); + function setUp() public virtual { + fork(); + } + + function fork() internal { + // BEFORE WE DO ANYTHING, FORK!! + //uint256 mainnetFork; + if (forkBlock == 0) { + mainnetFork = vm.createFork(vm.envString("BSC_RPC_URL")); + } else { + mainnetFork = vm.createFork(vm.envString("BSC_RPC_URL"), forkBlock); + } + + vm.selectFork(mainnetFork); + assertEq(vm.activeFork(), mainnetFork, "forks don't match"); + } + +} \ No newline at end of file diff --git a/test/utils/Addresses.sol b/test/utils/Addresses.sol new file mode 100644 index 0000000..64d430e --- /dev/null +++ b/test/utils/Addresses.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: UNLICENSED +// BSC +pragma solidity 0.8.10; + +address constant RANDOM = 0x86F65BF7298543655A913Edf463CcFC04691eF13; + +//wombat +address constant SMART_HAY_LP = 0x1fa71DF4b344ffa5755726Ea7a9a56fbbEe0D38b; +address constant WOM = 0xAD6742A35fB341A9Cc6ad674738Dd8da98b94Fb1; +address constant MASTER_WOMBAT = 0x489833311676B566f888119c29bd997Dc6C95830; +address constant SMART_HAY_POOL = 0x0520451B19AD0bb00eD35ef391086A692CFC74B2; +// magpie +address constant MASTER_MAGPIE = 0xa3B615667CBd33cfc69843Bf11Fbb2A1D926BD46; +address constant WOMBAT_HELPER_SMART_HAY_LP = 0xe61eBb17b11Cd995f0564A8Ff70f17d10D850872; +address constant MGP = 0xD06716E1Ff2E492Cc5034c2E81805562dd3b45fa; + +// helio +address constant HAY = 0x0782b6d8c4551B9760e74c0545a9bCD90bdc41E5; + +// internal lending +address constant ADDRESSES_PROVIDER = 0xCa20a50ea454Bd9F37a895182ff3309F251Fd7cE; +address constant POOLDATA_PROVIDER = 0x09Ddc4AE826601b0F9671b9edffDf75e7E6f5D61; +address constant POOL = 0xcB0620b181140e57D1C0D8b724cde623cA963c8C; +address constant POOL_CONFIGURATOR = 0xA5776459837651ed4DE8Ed922e123D5898EfE5a2; +address constant EMISSION_MANAGER = 0xE85d5D7f0B627A545E29248Cb1A6807b28ca2D51; +address constant EMISSION_MANAGER_ADMIN = 0x9cBde15Db0A6910696fED74B0694d024809D289b; +address constant ACL_MANAGER = 0x625EdAB184b3B517654097875F1D8C9820163e31; +address constant POOL_ADMIN = 0x9cBde15Db0A6910696fED74B0694d024809D289b; +address constant TREASURY = 0x9cBde15Db0A6910696fED74B0694d024809D289b; +address constant TIMELOCK = 0x7a085A60Ce5eD569C1dAd219a41e375c40283d6A; +address constant ATOKENIMPL = 0xFfD80ae06987D8a14C4742f9998B926343fc8F35; +address constant SDTOKENIMPL = 0xc3752D2ce05CD638523CcCaA090EF5e25A2B87B4; +address constant VDTOKENIMPL = 0x00170FbBC27793837f1b7fb073F91F5ED8dBAEe8; +address constant RESERVES_SETUP_HELPER = 0xD9C5bdF9C17934d480Dfa47c3c1276458f788f57; +address constant ORACLE = 0xec203E7676C45455BF8cb43D28F9556F014Ab461; +address constant LIQUIDATION_ADAPTOR = 0x534a55adE31e7387b15b0A92b44b19806440b49e; +address constant BORROWABLE_DATA_PROVIDER = 0xD6a287DaF9B35ED8E59742c0E8B00AeBB065C5f2; + +// EXTERNAL AGGREGATOR +address constant HAY_AGGREGATOR = 0x3D29C3b0B0267EC6fB3e417C64a7835B748D4C38; +address constant USDC_AGGREGATOR = 0x51597f405303C4377E36123cBc172b13269EA163; +address constant USDT_AGGREGATOR = 0xB97Ad0E74fa7d920791E90258A6E2085088b4320; +// misc +address constant USDC = 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d; +address constant USDT = 0x55d398326f99059fF775485246999027B3197955; + +address constant MOCK_WOM_ORACLE = 0xa3334A9762090E827413A7495AfeCE76F41dFc06; \ No newline at end of file