diff --git a/.env.example b/.env.example index bb0b27e1..c8d4ba4b 100644 --- a/.env.example +++ b/.env.example @@ -3,9 +3,11 @@ BASE_RPC_URL= ARBITRUM_RPC_URL= ETHEREUM_RPC_URL= REAL_RPC_URL= +SONIC_RPC_URL= POLYGONSCAN_API_KEY= BASESCAN_API_KEY= ARBITRUMSCAN_API_KEY= ETHERSCAN_API_KEY= +SONICSCAN_API_KEY= PRIVATE_KEY= FOUNDRY_PROFILE=lite diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cb0a9b89..3ef2adea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,6 +42,7 @@ jobs: ARBITRUM_RPC_URL: ${{secrets.ARBITRUM_RPC_URL}} ETHEREUM_RPC_URL: ${{secrets.ETHEREUM_RPC_URL}} REAL_RPC_URL: ${{secrets.REAL_RPC_URL}} + SONIC_RPC_URL: ${{secrets.SONIC_RPC_URL}} id: test - name: Run Forge coverage @@ -53,6 +54,7 @@ jobs: ARBITRUM_RPC_URL: ${{secrets.ARBITRUM_RPC_URL}} ETHEREUM_RPC_URL: ${{secrets.ETHEREUM_RPC_URL}} REAL_RPC_URL: ${{secrets.REAL_RPC_URL}} + SONIC_RPC_URL: ${{secrets.SONIC_RPC_URL}} id: coverage - name: Upload coverage lcov report to Codecov diff --git a/README.md b/README.md index 4eb40c69..f7d6e9a3 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Platform address. * **Polygon** [137] `0xb2a0737ef27b5Cc474D24c779af612159b1c3e60` [polygonscan](https://polygonscan.com/address/0xb2a0737ef27b5Cc474D24c779af612159b1c3e60) * **Base** [8453] `0x7eAeE5CfF17F7765d89F4A46b484256929C62312` [basescan](https://basescan.org/address/0x7eaee5cff17f7765d89f4a46b484256929c62312) * **Re.al** [111188] `0xB7838d447deece2a9A5794De0f342B47d0c1B9DC` [explorer.re.al](https://explorer.re.al/address/0xB7838d447deece2a9A5794De0f342B47d0c1B9DC) +* **Sonic** [146] `0x4Aca671A420eEB58ecafE83700686a2AD06b20D8` [sonicscan](https://sonicscan.org/address/0x4aca671a420eeb58ecafe83700686a2ad06b20d8) ## Audits diff --git a/chains/ArbitrumLib.sol b/chains/ArbitrumLib.sol index 82e478d4..948fa659 100644 --- a/chains/ArbitrumLib.sol +++ b/chains/ArbitrumLib.sol @@ -91,6 +91,9 @@ library ArbitrumLib { p.gelatoAutomate = address(0); p.gelatoMinBalance = 1e16; p.gelatoDepositAmount = 2e16; + p.fee = 6_000; + p.feeShareVaultManager = 30_000; + p.feeShareStrategyLogic = 30_000; } function deployAndSetupInfrastructure(address platform, bool showLog) internal { diff --git a/chains/BaseLib.sol b/chains/BaseLib.sol index 14ff06fa..7025644a 100644 --- a/chains/BaseLib.sol +++ b/chains/BaseLib.sol @@ -88,6 +88,9 @@ library BaseLib { p.gelatoAutomate = address(0); p.gelatoMinBalance = 1e16; p.gelatoDepositAmount = 2e16; + p.fee = 6_000; + p.feeShareVaultManager = 30_000; + p.feeShareStrategyLogic = 30_000; } function deployAndSetupInfrastructure(address platform, bool showLog) internal { diff --git a/chains/EthereumLib.sol b/chains/EthereumLib.sol index 7261c505..457a66d2 100644 --- a/chains/EthereumLib.sol +++ b/chains/EthereumLib.sol @@ -69,6 +69,9 @@ library EthereumLib { p.gelatoAutomate = address(0); p.gelatoMinBalance = 1e18; p.gelatoDepositAmount = 2e18; + p.fee = 6_000; + p.feeShareVaultManager = 30_000; + p.feeShareStrategyLogic = 30_000; } function deployAndSetupInfrastructure(address platform, bool showLog) internal { diff --git a/chains/PolygonLib.sol b/chains/PolygonLib.sol index 13aa461f..f5c51091 100644 --- a/chains/PolygonLib.sol +++ b/chains/PolygonLib.sol @@ -213,6 +213,9 @@ library PolygonLib { p.gelatoAutomate = GELATO_AUTOMATE; p.gelatoMinBalance = 1e18; p.gelatoDepositAmount = 2e18; + p.fee = 6_000; + p.feeShareVaultManager = 30_000; + p.feeShareStrategyLogic = 30_000; } function deployAndSetupInfrastructure(address platform, bool showLog) internal { diff --git a/chains/RealLib.sol b/chains/RealLib.sol index 32d1ebfe..ac6236aa 100644 --- a/chains/RealLib.sol +++ b/chains/RealLib.sol @@ -99,6 +99,9 @@ library RealLib { p.gelatoAutomate = address(0); p.gelatoMinBalance = 1e16; p.gelatoDepositAmount = 2e16; + p.fee = 6_000; + p.feeShareVaultManager = 30_000; + p.feeShareStrategyLogic = 30_000; } function deployAndSetupInfrastructure(address platform, bool showLog) internal { diff --git a/chains/SonicLib.sol b/chains/SonicLib.sol new file mode 100644 index 00000000..cdaae3c2 --- /dev/null +++ b/chains/SonicLib.sol @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "../script/libs/LogDeployLib.sol"; +import {IPlatformDeployer} from "../src/interfaces/IPlatformDeployer.sol"; +import {IBalancerAdapter} from "../src/interfaces/IBalancerAdapter.sol"; +import {CommonLib} from "../src/core/libs/CommonLib.sol"; +import {AmmAdapterIdLib} from "../src/adapters/libs/AmmAdapterIdLib.sol"; +import {DeployAdapterLib} from "../script/libs/DeployAdapterLib.sol"; +import {Api3Adapter} from "../src/adapters/Api3Adapter.sol"; +import {IBalancerGauge} from "../src/integrations/balancer/IBalancerGauge.sol"; +import {StrategyIdLib} from "../src/strategies/libs/StrategyIdLib.sol"; +import {BeetsStableFarm} from "../src/strategies/BeetsStableFarm.sol"; +import {StrategyDeveloperLib} from "../src/strategies/libs/StrategyDeveloperLib.sol"; + +/// @dev Sonic network [chainId: 146] data library +// _____ _ +// / ____| (_) +// | (___ ___ _ __ _ ___ +// \___ \ / _ \| '_ \| |/ __| +// ____) | (_) | | | | | (__ +// |_____/ \___/|_| |_|_|\___| +// +/// @author Alien Deployer (https://github.com/a17) +library SonicLib { + // initial addresses + address public constant MULTISIG = 0xF564EBaC1182578398E94868bea1AbA6ba339652; + + // ERC20 + // https://docs.soniclabs.com/technology/contract-addresses + address public constant TOKEN_wS = 0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38; + address public constant TOKEN_wETH = 0x309C92261178fA0CF748A855e90Ae73FDb79EBc7; + address public constant TOKEN_USDC = 0x29219dd400f2Bf60E5a23d13Be72B486D4038894; + address public constant TOKEN_stS = 0xE5DA20F15420aD15DE0fa650600aFc998bbE3955; + address public constant TOKEN_BEETS = 0x2D0E0814E62D80056181F5cd932274405966e4f0; + address public constant TOKEN_EURC = 0xe715cbA7B5cCb33790ceBFF1436809d36cb17E57; + + // AMMs + address public constant POOL_BEETHOVENX_wS_stS = 0x374641076B68371e69D03C417DAc3E5F236c32FA; + address public constant POOL_BEETHOVENX_BEETS_stS = 0x10ac2F9DaE6539E77e372aDB14B1BF8fBD16b3e8; + address public constant POOL_BEETHOVENX_wS_USDC = 0xE93a5fc4Ba77179F6843b30cff33a97d89FF441C; + address public constant POOL_SUSHI_wS_USDC = 0xE72b6DD415cDACeAC76616Df2C9278B33079E0D3; + + // Beethoven X + address public constant BEETS_BALANCER_HELPERS = 0x8E9aa87E45e92bad84D5F8DD1bff34Fb92637dE9; + address public constant BEETS_GAUGE_wS_stS = 0x8476F3A8DA52092e7835167AFe27835dC171C133; + + // Oracles + address public constant ORACLE_API3_USDC_USD = 0xD3C586Eec1C6C3eC41D276a23944dea080eDCf7f; + + //noinspection NoReturn + function platformDeployParams() internal pure returns (IPlatformDeployer.DeployPlatformParams memory p) { + p.multisig = MULTISIG; + p.version = "25.01.0-alpha"; + p.buildingPermitToken = address(0); + p.buildingPayPerVaultToken = TOKEN_wS; + p.networkName = "Sonic"; + p.networkExtra = CommonLib.bytesToBytes32(abi.encodePacked(bytes3(0xfec160), bytes3(0x000000))); + p.targetExchangeAsset = TOKEN_wS; + p.gelatoAutomate = address(0); + p.gelatoMinBalance = 1e16; + p.gelatoDepositAmount = 2e16; + p.fee = 30_000; + p.feeShareVaultManager = 10_000; + p.feeShareStrategyLogic = 40_000; + } + + function deployAndSetupInfrastructure(address platform, bool showLog) internal { + IFactory factory = IFactory(IPlatform(platform).factory()); + + //region ----- Deployed Platform ----- + if (showLog) { + console.log("Deployed Stability platform", IPlatform(platform).platformVersion()); + console.log("Platform address: ", platform); + } + //endregion ----- Deployed Platform ----- + + //region ----- Deploy and setup vault types ----- + _addVaultType(factory, VaultTypeLib.COMPOUNDING, address(new CVault()), 10e6); + //endregion ----- Deploy and setup vault types ----- + + //region ----- Deploy and setup oracle adapters ----- + IPriceReader priceReader = PriceReader(IPlatform(platform).priceReader()); + // Api3 + { + Proxy proxy = new Proxy(); + proxy.initProxy(address(new Api3Adapter())); + Api3Adapter adapter = Api3Adapter(address(proxy)); + adapter.initialize(platform); + address[] memory assets = new address[](1); + assets[0] = TOKEN_USDC; + address[] memory priceFeeds = new address[](1); + priceFeeds[0] = ORACLE_API3_USDC_USD; + adapter.addPriceFeeds(assets, priceFeeds); + priceReader.addAdapter(address(adapter)); + LogDeployLib.logDeployAndSetupOracleAdapter("Api3", address(adapter), showLog); + } + //endregion ----- Deploy and setup oracle adapters ----- + + //region ----- Deploy AMM adapters ----- + DeployAdapterLib.deployAmmAdapter(platform, AmmAdapterIdLib.UNISWAPV3); + DeployAdapterLib.deployAmmAdapter(platform, AmmAdapterIdLib.BALANCER_COMPOSABLE_STABLE); + IBalancerAdapter( + IPlatform(platform).ammAdapter(keccak256(bytes(AmmAdapterIdLib.BALANCER_COMPOSABLE_STABLE))).proxy + ).setupHelpers(BEETS_BALANCER_HELPERS); + DeployAdapterLib.deployAmmAdapter(platform, AmmAdapterIdLib.BALANCER_WEIGHTED); + IBalancerAdapter(IPlatform(platform).ammAdapter(keccak256(bytes(AmmAdapterIdLib.BALANCER_WEIGHTED))).proxy) + .setupHelpers(BEETS_BALANCER_HELPERS); + LogDeployLib.logDeployAmmAdapters(platform, showLog); + //endregion ----- Deploy AMM adapters ----- + + //region ----- Setup Swapper ----- + { + (ISwapper.AddPoolData[] memory bcPools, ISwapper.AddPoolData[] memory pools) = routes(); + ISwapper swapper = ISwapper(IPlatform(platform).swapper()); + swapper.addBlueChipsPools(bcPools, false); + swapper.addPools(pools, false); + address[] memory tokenIn = new address[](3); + tokenIn[0] = TOKEN_wS; + tokenIn[1] = TOKEN_stS; + tokenIn[2] = TOKEN_BEETS; + uint[] memory thresholdAmount = new uint[](3); + thresholdAmount[0] = 1e10; + thresholdAmount[1] = 1e10; + thresholdAmount[2] = 1e10; + swapper.setThresholds(tokenIn, thresholdAmount); + LogDeployLib.logSetupSwapper(platform, showLog); + } + //endregion ----- Setup Swapper ----- + + //region ----- Add farms ----- + factory.addFarms(farms()); + LogDeployLib.logAddedFarms(address(factory), showLog); + //endregion ----- Add farms ----- + + //region ----- Deploy strategy logics ----- + _addStrategyLogic(factory, StrategyIdLib.BEETS_STABLE_FARM, address(new BeetsStableFarm()), true); + LogDeployLib.logDeployStrategies(platform, showLog); + //endregion ----- Deploy strategy logics ----- + + //region ----- Add DeX aggregators ----- + address[] memory dexAggRouter = new address[](1); + dexAggRouter[0] = IPlatform(platform).swapper(); + IPlatform(platform).addDexAggregators(dexAggRouter); + //endregion -- Add DeX aggregators ----- + } + + function routes() + public + pure + returns (ISwapper.AddPoolData[] memory bcPools, ISwapper.AddPoolData[] memory pools) + { + //region ----- BC pools ---- + bcPools = new ISwapper.AddPoolData[](2); + bcPools[0] = + _makePoolData(POOL_BEETHOVENX_wS_stS, AmmAdapterIdLib.BALANCER_COMPOSABLE_STABLE, TOKEN_stS, TOKEN_wS); + // bcPools[1] = _makePoolData(POOL_BEETHOVENX_wS_USDC, AmmAdapterIdLib.BALANCER_WEIGHTED, TOKEN_USDC, TOKEN_wS); + bcPools[1] = _makePoolData(POOL_SUSHI_wS_USDC, AmmAdapterIdLib.UNISWAPV3, TOKEN_USDC, TOKEN_wS); + //endregion ----- BC pools ---- + + //region ----- Pools ---- + pools = new ISwapper.AddPoolData[](4); + uint i; + pools[i++] = + _makePoolData(POOL_BEETHOVENX_wS_stS, AmmAdapterIdLib.BALANCER_COMPOSABLE_STABLE, TOKEN_wS, TOKEN_stS); + pools[i++] = + _makePoolData(POOL_BEETHOVENX_wS_stS, AmmAdapterIdLib.BALANCER_COMPOSABLE_STABLE, TOKEN_stS, TOKEN_wS); + pools[i++] = _makePoolData(POOL_BEETHOVENX_BEETS_stS, AmmAdapterIdLib.BALANCER_WEIGHTED, TOKEN_BEETS, TOKEN_stS); + pools[i++] = _makePoolData(POOL_BEETHOVENX_wS_USDC, AmmAdapterIdLib.BALANCER_WEIGHTED, TOKEN_USDC, TOKEN_wS); + //endregion ----- Pools ---- + } + + function farms() public view returns (IFactory.Farm[] memory _farms) { + _farms = new IFactory.Farm[](1); + uint i; + + _farms[i++] = _makeBeetsStableFarm(BEETS_GAUGE_wS_stS); + } + + function _makeBeetsStableFarm(address gauge) internal view returns (IFactory.Farm memory) { + IFactory.Farm memory farm; + farm.status = 0; + farm.pool = IBalancerGauge(gauge).lp_token(); + farm.strategyLogicId = StrategyIdLib.BEETS_STABLE_FARM; + uint len = IBalancerGauge(gauge).reward_count(); + farm.rewardAssets = new address[](len); + for (uint i; i < len; ++i) { + farm.rewardAssets[i] = IBalancerGauge(gauge).reward_tokens(i); + } + farm.addresses = new address[](1); + farm.addresses[0] = gauge; +// farm.addresses[2] = boxManager; + farm.nums = new uint[](0); + farm.ticks = new int24[](0); + return farm; + } + + function _makePoolData( + address pool, + string memory ammAdapterId, + address tokenIn, + address tokenOut + ) internal pure returns (ISwapper.AddPoolData memory) { + return ISwapper.AddPoolData({pool: pool, ammAdapterId: ammAdapterId, tokenIn: tokenIn, tokenOut: tokenOut}); + } + + function _addVaultType(IFactory factory, string memory id, address implementation, uint buildingPrice) internal { + factory.setVaultConfig( + IFactory.VaultConfig({ + vaultType: id, + implementation: implementation, + deployAllowed: true, + upgradeAllowed: true, + buildingPrice: buildingPrice + }) + ); + } + + function _addStrategyLogic(IFactory factory, string memory id, address implementation, bool farming) internal { + factory.setStrategyLogicConfig( + IFactory.StrategyLogicConfig({ + id: id, + implementation: address(implementation), + deployAllowed: true, + upgradeAllowed: true, + farming: farming, + tokenId: type(uint).max + }), + StrategyDeveloperLib.getDeveloper(id) + ); + } + + function testChainLib() external {} +} diff --git a/foundry.toml b/foundry.toml index 226b8a68..fb55dd50 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,7 +6,7 @@ fs_permissions = [{ access = "read-write", path = "./"}] solc_version = "0.8.23" gas_limit = "18446744073709551615" optimizer-runs = 200 -evm_version = "Shanghai" +evm_version = "Cancun" [profile.lite] optimizer = false @@ -27,9 +27,11 @@ base = "${BASE_RPC_URL}" arbitrum = "${ARBITRUM_RPC_URL}" ethereum = "${ETHEREUM_RPC_URL}" real = "${REAL_RPC_URL}" +sonic = "${SONIC_RPC_URL}" [etherscan] polygon = { key = "${POLYGONSCAN_API_KEY}", chain = 137 } base = { key = "${BASESCAN_API_KEY}", chain = 8453 } arbitrum = { key = "${ARBITRUMSCAN_API_KEY}", chain = 42161 } ethereum = { key = "${ETHERSCAN_API_KEY}", chain = 1 } +sonic = { key = "${SONICSCAN_API_KEY}", chain = 146, url = "https://api.sonicscan.org/api" } diff --git a/script/base/DeployCore.sol b/script/base/DeployCore.sol index 8b752a1d..9f2026aa 100644 --- a/script/base/DeployCore.sol +++ b/script/base/DeployCore.sol @@ -106,12 +106,12 @@ abstract contract DeployCore { IPlatform.PlatformSettings({ networkName: p.networkName, networkExtra: p.networkExtra, - fee: 6_000, // todo pass in args - feeShareVaultManager: 30_000, // todo pass in args - feeShareStrategyLogic: 30_000, // todo pass in args - feeShareEcosystem: 0, // todo pass in args - minInitialBoostPerDay: 30e18, // $30 // todo pass in args - minInitialBoostDuration: 30 * 86400 // 30 days // todo pass in args + fee: p.fee, + feeShareVaultManager: p.feeShareVaultManager, + feeShareStrategyLogic: p.feeShareStrategyLogic, + feeShareEcosystem: 0, + minInitialBoostPerDay: 30e18, // $30 + minInitialBoostDuration: 30 * 86400 // 30 days }) ); diff --git a/script/deploy-core/Deploy.Sonic.s.sol b/script/deploy-core/Deploy.Sonic.s.sol new file mode 100644 index 00000000..7eeb8655 --- /dev/null +++ b/script/deploy-core/Deploy.Sonic.s.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "forge-std/Script.sol"; +import "../../chains/SonicLib.sol"; +import {DeployCore} from "../base/DeployCore.sol"; + +contract DeploySonic is Script, DeployCore { + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + address platform = _deployCore(SonicLib.platformDeployParams()); + SonicLib.deployAndSetupInfrastructure(platform, false); + vm.stopBroadcast(); + } + + function testDeploySonic() external {} +} diff --git a/script/libs/DeployAdapterLib.sol b/script/libs/DeployAdapterLib.sol index f340dced..8b8c7bb8 100644 --- a/script/libs/DeployAdapterLib.sol +++ b/script/libs/DeployAdapterLib.sol @@ -8,6 +8,8 @@ import "../../src/adapters/UniswapV3Adapter.sol"; import "../../src/adapters/AlgebraAdapter.sol"; import "../../src/adapters/KyberAdapter.sol"; import "../../src/adapters/CurveAdapter.sol"; +import {BalancerComposableStableAdapter} from "../../src/adapters/BalancerComposableStableAdapter.sol"; +import {BalancerWeightedAdapter} from "../../src/adapters/BalancerWeightedAdapter.sol"; library DeployAdapterLib { function deployAmmAdapter(address platform, string memory id) internal returns (address) { @@ -34,6 +36,14 @@ library DeployAdapterLib { proxy.initProxy(address(new CurveAdapter())); } + if (eq(id, AmmAdapterIdLib.BALANCER_COMPOSABLE_STABLE)) { + proxy.initProxy(address(new BalancerComposableStableAdapter())); + } + + if (eq(id, AmmAdapterIdLib.BALANCER_WEIGHTED)) { + proxy.initProxy(address(new BalancerWeightedAdapter())); + } + require(proxy.implementation() != address(0), "Unknown AmmAdapter"); IAmmAdapter(address(proxy)).init(platform); IPlatform(platform).addAmmAdapter(id, address(proxy)); diff --git a/src/adapters/Api3Adapter.sol b/src/adapters/Api3Adapter.sol new file mode 100644 index 00000000..cdf43418 --- /dev/null +++ b/src/adapters/Api3Adapter.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import "../interfaces/IOracleAdapter.sol"; +import "../core/base/Controllable.sol"; +import {IApi3ReaderProxy} from "../integrations/api3/IApi3ReaderProxy.sol"; + +/// @title Oracle adapter for API3 price feeds +/// @author Alien Deployer (https://github.com/a17) +contract Api3Adapter is Controllable, IOracleAdapter { + using EnumerableSet for EnumerableSet.AddressSet; + + event NewPriceFeeds(address[] assets, address[] priceFeeds); + event RemovedPriceFeeds(address[] assets); + + /// @inheritdoc IControllable + string public constant VERSION = "1.0.0"; + + mapping(address asset => address priceFeed) public priceFeeds; + EnumerableSet.AddressSet internal _assets; + + function initialize(address platform_) public initializer { + __Controllable_init(platform_); + } + + function addPriceFeeds(address[] memory assets_, address[] memory priceFeeds_) external onlyOperator { + uint len = assets_.length; + if (len != priceFeeds_.length) { + revert IControllable.IncorrectArrayLength(); + } + // nosemgrep + for (uint i; i < len; ++i) { + // nosemgrep + if (!_assets.add(assets_[i])) { + revert IControllable.AlreadyExist(); + } + // nosemgrep + priceFeeds[assets_[i]] = priceFeeds_[i]; + } + + emit NewPriceFeeds(assets_, priceFeeds_); + } + + function removePriceFeeds(address[] memory assets_) external onlyOperator { + uint len = assets_.length; + // nosemgrep + for (uint i; i < len; ++i) { + // nosemgrep + if (!_assets.remove(assets_[i])) { + revert IControllable.NotExist(); + } + // nosemgrep + priceFeeds[assets_[i]] = address(0); + } + emit RemovedPriceFeeds(assets_); + } + + /// @inheritdoc IOracleAdapter + function getPrice(address asset) external view returns (uint price, uint timestamp) { + if (!_assets.contains(asset)) { + return (0, 0); + } + (int224 value, uint32 timestampU32) = IApi3ReaderProxy(priceFeeds[asset]).read(); + price = uint(uint224(value)); + timestamp = uint(timestampU32); + } + + /// @inheritdoc IOracleAdapter + function getAllPrices() + external + view + returns (address[] memory assets_, uint[] memory prices, uint[] memory timestamps) + { + uint len = _assets.length(); + assets_ = _assets.values(); + prices = new uint[](len); + timestamps = new uint[](len); + // nosemgrep + for (uint i; i < len; ++i) { + //slither-disable-next-line calls-loop + (int224 value, uint32 timestampU32) = IApi3ReaderProxy(priceFeeds[assets_[i]]).read(); + prices[i] = uint(uint224(value)); + timestamps[i] = uint(timestampU32); + } + } + + /// @inheritdoc IOracleAdapter + function assets() external view returns (address[] memory) { + return _assets.values(); + } +} diff --git a/src/adapters/BalancerComposableStableAdapter.sol b/src/adapters/BalancerComposableStableAdapter.sol new file mode 100644 index 00000000..750cecfd --- /dev/null +++ b/src/adapters/BalancerComposableStableAdapter.sol @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {Controllable} from "../core/base/Controllable.sol"; +import {ConstantsLib} from "../core/libs/ConstantsLib.sol"; +import {AmmAdapterIdLib} from "./libs/AmmAdapterIdLib.sol"; +import {Errors} from "./libs/balancer/BalancerErrors.sol"; +import {FixedPoint} from "./libs/balancer/FixedPoint.sol"; +import {ScaleLib} from "./libs/balancer/ScaleLib.sol"; +import {StableMath} from "./libs/balancer/StableMath.sol"; +import {IBComposableStablePoolMinimal} from "../integrations/balancer/IBComposableStablePoolMinimal.sol"; +import {IBVault, IAsset} from "../integrations/balancer/IBVault.sol"; +import {IBalancerHelper, IVault} from "../integrations/balancer/IBalancerHelper.sol"; +import {IAmmAdapter} from "../interfaces/IAmmAdapter.sol"; +import {IControllable} from "../interfaces/IControllable.sol"; +import {IBalancerAdapter} from "../interfaces/IBalancerAdapter.sol"; + +/// @title AMM adapter for Balancer ComposableStable pools +/// @author Alien Deployer (https://github.com/a17) +contract BalancerComposableStableAdapter is Controllable, IAmmAdapter, IBalancerAdapter { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IControllable + string public constant VERSION = "1.0.0"; + + // keccak256(abi.encode(uint256(keccak256("erc7201:stability.BalancerComposableStableAdapter")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant STORAGE_LOCATION = 0x4235c883b69d0c060f4f9a2c87fa015d10166773b6a97be421a79340d62c1e00; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* DATA TYPES */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + struct GetLiquidityForAmountsVars { + bytes32 poolId; + address[] assets; + uint bptIndex; + uint len; + } + + struct GetProportionsVars { + uint bptIndex; + uint asset0Index; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STORAGE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @custom:storage-location erc7201:stability.BalancerComposableStableAdapter + struct AdapterStorage { + address balancerHelpers; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INITIALIZATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IAmmAdapter + function init(address platform_) external initializer { + __Controllable_init(platform_); + } + + /// @inheritdoc IBalancerAdapter + function setupHelpers(address balancerHelpers) external { + AdapterStorage storage $ = _getStorage(); + if ($.balancerHelpers != address(0)) { + revert AlreadyExist(); + } + $.balancerHelpers = balancerHelpers; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* USER ACTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IAmmAdapter + //slither-disable-next-line reentrancy-events + function swap( + address pool, + address tokenIn, + address tokenOut, + address recipient, + uint priceImpactTolerance + ) external { + uint amountIn = IERC20(tokenIn).balanceOf(address(this)); + + address balancerVault = IBComposableStablePoolMinimal(pool).getVault(); + + // Initializing each struct field one-by-one uses less gas than setting all at once. + IBVault.FundManagement memory funds; + funds.sender = address(this); + funds.fromInternalBalance = false; + funds.recipient = payable(recipient); + funds.toInternalBalance = false; + + // Initializing each struct field one-by-one uses less gas than setting all at once. + IBVault.SingleSwap memory singleSwap; + singleSwap.poolId = IBComposableStablePoolMinimal(pool).getPoolId(); + singleSwap.kind = IBVault.SwapKind.GIVEN_IN; + singleSwap.assetIn = IAsset(address(tokenIn)); + singleSwap.assetOut = IAsset(address(tokenOut)); + singleSwap.amount = amountIn; + singleSwap.userData = ""; + + // scope for checking price impact + uint amountOutMax; + { + uint minimalAmount = amountIn / 1000; + require(minimalAmount != 0, "Too low amountIn"); + uint price = getPrice(pool, tokenIn, tokenOut, minimalAmount); + amountOutMax = price * amountIn / minimalAmount; + } + + IERC20(tokenIn).approve(balancerVault, amountIn); + uint amountOut = IBVault(balancerVault).swap(singleSwap, funds, 1, block.timestamp); + + uint priceImpact = + amountOutMax < amountOut ? 0 : (amountOutMax - amountOut) * ConstantsLib.DENOMINATOR / amountOutMax; + if (priceImpact > priceImpactTolerance) { + revert(string(abi.encodePacked("!PRICE ", Strings.toString(priceImpact)))); + } + + emit SwapInPool(pool, tokenIn, tokenOut, recipient, priceImpactTolerance, amountIn, amountOut); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* VIEW FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IAmmAdapter + function ammAdapterId() external pure returns (string memory) { + return AmmAdapterIdLib.BALANCER_COMPOSABLE_STABLE; + } + + /// @inheritdoc IAmmAdapter + function poolTokens(address pool) public view returns (address[] memory tokens) { + IBComposableStablePoolMinimal _pool = IBComposableStablePoolMinimal(pool); + (address[] memory bTokens,,) = IBVault(_pool.getVault()).getPoolTokens(_pool.getPoolId()); + uint bptIndex = _pool.getBptIndex(); + uint len = bTokens.length - 1; + tokens = new address[](len); + for (uint i; i < len; ++i) { + tokens[i] = bTokens[i < bptIndex ? i : i + 1]; + } + } + + /// @inheritdoc IAmmAdapter + function getLiquidityForAmounts(address, uint[] memory) external pure returns (uint, uint[] memory) { + revert("Unavailable"); + } + + /// @inheritdoc IBalancerAdapter + function getLiquidityForAmountsWrite( + address pool, + uint[] memory amounts + ) external returns (uint liquidity, uint[] memory amountsConsumed) { + GetLiquidityForAmountsVars memory v; + IBComposableStablePoolMinimal _pool = IBComposableStablePoolMinimal(pool); + v.poolId = _pool.getPoolId(); + (v.assets,,) = IBVault(_pool.getVault()).getPoolTokens(v.poolId); + v.len = v.assets.length; + v.bptIndex = _pool.getBptIndex(); + uint k; + uint[] memory amountsIn; + (liquidity, amountsIn) = IBalancerHelper(_getStorage().balancerHelpers).queryJoin( + v.poolId, + address(this), + address(this), + IVault.JoinPoolRequest({ + assets: v.assets, + maxAmountsIn: amounts, + userData: abi.encode(IBVault.JoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT, amounts, 0), + fromInternalBalance: false + }) + ); + k = 0; + amountsConsumed = new uint[](v.len - 1); + for (uint i; i < v.len; ++i) { + if (i != v.bptIndex) { + amountsConsumed[k] = amountsIn[i]; + k++; + } + } + } + + /// @inheritdoc IAmmAdapter + function getProportions(address pool) external view returns (uint[] memory props) { + GetProportionsVars memory v; + IBComposableStablePoolMinimal _pool = IBComposableStablePoolMinimal(pool); + v.bptIndex = _pool.getBptIndex(); + v.asset0Index = v.bptIndex == 0 ? 1 : 0; + (address[] memory tokens, uint[] memory balances,) = IBVault(_pool.getVault()).getPoolTokens(_pool.getPoolId()); + uint totalInAsset0; + uint len = tokens.length; + uint[] memory pricedBalances = new uint[](len - 1); + uint k; + for (uint i; i < len; ++i) { + if (i != v.bptIndex) { + uint tokenDecimals = IERC20Metadata(tokens[i]).decimals(); + uint price = i == v.asset0Index + ? 10 ** tokenDecimals + : getPrice(pool, address(tokens[i]), address(tokens[v.asset0Index]), 10 ** (tokenDecimals - 3)) * 1000; + pricedBalances[k] = balances[i] * price / 10 ** tokenDecimals; + totalInAsset0 += pricedBalances[k]; + k++; + } + } + props = new uint[](len - 1); + for (uint i; i < len - 1; ++i) { + props[i] = pricedBalances[i] * 1e18 / totalInAsset0; + } + } + + /// @inheritdoc IAmmAdapter + function getPrice(address pool, address tokenIn, address tokenOut, uint amount) public view returns (uint) { + IBComposableStablePoolMinimal _pool = IBComposableStablePoolMinimal(pool); + { + // take pool commission + uint swapFeePercentage = _pool.getSwapFeePercentage(); + amount -= FixedPoint.mulUp(amount, swapFeePercentage); + } + bytes32 poolId = _pool.getPoolId(); + (address[] memory tokens, uint[] memory balances,) = IBVault(_pool.getVault()).getPoolTokens(poolId); + + uint tokenInIndex = type(uint).max; + uint tokenOutIndex = type(uint).max; + + uint len = tokens.length; + + for (uint i; i < len; ++i) { + if (tokens[i] == tokenIn) { + tokenInIndex = i; + break; + } + } + + for (uint i; i < len; ++i) { + if (tokens[i] == tokenOut) { + tokenOutIndex = i; + break; + } + } + + // require(tokenInIndex < len, 'Wrong tokenIn'); + // require(tokenOutIndex < len, 'Wrong tokenOut'); + + uint[] memory scalingFactors = _pool.getScalingFactors(); + ScaleLib._upscaleArray(balances, scalingFactors); + + uint bptIndex = _pool.getBptIndex(); + balances = _dropBptItem(balances, bptIndex); + + uint upscaledAmount = ScaleLib._upscale(amount, scalingFactors[tokenInIndex]); + + tokenInIndex = _skipBptIndex(tokenInIndex, bptIndex); + uint tokenOutIndexWoBpt = _skipBptIndex(tokenOutIndex, bptIndex); + + (uint currentAmp,,) = _pool.getAmplificationParameter(); + { + uint invariant = StableMath._calculateInvariant(currentAmp, balances, false); + + uint amountOutUpscaled = StableMath._calcOutGivenIn( + currentAmp, balances, tokenInIndex, tokenOutIndexWoBpt, upscaledAmount, invariant + ); + return ScaleLib._downscaleDown(amountOutUpscaled, scalingFactors[tokenOutIndex]); + } + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view override(Controllable, IERC165) returns (bool) { + return interfaceId == type(IAmmAdapter).interfaceId || interfaceId == type(IBalancerAdapter).interfaceId + || super.supportsInterface(interfaceId); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL LOGIC */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + function _dropBptItem(uint[] memory amounts, uint bptIndex) internal pure returns (uint[] memory) { + uint len = amounts.length - 1; + uint[] memory amountsWithoutBpt = new uint[](len); + for (uint i; i < len; ++i) { + amountsWithoutBpt[i] = amounts[i < bptIndex ? i : i + 1]; + } + + return amountsWithoutBpt; + } + + function _skipBptIndex(uint index, uint bptIndex) internal pure returns (uint) { + return index < bptIndex ? index : index - 1; + } + + function _getStorage() private pure returns (AdapterStorage storage $) { + //slither-disable-next-line assembly + assembly { + $.slot := STORAGE_LOCATION + } + } +} diff --git a/src/adapters/BalancerWeightedAdapter.sol b/src/adapters/BalancerWeightedAdapter.sol new file mode 100644 index 00000000..b539e0e2 --- /dev/null +++ b/src/adapters/BalancerWeightedAdapter.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {Controllable} from "../core/base/Controllable.sol"; +import {ConstantsLib} from "../core/libs/ConstantsLib.sol"; +import {AmmAdapterIdLib} from "./libs/AmmAdapterIdLib.sol"; +import {Errors} from "./libs/balancer/BalancerErrors.sol"; +import {WeightedMath} from "./libs/balancer/WeightedMath.sol"; +import {IBVault, IAsset} from "../integrations/balancer/IBVault.sol"; +import {IBalancerHelper, IVault} from "../integrations/balancer/IBalancerHelper.sol"; +import {IBWeightedPoolMinimal} from "../integrations/balancer/IBWeightedPoolMinimal.sol"; +import {IAmmAdapter} from "../interfaces/IAmmAdapter.sol"; +import {IControllable} from "../interfaces/IControllable.sol"; +import {IBalancerAdapter} from "../interfaces/IBalancerAdapter.sol"; + +/// @title AMM adapter for Balancer Weighted pools +/// @author Alien Deployer (https://github.com/a17) +contract BalancerWeightedAdapter is Controllable, IAmmAdapter, IBalancerAdapter { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IControllable + string public constant VERSION = "1.0.0"; + + // keccak256(abi.encode(uint256(keccak256("erc7201:stability.BalancerWeightedAdapter")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant STORAGE_LOCATION = 0xa4f6828e593c072b951693fc34ca2cd3971b69396d7ba6ed5b73febddd360b00; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STORAGE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @custom:storage-location erc7201:stability.BalancerWeightedAdapter + struct AdapterStorage { + address balancerHelpers; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INITIALIZATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IAmmAdapter + function init(address platform_) external initializer { + __Controllable_init(platform_); + } + + /// @inheritdoc IBalancerAdapter + function setupHelpers(address balancerHelpers) external { + AdapterStorage storage $ = _getStorage(); + if ($.balancerHelpers != address(0)) { + revert AlreadyExist(); + } + $.balancerHelpers = balancerHelpers; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* USER ACTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IAmmAdapter + //slither-disable-next-line reentrancy-events + function swap( + address pool, + address tokenIn, + address tokenOut, + address recipient, + uint priceImpactTolerance + ) external { + uint amountIn = IERC20(tokenIn).balanceOf(address(this)); + + address balancerVault = IBWeightedPoolMinimal(pool).getVault(); + + // Initializing each struct field one-by-one uses less gas than setting all at once. + IBVault.FundManagement memory funds; + funds.sender = address(this); + funds.fromInternalBalance = false; + funds.recipient = payable(recipient); + funds.toInternalBalance = false; + + // Initializing each struct field one-by-one uses less gas than setting all at once. + IBVault.SingleSwap memory singleSwap; + singleSwap.poolId = IBWeightedPoolMinimal(pool).getPoolId(); + singleSwap.kind = IBVault.SwapKind.GIVEN_IN; + singleSwap.assetIn = IAsset(address(tokenIn)); + singleSwap.assetOut = IAsset(address(tokenOut)); + singleSwap.amount = amountIn; + singleSwap.userData = ""; + + // scope for checking price impact + uint amountOutMax; + { + uint minimalAmount = amountIn / 1000; + require(minimalAmount != 0, "Too low amountIn"); + uint price = getPrice(pool, tokenIn, tokenOut, minimalAmount); + amountOutMax = price * amountIn / minimalAmount; + } + + IERC20(tokenIn).approve(balancerVault, amountIn); + uint amountOut = IBVault(balancerVault).swap(singleSwap, funds, 1, block.timestamp); + + uint priceImpact = + amountOutMax < amountOut ? 0 : (amountOutMax - amountOut) * ConstantsLib.DENOMINATOR / amountOutMax; + if (priceImpact > priceImpactTolerance) { + revert(string(abi.encodePacked("!PRICE ", Strings.toString(priceImpact)))); + } + + emit SwapInPool(pool, tokenIn, tokenOut, recipient, priceImpactTolerance, amountIn, amountOut); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* VIEW FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IAmmAdapter + function ammAdapterId() external pure returns (string memory) { + return AmmAdapterIdLib.BALANCER_WEIGHTED; + } + + /// @inheritdoc IAmmAdapter + function poolTokens(address pool) public view returns (address[] memory tokens) { + IBWeightedPoolMinimal _pool = IBWeightedPoolMinimal(pool); + (tokens,,) = IBVault(_pool.getVault()).getPoolTokens(_pool.getPoolId()); + } + + /// @inheritdoc IAmmAdapter + function getLiquidityForAmounts(address, uint[] memory) external pure returns (uint, uint[] memory) { + revert("Unavailable"); + } + + /// @inheritdoc IBalancerAdapter + function getLiquidityForAmountsWrite( + address pool, + uint[] memory amounts + ) external returns (uint liquidity, uint[] memory amountsConsumed) { + address[] memory assets = poolTokens(pool); + (liquidity, amountsConsumed) = IBalancerHelper(_getStorage().balancerHelpers).queryJoin( + IBWeightedPoolMinimal(pool).getPoolId(), + address(this), + address(this), + IVault.JoinPoolRequest({ + assets: assets, + maxAmountsIn: amounts, + userData: abi.encode(IBVault.JoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT, amounts, 0), + fromInternalBalance: false + }) + ); + } + + /// @inheritdoc IAmmAdapter + function getProportions(address pool) external view returns (uint[] memory props) { + props = IBWeightedPoolMinimal(pool).getNormalizedWeights(); + } + + /// @inheritdoc IAmmAdapter + function getPrice(address pool, address tokenIn, address tokenOut, uint amount) public view returns (uint) { + { + // take pool commission + uint swapFeePercentage = IBWeightedPoolMinimal(pool).getSwapFeePercentage(); + amount -= amount * swapFeePercentage / 10 ** 18; + } + address balancerVault = IBWeightedPoolMinimal(pool).getVault(); + bytes32 poolId = IBWeightedPoolMinimal(pool).getPoolId(); + (address[] memory tokens, uint[] memory balances,) = IBVault(balancerVault).getPoolTokens(poolId); + + uint[] memory weights = IBWeightedPoolMinimal(pool).getNormalizedWeights(); + + uint tokenInIndex = type(uint).max; + uint tokenOutIndex = type(uint).max; + + uint len = tokens.length; + + for (uint i; i < len; ++i) { + if (tokens[i] == tokenIn) { + tokenInIndex = i; + break; + } + } + + for (uint i; i < len; ++i) { + if (tokens[i] == tokenOut) { + tokenOutIndex = i; + break; + } + } + + // require(tokenInIndex < len, 'Wrong tokenIn'); + // require(tokenOutIndex < len, 'Wrong tokenOut'); + + return WeightedMath._calcOutGivenIn( + balances[tokenInIndex], weights[tokenInIndex], balances[tokenOutIndex], weights[tokenOutIndex], amount + ); + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view override(Controllable, IERC165) returns (bool) { + return interfaceId == type(IAmmAdapter).interfaceId || interfaceId == type(IBalancerAdapter).interfaceId + || super.supportsInterface(interfaceId); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL LOGIC */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + function _getStorage() private pure returns (AdapterStorage storage $) { + //slither-disable-next-line assembly + assembly { + $.slot := STORAGE_LOCATION + } + } +} diff --git a/src/adapters/libs/AmmAdapterIdLib.sol b/src/adapters/libs/AmmAdapterIdLib.sol index f109a429..fdfd3b1b 100644 --- a/src/adapters/libs/AmmAdapterIdLib.sol +++ b/src/adapters/libs/AmmAdapterIdLib.sol @@ -7,4 +7,6 @@ library AmmAdapterIdLib { string public constant KYBER = "KyberSwap"; string public constant CURVE = "Curve"; string public constant SOLIDLY = "Solidly"; + string public constant BALANCER_COMPOSABLE_STABLE = "BalancerComposableStable"; + string public constant BALANCER_WEIGHTED = "BalancerWeighted"; } diff --git a/src/adapters/libs/balancer/BalancerErrors.sol b/src/adapters/libs/balancer/BalancerErrors.sol new file mode 100644 index 00000000..ba6d8906 --- /dev/null +++ b/src/adapters/libs/balancer/BalancerErrors.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.23; + +/** + * @dev Reverts if `condition` is false, with a revert reason containing `errorCode`. Only codes up to 999 are + * supported. + */ +function _require(bool condition, uint errorCode) pure { + if (!condition) _revert(errorCode); +} + +/** + * @dev Reverts with a revert reason containing `errorCode`. Only codes up to 999 are supported. + */ +function _revert(uint errorCode) pure { + // We're going to dynamically create a revert string based on the error code, with the following format: + // 'BAL#{errorCode}' + // where the code is left-padded with zeroes to three digits (so they range from 000 to 999). + // + // We don't have revert strings embedded in the contract to save bytecode size: it takes much less space to store a + // number (8 to 16 bits) than the individual string characters. + // + // The dynamic string creation algorithm that follows could be implemented in Solidity, but assembly allows for a + // much denser implementation, again saving bytecode size. Given this function unconditionally reverts, this is a + // safe place to rely on it without worrying about how its usage might affect e.g. memory contents. + assembly { + // First, we need to compute the ASCII representation of the error code. We assume that it is in the 0-999 + // range, so we only need to convert three digits. To convert the digits to ASCII, we add 0x30, the value for + // the '0' character. + + let units := add(mod(errorCode, 10), 0x30) + + errorCode := div(errorCode, 10) + let tenths := add(mod(errorCode, 10), 0x30) + + errorCode := div(errorCode, 10) + let hundreds := add(mod(errorCode, 10), 0x30) + + // With the individual characters, we can now construct the full string. The "BAL#" part is a known constant + // (0x42414c23): we simply shift this by 24 (to provide space for the 3 bytes of the error code), and add the + // characters to it, each shifted by a multiple of 8. + // The revert reason is then shifted left by 200 bits (256 minus the length of the string, 7 characters * 8 bits + // per character = 56) to locate it in the most significant part of the 256 slot (the beginning of a byte + // array). + + let revertReason := shl(200, add(0x42414c23000000, add(add(units, shl(8, tenths)), shl(16, hundreds)))) + + // We can now encode the reason in memory, which can be safely overwritten as we're about to revert. The encoded + // message will have the following layout: + // [ revert reason identifier ] [ string location offset ] [ string length ] [ string contents ] + + // The Solidity revert reason identifier is 0x08c739a0, the function selector of the Error(string) function. We + // also write zeroes to the next 28 bytes of memory, but those are about to be overwritten. + mstore(0x0, 0x08c379a000000000000000000000000000000000000000000000000000000000) + // Next is the offset to the location of the string, which will be placed immediately after (20 bytes away). + mstore(0x04, 0x0000000000000000000000000000000000000000000000000000000000000020) + // The string length is fixed: 7 characters. + mstore(0x24, 7) + // Finally, the string itself is stored. + mstore(0x44, revertReason) + + // Even if the string is only 7 bytes long, we need to return a full 32 byte slot containing it. The length of + // the encoded message is therefore 4 + 32 + 32 + 32 = 100. + revert(0, 100) + } +} + +library Errors { + // Math + uint internal constant ADD_OVERFLOW = 0; + uint internal constant SUB_OVERFLOW = 1; + uint internal constant SUB_UNDERFLOW = 2; + uint internal constant MUL_OVERFLOW = 3; + uint internal constant ZERO_DIVISION = 4; + uint internal constant DIV_INTERNAL = 5; + uint internal constant X_OUT_OF_BOUNDS = 6; + uint internal constant Y_OUT_OF_BOUNDS = 7; + uint internal constant PRODUCT_OUT_OF_BOUNDS = 8; + uint internal constant INVALID_EXPONENT = 9; + + // Input + uint internal constant OUT_OF_BOUNDS = 100; + uint internal constant UNSORTED_ARRAY = 101; + uint internal constant UNSORTED_TOKENS = 102; + uint internal constant INPUT_LENGTH_MISMATCH = 103; + uint internal constant ZERO_TOKEN = 104; + + // Shared pools + uint internal constant MIN_TOKENS = 200; + uint internal constant MAX_TOKENS = 201; + uint internal constant MAX_SWAP_FEE_PERCENTAGE = 202; + uint internal constant MIN_SWAP_FEE_PERCENTAGE = 203; + uint internal constant MINIMUM_BPT = 204; + uint internal constant CALLER_NOT_VAULT = 205; + uint internal constant UNINITIALIZED = 206; + uint internal constant BPT_IN_MAX_AMOUNT = 207; + uint internal constant BPT_OUT_MIN_AMOUNT = 208; + uint internal constant EXPIRED_PERMIT = 209; + uint internal constant NOT_TWO_TOKENS = 210; + + // Pools + uint internal constant MIN_AMP = 300; + uint internal constant MAX_AMP = 301; + uint internal constant MIN_WEIGHT = 302; + uint internal constant MAX_STABLE_TOKENS = 303; + uint internal constant MAX_IN_RATIO = 304; + uint internal constant MAX_OUT_RATIO = 305; + uint internal constant MIN_BPT_IN_FOR_TOKEN_OUT = 306; + uint internal constant MAX_OUT_BPT_FOR_TOKEN_IN = 307; + uint internal constant NORMALIZED_WEIGHT_INVARIANT = 308; + uint internal constant INVALID_TOKEN = 309; + uint internal constant UNHANDLED_JOIN_KIND = 310; + uint internal constant ZERO_INVARIANT = 311; + uint internal constant ORACLE_INVALID_SECONDS_QUERY = 312; + uint internal constant ORACLE_NOT_INITIALIZED = 313; + uint internal constant ORACLE_QUERY_TOO_OLD = 314; + uint internal constant ORACLE_INVALID_INDEX = 315; + uint internal constant ORACLE_BAD_SECS = 316; + uint internal constant AMP_END_TIME_TOO_CLOSE = 317; + uint internal constant AMP_ONGOING_UPDATE = 318; + uint internal constant AMP_RATE_TOO_HIGH = 319; + uint internal constant AMP_NO_ONGOING_UPDATE = 320; + uint internal constant STABLE_INVARIANT_DIDNT_CONVERGE = 321; + uint internal constant STABLE_GET_BALANCE_DIDNT_CONVERGE = 322; + uint internal constant RELAYER_NOT_CONTRACT = 323; + uint internal constant BASE_POOL_RELAYER_NOT_CALLED = 324; + uint internal constant REBALANCING_RELAYER_REENTERED = 325; + uint internal constant GRADUAL_UPDATE_TIME_TRAVEL = 326; + uint internal constant SWAPS_DISABLED = 327; + uint internal constant CALLER_IS_NOT_LBP_OWNER = 328; + uint internal constant PRICE_RATE_OVERFLOW = 329; + uint internal constant INVALID_JOIN_EXIT_KIND_WHILE_SWAPS_DISABLED = 330; + uint internal constant WEIGHT_CHANGE_TOO_FAST = 331; + uint internal constant LOWER_GREATER_THAN_UPPER_TARGET = 332; + uint internal constant UPPER_TARGET_TOO_HIGH = 333; + uint internal constant UNHANDLED_BY_LINEAR_POOL = 334; + uint internal constant OUT_OF_TARGET_RANGE = 335; + uint internal constant UNHANDLED_EXIT_KIND = 336; + uint internal constant UNAUTHORIZED_EXIT = 337; + uint internal constant MAX_MANAGEMENT_SWAP_FEE_PERCENTAGE = 338; + uint internal constant UNHANDLED_BY_INVESTMENT_POOL = 339; + uint internal constant UNHANDLED_BY_PHANTOM_POOL = 340; + uint internal constant TOKEN_DOES_NOT_HAVE_RATE_PROVIDER = 341; + uint internal constant INVALID_INITIALIZATION = 342; + + // Lib + uint internal constant REENTRANCY = 400; + uint internal constant SENDER_NOT_ALLOWED = 401; + uint internal constant PAUSED = 402; + uint internal constant PAUSE_WINDOW_EXPIRED = 403; + uint internal constant MAX_PAUSE_WINDOW_DURATION = 404; + uint internal constant MAX_BUFFER_PERIOD_DURATION = 405; + uint internal constant INSUFFICIENT_BALANCE = 406; + uint internal constant INSUFFICIENT_ALLOWANCE = 407; + uint internal constant ERC20_TRANSFER_FROM_ZERO_ADDRESS = 408; + uint internal constant ERC20_TRANSFER_TO_ZERO_ADDRESS = 409; + uint internal constant ERC20_MINT_TO_ZERO_ADDRESS = 410; + uint internal constant ERC20_BURN_FROM_ZERO_ADDRESS = 411; + uint internal constant ERC20_APPROVE_FROM_ZERO_ADDRESS = 412; + uint internal constant ERC20_APPROVE_TO_ZERO_ADDRESS = 413; + uint internal constant ERC20_TRANSFER_EXCEEDS_ALLOWANCE = 414; + uint internal constant ERC20_DECREASED_ALLOWANCE_BELOW_ZERO = 415; + uint internal constant ERC20_TRANSFER_EXCEEDS_BALANCE = 416; + uint internal constant ERC20_BURN_EXCEEDS_ALLOWANCE = 417; + uint internal constant SAFE_ERC20_CALL_FAILED = 418; + uint internal constant ADDRESS_INSUFFICIENT_BALANCE = 419; + uint internal constant ADDRESS_CANNOT_SEND_VALUE = 420; + uint internal constant SAFE_CAST_VALUE_CANT_FIT_INT256 = 421; + uint internal constant GRANT_SENDER_NOT_ADMIN = 422; + uint internal constant REVOKE_SENDER_NOT_ADMIN = 423; + uint internal constant RENOUNCE_SENDER_NOT_ALLOWED = 424; + uint internal constant BUFFER_PERIOD_EXPIRED = 425; + uint internal constant CALLER_IS_NOT_OWNER = 426; + uint internal constant NEW_OWNER_IS_ZERO = 427; + uint internal constant CODE_DEPLOYMENT_FAILED = 428; + uint internal constant CALL_TO_NON_CONTRACT = 429; + uint internal constant LOW_LEVEL_CALL_FAILED = 430; + + // Vault + uint internal constant INVALID_POOL_ID = 500; + uint internal constant CALLER_NOT_POOL = 501; + uint internal constant SENDER_NOT_ASSET_MANAGER = 502; + uint internal constant USER_DOESNT_ALLOW_RELAYER = 503; + uint internal constant INVALID_SIGNATURE = 504; + uint internal constant EXIT_BELOW_MIN = 505; + uint internal constant JOIN_ABOVE_MAX = 506; + uint internal constant SWAP_LIMIT = 507; + uint internal constant SWAP_DEADLINE = 508; + uint internal constant CANNOT_SWAP_SAME_TOKEN = 509; + uint internal constant UNKNOWN_AMOUNT_IN_FIRST_SWAP = 510; + uint internal constant MALCONSTRUCTED_MULTIHOP_SWAP = 511; + uint internal constant INTERNAL_BALANCE_OVERFLOW = 512; + uint internal constant INSUFFICIENT_INTERNAL_BALANCE = 513; + uint internal constant INVALID_ETH_INTERNAL_BALANCE = 514; + uint internal constant INVALID_POST_LOAN_BALANCE = 515; + uint internal constant INSUFFICIENT_ETH = 516; + uint internal constant UNALLOCATED_ETH = 517; + uint internal constant ETH_TRANSFER = 518; + uint internal constant CANNOT_USE_ETH_SENTINEL = 519; + uint internal constant TOKENS_MISMATCH = 520; + uint internal constant TOKEN_NOT_REGISTERED = 521; + uint internal constant TOKEN_ALREADY_REGISTERED = 522; + uint internal constant TOKENS_ALREADY_SET = 523; + uint internal constant TOKENS_LENGTH_MUST_BE_2 = 524; + uint internal constant NONZERO_TOKEN_BALANCE = 525; + uint internal constant BALANCE_TOTAL_OVERFLOW = 526; + uint internal constant POOL_NO_TOKENS = 527; + uint internal constant INSUFFICIENT_FLASH_LOAN_BALANCE = 528; + + // Fees + uint internal constant SWAP_FEE_PERCENTAGE_TOO_HIGH = 600; + uint internal constant FLASH_LOAN_FEE_PERCENTAGE_TOO_HIGH = 601; + uint internal constant INSUFFICIENT_FLASH_LOAN_FEE_AMOUNT = 602; +} diff --git a/src/adapters/libs/balancer/FixedPoint.sol b/src/adapters/libs/balancer/FixedPoint.sol new file mode 100644 index 00000000..2d38c24e --- /dev/null +++ b/src/adapters/libs/balancer/FixedPoint.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.23; + +import "./BalancerErrors.sol"; +import "./LogExpMath.sol"; + +library FixedPoint { + uint internal constant ONE = 1e18; // 18 decimal places + uint internal constant MAX_POW_RELATIVE_ERROR = 10000; // 10^(-14) + + // Minimum base for the power function when the exponent is 'free' (larger than ONE). + uint internal constant MIN_POW_BASE_FREE_EXPONENT = 0.7e18; + + function add(uint a, uint b) internal pure returns (uint) { + // Fixed Point addition is the same as regular checked addition + + uint c = a + b; + _require(c >= a, Errors.ADD_OVERFLOW); + return c; + } + + function sub(uint a, uint b) internal pure returns (uint) { + // Fixed Point addition is the same as regular checked addition + + _require(b <= a, Errors.SUB_OVERFLOW); + uint c = a - b; + return c; + } + + function mulDown(uint a, uint b) internal pure returns (uint) { + uint product = a * b; + _require(a == 0 || product / a == b, Errors.MUL_OVERFLOW); + + return product / ONE; + } + + function mulUp(uint a, uint b) internal pure returns (uint) { + uint product = a * b; + _require(a == 0 || product / a == b, Errors.MUL_OVERFLOW); + + if (product == 0) { + return 0; + } else { + // The traditional divUp formula is: + // divUp(x, y) := (x + y - 1) / y + // To avoid intermediate overflow in the addition, we distribute the division and get: + // divUp(x, y) := (x - 1) / y + 1 + // Note that this requires x != 0, which we already tested for. + + return ((product - 1) / ONE) + 1; + } + } + + function divDown(uint a, uint b) internal pure returns (uint) { + _require(b != 0, Errors.ZERO_DIVISION); + + if (a == 0) { + return 0; + } else { + uint aInflated = a * ONE; + _require(aInflated / a == ONE, Errors.DIV_INTERNAL); // mul overflow + + return aInflated / b; + } + } + + function divUp(uint a, uint b) internal pure returns (uint) { + _require(b != 0, Errors.ZERO_DIVISION); + + if (a == 0) { + return 0; + } else { + uint aInflated = a * ONE; + _require(aInflated / a == ONE, Errors.DIV_INTERNAL); // mul overflow + + // The traditional divUp formula is: + // divUp(x, y) := (x + y - 1) / y + // To avoid intermediate overflow in the addition, we distribute the division and get: + // divUp(x, y) := (x - 1) / y + 1 + // Note that this requires x != 0, which we already tested for. + + return ((aInflated - 1) / b) + 1; + } + } + + /** + * @dev Returns x^y, assuming both are fixed point numbers, rounding down. The result is guaranteed to not be above + * the true value (that is, the error function expected - actual is always positive). + */ + function powDown(uint x, uint y) internal pure returns (uint) { + uint raw = LogExpMath.pow(x, y); + uint maxError = add(mulUp(raw, MAX_POW_RELATIVE_ERROR), 1); + + if (raw < maxError) { + return 0; + } else { + return sub(raw, maxError); + } + } + + /** + * @dev Returns x^y, assuming both are fixed point numbers, rounding up. The result is guaranteed to not be below + * the true value (that is, the error function expected - actual is always negative). + */ + function powUp(uint x, uint y) internal pure returns (uint) { + uint raw = LogExpMath.pow(x, y); + uint maxError = add(mulUp(raw, MAX_POW_RELATIVE_ERROR), 1); + + return add(raw, maxError); + } + + /** + * @dev Returns the complement of a value (1 - x), capped to 0 if x is larger than 1. + * + * Useful when computing the complement for values with some level of relative error, as it strips this error and + * prevents intermediate negative values. + */ + function complement(uint x) internal pure returns (uint) { + return (x < ONE) ? (ONE - x) : 0; + } +} diff --git a/src/adapters/libs/balancer/LegacyOZMath.sol b/src/adapters/libs/balancer/LegacyOZMath.sol new file mode 100644 index 00000000..30e3a390 --- /dev/null +++ b/src/adapters/libs/balancer/LegacyOZMath.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +library LegacyOZMath { + function mul(uint a, uint b) internal pure returns (uint) { + return a * b; + } + + function div(uint a, uint b, bool roundUp) internal pure returns (uint) { + return roundUp ? divUp(a, b) : divDown(a, b); + } + + function divUp(uint a, uint b) internal pure returns (uint) { + if (a == 0) { + return 0; + } else { + return 1 + (a - 1) / b; + } + } + + function divDown(uint a, uint b) internal pure returns (uint) { + return a / b; + } + + /** + * @dev Returns the largest of two numbers. + */ + function max(uint a, uint b) internal pure returns (uint) { + return a > b ? a : b; + } +} diff --git a/src/adapters/libs/balancer/LogExpMath.sol b/src/adapters/libs/balancer/LogExpMath.sol new file mode 100644 index 00000000..4bd77437 --- /dev/null +++ b/src/adapters/libs/balancer/LogExpMath.sol @@ -0,0 +1,498 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./BalancerErrors.sol"; + +/** + * @dev Exponentiation and logarithm functions for 18 decimal fixed point numbers (both base and exponent/argument). + * + * Exponentiation and logarithm with arbitrary bases (x^y and log_x(y)) are implemented by conversion to natural + * exponentiation and logarithm (where the base is Euler's number). + * + * @author Fernando Martinelli - @fernandomartinelli + * @author Sergio Yuhjtman - @sergioyuhjtman + * @author Daniel Fernandez - @dmf7z + */ +library LogExpMath { + // All fixed point multiplications and divisions are inlined. This means we need to divide by ONE when multiplying + // two numbers, and multiply by ONE when dividing them. + + // All arguments and return values are 18 decimal fixed point numbers. + int constant ONE_18 = 1e18; + + // Internally, intermediate values are computed with higher precision as 20 decimal fixed point numbers, and in the + // case of ln36, 36 decimals. + int constant ONE_20 = 1e20; + int constant ONE_36 = 1e36; + + // The domain of natural exponentiation is bound by the word size and number of decimals used. + // + // Because internally the result will be stored using 20 decimals, the largest possible result is + // (2^255 - 1) / 10^20, which makes the largest exponent ln((2^255 - 1) / 10^20) = 130.700829182905140221. + // The smallest possible result is 10^(-18), which makes largest negative argument + // ln(10^(-18)) = -41.446531673892822312. + // We use 130.0 and -41.0 to have some safety margin. + int constant MAX_NATURAL_EXPONENT = 130e18; + int constant MIN_NATURAL_EXPONENT = -41e18; + + // Bounds for ln_36's argument. Both ln(0.9) and ln(1.1) can be represented with 36 decimal places in a fixed point + // 256 bit integer. + int constant LN_36_LOWER_BOUND = ONE_18 - 1e17; + int constant LN_36_UPPER_BOUND = ONE_18 + 1e17; + + uint constant MILD_EXPONENT_BOUND = 2 ** 254 / uint(ONE_20); + + // 18 decimal constants + int constant x0 = 128000000000000000000; // 2ˆ7 + int constant a0 = 38877084059945950922200000000000000000000000000000000000; // eˆ(x0) (no decimals) + int constant x1 = 64000000000000000000; // 2ˆ6 + int constant a1 = 6235149080811616882910000000; // eˆ(x1) (no decimals) + + // 20 decimal constants + int constant x2 = 3200000000000000000000; // 2ˆ5 + int constant a2 = 7896296018268069516100000000000000; // eˆ(x2) + int constant x3 = 1600000000000000000000; // 2ˆ4 + int constant a3 = 888611052050787263676000000; // eˆ(x3) + int constant x4 = 800000000000000000000; // 2ˆ3 + int constant a4 = 298095798704172827474000; // eˆ(x4) + int constant x5 = 400000000000000000000; // 2ˆ2 + int constant a5 = 5459815003314423907810; // eˆ(x5) + int constant x6 = 200000000000000000000; // 2ˆ1 + int constant a6 = 738905609893065022723; // eˆ(x6) + int constant x7 = 100000000000000000000; // 2ˆ0 + int constant a7 = 271828182845904523536; // eˆ(x7) + int constant x8 = 50000000000000000000; // 2ˆ-1 + int constant a8 = 164872127070012814685; // eˆ(x8) + int constant x9 = 25000000000000000000; // 2ˆ-2 + int constant a9 = 128402541668774148407; // eˆ(x9) + int constant x10 = 12500000000000000000; // 2ˆ-3 + int constant a10 = 113314845306682631683; // eˆ(x10) + int constant x11 = 6250000000000000000; // 2ˆ-4 + int constant a11 = 106449445891785942956; // eˆ(x11) + + /** + * @dev Exponentiation (x^y) with unsigned 18 decimal fixed point base and exponent. + * + * Reverts if ln(x) * y is smaller than `MIN_NATURAL_EXPONENT`, or larger than `MAX_NATURAL_EXPONENT`. + */ + function pow(uint x, uint y) internal pure returns (uint) { + if (y == 0) { + // We solve the 0^0 indetermination by making it equal one. + return uint(ONE_18); + } + + if (x == 0) { + return 0; + } + + // Instead of computing x^y directly, we instead rely on the properties of logarithms and exponentiation to + // arrive at that result. In particular, exp(ln(x)) = x, and ln(x^y) = y * ln(x). This means + // x^y = exp(y * ln(x)). + + // The ln function takes a signed value, so we need to make sure x fits in the signed 256 bit range. + _require(x < 2 ** 255, Errors.X_OUT_OF_BOUNDS); + int x_int256 = int(x); + + // We will compute y * ln(x) in a single step. Depending on the value of x, we can either use ln or ln_36. In + // both cases, we leave the division by ONE_18 (due to fixed point multiplication) to the end. + + // This prevents y * ln(x) from overflowing, and at the same time guarantees y fits in the signed 256 bit range. + _require(y < MILD_EXPONENT_BOUND, Errors.Y_OUT_OF_BOUNDS); + int y_int256 = int(y); + + int logx_times_y; + if (LN_36_LOWER_BOUND < x_int256 && x_int256 < LN_36_UPPER_BOUND) { + int ln_36_x = _ln_36(x_int256); + + // ln_36_x has 36 decimal places, so multiplying by y_int256 isn't as straightforward, since we can't just + // bring y_int256 to 36 decimal places, as it might overflow. Instead, we perform two 18 decimal + // multiplications and add the results: one with the first 18 decimals of ln_36_x, and one with the + // (downscaled) last 18 decimals. + logx_times_y = ((ln_36_x / ONE_18) * y_int256 + ((ln_36_x % ONE_18) * y_int256) / ONE_18); + } else { + logx_times_y = _ln(x_int256) * y_int256; + } + logx_times_y /= ONE_18; + + // Finally, we compute exp(y * ln(x)) to arrive at x^y + _require( + MIN_NATURAL_EXPONENT <= logx_times_y && logx_times_y <= MAX_NATURAL_EXPONENT, Errors.PRODUCT_OUT_OF_BOUNDS + ); + + return uint(exp(logx_times_y)); + } + + /** + * @dev Natural exponentiation (e^x) with signed 18 decimal fixed point exponent. + * + * Reverts if `x` is smaller than MIN_NATURAL_EXPONENT, or larger than `MAX_NATURAL_EXPONENT`. + */ + function exp(int x) internal pure returns (int) { + _require(x >= MIN_NATURAL_EXPONENT && x <= MAX_NATURAL_EXPONENT, Errors.INVALID_EXPONENT); + + if (x < 0) { + // We only handle positive exponents: e^(-x) is computed as 1 / e^x. We can safely make x positive since it + // fits in the signed 256 bit range (as it is larger than MIN_NATURAL_EXPONENT). + // Fixed point division requires multiplying by ONE_18. + return ((ONE_18 * ONE_18) / exp(-x)); + } + + // First, we use the fact that e^(x+y) = e^x * e^y to decompose x into a sum of powers of two, which we call x_n, + // where x_n == 2^(7 - n), and e^x_n = a_n has been precomputed. We choose the first x_n, x0, to equal 2^7 + // because all larger powers are larger than MAX_NATURAL_EXPONENT, and therefore not present in the + // decomposition. + // At the end of this process we will have the product of all e^x_n = a_n that apply, and the remainder of this + // decomposition, which will be lower than the smallest x_n. + // exp(x) = k_0 * a_0 * k_1 * a_1 * ... + k_n * a_n * exp(remainder), where each k_n equals either 0 or 1. + // We mutate x by subtracting x_n, making it the remainder of the decomposition. + + // The first two a_n (e^(2^7) and e^(2^6)) are too large if stored as 18 decimal numbers, and could cause + // intermediate overflows. Instead we store them as plain integers, with 0 decimals. + // Additionally, x0 + x1 is larger than MAX_NATURAL_EXPONENT, which means they will not both be present in the + // decomposition. + + // For each x_n, we test if that term is present in the decomposition (if x is larger than it), and if so deduct + // it and compute the accumulated product. + + int firstAN; + if (x >= x0) { + x -= x0; + firstAN = a0; + } else if (x >= x1) { + x -= x1; + firstAN = a1; + } else { + firstAN = 1; // One with no decimal places + } + + // We now transform x into a 20 decimal fixed point number, to have enhanced precision when computing the + // smaller terms. + x *= 100; + + // `product` is the accumulated product of all a_n (except a0 and a1), which starts at 20 decimal fixed point + // one. Recall that fixed point multiplication requires dividing by ONE_20. + int product = ONE_20; + + if (x >= x2) { + x -= x2; + product = (product * a2) / ONE_20; + } + if (x >= x3) { + x -= x3; + product = (product * a3) / ONE_20; + } + if (x >= x4) { + x -= x4; + product = (product * a4) / ONE_20; + } + if (x >= x5) { + x -= x5; + product = (product * a5) / ONE_20; + } + if (x >= x6) { + x -= x6; + product = (product * a6) / ONE_20; + } + if (x >= x7) { + x -= x7; + product = (product * a7) / ONE_20; + } + if (x >= x8) { + x -= x8; + product = (product * a8) / ONE_20; + } + if (x >= x9) { + x -= x9; + product = (product * a9) / ONE_20; + } + + // x10 and x11 are unnecessary here since we have high enough precision already. + + // Now we need to compute e^x, where x is small (in particular, it is smaller than x9). We use the Taylor series + // expansion for e^x: 1 + x + (x^2 / 2!) + (x^3 / 3!) + ... + (x^n / n!). + + int seriesSum = ONE_20; // The initial one in the sum, with 20 decimal places. + int term; // Each term in the sum, where the nth term is (x^n / n!). + + // The first term is simply x. + term = x; + seriesSum += term; + + // Each term (x^n / n!) equals the previous one times x, divided by n. Since x is a fixed point number, + // multiplying by it requires dividing by ONE_20, but dividing by the non-fixed point n values does not. + + term = ((term * x) / ONE_20) / 2; + seriesSum += term; + + term = ((term * x) / ONE_20) / 3; + seriesSum += term; + + term = ((term * x) / ONE_20) / 4; + seriesSum += term; + + term = ((term * x) / ONE_20) / 5; + seriesSum += term; + + term = ((term * x) / ONE_20) / 6; + seriesSum += term; + + term = ((term * x) / ONE_20) / 7; + seriesSum += term; + + term = ((term * x) / ONE_20) / 8; + seriesSum += term; + + term = ((term * x) / ONE_20) / 9; + seriesSum += term; + + term = ((term * x) / ONE_20) / 10; + seriesSum += term; + + term = ((term * x) / ONE_20) / 11; + seriesSum += term; + + term = ((term * x) / ONE_20) / 12; + seriesSum += term; + + // 12 Taylor terms are sufficient for 18 decimal precision. + + // We now have the first a_n (with no decimals), and the product of all other a_n present, and the Taylor + // approximation of the exponentiation of the remainder (both with 20 decimals). All that remains is to multiply + // all three (one 20 decimal fixed point multiplication, dividing by ONE_20, and one integer multiplication), + // and then drop two digits to return an 18 decimal value. + + return (((product * seriesSum) / ONE_20) * firstAN) / 100; + } + + /** + * @dev Logarithm (log(arg, base), with signed 18 decimal fixed point base and argument. + */ + function log(int arg, int base) internal pure returns (int) { + // This performs a simple base change: log(arg, base) = ln(arg) / ln(base). + + // Both logBase and logArg are computed as 36 decimal fixed point numbers, either by using ln_36, or by + // upscaling. + + int logBase; + if (LN_36_LOWER_BOUND < base && base < LN_36_UPPER_BOUND) { + logBase = _ln_36(base); + } else { + logBase = _ln(base) * ONE_18; + } + + int logArg; + if (LN_36_LOWER_BOUND < arg && arg < LN_36_UPPER_BOUND) { + logArg = _ln_36(arg); + } else { + logArg = _ln(arg) * ONE_18; + } + + // When dividing, we multiply by ONE_18 to arrive at a result with 18 decimal places + return (logArg * ONE_18) / logBase; + } + + /** + * @dev Natural logarithm (ln(a)) with signed 18 decimal fixed point argument. + */ + function ln(int a) internal pure returns (int) { + // The real natural logarithm is not defined for negative numbers or zero. + _require(a > 0, Errors.OUT_OF_BOUNDS); + if (LN_36_LOWER_BOUND < a && a < LN_36_UPPER_BOUND) { + return _ln_36(a) / ONE_18; + } else { + return _ln(a); + } + } + + /** + * @dev Internal natural logarithm (ln(a)) with signed 18 decimal fixed point argument. + */ + function _ln(int a) private pure returns (int) { + if (a < ONE_18) { + // Since ln(a^k) = k * ln(a), we can compute ln(a) as ln(a) = ln((1/a)^(-1)) = - ln((1/a)). If a is less + // than one, 1/a will be greater than one, and this if statement will not be entered in the recursive call. + // Fixed point division requires multiplying by ONE_18. + return (-_ln((ONE_18 * ONE_18) / a)); + } + + // First, we use the fact that ln^(a * b) = ln(a) + ln(b) to decompose ln(a) into a sum of powers of two, which + // we call x_n, where x_n == 2^(7 - n), which are the natural logarithm of precomputed quantities a_n (that is, + // ln(a_n) = x_n). We choose the first x_n, x0, to equal 2^7 because the exponential of all larger powers cannot + // be represented as 18 fixed point decimal numbers in 256 bits, and are therefore larger than a. + // At the end of this process we will have the sum of all x_n = ln(a_n) that apply, and the remainder of this + // decomposition, which will be lower than the smallest a_n. + // ln(a) = k_0 * x_0 + k_1 * x_1 + ... + k_n * x_n + ln(remainder), where each k_n equals either 0 or 1. + // We mutate a by subtracting a_n, making it the remainder of the decomposition. + + // For reasons related to how `exp` works, the first two a_n (e^(2^7) and e^(2^6)) are not stored as fixed point + // numbers with 18 decimals, but instead as plain integers with 0 decimals, so we need to multiply them by + // ONE_18 to convert them to fixed point. + // For each a_n, we test if that term is present in the decomposition (if a is larger than it), and if so divide + // by it and compute the accumulated sum. + + int sum = 0; + if (a >= a0 * ONE_18) { + a /= a0; // Integer, not fixed point division + sum += x0; + } + + if (a >= a1 * ONE_18) { + a /= a1; // Integer, not fixed point division + sum += x1; + } + + // All other a_n and x_n are stored as 20 digit fixed point numbers, so we convert the sum and a to this format. + sum *= 100; + a *= 100; + + // Because further a_n are 20 digit fixed point numbers, we multiply by ONE_20 when dividing by them. + + if (a >= a2) { + a = (a * ONE_20) / a2; + sum += x2; + } + + if (a >= a3) { + a = (a * ONE_20) / a3; + sum += x3; + } + + if (a >= a4) { + a = (a * ONE_20) / a4; + sum += x4; + } + + if (a >= a5) { + a = (a * ONE_20) / a5; + sum += x5; + } + + if (a >= a6) { + a = (a * ONE_20) / a6; + sum += x6; + } + + if (a >= a7) { + a = (a * ONE_20) / a7; + sum += x7; + } + + if (a >= a8) { + a = (a * ONE_20) / a8; + sum += x8; + } + + if (a >= a9) { + a = (a * ONE_20) / a9; + sum += x9; + } + + if (a >= a10) { + a = (a * ONE_20) / a10; + sum += x10; + } + + if (a >= a11) { + a = (a * ONE_20) / a11; + sum += x11; + } + + // a is now a small number (smaller than a_11, which roughly equals 1.06). This means we can use a Taylor series + // that converges rapidly for values of `a` close to one - the same one used in ln_36. + // Let z = (a - 1) / (a + 1). + // ln(a) = 2 * (z + z^3 / 3 + z^5 / 5 + z^7 / 7 + ... + z^(2 * n + 1) / (2 * n + 1)) + + // Recall that 20 digit fixed point division requires multiplying by ONE_20, and multiplication requires + // division by ONE_20. + int z = ((a - ONE_20) * ONE_20) / (a + ONE_20); + int z_squared = (z * z) / ONE_20; + + // num is the numerator of the series: the z^(2 * n + 1) term + int num = z; + + // seriesSum holds the accumulated sum of each term in the series, starting with the initial z + int seriesSum = num; + + // In each step, the numerator is multiplied by z^2 + num = (num * z_squared) / ONE_20; + seriesSum += num / 3; + + num = (num * z_squared) / ONE_20; + seriesSum += num / 5; + + num = (num * z_squared) / ONE_20; + seriesSum += num / 7; + + num = (num * z_squared) / ONE_20; + seriesSum += num / 9; + + num = (num * z_squared) / ONE_20; + seriesSum += num / 11; + + // 6 Taylor terms are sufficient for 36 decimal precision. + + // Finally, we multiply by 2 (non fixed point) to compute ln(remainder) + seriesSum *= 2; + + // We now have the sum of all x_n present, and the Taylor approximation of the logarithm of the remainder (both + // with 20 decimals). All that remains is to sum these two, and then drop two digits to return a 18 decimal + // value. + + return (sum + seriesSum) / 100; + } + + /** + * @dev Intrnal high precision (36 decimal places) natural logarithm (ln(x)) with signed 18 decimal fixed point argument, + * for x close to one. + * + * Should only be used if x is between LN_36_LOWER_BOUND and LN_36_UPPER_BOUND. + */ + function _ln_36(int x) private pure returns (int) { + // Since ln(1) = 0, a value of x close to one will yield a very small result, which makes using 36 digits + // worthwhile. + + // First, we transform x to a 36 digit fixed point value. + x *= ONE_18; + + // We will use the following Taylor expansion, which converges very rapidly. Let z = (x - 1) / (x + 1). + // ln(x) = 2 * (z + z^3 / 3 + z^5 / 5 + z^7 / 7 + ... + z^(2 * n + 1) / (2 * n + 1)) + + // Recall that 36 digit fixed point division requires multiplying by ONE_36, and multiplication requires + // division by ONE_36. + int z = ((x - ONE_36) * ONE_36) / (x + ONE_36); + int z_squared = (z * z) / ONE_36; + + // num is the numerator of the series: the z^(2 * n + 1) term + int num = z; + + // seriesSum holds the accumulated sum of each term in the series, starting with the initial z + int seriesSum = num; + + // In each step, the numerator is multiplied by z^2 + num = (num * z_squared) / ONE_36; + seriesSum += num / 3; + + num = (num * z_squared) / ONE_36; + seriesSum += num / 5; + + num = (num * z_squared) / ONE_36; + seriesSum += num / 7; + + num = (num * z_squared) / ONE_36; + seriesSum += num / 9; + + num = (num * z_squared) / ONE_36; + seriesSum += num / 11; + + num = (num * z_squared) / ONE_36; + seriesSum += num / 13; + + num = (num * z_squared) / ONE_36; + seriesSum += num / 15; + + // 8 Taylor terms are sufficient for 36 decimal precision. + + // All that remains is multiplying by 2 (non fixed point). + return seriesSum * 2; + } +} diff --git a/src/adapters/libs/balancer/ScaleLib.sol b/src/adapters/libs/balancer/ScaleLib.sol new file mode 100644 index 00000000..94d6ea37 --- /dev/null +++ b/src/adapters/libs/balancer/ScaleLib.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./FixedPoint.sol"; + +/// @dev Library for up scaling / downscaling amounts for tokens with different decimals +/// @dev Used for Balancer swappers +/// @dev taken from https://github.com/balancer-labs/balancer-v2-monorepo/blob/c18ff2686c61a8cbad72cdcfc65e9b11476fdbc3/pkg/pool-utils/contracts/BasePool.sol#L520 +library ScaleLib { + function _upscale(uint amount, uint scalingFactor) internal pure returns (uint) { + return FixedPoint.mulDown(amount, scalingFactor); + } + + function _upscaleArray(uint[] memory amounts, uint[] memory scalingFactors) internal pure { + uint len = amounts.length; + for (uint i = 0; i < len; ++i) { + amounts[i] = FixedPoint.mulDown(amounts[i], scalingFactors[i]); + } + } + + function _downscaleDown(uint amount, uint scalingFactor) internal pure returns (uint) { + return FixedPoint.divDown(amount, scalingFactor); + } +} diff --git a/src/adapters/libs/balancer/StableMath.sol b/src/adapters/libs/balancer/StableMath.sol new file mode 100644 index 00000000..64e4d934 --- /dev/null +++ b/src/adapters/libs/balancer/StableMath.sol @@ -0,0 +1,496 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.23; + +import "./FixedPoint.sol"; +import {LegacyOZMath} from "./LegacyOZMath.sol"; + +library StableMath { + using FixedPoint for uint; + + uint internal constant _MIN_AMP = 1; + uint internal constant _MAX_AMP = 5000; + uint internal constant _AMP_PRECISION = 1e3; + + uint internal constant _MAX_STABLE_TOKENS = 5; + + // Note on unchecked arithmetic: + // This contract performs a large number of additions, subtractions, multiplications and divisions, often inside + // loops. Since many of these operations are gas-sensitive (as they happen e.g. during a swap), it is important to + // not make any unnecessary checks. We rely on a set of invariants to avoid having to use checked arithmetic (the + // Math library), including: + // - the number of tokens is bounded by _MAX_STABLE_TOKENS + // - the amplification parameter is bounded by _MAX_AMP * _AMP_PRECISION, which fits in 23 bits + // - the token balances are bounded by 2^112 (guaranteed by the Vault) times 1e18 (the maximum scaling factor), + // which fits in 172 bits + // + // This means e.g. we can safely multiply a balance by the amplification parameter without worrying about overflow. + + // About swap fees on joins and exits: + // Any join or exit that is not perfectly balanced (e.g. all single token joins or exits) is mathematically + // equivalent to a perfectly balanced join or exit followed by a series of swaps. Since these swaps would charge + // swap fees, it follows that (some) joins and exits should as well. + // On these operations, we split the token amounts in 'taxable' and 'non-taxable' portions, where the 'taxable' part + // is the one to which swap fees are applied. + + // Computes the invariant given the current balances, using the Newton-Raphson approximation. + // The amplification parameter equals: A n^(n-1) + function _calculateInvariant( + uint amplificationParameter, + uint[] memory balances, + bool roundUp + ) internal pure returns (uint) { + /** + * + * // invariant // + * // D = invariant D^(n+1) // + * // A = amplification coefficient A n^n S + D = A D n^n + ----------- // + * // S = sum of balances n^n P // + * // P = product of balances // + * // n = number of tokens // + * + */ + + // We support rounding up or down. + + uint sum = 0; + uint numTokens = balances.length; + for (uint i = 0; i < numTokens; i++) { + sum = sum.add(balances[i]); + } + if (sum == 0) { + return 0; + } + + uint prevInvariant = 0; + uint invariant = sum; + uint ampTimesTotal = amplificationParameter * numTokens; + + for (uint i = 0; i < 255; i++) { + uint P_D = balances[0] * numTokens; + for (uint j = 1; j < numTokens; j++) { + P_D = LegacyOZMath.div( + LegacyOZMath.mul(LegacyOZMath.mul(P_D, balances[j]), numTokens), invariant, roundUp + ); + } + prevInvariant = invariant; + invariant = LegacyOZMath.div( + LegacyOZMath.mul(LegacyOZMath.mul(numTokens, invariant), invariant).add( + LegacyOZMath.div( + LegacyOZMath.mul(LegacyOZMath.mul(ampTimesTotal, sum), P_D), _AMP_PRECISION, roundUp + ) + ), + LegacyOZMath.mul(numTokens + 1, invariant).add( + // No need to use checked arithmetic for the amp precision, the amp is guaranteed to be at least 1 + LegacyOZMath.div(LegacyOZMath.mul(ampTimesTotal - _AMP_PRECISION, P_D), _AMP_PRECISION, !roundUp) + ), + roundUp + ); + + if (invariant > prevInvariant) { + if (invariant - prevInvariant <= 1) { + return invariant; + } + } else if (prevInvariant - invariant <= 1) { + return invariant; + } + } + + _revert(Errors.STABLE_INVARIANT_DIDNT_CONVERGE); + } + + // Computes how many tokens can be taken out of a pool if `tokenAmountIn` are sent, given the current balances. + // The amplification parameter equals: A n^(n-1) + // The invariant should be rounded up. + function _calcOutGivenIn( + uint amplificationParameter, + uint[] memory balances, + uint tokenIndexIn, + uint tokenIndexOut, + uint tokenAmountIn, + uint invariant + ) internal pure returns (uint) { + /** + * + * // outGivenIn token x for y - polynomial equation to solve // + * // ay = amount out to calculate // + * // by = balance token out // + * // y = by - ay (finalBalanceOut) // + * // D = invariant D D^(n+1) // + * // A = amplification coefficient y^2 + ( S - ---------- - D) * y - ------------- = 0 // + * // n = number of tokens (A * n^n) A * n^2n * P // + * // S = sum of final balances but y // + * // P = product of final balances but y // + * + */ + + // Amount out, so we round down overall. + balances[tokenIndexIn] = balances[tokenIndexIn].add(tokenAmountIn); + + uint finalBalanceOut = _getTokenBalanceGivenInvariantAndAllOtherBalances( + amplificationParameter, balances, invariant, tokenIndexOut + ); + + // No need to use checked arithmetic since `tokenAmountIn` was actually added to the same balance right before + // calling `_getTokenBalanceGivenInvariantAndAllOtherBalances` which doesn't alter the balances array. + balances[tokenIndexIn] = balances[tokenIndexIn] - tokenAmountIn; + + return balances[tokenIndexOut].sub(finalBalanceOut).sub(1); + } + + // Computes how many tokens must be sent to a pool if `tokenAmountOut` are sent given the + // current balances, using the Newton-Raphson approximation. + // The amplification parameter equals: A n^(n-1) + // The invariant should be rounded up. + function _calcInGivenOut( + uint amplificationParameter, + uint[] memory balances, + uint tokenIndexIn, + uint tokenIndexOut, + uint tokenAmountOut, + uint invariant + ) internal pure returns (uint) { + /** + * + * // inGivenOut token x for y - polynomial equation to solve // + * // ax = amount in to calculate // + * // bx = balance token in // + * // x = bx + ax (finalBalanceIn) // + * // D = invariant D D^(n+1) // + * // A = amplification coefficient x^2 + ( S - ---------- - D) * x - ------------- = 0 // + * // n = number of tokens (A * n^n) A * n^2n * P // + * // S = sum of final balances but x // + * // P = product of final balances but x // + * + */ + + // Amount in, so we round up overall. + balances[tokenIndexOut] = balances[tokenIndexOut].sub(tokenAmountOut); + + uint finalBalanceIn = + _getTokenBalanceGivenInvariantAndAllOtherBalances(amplificationParameter, balances, invariant, tokenIndexIn); + + // No need to use checked arithmetic since `tokenAmountOut` was actually subtracted from the same balance right + // before calling `_getTokenBalanceGivenInvariantAndAllOtherBalances` which doesn't alter the balances array. + balances[tokenIndexOut] = balances[tokenIndexOut] + tokenAmountOut; + + return finalBalanceIn.sub(balances[tokenIndexIn]).add(1); + } + + function _calcBptOutGivenExactTokensIn( + uint amp, + uint[] memory balances, + uint[] memory amountsIn, + uint bptTotalSupply, + uint swapFeePercentage + ) internal pure returns (uint) { + // BPT out, so we round down overall. + + // First loop calculates the sum of all token balances, which will be used to calculate + // the current weights of each token, relative to this sum + uint sumBalances = 0; + for (uint i = 0; i < balances.length; i++) { + sumBalances = sumBalances.add(balances[i]); + } + + // Calculate the weighted balance ratio without considering fees + uint[] memory balanceRatiosWithFee = new uint[](amountsIn.length); + // The weighted sum of token balance ratios with fee + uint invariantRatioWithFees = 0; + for (uint i = 0; i < balances.length; i++) { + uint currentWeight = balances[i].divDown(sumBalances); + balanceRatiosWithFee[i] = balances[i].add(amountsIn[i]).divDown(balances[i]); + invariantRatioWithFees = invariantRatioWithFees.add(balanceRatiosWithFee[i].mulDown(currentWeight)); + } + + // Second loop calculates new amounts in, taking into account the fee on the percentage excess + uint[] memory newBalances = new uint[](balances.length); + for (uint i = 0; i < balances.length; i++) { + uint amountInWithoutFee; + + // Check if the balance ratio is greater than the ideal ratio to charge fees or not + if (balanceRatiosWithFee[i] > invariantRatioWithFees) { + uint nonTaxableAmount = balances[i].mulDown(invariantRatioWithFees.sub(FixedPoint.ONE)); + uint taxableAmount = amountsIn[i].sub(nonTaxableAmount); + // No need to use checked arithmetic for the swap fee, it is guaranteed to be lower than 50% + amountInWithoutFee = nonTaxableAmount.add(taxableAmount.mulDown(FixedPoint.ONE - swapFeePercentage)); + } else { + amountInWithoutFee = amountsIn[i]; + } + + newBalances[i] = balances[i].add(amountInWithoutFee); + } + + // Get current and new invariants, taking swap fees into account + uint currentInvariant = _calculateInvariant(amp, balances, true); + uint newInvariant = _calculateInvariant(amp, newBalances, false); + uint invariantRatio = newInvariant.divDown(currentInvariant); + + // If the invariant didn't increase for any reason, we simply don't mint BPT + if (invariantRatio > FixedPoint.ONE) { + return bptTotalSupply.mulDown(invariantRatio - FixedPoint.ONE); + } else { + return 0; + } + } + + function _calcTokenInGivenExactBptOut( + uint amp, + uint[] memory balances, + uint tokenIndex, + uint bptAmountOut, + uint bptTotalSupply, + uint swapFeePercentage + ) internal pure returns (uint) { + // Token in, so we round up overall. + + // Get the current invariant + uint currentInvariant = _calculateInvariant(amp, balances, true); + + // Calculate new invariant + uint newInvariant = bptTotalSupply.add(bptAmountOut).divUp(bptTotalSupply).mulUp(currentInvariant); + + // Calculate amount in without fee. + uint newBalanceTokenIndex = + _getTokenBalanceGivenInvariantAndAllOtherBalances(amp, balances, newInvariant, tokenIndex); + uint amountInWithoutFee = newBalanceTokenIndex.sub(balances[tokenIndex]); + + // First calculate the sum of all token balances, which will be used to calculate + // the current weight of each token + uint sumBalances = 0; + for (uint i = 0; i < balances.length; i++) { + sumBalances = sumBalances.add(balances[i]); + } + + // We can now compute how much extra balance is being deposited and used in virtual swaps, and charge swap fees + // accordingly. + uint currentWeight = balances[tokenIndex].divDown(sumBalances); + uint taxablePercentage = currentWeight.complement(); + uint taxableAmount = amountInWithoutFee.mulUp(taxablePercentage); + uint nonTaxableAmount = amountInWithoutFee.sub(taxableAmount); + + // No need to use checked arithmetic for the swap fee, it is guaranteed to be lower than 50% + return nonTaxableAmount.add(taxableAmount.divUp(FixedPoint.ONE - swapFeePercentage)); + } + + /* + Flow of calculations: + amountsTokenOut -> amountsOutProportional -> + amountOutPercentageExcess -> amountOutBeforeFee -> newInvariant -> amountBPTIn + */ + function _calcBptInGivenExactTokensOut( + uint amp, + uint[] memory balances, + uint[] memory amountsOut, + uint bptTotalSupply, + uint swapFeePercentage + ) internal pure returns (uint) { + // BPT in, so we round up overall. + + // First loop calculates the sum of all token balances, which will be used to calculate + // the current weights of each token relative to this sum + uint sumBalances = 0; + for (uint i = 0; i < balances.length; i++) { + sumBalances = sumBalances.add(balances[i]); + } + + // Calculate the weighted balance ratio without considering fees + uint[] memory balanceRatiosWithoutFee = new uint[](amountsOut.length); + uint invariantRatioWithoutFees = 0; + for (uint i = 0; i < balances.length; i++) { + uint currentWeight = balances[i].divUp(sumBalances); + balanceRatiosWithoutFee[i] = balances[i].sub(amountsOut[i]).divUp(balances[i]); + invariantRatioWithoutFees = invariantRatioWithoutFees.add(balanceRatiosWithoutFee[i].mulUp(currentWeight)); + } + + // Second loop calculates new amounts in, taking into account the fee on the percentage excess + uint[] memory newBalances = new uint[](balances.length); + for (uint i = 0; i < balances.length; i++) { + // Swap fees are typically charged on 'token in', but there is no 'token in' here, so we apply it to + // 'token out'. This results in slightly larger price impact. + + uint amountOutWithFee; + if (invariantRatioWithoutFees > balanceRatiosWithoutFee[i]) { + uint nonTaxableAmount = balances[i].mulDown(invariantRatioWithoutFees.complement()); + uint taxableAmount = amountsOut[i].sub(nonTaxableAmount); + // No need to use checked arithmetic for the swap fee, it is guaranteed to be lower than 50% + amountOutWithFee = nonTaxableAmount.add(taxableAmount.divUp(FixedPoint.ONE - swapFeePercentage)); + } else { + amountOutWithFee = amountsOut[i]; + } + + newBalances[i] = balances[i].sub(amountOutWithFee); + } + + // Get current and new invariants, taking into account swap fees + uint currentInvariant = _calculateInvariant(amp, balances, true); + uint newInvariant = _calculateInvariant(amp, newBalances, false); + uint invariantRatio = newInvariant.divDown(currentInvariant); + + // return amountBPTIn + return bptTotalSupply.mulUp(invariantRatio.complement()); + } + + function _calcTokenOutGivenExactBptIn( + uint amp, + uint[] memory balances, + uint tokenIndex, + uint bptAmountIn, + uint bptTotalSupply, + uint swapFeePercentage + ) internal pure returns (uint) { + // Token out, so we round down overall. + + // Get the current and new invariants. Since we need a bigger new invariant, we round the current one up. + uint currentInvariant = _calculateInvariant(amp, balances, true); + uint newInvariant = bptTotalSupply.sub(bptAmountIn).divUp(bptTotalSupply).mulUp(currentInvariant); + + // Calculate amount out without fee + uint newBalanceTokenIndex = + _getTokenBalanceGivenInvariantAndAllOtherBalances(amp, balances, newInvariant, tokenIndex); + uint amountOutWithoutFee = balances[tokenIndex].sub(newBalanceTokenIndex); + + // First calculate the sum of all token balances, which will be used to calculate + // the current weight of each token + uint sumBalances = 0; + for (uint i = 0; i < balances.length; i++) { + sumBalances = sumBalances.add(balances[i]); + } + + // We can now compute how much excess balance is being withdrawn as a result of the virtual swaps, which result + // in swap fees. + uint currentWeight = balances[tokenIndex].divDown(sumBalances); + uint taxablePercentage = currentWeight.complement(); + + // Swap fees are typically charged on 'token in', but there is no 'token in' here, so we apply it + // to 'token out'. This results in slightly larger price impact. Fees are rounded up. + uint taxableAmount = amountOutWithoutFee.mulUp(taxablePercentage); + uint nonTaxableAmount = amountOutWithoutFee.sub(taxableAmount); + + // No need to use checked arithmetic for the swap fee, it is guaranteed to be lower than 50% + return nonTaxableAmount.add(taxableAmount.mulDown(FixedPoint.ONE - swapFeePercentage)); + } + + function _calcTokensOutGivenExactBptIn( + uint[] memory balances, + uint bptAmountIn, + uint bptTotalSupply + ) internal pure returns (uint[] memory) { + /** + * + * // exactBPTInForTokensOut // + * // (per token) // + * // aO = tokenAmountOut / bptIn \ // + * // b = tokenBalance a0 = b * | --------------------- | // + * // bptIn = bptAmountIn \ bptTotalSupply / // + * // bpt = bptTotalSupply // + * + */ + + // Since we're computing an amount out, we round down overall. This means rounding down on both the + // multiplication and division. + + uint bptRatio = bptAmountIn.divDown(bptTotalSupply); + + uint[] memory amountsOut = new uint[](balances.length); + for (uint i = 0; i < balances.length; i++) { + amountsOut[i] = balances[i].mulDown(bptRatio); + } + + return amountsOut; + } + + // The amplification parameter equals: A n^(n-1) + function _calcDueTokenProtocolSwapFeeAmount( + uint amplificationParameter, + uint[] memory balances, + uint lastInvariant, + uint tokenIndex, + uint protocolSwapFeePercentage + ) internal pure returns (uint) { + /** + * + * // oneTokenSwapFee - polynomial equation to solve // + * // af = fee amount to calculate in one token // + * // bf = balance of fee token // + * // f = bf - af (finalBalanceFeeToken) // + * // D = old invariant D D^(n+1) // + * // A = amplification coefficient f^2 + ( S - ---------- - D) * f - ------------- = 0 // + * // n = number of tokens (A * n^n) A * n^2n * P // + * // S = sum of final balances but f // + * // P = product of final balances but f // + * + */ + + // Protocol swap fee amount, so we round down overall. + + uint finalBalanceFeeToken = _getTokenBalanceGivenInvariantAndAllOtherBalances( + amplificationParameter, balances, lastInvariant, tokenIndex + ); + + if (balances[tokenIndex] <= finalBalanceFeeToken) { + // This shouldn't happen outside of rounding errors, but have this safeguard nonetheless to prevent the Pool + // from entering a locked state in which joins and exits revert while computing accumulated swap fees. + return 0; + } + + // Result is rounded down + uint accumulatedTokenSwapFees = balances[tokenIndex] - finalBalanceFeeToken; + return accumulatedTokenSwapFees.mulDown(protocolSwapFeePercentage); + } + + // Private functions + + // This function calculates the balance of a given token (tokenIndex) + // given all the other balances and the invariant + function _getTokenBalanceGivenInvariantAndAllOtherBalances( + uint amplificationParameter, + uint[] memory balances, + uint invariant, + uint tokenIndex + ) internal pure returns (uint) { + // Rounds result up overall + + uint ampTimesTotal = amplificationParameter * balances.length; + uint sum = balances[0]; + uint P_D = balances[0] * balances.length; + for (uint j = 1; j < balances.length; j++) { + P_D = LegacyOZMath.divDown(LegacyOZMath.mul(LegacyOZMath.mul(P_D, balances[j]), balances.length), invariant); + sum = sum.add(balances[j]); + } + // No need to use safe math, based on the loop above `sum` is greater than or equal to `balances[tokenIndex]` + sum = sum - balances[tokenIndex]; + + uint inv2 = LegacyOZMath.mul(invariant, invariant); + // We remove the balance from c by multiplying it + uint c = LegacyOZMath.mul( + LegacyOZMath.mul(LegacyOZMath.divUp(inv2, LegacyOZMath.mul(ampTimesTotal, P_D)), _AMP_PRECISION), + balances[tokenIndex] + ); + uint b = sum.add(LegacyOZMath.mul(LegacyOZMath.divDown(invariant, ampTimesTotal), _AMP_PRECISION)); + + // We iterate to find the balance + uint prevTokenBalance = 0; + // We multiply the first iteration outside the loop with the invariant to set the value of the + // initial approximation. + uint tokenBalance = LegacyOZMath.divUp(inv2.add(c), invariant.add(b)); + + for (uint i = 0; i < 255; i++) { + prevTokenBalance = tokenBalance; + + tokenBalance = LegacyOZMath.divUp( + LegacyOZMath.mul(tokenBalance, tokenBalance).add(c), + LegacyOZMath.mul(tokenBalance, 2).add(b).sub(invariant) + ); + + if (tokenBalance > prevTokenBalance) { + if (tokenBalance - prevTokenBalance <= 1) { + return tokenBalance; + } + } else if (prevTokenBalance - tokenBalance <= 1) { + return tokenBalance; + } + } + + _revert(Errors.STABLE_GET_BALANCE_DIDNT_CONVERGE); + } +} diff --git a/src/adapters/libs/balancer/WeightedMath.sol b/src/adapters/libs/balancer/WeightedMath.sol new file mode 100644 index 00000000..cfb3536b --- /dev/null +++ b/src/adapters/libs/balancer/WeightedMath.sol @@ -0,0 +1,445 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.23; + +import "./FixedPoint.sol"; +import "./LegacyOZMath.sol"; + +// These functions start with an underscore, as if they were part of a contract and not a library. At some point this +// should be fixed. +// solhint-disable private-vars-leading-underscore + +library WeightedMath { + using FixedPoint for uint; + // A minimum normalized weight imposes a maximum weight ratio. We need this due to limitations in the + // implementation of the power function, as these ratios are often exponents. + + uint internal constant _MIN_WEIGHT = 0.01e18; + // Having a minimum normalized weight imposes a limit on the maximum number of tokens; + // i.e., the largest possible pool is one where all tokens have exactly the minimum weight. + uint internal constant _MAX_WEIGHTED_TOKENS = 100; + + // Pool limits that arise from limitations in the fixed point power function (and the imposed 1:100 maximum weight + // ratio). + + // Swap limits: amounts swapped may not be larger than this percentage of total balance. + uint internal constant _MAX_IN_RATIO = 0.3e18; + uint internal constant _MAX_OUT_RATIO = 0.3e18; + + // Invariant growth limit: non-proportional joins cannot cause the invariant to increase by more than this ratio. + uint internal constant _MAX_INVARIANT_RATIO = 3e18; + // Invariant shrink limit: non-proportional exits cannot cause the invariant to decrease by less than this ratio. + uint internal constant _MIN_INVARIANT_RATIO = 0.7e18; + + // About swap fees on joins and exits: + // Any join or exit that is not perfectly balanced (e.g. all single token joins or exits) is mathematically + // equivalent to a perfectly balanced join or exit followed by a series of swaps. Since these swaps would charge + // swap fees, it follows that (some) joins and exits should as well. + // On these operations, we split the token amounts in 'taxable' and 'non-taxable' portions, where the 'taxable' part + // is the one to which swap fees are applied. + + // Invariant is used to collect protocol swap fees by comparing its value between two times. + // So we can round always to the same direction. It is also used to initiate the BPT amount + // and, because there is a minimum BPT, we round down the invariant. + function _calculateInvariant( + uint[] memory normalizedWeights, + uint[] memory balances + ) internal pure returns (uint invariant) { + /** + * + * // invariant _____ // + * // wi = weight index i | | wi // + * // bi = balance index i | | bi ^ = i // + * // i = invariant // + * + */ + invariant = FixedPoint.ONE; + for (uint i = 0; i < normalizedWeights.length; i++) { + invariant = invariant.mulDown(balances[i].powDown(normalizedWeights[i])); + } + + _require(invariant > 0, Errors.ZERO_INVARIANT); + } + + // Computes how many tokens can be taken out of a pool if `amountIn` are sent, given the + // current balances and weights. + function _calcOutGivenIn( + uint balanceIn, + uint weightIn, + uint balanceOut, + uint weightOut, + uint amountIn + ) internal pure returns (uint) { + /** + * + * // outGivenIn // + * // aO = amountOut // + * // bO = balanceOut // + * // bI = balanceIn / / bI \ (wI / wO) \ // + * // aI = amountIn aO = bO * | 1 - | -------------------------- | ^ | // + * // wI = weightIn \ \ ( bI + aI ) / / // + * // wO = weightOut // + * + */ + + // Amount out, so we round down overall. + + // The multiplication rounds down, and the subtrahend (power) rounds up (so the base rounds up too). + // Because bI / (bI + aI) <= 1, the exponent rounds down. + + // Cannot exceed maximum in ratio + _require(amountIn <= balanceIn.mulDown(_MAX_IN_RATIO), Errors.MAX_IN_RATIO); + + uint denominator = balanceIn.add(amountIn); + uint base = balanceIn.divUp(denominator); + uint exponent = weightIn.divDown(weightOut); + uint power = base.powUp(exponent); + + return balanceOut.mulDown(power.complement()); + } + + // Computes how many tokens must be sent to a pool in order to take `amountOut`, given the + // current balances and weights. + function _calcInGivenOut( + uint balanceIn, + uint weightIn, + uint balanceOut, + uint weightOut, + uint amountOut + ) internal pure returns (uint) { + /** + * + * // inGivenOut // + * // aO = amountOut // + * // bO = balanceOut // + * // bI = balanceIn / / bO \ (wO / wI) \ // + * // aI = amountIn aI = bI * | | -------------------------- | ^ - 1 | // + * // wI = weightIn \ \ ( bO - aO ) / / // + * // wO = weightOut // + * + */ + + // Amount in, so we round up overall. + + // The multiplication rounds up, and the power rounds up (so the base rounds up too). + // Because b0 / (b0 - a0) >= 1, the exponent rounds up. + + // Cannot exceed maximum out ratio + _require(amountOut <= balanceOut.mulDown(_MAX_OUT_RATIO), Errors.MAX_OUT_RATIO); + + uint base = balanceOut.divUp(balanceOut.sub(amountOut)); + uint exponent = weightOut.divUp(weightIn); + uint power = base.powUp(exponent); + + // Because the base is larger than one (and the power rounds up), the power should always be larger than one, so + // the following subtraction should never revert. + uint ratio = power.sub(FixedPoint.ONE); + + return balanceIn.mulUp(ratio); + } + + function _calcBptOutGivenExactTokensIn( + uint[] memory balances, + uint[] memory normalizedWeights, + uint[] memory amountsIn, + uint bptTotalSupply, + uint swapFeePercentage + ) internal pure returns (uint, uint[] memory) { + // BPT out, so we round down overall. + + uint[] memory balanceRatiosWithFee = new uint[](amountsIn.length); + + uint invariantRatioWithFees = 0; + for (uint i = 0; i < balances.length; i++) { + balanceRatiosWithFee[i] = balances[i].add(amountsIn[i]).divDown(balances[i]); + invariantRatioWithFees = invariantRatioWithFees.add(balanceRatiosWithFee[i].mulDown(normalizedWeights[i])); + } + + (uint invariantRatio, uint[] memory swapFees) = _computeJoinExactTokensInInvariantRatio( + balances, normalizedWeights, amountsIn, balanceRatiosWithFee, invariantRatioWithFees, swapFeePercentage + ); + + uint bptOut = (invariantRatio > FixedPoint.ONE) ? bptTotalSupply.mulDown(invariantRatio.sub(FixedPoint.ONE)) : 0; + return (bptOut, swapFees); + } + + /** + * @dev Intermediate function to avoid stack-too-deep errors. + */ + function _computeJoinExactTokensInInvariantRatio( + uint[] memory balances, + uint[] memory normalizedWeights, + uint[] memory amountsIn, + uint[] memory balanceRatiosWithFee, + uint invariantRatioWithFees, + uint swapFeePercentage + ) private pure returns (uint invariantRatio, uint[] memory swapFees) { + // Swap fees are charged on all tokens that are being added in a larger proportion than the overall invariant + // increase. + swapFees = new uint[](amountsIn.length); + invariantRatio = FixedPoint.ONE; + + for (uint i = 0; i < balances.length; i++) { + uint amountInWithoutFee; + + if (balanceRatiosWithFee[i] > invariantRatioWithFees) { + uint nonTaxableAmount = balances[i].mulDown(invariantRatioWithFees.sub(FixedPoint.ONE)); + uint taxableAmount = amountsIn[i].sub(nonTaxableAmount); + uint swapFee = taxableAmount.mulUp(swapFeePercentage); + + amountInWithoutFee = nonTaxableAmount.add(taxableAmount.sub(swapFee)); + swapFees[i] = swapFee; + } else { + amountInWithoutFee = amountsIn[i]; + } + + uint balanceRatio = balances[i].add(amountInWithoutFee).divDown(balances[i]); + + invariantRatio = invariantRatio.mulDown(balanceRatio.powDown(normalizedWeights[i])); + } + } + + function _calcTokenInGivenExactBptOut( + uint balance, + uint normalizedWeight, + uint bptAmountOut, + uint bptTotalSupply, + uint swapFeePercentage + ) internal pure returns (uint amountIn, uint swapFee) { + /** + * + * // tokenInForExactBPTOut // + * // a = amountIn // + * // b = balance / / totalBPT + bptOut \ (1 / w) \ // + * // bptOut = bptAmountOut a = b * | | -------------------------- | ^ - 1 | // + * // bpt = totalBPT \ \ totalBPT / / // + * // w = weight // + * + */ + + // Token in, so we round up overall. + + // Calculate the factor by which the invariant will increase after minting BPTAmountOut + uint invariantRatio = bptTotalSupply.add(bptAmountOut).divUp(bptTotalSupply); + _require(invariantRatio <= _MAX_INVARIANT_RATIO, Errors.MAX_OUT_BPT_FOR_TOKEN_IN); + + // Calculate by how much the token balance has to increase to match the invariantRatio + uint balanceRatio = invariantRatio.powUp(FixedPoint.ONE.divUp(normalizedWeight)); + + uint amountInWithoutFee = balance.mulUp(balanceRatio.sub(FixedPoint.ONE)); + + // We can now compute how much extra balance is being deposited and used in virtual swaps, and charge swap fees + // accordingly. + uint taxablePercentage = normalizedWeight.complement(); + uint taxableAmount = amountInWithoutFee.mulUp(taxablePercentage); + uint nonTaxableAmount = amountInWithoutFee.sub(taxableAmount); + + uint taxableAmountPlusFees = taxableAmount.divUp(FixedPoint.ONE.sub(swapFeePercentage)); + + swapFee = taxableAmountPlusFees - taxableAmount; + amountIn = nonTaxableAmount.add(taxableAmountPlusFees); + } + + function _calcAllTokensInGivenExactBptOut( + uint[] memory balances, + uint bptAmountOut, + uint totalBPT + ) internal pure returns (uint[] memory) { + /** + * + * // tokensInForExactBptOut // + * // (per token) // + * // aI = amountIn / bptOut \ // + * // b = balance aI = b * | ------------ | // + * // bptOut = bptAmountOut \ totalBPT / // + * // bpt = totalBPT // + * + */ + + // Tokens in, so we round up overall. + uint bptRatio = bptAmountOut.divUp(totalBPT); + + uint[] memory amountsIn = new uint[](balances.length); + for (uint i = 0; i < balances.length; i++) { + amountsIn[i] = balances[i].mulUp(bptRatio); + } + + return amountsIn; + } + + function _calcBptInGivenExactTokensOut( + uint[] memory balances, + uint[] memory normalizedWeights, + uint[] memory amountsOut, + uint bptTotalSupply, + uint swapFeePercentage + ) internal pure returns (uint, uint[] memory) { + // BPT in, so we round up overall. + + uint[] memory balanceRatiosWithoutFee = new uint[](amountsOut.length); + uint invariantRatioWithoutFees = 0; + for (uint i = 0; i < balances.length; i++) { + balanceRatiosWithoutFee[i] = balances[i].sub(amountsOut[i]).divUp(balances[i]); + invariantRatioWithoutFees = + invariantRatioWithoutFees.add(balanceRatiosWithoutFee[i].mulUp(normalizedWeights[i])); + } + + (uint invariantRatio, uint[] memory swapFees) = _computeExitExactTokensOutInvariantRatio( + balances, + normalizedWeights, + amountsOut, + balanceRatiosWithoutFee, + invariantRatioWithoutFees, + swapFeePercentage + ); + + uint bptIn = bptTotalSupply.mulUp(invariantRatio.complement()); + return (bptIn, swapFees); + } + + /** + * @dev Intermediate function to avoid stack-too-deep errors. + */ + function _computeExitExactTokensOutInvariantRatio( + uint[] memory balances, + uint[] memory normalizedWeights, + uint[] memory amountsOut, + uint[] memory balanceRatiosWithoutFee, + uint invariantRatioWithoutFees, + uint swapFeePercentage + ) private pure returns (uint invariantRatio, uint[] memory swapFees) { + swapFees = new uint[](amountsOut.length); + invariantRatio = FixedPoint.ONE; + + for (uint i = 0; i < balances.length; i++) { + // Swap fees are typically charged on 'token in', but there is no 'token in' here, so we apply it to + // 'token out'. This results in slightly larger price impact. + + uint amountOutWithFee; + if (invariantRatioWithoutFees > balanceRatiosWithoutFee[i]) { + uint nonTaxableAmount = balances[i].mulDown(invariantRatioWithoutFees.complement()); + uint taxableAmount = amountsOut[i].sub(nonTaxableAmount); + uint taxableAmountPlusFees = taxableAmount.divUp(FixedPoint.ONE.sub(swapFeePercentage)); + + swapFees[i] = taxableAmountPlusFees - taxableAmount; + amountOutWithFee = nonTaxableAmount.add(taxableAmountPlusFees); + } else { + amountOutWithFee = amountsOut[i]; + } + + uint balanceRatio = balances[i].sub(amountOutWithFee).divDown(balances[i]); + + invariantRatio = invariantRatio.mulDown(balanceRatio.powDown(normalizedWeights[i])); + } + } + + function _calcTokenOutGivenExactBptIn( + uint balance, + uint normalizedWeight, + uint bptAmountIn, + uint bptTotalSupply, + uint swapFeePercentage + ) internal pure returns (uint amountOut, uint swapFee) { + /** + * + * // exactBPTInForTokenOut // + * // a = amountOut // + * // b = balance / / totalBPT - bptIn \ (1 / w) \ // + * // bptIn = bptAmountIn a = b * | 1 - | -------------------------- | ^ | // + * // bpt = totalBPT \ \ totalBPT / / // + * // w = weight // + * + */ + + // Token out, so we round down overall. The multiplication rounds down, but the power rounds up (so the base + // rounds up). Because (totalBPT - bptIn) / totalBPT <= 1, the exponent rounds down. + + // Calculate the factor by which the invariant will decrease after burning BPTAmountIn + uint invariantRatio = bptTotalSupply.sub(bptAmountIn).divUp(bptTotalSupply); + _require(invariantRatio >= _MIN_INVARIANT_RATIO, Errors.MIN_BPT_IN_FOR_TOKEN_OUT); + + // Calculate by how much the token balance has to decrease to match invariantRatio + uint balanceRatio = invariantRatio.powUp(FixedPoint.ONE.divDown(normalizedWeight)); + + // Because of rounding up, balanceRatio can be greater than one. Using complement prevents reverts. + uint amountOutWithoutFee = balance.mulDown(balanceRatio.complement()); + + // We can now compute how much excess balance is being withdrawn as a result of the virtual swaps, which result + // in swap fees. + uint taxablePercentage = normalizedWeight.complement(); + + // Swap fees are typically charged on 'token in', but there is no 'token in' here, so we apply it + // to 'token out'. This results in slightly larger price impact. Fees are rounded up. + uint taxableAmount = amountOutWithoutFee.mulUp(taxablePercentage); + uint nonTaxableAmount = amountOutWithoutFee.sub(taxableAmount); + + swapFee = taxableAmount.mulUp(swapFeePercentage); + amountOut = nonTaxableAmount.add(taxableAmount.sub(swapFee)); + } + + function _calcTokensOutGivenExactBptIn( + uint[] memory balances, + uint bptAmountIn, + uint totalBPT + ) internal pure returns (uint[] memory) { + /** + * + * // exactBPTInForTokensOut // + * // (per token) // + * // aO = amountOut / bptIn \ // + * // b = balance a0 = b * | --------------------- | // + * // bptIn = bptAmountIn \ totalBPT / // + * // bpt = totalBPT // + * + */ + + // Since we're computing an amount out, we round down overall. This means rounding down on both the + // multiplication and division. + + uint bptRatio = bptAmountIn.divDown(totalBPT); + + uint[] memory amountsOut = new uint[](balances.length); + for (uint i = 0; i < balances.length; i++) { + amountsOut[i] = balances[i].mulDown(bptRatio); + } + + return amountsOut; + } + + function _calcDueTokenProtocolSwapFeeAmount( + uint balance, + uint normalizedWeight, + uint previousInvariant, + uint currentInvariant, + uint protocolSwapFeePercentage + ) internal pure returns (uint) { + /** + * + * /* protocolSwapFeePercentage * balanceToken * ( 1 - (previousInvariant / currentInvariant) ^ (1 / weightToken)) + * + */ + if (currentInvariant <= previousInvariant) { + // This shouldn't happen outside of rounding errors, but have this safeguard nonetheless to prevent the Pool + // from entering a locked state in which joins and exits revert while computing accumulated swap fees. + return 0; + } + + // We round down to prevent issues in the Pool's accounting, even if it means paying slightly less in protocol + // fees to the Vault. + + // Fee percentage and balance multiplications round down, while the subtrahend (power) rounds up (as does the + // base). Because previousInvariant / currentInvariant <= 1, the exponent rounds down. + + uint base = previousInvariant.divUp(currentInvariant); + uint exponent = FixedPoint.ONE.divDown(normalizedWeight); + + // Because the exponent is larger than one, the base of the power function has a lower bound. We cap to this + // value to avoid numeric issues, which means in the extreme case (where the invariant growth is larger than + // 1 / min exponent) the Pool will pay less in protocol fees than it should. + base = LegacyOZMath.max(base, FixedPoint.MIN_POW_BASE_FREE_EXPONENT); + + uint power = base.powUp(exponent); + + uint tokenAccruedFees = balance.mulDown(power.complement()); + return tokenAccruedFees.mulDown(protocolSwapFeePercentage); + } +} diff --git a/src/core/Platform.sol b/src/core/Platform.sol index 64971cf3..89ac8cfc 100644 --- a/src/core/Platform.sol +++ b/src/core/Platform.sol @@ -33,7 +33,7 @@ contract Platform is Controllable, IPlatform { /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @dev Version of Platform contract implementation - string public constant VERSION = "1.0.1"; + string public constant VERSION = "1.1.0"; /// @inheritdoc IPlatform uint public constant TIME_LOCK = 16 hours; @@ -42,7 +42,7 @@ contract Platform is Controllable, IPlatform { uint public constant MIN_FEE = 5_000; // 5% /// @dev Maximal revenue fee - uint public constant MAX_FEE = 10_000; // 10% + uint public constant MAX_FEE = 50_000; // 50% /// @dev Minimal VaultManager tokenId owner fee share uint public constant MIN_FEE_SHARE_VAULT_MANAGER = 10_000; // 10% @@ -121,6 +121,7 @@ contract Platform is Controllable, IPlatform { uint feeShareVaultManager; uint feeShareStrategyLogic; uint feeShareEcosystem; + mapping(address vault => uint platformFee) customVaultFee; } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ @@ -134,6 +135,8 @@ contract Platform is Controllable, IPlatform { __Controllable_init(address(this)); //slither-disable-next-line unused-return $.operators.add(msg.sender); + //slither-disable-next-line unused-return + $.operators.add(multisig_); $.platformVersion = version; emit PlatformVersion(version); } @@ -438,6 +441,13 @@ contract Platform is Controllable, IPlatform { $.minTvlForFreeHardWork = value; } + /// @inheritdoc IPlatform + function setCustomVaultFee(address vault, uint platformFee) external onlyGovernanceOrMultisig { + PlatformStorage storage $ = _getStorage(); + emit CustomVaultFee(vault, platformFee); + $.customVaultFee[vault] = platformFee; + } + /// @inheritdoc IPlatform function setupRebalancer(address rebalancer_) external onlyGovernanceOrMultisig { PlatformStorage storage $ = _getStorage(); @@ -483,6 +493,12 @@ contract Platform is Controllable, IPlatform { return ($.fee, $.feeShareVaultManager, $.feeShareStrategyLogic, $.feeShareEcosystem); } + /// @inheritdoc IPlatform + function getCustomVaultFee(address vault) external view returns (uint fee) { + PlatformStorage storage $ = _getStorage(); + return $.customVaultFee[vault]; + } + /// @inheritdoc IPlatform function getPlatformSettings() external view returns (PlatformSettings memory) { PlatformStorage storage $ = _getStorage(); diff --git a/src/core/README.md b/src/core/README.md index 74417cbb..c3d39aa7 100644 --- a/src/core/README.md +++ b/src/core/README.md @@ -81,3 +81,9 @@ forge script --rpc-url polygon script/deploy-strategy/IQMF.Polygon.s.sol -vvvv - ```shell forge script --rpc-url real script/deploy-core/Deploy.Real.s.sol --verify --verifier blockscout --verifier-url https://explorer.re.al/api? --slow --with-gas-price 30000000 -g 200 --broadcast ``` + +### Sonic + +```shell +forge script --rpc-url sonic --slow --broadcast --verify --etherscan-api-key sonic script/deploy-core/Deploy.Sonic.s.sol +``` diff --git a/src/core/base/VaultBase.sol b/src/core/base/VaultBase.sol index c08e541c..17878fa5 100644 --- a/src/core/base/VaultBase.sol +++ b/src/core/base/VaultBase.sol @@ -21,6 +21,7 @@ import "../../interfaces/IFactory.sol"; /// Start price of vault share is $1. /// @dev Used by all vault implementations (CVault, RVault, etc) /// Changelog: +/// 2.0.0: use strategy.previewDepositAssetsWrite; hardWorkMintFeeCallback use platform.getCustomVaultFee /// 1.3.0: hardWorkMintFeeCallback /// 1.2.0: isHardWorkOnDepositAllowed /// 1.1.0: setName, setSymbol, gas optimization @@ -35,7 +36,7 @@ abstract contract VaultBase is Controllable, ERC20Upgradeable, ReentrancyGuardUp /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @dev Version of VaultBase implementation - string public constant VERSION_VAULT_BASE = "1.3.0"; + string public constant VERSION_VAULT_BASE = "2.0.0"; /// @dev Delay between deposits/transfers and withdrawals uint internal constant _WITHDRAW_REQUEST_BLOCKS = 5; @@ -237,7 +238,7 @@ abstract contract VaultBase is Controllable, ERC20Upgradeable, ReentrancyGuardUp IERC20(v.underlying).safeTransferFrom(msg.sender, address(v.strategy), v.value); (v.amountsConsumed) = v.strategy.depositUnderlying(v.value); } else { - (v.amountsConsumed, v.value) = v.strategy.previewDepositAssets(assets_, amountsMax); + (v.amountsConsumed, v.value) = v.strategy.previewDepositAssetsWrite(assets_, amountsMax); // nosemgrep for (uint i; i < v.len; ++i) { IERC20(v.assets[i]).safeTransferFrom(msg.sender, address(v.strategy), v.amountsConsumed[i]); diff --git a/src/core/libs/VaultBaseLib.sol b/src/core/libs/VaultBaseLib.sol index 0b434a5b..b104ed5a 100644 --- a/src/core/libs/VaultBaseLib.sol +++ b/src/core/libs/VaultBaseLib.sol @@ -39,6 +39,11 @@ library VaultBaseLib { (, uint revenueSharesOut,) = IVault(address(this)).previewDepositAssets(revenueAssets, revenueAmounts); (v.feePlatform, v.feeShareVaultManager, v.feeShareStrategyLogic, v.feeShareEcosystem) = platform.getFees(); + try platform.getCustomVaultFee(address(this)) returns (uint vaultCustomFee) { + if (vaultCustomFee != 0) { + v.feePlatform = vaultCustomFee; + } + } catch {} uint strategyLogicTokenId = IFactory(platform.factory()).strategyLogicConfig(keccak256(bytes(s.strategyLogicId()))).tokenId; diff --git a/src/integrations/api3/IApi3ReaderProxy.sol b/src/integrations/api3/IApi3ReaderProxy.sol new file mode 100644 index 00000000..c0c7043d --- /dev/null +++ b/src/integrations/api3/IApi3ReaderProxy.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +/// @title Interface of the proxy contract that is used to read a specific API3 +/// data feed +/// @notice While reading API3 data feeds, users are strongly recommended to +/// use this interface to interact with data feed-specific proxy contracts, +/// rather than accessing the underlying contracts directly +interface IApi3ReaderProxy { + /// @notice Returns the current value and timestamp of the API3 data feed + /// associated with the proxy contract + /// @dev The user is responsible for validating the returned data. For + /// example, if `value` is the spot price of an asset, it would be + /// reasonable to reject values that are not positive. + /// `timestamp` does not necessarily refer to a timestamp of the chain that + /// the read proxy is deployed on. Considering that it may refer to an + /// off-chain time (such as the system time of the data sources, or the + /// timestamp of another chain), the user should not expect it to be + /// strictly bounded by `block.timestamp`. + /// Considering that the read proxy contract may be upgradeable, the user + /// should not assume any hard guarantees about the behavior in general. + /// For example, even though it may sound reasonable to expect `timestamp` + /// to never decrease over time and the current implementation of the proxy + /// contract guarantees it, technically, an upgrade can cause `timestamp` + /// to decrease. Therefore, the user should be able to handle any change in + /// behavior, which may include reverting gracefully. + /// @return value Data feed value + /// @return timestamp Data feed timestamp + function read() external view returns (int224 value, uint32 timestamp); +} diff --git a/src/integrations/balancer/IBComposableStablePoolMinimal.sol b/src/integrations/balancer/IBComposableStablePoolMinimal.sol new file mode 100644 index 00000000..c53d683e --- /dev/null +++ b/src/integrations/balancer/IBComposableStablePoolMinimal.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.23; + +interface IBComposableStablePoolMinimal { + /** + * @dev Returns all normalized weights, in the same order as the Pool's tokens. + */ + function getPoolId() external view returns (bytes32); + function getSwapFeePercentage() external view returns (uint); + function getAmplificationParameter() external view returns (uint value, bool isUpdating, uint precision); + function getScalingFactors() external view returns (uint[] memory); + function getBptIndex() external view returns (uint); + function getVault() external view returns (address); + + function updateTokenRateCache(address token) external; +} diff --git a/src/integrations/balancer/IBVault.sol b/src/integrations/balancer/IBVault.sol new file mode 100644 index 00000000..19668c74 --- /dev/null +++ b/src/integrations/balancer/IBVault.sol @@ -0,0 +1,584 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IAsset {} + +interface IBVault { + // Internal Balance + // + // Users can deposit tokens into the Vault, where they are allocated to their Internal Balance, and later + // transferred or withdrawn. It can also be used as a source of tokens when joining Pools, as a destination + // when exiting them, and as either when performing swaps. This usage of Internal Balance results in greatly reduced + // gas costs when compared to relying on plain ERC20 transfers, leading to large savings for frequent users. + // + // Internal Balance management features batching, which means a single contract call can be used to perform multiple + // operations of different kinds, with different senders and recipients, at once. + + /** + * @dev Returns `user`'s Internal Balance for a set of tokens. + */ + function getInternalBalance(address user, IERC20[] calldata tokens) external view returns (uint[] memory); + + /** + * @dev Performs a set of user balance operations, which involve Internal Balance (deposit, withdraw or transfer) + * and plain ERC20 transfers using the Vault's allowance. This last feature is particularly useful for relayers, as + * it lets integrators reuse a user's Vault allowance. + * + * For each operation, if the caller is not `sender`, it must be an authorized relayer for them. + */ + function manageUserBalance(UserBalanceOp[] calldata ops) external payable; + + /** + * @dev Data for `manageUserBalance` operations, which include the possibility for ETH to be sent and received + * without manual WETH wrapping or unwrapping. + */ + struct UserBalanceOp { + UserBalanceOpKind kind; + IAsset asset; + uint amount; + address sender; + address payable recipient; + } + + // There are four possible operations in `manageUserBalance`: + // + // - DEPOSIT_INTERNAL + // Increases the Internal Balance of the `recipient` account by transferring tokens from the corresponding + // `sender`. The sender must have allowed the Vault to use their tokens via `IERC20.approve()`. + // + // ETH can be used by passing the ETH sentinel value as the asset and forwarding ETH in the call: it will be wrapped + // and deposited as WETH. Any ETH amount remaining will be sent back to the caller (not the sender, which is + // relevant for relayers). + // + // Emits an `InternalBalanceChanged` event. + // + // + // - WITHDRAW_INTERNAL + // Decreases the Internal Balance of the `sender` account by transferring tokens to the `recipient`. + // + // ETH can be used by passing the ETH sentinel value as the asset. This will deduct WETH instead, unwrap it and send + // it to the recipient as ETH. + // + // Emits an `InternalBalanceChanged` event. + // + // + // - TRANSFER_INTERNAL + // Transfers tokens from the Internal Balance of the `sender` account to the Internal Balance of `recipient`. + // + // Reverts if the ETH sentinel value is passed. + // + // Emits an `InternalBalanceChanged` event. + // + // + // - TRANSFER_EXTERNAL + // Transfers tokens from `sender` to `recipient`, using the Vault's ERC20 allowance. This is typically used by + // relayers, as it lets them reuse a user's Vault allowance. + // + // Reverts if the ETH sentinel value is passed. + // + // Emits an `ExternalBalanceTransfer` event. + + enum UserBalanceOpKind { + DEPOSIT_INTERNAL, + WITHDRAW_INTERNAL, + TRANSFER_INTERNAL, + TRANSFER_EXTERNAL + } + + /** + * @dev Emitted when a user's Internal Balance changes, either from calls to `manageUserBalance`, or through + * interacting with Pools using Internal Balance. + * + * Because Internal Balance works exclusively with ERC20 tokens, ETH deposits and withdrawals will use the WETH + * address. + */ + event InternalBalanceChanged(address indexed user, IERC20 indexed token, int delta); + + /** + * @dev Emitted when a user's Vault ERC20 allowance is used by the Vault to transfer tokens to an external account. + */ + event ExternalBalanceTransfer(IERC20 indexed token, address indexed sender, address recipient, uint amount); + + // Pools + // + // There are three specialization settings for Pools, which allow for cheaper swaps at the cost of reduced + // functionality: + // + // - General: no specialization, suited for all Pools. IGeneralPool is used for swap request callbacks, passing the + // balance of all tokens in the Pool. These Pools have the largest swap costs (because of the extra storage reads), + // which increase with the number of registered tokens. + // + // - Minimal Swap Info: IMinimalSwapInfoPool is used instead of IGeneralPool, which saves gas by only passing the + // balance of the two tokens involved in the swap. This is suitable for some pricing algorithms, like the weighted + // constant product one popularized by Balancer V1. Swap costs are smaller compared to general Pools, and are + // independent of the number of registered tokens. + // + // - Two Token: only allows two tokens to be registered. This achieves the lowest possible swap gas cost. Like + // minimal swap info Pools, these are called via IMinimalSwapInfoPool. + + enum PoolSpecialization { + GENERAL, + MINIMAL_SWAP_INFO, + TWO_TOKEN + } + + /** + * @dev Registers the caller account as a Pool with a given specialization setting. Returns the Pool's ID, which + * is used in all Pool-related functions. Pools cannot be deregistered, nor can the Pool's specialization be + * changed. + * + * The caller is expected to be a smart contract that implements either `IGeneralPool` or `IMinimalSwapInfoPool`, + * depending on the chosen specialization setting. This contract is known as the Pool's contract. + * + * Note that the same contract may register itself as multiple Pools with unique Pool IDs, or in other words, + * multiple Pools may share the same contract. + * + * Emits a `PoolRegistered` event. + */ + function registerPool(PoolSpecialization specialization) external returns (bytes32); + + /** + * @dev Emitted when a Pool is registered by calling `registerPool`. + */ + event PoolRegistered(bytes32 indexed poolId, address indexed poolAddress, PoolSpecialization specialization); + + /** + * @dev Returns a Pool's contract address and specialization setting. + */ + function getPool(bytes32 poolId) external view returns (address, PoolSpecialization); + + /** + * @dev Registers `tokens` for the `poolId` Pool. Must be called by the Pool's contract. + * + * Pools can only interact with tokens they have registered. Users join a Pool by transferring registered tokens, + * exit by receiving registered tokens, and can only swap registered tokens. + * + * Each token can only be registered once. For Pools with the Two Token specialization, `tokens` must have a length + * of two, that is, both tokens must be registered in the same `registerTokens` call, and they must be sorted in + * ascending order. + * + * The `tokens` and `assetManagers` arrays must have the same length, and each entry in these indicates the Asset + * Manager for the corresponding token. Asset Managers can manage a Pool's tokens via `managePoolBalance`, + * depositing and withdrawing them directly, and can even set their balance to arbitrary amounts. They are therefore + * expected to be highly secured smart contracts with sound design principles, and the decision to register an + * Asset Manager should not be made lightly. + * + * Pools can choose not to assign an Asset Manager to a given token by passing in the zero address. Once an Asset + * Manager is set, it cannot be changed except by deregistering the associated token and registering again with a + * different Asset Manager. + * + * Emits a `TokensRegistered` event. + */ + function registerTokens(bytes32 poolId, IERC20[] calldata tokens, address[] calldata assetManagers) external; + + /** + * @dev Emitted when a Pool registers tokens by calling `registerTokens`. + */ + event TokensRegistered(bytes32 indexed poolId, IERC20[] tokens, address[] assetManagers); + + /** + * @dev Deregisters `tokens` for the `poolId` Pool. Must be called by the Pool's contract. + * + * Only registered tokens (via `registerTokens`) can be deregistered. Additionally, they must have zero total + * balance. For Pools with the Two Token specialization, `tokens` must have a length of two, that is, both tokens + * must be deregistered in the same `deregisterTokens` call. + * + * A deregistered token can be re-registered later on, possibly with a different Asset Manager. + * + * Emits a `TokensDeregistered` event. + */ + function deregisterTokens(bytes32 poolId, IERC20[] calldata tokens) external; + + /** + * @dev Emitted when a Pool deregisters tokens by calling `deregisterTokens`. + */ + event TokensDeregistered(bytes32 indexed poolId, IERC20[] tokens); + + /** + * @dev Returns detailed information for a Pool's registered token. + * + * `cash` is the number of tokens the Vault currently holds for the Pool. `managed` is the number of tokens + * withdrawn and held outside the Vault by the Pool's token Asset Manager. The Pool's total balance for `token` + * equals the sum of `cash` and `managed`. + * + * Internally, `cash` and `managed` are stored using 112 bits. No action can ever cause a Pool's token `cash`, + * `managed` or `total` balance to be greater than 2^112 - 1. + * + * `lastChangeBlock` is the number of the block in which `token`'s total balance was last modified (via either a + * join, exit, swap, or Asset Manager update). This value is useful to avoid so-called 'sandwich attacks', for + * example when developing price oracles. A change of zero (e.g. caused by a swap with amount zero) is considered a + * change for this purpose, and will update `lastChangeBlock`. + * + * `assetManager` is the Pool's token Asset Manager. + */ + function getPoolTokenInfo( + bytes32 poolId, + IERC20 token + ) external view returns (uint cash, uint managed, uint lastChangeBlock, address assetManager); + + /** + * @dev Returns a Pool's registered tokens, the total balance for each, and the latest block when *any* of + * the tokens' `balances` changed. + * + * The order of the `tokens` array is the same order that will be used in `joinPool`, `exitPool`, as well as in all + * Pool hooks (where applicable). Calls to `registerTokens` and `deregisterTokens` may change this order. + * + * If a Pool only registers tokens once, and these are sorted in ascending order, they will be stored in the same + * order as passed to `registerTokens`. + * + * Total balances include both tokens held by the Vault and those withdrawn by the Pool's Asset Managers. These are + * the amounts used by joins, exits and swaps. For a detailed breakdown of token balances, use `getPoolTokenInfo` + * instead. + * renamed IERC20[] to address[] + */ + function getPoolTokens(bytes32 poolId) + external + view + returns (address[] memory tokens, uint[] memory balances, uint lastChangeBlock); + + /** + * @dev Called by users to join a Pool, which transfers tokens from `sender` into the Pool's balance. This will + * trigger custom Pool behavior, which will typically grant something in return to `recipient` - often tokenized + * Pool shares. + * + * If the caller is not `sender`, it must be an authorized relayer for them. + * + * The `assets` and `maxAmountsIn` arrays must have the same length, and each entry indicates the maximum amount + * to send for each asset. The amounts to send are decided by the Pool and not the Vault: it just enforces + * these maximums. + * + * If joining a Pool that holds WETH, it is possible to send ETH directly: the Vault will do the wrapping. To enable + * this mechanism, the IAsset sentinel value (the zero address) must be passed in the `assets` array instead of the + * WETH address. Note that it is not possible to combine ETH and WETH in the same join. Any excess ETH will be sent + * back to the caller (not the sender, which is important for relayers). + * + * `assets` must have the same length and order as the array returned by `getPoolTokens`. This prevents issues when + * interacting with Pools that register and deregister tokens frequently. If sending ETH however, the array must be + * sorted *before* replacing the WETH address with the ETH sentinel value (the zero address), which means the final + * `assets` array might not be sorted. Pools with no registered tokens cannot be joined. + * + * If `fromInternalBalance` is true, the caller's Internal Balance will be preferred: ERC20 transfers will only + * be made for the difference between the requested amount and Internal Balance (if any). Note that ETH cannot be + * withdrawn from Internal Balance: attempting to do so will trigger a revert. + * + * This causes the Vault to call the `IBasePool.onJoinPool` hook on the Pool's contract, where Pools implement + * their own custom logic. This typically requires additional information from the user (such as the expected number + * of Pool shares). This can be encoded in the `userData` argument, which is ignored by the Vault and passed + * directly to the Pool's contract, as is `recipient`. + * + * Emits a `PoolBalanceChanged` event. + */ + function joinPool( + bytes32 poolId, + address sender, + address recipient, + JoinPoolRequest calldata request + ) external payable; + + enum JoinKind { + INIT, + EXACT_TOKENS_IN_FOR_BPT_OUT, + TOKEN_IN_FOR_EXACT_BPT_OUT + } + enum ExitKind { + EXACT_BPT_IN_FOR_ONE_TOKEN_OUT, + EXACT_BPT_IN_FOR_TOKENS_OUT, + BPT_IN_FOR_EXACT_TOKENS_OUT + } + + /// @dev modified to address[] + struct JoinPoolRequest { + address[] assets; + uint[] maxAmountsIn; + bytes userData; + bool fromInternalBalance; + } + + /** + * @dev Called by users to exit a Pool, which transfers tokens from the Pool's balance to `recipient`. This will + * trigger custom Pool behavior, which will typically ask for something in return from `sender` - often tokenized + * Pool shares. The amount of tokens that can be withdrawn is limited by the Pool's `cash` balance (see + * `getPoolTokenInfo`). + * + * If the caller is not `sender`, it must be an authorized relayer for them. + * + * The `tokens` and `minAmountsOut` arrays must have the same length, and each entry in these indicates the minimum + * token amount to receive for each token contract. The amounts to send are decided by the Pool and not the Vault: + * it just enforces these minimums. + * + * If exiting a Pool that holds WETH, it is possible to receive ETH directly: the Vault will do the unwrapping. To + * enable this mechanism, the IAsset sentinel value (the zero address) must be passed in the `assets` array instead + * of the WETH address. Note that it is not possible to combine ETH and WETH in the same exit. + * + * `assets` must have the same length and order as the array returned by `getPoolTokens`. This prevents issues when + * interacting with Pools that register and deregister tokens frequently. If receiving ETH however, the array must + * be sorted *before* replacing the WETH address with the ETH sentinel value (the zero address), which means the + * final `assets` array might not be sorted. Pools with no registered tokens cannot be exited. + * + * If `toInternalBalance` is true, the tokens will be deposited to `recipient`'s Internal Balance. Otherwise, + * an ERC20 transfer will be performed. Note that ETH cannot be deposited to Internal Balance: attempting to + * do so will trigger a revert. + * + * `minAmountsOut` is the minimum amount of tokens the user expects to get out of the Pool, for each token in the + * `tokens` array. This array must match the Pool's registered tokens. + * + * This causes the Vault to call the `IBasePool.onExitPool` hook on the Pool's contract, where Pools implement + * their own custom logic. This typically requires additional information from the user (such as the expected number + * of Pool shares to return). This can be encoded in the `userData` argument, which is ignored by the Vault and + * passed directly to the Pool's contract. + * + * Emits a `PoolBalanceChanged` event. + */ + function exitPool( + bytes32 poolId, + address sender, + address payable recipient, + ExitPoolRequest calldata request + ) external; + + /// @dev modified to address[] + struct ExitPoolRequest { + address[] assets; + uint[] minAmountsOut; + bytes userData; + bool toInternalBalance; + } + + /** + * @dev Emitted when a user joins or exits a Pool by calling `joinPool` or `exitPool`, respectively. + */ + event PoolBalanceChanged( + bytes32 indexed poolId, + address indexed liquidityProvider, + IERC20[] tokens, + int[] deltas, + uint[] protocolFeeAmounts + ); + + enum PoolBalanceChangeKind { + JOIN, + EXIT + } + + // Swaps + // + // Users can swap tokens with Pools by calling the `swap` and `batchSwap` functions. To do this, + // they need not trust Pool contracts in any way: all security checks are made by the Vault. They must however be + // aware of the Pools' pricing algorithms in order to estimate the prices Pools will quote. + // + // The `swap` function executes a single swap, while `batchSwap` can perform multiple swaps in sequence. + // In each individual swap, tokens of one kind are sent from the sender to the Pool (this is the 'token in'), + // and tokens of another kind are sent from the Pool to the recipient in exchange (this is the 'token out'). + // More complex swaps, such as one token in to multiple tokens out can be achieved by batching together + // individual swaps. + // + // There are two swap kinds: + // - 'given in' swaps, where the amount of tokens in (sent to the Pool) is known, and the Pool determines (via the + // `onSwap` hook) the amount of tokens out (to send to the recipient). + // - 'given out' swaps, where the amount of tokens out (received from the Pool) is known, and the Pool determines + // (via the `onSwap` hook) the amount of tokens in (to receive from the sender). + // + // Additionally, it is possible to chain swaps using a placeholder input amount, which the Vault replaces with + // the calculated output of the previous swap. If the previous swap was 'given in', this will be the calculated + // tokenOut amount. If the previous swap was 'given out', it will use the calculated tokenIn amount. These extended + // swaps are known as 'multihop' swaps, since they 'hop' through a number of intermediate tokens before arriving at + // the final intended token. + // + // In all cases, tokens are only transferred in and out of the Vault (or withdrawn from and deposited into Internal + // Balance) after all individual swaps have been completed, and the net token balance change computed. This makes + // certain swap patterns, such as multihops, or swaps that interact with the same token pair in multiple Pools, cost + // much less gas than they would otherwise. + // + // It also means that under certain conditions it is possible to perform arbitrage by swapping with multiple + // Pools in a way that results in net token movement out of the Vault (profit), with no tokens being sent in (only + // updating the Pool's internal accounting). + // + // To protect users from front-running or the market changing rapidly, they supply a list of 'limits' for each token + // involved in the swap, where either the maximum number of tokens to send (by passing a positive value) or the + // minimum amount of tokens to receive (by passing a negative value) is specified. + // + // Additionally, a 'deadline' timestamp can also be provided, forcing the swap to fail if it occurs after + // this point in time (e.g. if the transaction failed to be included in a block promptly). + // + // If interacting with Pools that hold WETH, it is possible to both send and receive ETH directly: the Vault will do + // the wrapping and unwrapping. To enable this mechanism, the IAsset sentinel value (the zero address) must be + // passed in the `assets` array instead of the WETH address. Note that it is possible to combine ETH and WETH in the + // same swap. Any excess ETH will be sent back to the caller (not the sender, which is relevant for relayers). + // + // Finally, Internal Balance can be used when either sending or receiving tokens. + + enum SwapKind { + GIVEN_IN, + GIVEN_OUT + } + + /** + * @dev Performs a swap with a single Pool. + * + * If the swap is 'given in' (the number of tokens to send to the Pool is known), it returns the amount of tokens + * taken from the Pool, which must be greater than or equal to `limit`. + * + * If the swap is 'given out' (the number of tokens to take from the Pool is known), it returns the amount of tokens + * sent to the Pool, which must be less than or equal to `limit`. + * + * Internal Balance usage and the recipient are determined by the `funds` struct. + * + * Emits a `Swap` event. + */ + function swap( + SingleSwap calldata singleSwap, + FundManagement calldata funds, + uint limit, + uint deadline + ) external payable returns (uint); + + /** + * @dev Data for a single swap executed by `swap`. `amount` is either `amountIn` or `amountOut` depending on + * the `kind` value. + * + * `assetIn` and `assetOut` are either token addresses, or the IAsset sentinel value for ETH (the zero address). + * Note that Pools never interact with ETH directly: it will be wrapped to or unwrapped from WETH by the Vault. + * + * The `userData` field is ignored by the Vault, but forwarded to the Pool in the `onSwap` hook, and may be + * used to extend swap behavior. + */ + struct SingleSwap { + bytes32 poolId; + SwapKind kind; + IAsset assetIn; + IAsset assetOut; + uint amount; + bytes userData; + } + + /** + * @dev Performs a series of swaps with one or multiple Pools. In each individual swap, the caller determines either + * the amount of tokens sent to or received from the Pool, depending on the `kind` value. + * + * Returns an array with the net Vault asset balance deltas. Positive amounts represent tokens (or ETH) sent to the + * Vault, and negative amounts represent tokens (or ETH) sent by the Vault. Each delta corresponds to the asset at + * the same index in the `assets` array. + * + * Swaps are executed sequentially, in the order specified by the `swaps` array. Each array element describes a + * Pool, the token to be sent to this Pool, the token to receive from it, and an amount that is either `amountIn` or + * `amountOut` depending on the swap kind. + * + * Multihop swaps can be executed by passing an `amount` value of zero for a swap. This will cause the amount in/out + * of the previous swap to be used as the amount in for the current one. In a 'given in' swap, 'tokenIn' must equal + * the previous swap's `tokenOut`. For a 'given out' swap, `tokenOut` must equal the previous swap's `tokenIn`. + * + * The `assets` array contains the addresses of all assets involved in the swaps. These are either token addresses, + * or the IAsset sentinel value for ETH (the zero address). Each entry in the `swaps` array specifies tokens in and + * out by referencing an index in `assets`. Note that Pools never interact with ETH directly: it will be wrapped to + * or unwrapped from WETH by the Vault. + * + * Internal Balance usage, sender, and recipient are determined by the `funds` struct. The `limits` array specifies + * the minimum or maximum amount of each token the vault is allowed to transfer. + * + * `batchSwap` can be used to make a single swap, like `swap` does, but doing so requires more gas than the + * equivalent `swap` call. + * + * Emits `Swap` events. + */ + function batchSwap( + SwapKind kind, + BatchSwapStep[] calldata swaps, + IAsset[] calldata assets, + FundManagement calldata funds, + int[] calldata limits, + uint deadline + ) external payable returns (int[] memory); + + /** + * @dev Data for each individual swap executed by `batchSwap`. The asset in and out fields are indexes into the + * `assets` array passed to that function, and ETH assets are converted to WETH. + * + * If `amount` is zero, the multihop mechanism is used to determine the actual amount based on the amount in/out + * from the previous swap, depending on the swap kind. + * + * The `userData` field is ignored by the Vault, but forwarded to the Pool in the `onSwap` hook, and may be + * used to extend swap behavior. + */ + struct BatchSwapStep { + bytes32 poolId; + uint assetInIndex; + uint assetOutIndex; + uint amount; + bytes userData; + } + + /** + * @dev Emitted for each individual swap performed by `swap` or `batchSwap`. + */ + event Swap(bytes32 indexed poolId, IERC20 indexed tokenIn, IERC20 indexed tokenOut, uint amountIn, uint amountOut); + + /** + * @dev All tokens in a swap are either sent from the `sender` account to the Vault, or from the Vault to the + * `recipient` account. + * + * If the caller is not `sender`, it must be an authorized relayer for them. + * + * If `fromInternalBalance` is true, the `sender`'s Internal Balance will be preferred, performing an ERC20 + * transfer for the difference between the requested amount and the User's Internal Balance (if any). The `sender` + * must have allowed the Vault to use their tokens via `IERC20.approve()`. This matches the behavior of + * `joinPool`. + * + * If `toInternalBalance` is true, tokens will be deposited to `recipient`'s internal balance instead of + * transferred. This matches the behavior of `exitPool`. + * + * Note that ETH cannot be deposited to or withdrawn from Internal Balance: attempting to do so will trigger a + * revert. + */ + struct FundManagement { + address sender; + bool fromInternalBalance; + address payable recipient; + bool toInternalBalance; + } + + /** + * @dev Simulates a call to `batchSwap`, returning an array of Vault asset deltas. Calls to `swap` cannot be + * simulated directly, but an equivalent `batchSwap` call can and will yield the exact same result. + * + * Each element in the array corresponds to the asset at the same index, and indicates the number of tokens (or ETH) + * the Vault would take from the sender (if positive) or send to the recipient (if negative). The arguments it + * receives are the same that an equivalent `batchSwap` call would receive. + * + * Unlike `batchSwap`, this function performs no checks on the sender or recipient field in the `funds` struct. + * This makes it suitable to be called by off-chain applications via eth_call without needing to hold tokens, + * approve them for the Vault, or even know a user's address. + * + * Note that this function is not 'view' (due to implementation details): the client code must explicitly execute + * eth_call instead of eth_sendTransaction. + */ + function queryBatchSwap( + SwapKind kind, + BatchSwapStep[] calldata swaps, + IAsset[] calldata assets, + FundManagement calldata funds + ) external returns (int[] memory assetDeltas); + + // BasePool.sol + + /** + * @dev Returns the amount of BPT that would be burned from `sender` if the `onExitPool` hook were called by the + * Vault with the same arguments, along with the number of tokens `recipient` would receive. + * + * This function is not meant to be called directly, but rather from a helper contract that fetches current Vault + * data, such as the protocol swap fee percentage and Pool balances. + * + * Like `IVault.queryBatchSwap`, this function is not view due to internal implementation details: the caller must + * explicitly use eth_call instead of eth_sendTransaction. + */ + function queryExit( + bytes32 poolId, + address sender, + address recipient, + uint[] memory balances, + uint lastChangeBlock, + uint protocolSwapFeePercentage, + bytes memory userData + ) external returns (uint bptIn, uint[] memory amountsOut); +} diff --git a/src/integrations/balancer/IBWeightedPoolMinimal.sol b/src/integrations/balancer/IBWeightedPoolMinimal.sol new file mode 100644 index 00000000..587a0591 --- /dev/null +++ b/src/integrations/balancer/IBWeightedPoolMinimal.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.23; + +interface IBWeightedPoolMinimal { + /** + * @dev Returns all normalized weights, in the same order as the Pool's tokens. + */ + function getNormalizedWeights() external view returns (uint[] memory); + function getPoolId() external view returns (bytes32); + function getSwapFeePercentage() external view returns (uint); + function getVault() external view returns (address); +} diff --git a/src/integrations/balancer/IBalancerGauge.sol b/src/integrations/balancer/IBalancerGauge.sol new file mode 100644 index 00000000..683592d3 --- /dev/null +++ b/src/integrations/balancer/IBalancerGauge.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.23; + +/// @notice gauge-v2, see 0xc9b36096f5201ea332Db35d6D195774ea0D5988f +/// @dev see 20230316-child-chain-gauge-factory-v2 in balancer-deployments repository +interface IBalancerGauge { + event Approval(address indexed _owner, address indexed _spender, uint _value); + event Transfer(address indexed _from, address indexed _to, uint _value); + event Deposit(address indexed _user, uint _value); + event Withdraw(address indexed _user, uint _value); + event UpdateLiquidityLimit( + address indexed _user, + uint _original_balance, + uint _original_supply, + uint _working_balance, + uint _working_supply + ); + + function deposit(uint _value) external; + + function deposit(uint _value, address _user) external; + + function withdraw(uint _value) external; + + function withdraw(uint _value, address _user) external; + + function transferFrom(address _from, address _to, uint _value) external returns (bool); + + function approve(address _spender, uint _value) external returns (bool); + + function permit( + address _owner, + address _spender, + uint _value, + uint _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external returns (bool); + + function transfer(address _to, uint _value) external returns (bool); + + function increaseAllowance(address _spender, uint _added_value) external returns (bool); + + function decreaseAllowance(address _spender, uint _subtracted_value) external returns (bool); + + function user_checkpoint(address addr) external returns (bool); + + function claimable_tokens(address addr) external returns (uint); + + function claimed_reward(address _addr, address _token) external view returns (uint); + + function claimable_reward(address _user, address _reward_token) external view returns (uint); + + function set_rewards_receiver(address _receiver) external; + + function claim_rewards() external; + + function claim_rewards(address _addr) external; + + function claim_rewards(address _addr, address _receiver) external; + + function claim_rewards(address _addr, address _receiver, uint[] memory _reward_indexes) external; + + function add_reward(address _reward_token, address _distributor) external; + + function set_reward_distributor(address _reward_token, address _distributor) external; + + function deposit_reward_token(address _reward_token, uint _amount) external; + + function killGauge() external; + + function unkillGauge() external; + + function decimals() external view returns (uint); + + function allowance(address owner, address spender) external view returns (uint); + + function integrate_checkpoint() external view returns (uint); + + function bal_token() external view returns (address); + + function bal_pseudo_minter() external view returns (address); + + function voting_escrow_delegation_proxy() external view returns (address); + + function authorizer_adaptor() external view returns (address); + + function initialize(address _lp_token, string memory _version) external; + + function DOMAIN_SEPARATOR() external view returns (bytes32); + + function nonces(address arg0) external view returns (uint); + + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function balanceOf(address arg0) external view returns (uint); + + function totalSupply() external view returns (uint); + + function lp_token() external view returns (address); + + function version() external view returns (string memory); + + function factory() external view returns (address); + + function working_balances(address arg0) external view returns (uint); + + function working_supply() external view returns (uint); + + function period() external view returns (uint); + + function period_timestamp(uint arg0) external view returns (uint); + + function integrate_checkpoint_of(address arg0) external view returns (uint); + + function integrate_fraction(address arg0) external view returns (uint); + + function integrate_inv_supply(uint arg0) external view returns (uint); + + function integrate_inv_supply_of(address arg0) external view returns (uint); + + function reward_count() external view returns (uint); + + function reward_tokens(uint arg0) external view returns (address); + + function reward_data(address arg0) external view returns (S_0 memory); + + function rewards_receiver(address arg0) external view returns (address); + + function reward_integral_for(address arg0, address arg1) external view returns (uint); + + function is_killed() external view returns (bool); + + function inflation_rate(uint arg0) external view returns (uint); +} + +struct S_0 { + address distributor; + uint period_finish; + uint rate; + uint last_update; + uint integral; +} diff --git a/src/integrations/balancer/IBalancerHelper.sol b/src/integrations/balancer/IBalancerHelper.sol new file mode 100644 index 00000000..64fd29d8 --- /dev/null +++ b/src/integrations/balancer/IBalancerHelper.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: ISC +pragma solidity ^0.8.23; + +interface IBalancerHelper { + function queryExit( + bytes32 poolId, + address sender, + address recipient, + IVault.JoinPoolRequest memory request + ) external returns (uint bptIn, uint[] memory amountsOut); + + function queryJoin( + bytes32 poolId, + address sender, + address recipient, + IVault.JoinPoolRequest memory request + ) external returns (uint bptOut, uint[] memory amountsIn); + + function vault() external view returns (address); +} + +interface IVault { + struct JoinPoolRequest { + address[] assets; + uint[] maxAmountsIn; + bytes userData; + bool fromInternalBalance; + } +} diff --git a/src/interfaces/IBalancerAdapter.sol b/src/interfaces/IBalancerAdapter.sol new file mode 100644 index 00000000..7736f05f --- /dev/null +++ b/src/interfaces/IBalancerAdapter.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +interface IBalancerAdapter { + /// @dev Add BalancerHelpers contract address + function setupHelpers(address balancerHelpers) external; + + /// @notice Computes the maximum amount of liquidity received for given amounts of pool assets and the current + /// pool price. + /// This function signature can be used only for non-concentrated AMMs. + /// @dev This method used instead getLiquidityForAmounts because BalancerHelpers use queryJoin + /// write method. Can be used off-chain by callStatic. + /// @param pool Address of a pool supported by the adapter + /// @param amounts Amounts of pool assets + /// @return liquidity Liquidity out value + /// @return amountsConsumed Amounts of consumed assets when providing liquidity + function getLiquidityForAmountsWrite( + address pool, + uint[] memory amounts + ) external returns (uint liquidity, uint[] memory amountsConsumed); +} diff --git a/src/interfaces/IPlatform.sol b/src/interfaces/IPlatform.sol index 8271573c..2f5ec7d9 100644 --- a/src/interfaces/IPlatform.sol +++ b/src/interfaces/IPlatform.sol @@ -62,6 +62,7 @@ interface IPlatform { event AddDexAggregator(address router); event RemoveDexAggregator(address router); event MinTvlForFreeHardWorkChanged(uint oldValue, uint newValue); + event CustomVaultFee(address vault, uint platformFee); event Rebalancer(address rebalancer_); event Bridge(address bridge_); @@ -211,6 +212,10 @@ interface IPlatform { view returns (uint fee, uint feeShareVaultManager, uint feeShareStrategyLogic, uint feeShareEcosystem); + /// @notice Get custom vault platform fee + /// @return fee revenue fee % with DENOMINATOR precision + function getCustomVaultFee(address vault) external view returns (uint fee); + /// @notice Platform settings function getPlatformSettings() external view returns (PlatformSettings memory); @@ -434,10 +439,15 @@ interface IPlatform { /// @param minInitialBoostDuration_ Minimal boost rewards vesting duration for initial boost function setInitialBoost(uint minInitialBoostPerDay_, uint minInitialBoostDuration_) external; - /// @notice Update new minimum TVL for compansate. + /// @notice Update new minimum TVL for compensate. /// @param value New minimum TVL for compensate. function setMinTvlForFreeHardWork(uint value) external; + /// @notice Set custom platform fee for vault + /// @param vault Vault address + /// @param platformFee Custom platform fee + function setCustomVaultFee(address vault, uint platformFee) external; + /// @notice Setup Rebalancer. /// Only Goverannce or Multisig can do this when Rebalancer is not set. /// @param rebalancer_ Proxy address of Bridge diff --git a/src/interfaces/IPlatformDeployer.sol b/src/interfaces/IPlatformDeployer.sol index f38efa20..6f684878 100644 --- a/src/interfaces/IPlatformDeployer.sol +++ b/src/interfaces/IPlatformDeployer.sol @@ -13,5 +13,8 @@ interface IPlatformDeployer { address gelatoAutomate; uint gelatoMinBalance; uint gelatoDepositAmount; + uint fee; + uint feeShareVaultManager; + uint feeShareStrategyLogic; } } diff --git a/src/interfaces/IStrategy.sol b/src/interfaces/IStrategy.sol index f103bf8b..2d98b2d2 100644 --- a/src/interfaces/IStrategy.sol +++ b/src/interfaces/IStrategy.sol @@ -113,6 +113,16 @@ interface IStrategy is IERC165 { uint[] memory amountsMax ) external view returns (uint[] memory amountsConsumed, uint value); + /// @notice Write version of previewDepositAssets + /// @param assets_ Strategy assets or part of them, if necessary + /// @param amountsMax Amounts of specified assets available for investing + /// @return amountsConsumed Cosumed amounts of assets when investing + /// @return value Liquidity value or underlying token amount minted when investing + function previewDepositAssetsWrite( + address[] memory assets_, + uint[] memory amountsMax + ) external returns (uint[] memory amountsConsumed, uint value); + /// @notice All strategy revenue (pool fees, farm rewards etc) that not claimed by strategy yet /// @return assets_ Revenue assets /// @return amounts Amounts. Index of asset same as in previous array. diff --git a/src/strategies/BeetsStableFarm.sol b/src/strategies/BeetsStableFarm.sol new file mode 100644 index 00000000..b8e2f204 --- /dev/null +++ b/src/strategies/BeetsStableFarm.sol @@ -0,0 +1,454 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {StrategyBase} from "./base/StrategyBase.sol"; +import {LPStrategyBase} from "./base/LPStrategyBase.sol"; +import {FarmingStrategyBase} from "./base/FarmingStrategyBase.sol"; +import {StrategyLib} from "./libs/StrategyLib.sol"; +import {StrategyIdLib} from "./libs/StrategyIdLib.sol"; +import {FarmMechanicsLib} from "./libs/FarmMechanicsLib.sol"; +import {IFactory} from "../interfaces/IFactory.sol"; +import {IAmmAdapter} from "../interfaces/IAmmAdapter.sol"; +import {IStrategy} from "../interfaces/IStrategy.sol"; +import {IFarmingStrategy} from "../interfaces/IFarmingStrategy.sol"; +import {ILPStrategy} from "../interfaces/ILPStrategy.sol"; +import {IControllable} from "../interfaces/IControllable.sol"; +import {IPlatform} from "../interfaces/IPlatform.sol"; +import {VaultTypeLib} from "../core/libs/VaultTypeLib.sol"; +import {CommonLib} from "../core/libs/CommonLib.sol"; +import {AmmAdapterIdLib} from "../adapters/libs/AmmAdapterIdLib.sol"; +import {IBalancerAdapter} from "../interfaces/IBalancerAdapter.sol"; +import {IBVault} from "../integrations/balancer/IBVault.sol"; +import {IBComposableStablePoolMinimal} from "../integrations/balancer/IBComposableStablePoolMinimal.sol"; +import {IBalancerGauge} from "../integrations/balancer/IBalancerGauge.sol"; + +/// @title Earn Beets stable pool LP fees and gauge rewards +/// @author Alien Deployer (https://github.com/a17) +contract BeetsStableFarm is LPStrategyBase, FarmingStrategyBase { + using SafeERC20 for IERC20; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IControllable + string public constant VERSION = "1.0.0"; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* DATA TYPES */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + struct BalancerMethodVars { + bytes32 poolId; + address[] poolTokens; + uint bptIndex; + uint len; + uint[] allAmounts; + uint[] amounts; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INITIALIZATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IStrategy + function initialize(address[] memory addresses, uint[] memory nums, int24[] memory ticks) public initializer { + if (addresses.length != 2 || nums.length != 1 || ticks.length != 0) { + revert IControllable.IncorrectInitParams(); + } + + IFactory.Farm memory farm = _getFarm(addresses[0], nums[0]); + if (farm.addresses.length != 1 || farm.nums.length != 0 || farm.ticks.length != 0) { + revert IFarmingStrategy.BadFarm(); + } + + __LPStrategyBase_init( + LPStrategyBaseInitParams({ + id: StrategyIdLib.BEETS_STABLE_FARM, + platform: addresses[0], + vault: addresses[1], + pool: farm.pool, + underlying: farm.pool + }) + ); + + __FarmingStrategyBase_init(addresses[0], nums[0]); + + address[] memory _assets = assets(); + uint len = _assets.length; + address balancerVault = IBComposableStablePoolMinimal(farm.pool).getVault(); + for (uint i; i < len; ++i) { + IERC20(_assets[i]).forceApprove(balancerVault, type(uint).max); + } + + IERC20(farm.pool).forceApprove(farm.addresses[0], type(uint).max); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* VIEW FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) + public + view + override(LPStrategyBase, FarmingStrategyBase) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + + /// @inheritdoc IFarmingStrategy + function canFarm() external view override returns (bool) { + IFactory.Farm memory farm = _getFarm(); + return farm.status == 0; + } + + /// @inheritdoc FarmingStrategyBase + function stakingPool() external view override returns (address) { + IFactory.Farm memory farm = _getFarm(); + return farm.addresses[0]; + } + + /// @inheritdoc ILPStrategy + function ammAdapterId() public pure override returns (string memory) { + return AmmAdapterIdLib.BALANCER_COMPOSABLE_STABLE; + } + + /// @inheritdoc IStrategy + function getRevenue() external pure returns (address[] memory __assets, uint[] memory amounts) { + __assets = new address[](0); + amounts = new uint[](0); + } + + /// @inheritdoc IStrategy + function initVariants(address platform_) + public + view + returns (string[] memory variants, address[] memory addresses, uint[] memory nums, int24[] memory ticks) + { + IAmmAdapter _ammAdapter = IAmmAdapter(IPlatform(platform_).ammAdapter(keccak256(bytes(ammAdapterId()))).proxy); + addresses = new address[](0); + ticks = new int24[](0); + + IFactory.Farm[] memory farms = IFactory(IPlatform(platform_).factory()).farms(); + uint len = farms.length; + //slither-disable-next-line uninitialized-local + uint localTtotal; + //nosemgrep + for (uint i; i < len; ++i) { + //nosemgrep + IFactory.Farm memory farm = farms[i]; + //nosemgrep + if (farm.status == 0 && CommonLib.eq(farm.strategyLogicId, strategyLogicId())) { + ++localTtotal; + } + } + + variants = new string[](localTtotal); + nums = new uint[](localTtotal); + localTtotal = 0; + //nosemgrep + for (uint i; i < len; ++i) { + //nosemgrep + IFactory.Farm memory farm = farms[i]; + //nosemgrep + if (farm.status == 0 && CommonLib.eq(farm.strategyLogicId, strategyLogicId())) { + nums[localTtotal] = i; + //slither-disable-next-line calls-loop + variants[localTtotal] = _generateDescription(farm, _ammAdapter); + ++localTtotal; + } + } + } + + /// @inheritdoc IStrategy + function isHardWorkOnDepositAllowed() external pure returns (bool allowed) { + allowed = true; + } + + /// @inheritdoc IStrategy + function isReadyForHardWork() external view returns (bool) { + return total() != 0; + } + + /// @inheritdoc IFarmingStrategy + function farmMechanics() external pure returns (string memory) { + return FarmMechanicsLib.CLASSIC; + } + + /// @inheritdoc IStrategy + function supportedVaultTypes() + external + pure + override(LPStrategyBase, StrategyBase) + returns (string[] memory types) + { + types = new string[](1); + types[0] = VaultTypeLib.COMPOUNDING; + } + + /// @inheritdoc IStrategy + function strategyLogicId() public pure override returns (string memory) { + return StrategyIdLib.BEETS_STABLE_FARM; + } + + /// @inheritdoc IStrategy + function getAssetsProportions() public view returns (uint[] memory proportions) { + ILPStrategy.LPStrategyBaseStorage storage $lp = _getLPStrategyBaseStorage(); + proportions = $lp.ammAdapter.getProportions($lp.pool); + } + + /// @inheritdoc IStrategy + function extra() external pure returns (bytes32) { + //slither-disable-next-line too-many-digits + return CommonLib.bytesToBytes32(abi.encodePacked(bytes3(0xeeeeee), bytes3(0x000000))); + } + + /// @inheritdoc IStrategy + function getSpecificName() external pure override returns (string memory, bool) { + return ("", false); + } + + /// @inheritdoc IStrategy + function description() external view returns (string memory) { + IFarmingStrategy.FarmingStrategyBaseStorage storage $f = _getFarmingStrategyBaseStorage(); + ILPStrategy.LPStrategyBaseStorage storage $lp = _getLPStrategyBaseStorage(); + IFactory.Farm memory farm = IFactory(IPlatform(platform()).factory()).farm($f.farmId); + return _generateDescription(farm, $lp.ammAdapter); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STRATEGY BASE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc StrategyBase + function _depositAssets(uint[] memory amounts, bool) internal override returns (uint value) { + ILPStrategy.LPStrategyBaseStorage storage $lp = _getLPStrategyBaseStorage(); + StrategyBaseStorage storage $base = _getStrategyBaseStorage(); + IBComposableStablePoolMinimal _pool = IBComposableStablePoolMinimal($lp.pool); + BalancerMethodVars memory v; + + v.poolId = _pool.getPoolId(); + (v.poolTokens,,) = IBVault(_pool.getVault()).getPoolTokens(v.poolId); + + value = IERC20(address(_pool)).balanceOf(address(this)); + + v.bptIndex = _pool.getBptIndex(); + v.len = v.poolTokens.length; + v.allAmounts = new uint[](v.len); + uint k; + for (uint i; i < v.len; ++i) { + if (i != v.bptIndex) { + v.allAmounts[i] = amounts[k]; + k++; + } + } + + IBVault(_pool.getVault()).joinPool( + v.poolId, + address(this), + address(this), + IBVault.JoinPoolRequest({ + assets: v.poolTokens, + maxAmountsIn: v.allAmounts, + userData: abi.encode(IBVault.JoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT, amounts, 0), + fromInternalBalance: false + }) + ); + value = IERC20(address(_pool)).balanceOf(address(this)) - value; + $base.total += value; + + IFactory.Farm memory farm = _getFarm(); + IBalancerGauge(farm.addresses[0]).deposit(value); + } + + /// @inheritdoc StrategyBase + function _depositUnderlying(uint amount) internal override returns (uint[] memory amountsConsumed) { + StrategyBaseStorage storage $base = _getStrategyBaseStorage(); + $base.total += amount; + IFactory.Farm memory farm = _getFarm(); + IBalancerGauge(farm.addresses[0]).deposit(amount); + amountsConsumed = _calcAssetsAmounts(amount); + } + + /// @inheritdoc StrategyBase + function _withdrawAssets(uint value, address receiver) internal override returns (uint[] memory amountsOut) { + IFactory.Farm memory farm = _getFarm(); + IBComposableStablePoolMinimal _pool = IBComposableStablePoolMinimal(pool()); + BalancerMethodVars memory v; + + (v.poolTokens,,) = IBVault(_pool.getVault()).getPoolTokens(_pool.getPoolId()); + + IBalancerGauge(farm.addresses[0]).withdraw(value); + + address[] memory __assets = assets(); + v.len = __assets.length; + amountsOut = new uint[](v.len); + for (uint i; i < v.len; ++i) { + amountsOut[i] = IERC20(__assets[i]).balanceOf(receiver); + } + + v.amounts = _calcAssetsAmounts(value); + v.amounts = _extractFee(address(_pool), v.amounts); + + IBVault(_pool.getVault()).exitPool( + _pool.getPoolId(), + address(this), + payable(receiver), + IBVault.ExitPoolRequest({ + assets: v.poolTokens, + minAmountsOut: new uint[](v.poolTokens.length), + userData: abi.encode(1, v.amounts, value), + toInternalBalance: false + }) + ); + + for (uint i; i < v.len; ++i) { + amountsOut[i] = IERC20(__assets[i]).balanceOf(receiver) - amountsOut[i]; + } + + StrategyBaseStorage storage $base = _getStrategyBaseStorage(); + $base.total -= value; + } + + /// @inheritdoc StrategyBase + function _withdrawUnderlying(uint amount, address receiver) internal override { + StrategyBaseStorage storage $base = _getStrategyBaseStorage(); + $base.total -= amount; + IFactory.Farm memory farm = _getFarm(); + IBalancerGauge(farm.addresses[0]).withdraw(amount); + IERC20($base._underlying).safeTransfer(receiver, amount); + } + + /// @inheritdoc StrategyBase + function _claimRevenue() + internal + override + returns ( + address[] memory __assets, + uint[] memory __amounts, + address[] memory __rewardAssets, + uint[] memory __rewardAmounts + ) + { + __assets = assets(); + __amounts = new uint[](__assets.length); + FarmingStrategyBaseStorage storage $f = _getFarmingStrategyBaseStorage(); + __rewardAssets = $f._rewardAssets; + uint rwLen = __rewardAssets.length; + uint[] memory balanceBefore = new uint[](rwLen); + __rewardAmounts = new uint[](rwLen); + for (uint i; i < rwLen; ++i) { + balanceBefore[i] = StrategyLib.balance(__rewardAssets[i]); + } + IFactory.Farm memory farm = _getFarm(); + IBalancerGauge(farm.addresses[0]).claim_rewards(); + for (uint i; i < rwLen; ++i) { + __rewardAmounts[i] = StrategyLib.balance(__rewardAssets[i]) - balanceBefore[i]; + } + } + + /// @inheritdoc StrategyBase + function _compound() internal override { + address[] memory _assets = assets(); + uint len = _assets.length; + uint[] memory amounts = new uint[](len); + //slither-disable-next-line uninitialized-local + bool notZero; + for (uint i; i < len; ++i) { + amounts[i] = StrategyLib.balance(_assets[i]); + if (amounts[i] != 0) { + notZero = true; + } + } + if (notZero) { + _depositAssets(amounts, false); + } + } + + /// @inheritdoc StrategyBase + function _previewDepositAssets(uint[] memory) + internal + pure + override(StrategyBase, LPStrategyBase) + returns (uint[] memory, uint) + { + revert("Not supported"); + } + + /// @inheritdoc StrategyBase + function _previewDepositAssetsWrite(uint[] memory amountsMax) + internal + override(StrategyBase) + returns (uint[] memory amountsConsumed, uint value) + { + IBalancerAdapter _ammAdapter = + IBalancerAdapter(IPlatform(platform()).ammAdapter(keccak256(bytes(ammAdapterId()))).proxy); + ILPStrategy.LPStrategyBaseStorage storage $lp = _getLPStrategyBaseStorage(); + (value, amountsConsumed) = _ammAdapter.getLiquidityForAmountsWrite($lp.pool, amountsMax); + } + + /// @inheritdoc StrategyBase + function _previewDepositAssetsWrite( + address[] memory, + uint[] memory amountsMax + ) internal override(StrategyBase) returns (uint[] memory amountsConsumed, uint value) { + return _previewDepositAssetsWrite(amountsMax); + } + + /// @inheritdoc StrategyBase + function _previewDepositUnderlying(uint amount) internal view override returns (uint[] memory amountsConsumed) { + // todo + } + + /// @inheritdoc StrategyBase + function _assetsAmounts() internal view override returns (address[] memory assets_, uint[] memory amounts_) { + amounts_ = _calcAssetsAmounts(total()); + assets_ = assets(); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL LOGIC */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + function _calcAssetsAmounts(uint shares) internal view returns (uint[] memory amounts_) { + IBComposableStablePoolMinimal _pool = IBComposableStablePoolMinimal(pool()); + uint bptIndex = _pool.getBptIndex(); + (, uint[] memory balances,) = IBVault(_pool.getVault()).getPoolTokens(_pool.getPoolId()); + uint supply = IERC20(address(_pool)).totalSupply() - balances[bptIndex]; + uint len = balances.length - 1; + amounts_ = new uint[](len); + for (uint i; i < len; ++i) { + amounts_[i] = shares * balances[i < bptIndex ? i : i + 1] / supply; + } + } + + function _extractFee(address _pool, uint[] memory amounts_) internal view returns (uint[] memory __amounts) { + __amounts = amounts_; + uint len = amounts_.length; + uint fee = IBComposableStablePoolMinimal(_pool).getSwapFeePercentage(); + for (uint i; i < len; ++i) { + __amounts[i] -= __amounts[i] * fee / 1e18; + } + } + + function _generateDescription( + IFactory.Farm memory farm, + IAmmAdapter _ammAdapter + ) internal view returns (string memory) { + //slither-disable-next-line calls-loop + return string.concat( + "Earn ", + //slither-disable-next-line calls-loop + CommonLib.implode(CommonLib.getSymbols(farm.rewardAssets), ", "), + " and fees on Beets stable pool by ", + //slither-disable-next-line calls-loop + CommonLib.implode(CommonLib.getSymbols(_ammAdapter.poolTokens(farm.pool)), "-"), + " LP" + ); + } +} diff --git a/src/strategies/CompoundFarmStrategy.sol b/src/strategies/CompoundFarmStrategy.sol index 013d0490..af1b5b33 100644 --- a/src/strategies/CompoundFarmStrategy.sol +++ b/src/strategies/CompoundFarmStrategy.sol @@ -17,7 +17,7 @@ contract CompoundFarmStrategy is FarmingStrategyBase { /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "1.2.0"; + string public constant VERSION = "1.3.0"; // keccak256(abi.encode(uint256(keccak256("erc7201:stability.CompoundFarmStrategy")) - 1)) & ~bytes32(uint256(0xff)); bytes32 private constant COMPOUNDFARMSTRATEGY_STORAGE_LOCATION = diff --git a/src/strategies/CurveConvexFarmStrategy.sol b/src/strategies/CurveConvexFarmStrategy.sol index d79d7ab6..c8834dd6 100644 --- a/src/strategies/CurveConvexFarmStrategy.sol +++ b/src/strategies/CurveConvexFarmStrategy.sol @@ -22,7 +22,7 @@ contract CurveConvexFarmStrategy is LPStrategyBase, FarmingStrategyBase { /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "1.1.1"; + string public constant VERSION = "1.2.0"; // keccak256(abi.encode(uint256(keccak256("erc7201:stability.CurveConvexFarmStrategy")) - 1)) & ~bytes32(uint256(0xff)); bytes32 private constant CURVE_CONVEX_FARM_STRATEGY_STORAGE_LOCATION = diff --git a/src/strategies/DefiEdgeQuickSwapMerklFarmStrategy.sol b/src/strategies/DefiEdgeQuickSwapMerklFarmStrategy.sol index 9528b9e2..feb0296a 100644 --- a/src/strategies/DefiEdgeQuickSwapMerklFarmStrategy.sol +++ b/src/strategies/DefiEdgeQuickSwapMerklFarmStrategy.sol @@ -26,7 +26,7 @@ contract DefiEdgeQuickSwapMerklFarmStrategy is LPStrategyBase, MerklStrategyBase /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "1.3.1"; + string public constant VERSION = "1.4.0"; address internal constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; diff --git a/src/strategies/GammaQuickSwapMerklFarmStrategy.sol b/src/strategies/GammaQuickSwapMerklFarmStrategy.sol index ec36bc52..2e55bc3d 100644 --- a/src/strategies/GammaQuickSwapMerklFarmStrategy.sol +++ b/src/strategies/GammaQuickSwapMerklFarmStrategy.sol @@ -26,7 +26,7 @@ contract GammaQuickSwapMerklFarmStrategy is LPStrategyBase, MerklStrategyBase, F /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "1.4.1"; + string public constant VERSION = "1.5.0"; uint internal constant _PRECISION = 1e36; diff --git a/src/strategies/GammaRetroMerklFarmStrategy.sol b/src/strategies/GammaRetroMerklFarmStrategy.sol index 53221a92..641b5cfe 100644 --- a/src/strategies/GammaRetroMerklFarmStrategy.sol +++ b/src/strategies/GammaRetroMerklFarmStrategy.sol @@ -26,7 +26,7 @@ contract GammaRetroMerklFarmStrategy is LPStrategyBase, MerklStrategyBase, Farmi /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "2.2.1"; + string public constant VERSION = "2.3.0"; uint internal constant _PRECISION = 1e36; diff --git a/src/strategies/GammaUniswapV3MerklFarmStrategy.sol b/src/strategies/GammaUniswapV3MerklFarmStrategy.sol index 53bbbffc..b399d658 100644 --- a/src/strategies/GammaUniswapV3MerklFarmStrategy.sol +++ b/src/strategies/GammaUniswapV3MerklFarmStrategy.sol @@ -25,7 +25,7 @@ contract GammaUniswapV3MerklFarmStrategy is LPStrategyBase, MerklStrategyBase, F /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "1.2.1"; + string public constant VERSION = "1.3.0"; uint internal constant _PRECISION = 1e36; diff --git a/src/strategies/IchiQuickSwapMerklFarmStrategy.sol b/src/strategies/IchiQuickSwapMerklFarmStrategy.sol index d119f521..276b0241 100644 --- a/src/strategies/IchiQuickSwapMerklFarmStrategy.sol +++ b/src/strategies/IchiQuickSwapMerklFarmStrategy.sol @@ -21,7 +21,7 @@ contract IchiQuickSwapMerklFarmStrategy is LPStrategyBase, MerklStrategyBase, Fa /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "1.3.1"; + string public constant VERSION = "1.4.0"; uint internal constant PRECISION = 10 ** 18; diff --git a/src/strategies/IchiRetroMerklFarmStrategy.sol b/src/strategies/IchiRetroMerklFarmStrategy.sol index abb589bd..895bbaf0 100644 --- a/src/strategies/IchiRetroMerklFarmStrategy.sol +++ b/src/strategies/IchiRetroMerklFarmStrategy.sol @@ -22,7 +22,7 @@ contract IchiRetroMerklFarmStrategy is LPStrategyBase, MerklStrategyBase, Farmin /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "2.2.1"; + string public constant VERSION = "2.3.0"; uint internal constant _PRECISION = 10 ** 18; diff --git a/src/strategies/QuickSwapStaticMerklFarmStrategy.sol b/src/strategies/QuickSwapStaticMerklFarmStrategy.sol index 9956a0f8..a3c65d85 100644 --- a/src/strategies/QuickSwapStaticMerklFarmStrategy.sol +++ b/src/strategies/QuickSwapStaticMerklFarmStrategy.sol @@ -24,7 +24,7 @@ contract QuickSwapStaticMerklFarmStrategy is LPStrategyBase, MerklStrategyBase, /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "1.2.1"; + string public constant VERSION = "1.3.0"; // keccak256(abi.encode(uint256(keccak256("erc7201:stability.QuickSwapV3StaticMerkFarmStrategy")) - 1)) & ~bytes32(uint256(0xff)); bytes32 private constant QUICKSWAPV3STATICMERKLFARMSTRATEGY_STORAGE_LOCATION = diff --git a/src/strategies/SteerQuickSwapMerklFarmStrategy.sol b/src/strategies/SteerQuickSwapMerklFarmStrategy.sol index c730398a..d350e005 100644 --- a/src/strategies/SteerQuickSwapMerklFarmStrategy.sol +++ b/src/strategies/SteerQuickSwapMerklFarmStrategy.sol @@ -26,7 +26,7 @@ contract SteerQuickSwapMerklFarmStrategy is LPStrategyBase, FarmingStrategyBase /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "1.0.1"; + string public constant VERSION = "1.1.0"; uint internal constant _PRECISION = 1e36; diff --git a/src/strategies/TridentPearlFarmStrategy.sol b/src/strategies/TridentPearlFarmStrategy.sol index 9ef00f04..123bb2ce 100644 --- a/src/strategies/TridentPearlFarmStrategy.sol +++ b/src/strategies/TridentPearlFarmStrategy.sol @@ -22,7 +22,7 @@ contract TridentPearlFarmStrategy is LPStrategyBase, FarmingStrategyBase { /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "1.1.0"; + string public constant VERSION = "1.2.0"; uint internal constant _PRECISION = 10 ** 36; diff --git a/src/strategies/YearnStrategy.sol b/src/strategies/YearnStrategy.sol index c40ae3f9..37fafc1c 100644 --- a/src/strategies/YearnStrategy.sol +++ b/src/strategies/YearnStrategy.sol @@ -13,7 +13,7 @@ contract YearnStrategy is ERC4626StrategyBase { /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "1.0.0"; + string public constant VERSION = "1.1.0"; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* INITIALIZATION */ diff --git a/src/strategies/base/StrategyBase.sol b/src/strategies/base/StrategyBase.sol index ee7ea2ea..bb12b4c5 100644 --- a/src/strategies/base/StrategyBase.sol +++ b/src/strategies/base/StrategyBase.sol @@ -9,6 +9,7 @@ import "../../interfaces/IVault.sol"; /// @dev Base universal strategy /// Changelog: +/// 2.0.0: previewDepositAssetsWrite; use platform.getCustomVaultFee /// 1.1.0: autoCompoundingByUnderlyingProtocol(), virtual total() /// @author Alien Deployer (https://github.com/a17) /// @author JodsMigel (https://github.com/JodsMigel) @@ -20,7 +21,7 @@ abstract contract StrategyBase is Controllable, IStrategy { /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @dev Version of StrategyBase implementation - string public constant VERSION_STRATEGY_BASE = "1.1.0"; + string public constant VERSION_STRATEGY_BASE = "2.0.0"; // keccak256(abi.encode(uint256(keccak256("erc7201:stability.StrategyBase")) - 1)) & ~bytes32(uint256(0xff)); bytes32 private constant STRATEGYBASE_STORAGE_LOCATION = @@ -212,7 +213,7 @@ abstract contract StrategyBase is Controllable, IStrategy { function previewDepositAssets( address[] memory assets_, uint[] memory amountsMax - ) external view virtual returns (uint[] memory amountsConsumed, uint value) { + ) public view virtual returns (uint[] memory amountsConsumed, uint value) { // nosemgrep if (assets_.length == 1 && assets_[0] == _getStrategyBaseStorage()._underlying && assets_[0] != address(0)) { if (amountsMax.length != 1) { @@ -225,6 +226,23 @@ abstract contract StrategyBase is Controllable, IStrategy { } } + /// @inheritdoc IStrategy + function previewDepositAssetsWrite( + address[] memory assets_, + uint[] memory amountsMax + ) external virtual returns (uint[] memory amountsConsumed, uint value) { + // nosemgrep + if (assets_.length == 1 && assets_[0] == _getStrategyBaseStorage()._underlying && assets_[0] != address(0)) { + if (amountsMax.length != 1) { + revert IControllable.IncorrectArrayLength(); + } + value = amountsMax[0]; + amountsConsumed = _previewDepositUnderlyingWrite(amountsMax[0]); + } else { + return _previewDepositAssetsWrite(assets_, amountsMax); + } + } + /// @inheritdoc IStrategy function autoCompoundingByUnderlyingProtocol() public view virtual returns (bool) { return false; @@ -253,6 +271,15 @@ abstract contract StrategyBase is Controllable, IStrategy { returns (uint[] memory /*amountsConsumed*/ ) {} + function _previewDepositUnderlyingWrite(uint amount) + internal + view + virtual + returns (uint[] memory amountsConsumed) + { + return _previewDepositUnderlying(amount); + } + /// @dev Can be overrided by derived base strategies for custom logic function _beforeDeposit() internal virtual {} @@ -340,17 +367,29 @@ abstract contract StrategyBase is Controllable, IStrategy { /// @dev This full form of _previewDepositAssets can be implemented only in inherited base strategy contract /// @param assets_ Strategy assets or part of them, if necessary /// @param amountsMax Amounts of specified assets available for investing - /// @return amountsConsumed Cosumed amounts of assets when investing + /// @return amountsConsumed Consumed amounts of assets when investing /// @return value Liquidity value or underlying token amount minted when investing function _previewDepositAssets( address[] memory assets_, uint[] memory amountsMax ) internal view virtual returns (uint[] memory amountsConsumed, uint value); + /// @dev Write version of _previewDepositAssets + /// @param assets_ Strategy assets or part of them, if necessary + /// @param amountsMax Amounts of specified assets available for investing + /// @return amountsConsumed Consumed amounts of assets when investing + /// @return value Liquidity value or underlying token amount minted when investing + function _previewDepositAssetsWrite( + address[] memory assets_, + uint[] memory amountsMax + ) internal virtual returns (uint[] memory amountsConsumed, uint value) { + return _previewDepositAssets(assets_, amountsMax); + } + /// @dev Calculation of consumed amounts and liquidity/underlying value for provided strategy assets and amounts. /// Light form of _previewDepositAssets is suitable for implementation into final strategy contract. /// @param amountsMax Amounts of specified assets available for investing - /// @return amountsConsumed Cosumed amounts of assets when investing + /// @return amountsConsumed Consumed amounts of assets when investing /// @return value Liquidity value or underlying token amount minted when investing function _previewDepositAssets(uint[] memory amountsMax) internal @@ -358,6 +397,18 @@ abstract contract StrategyBase is Controllable, IStrategy { virtual returns (uint[] memory amountsConsumed, uint value); + /// @dev Write version of _previewDepositAssets + /// @param amountsMax Amounts of specified assets available for investing + /// @return amountsConsumed Consumed amounts of assets when investing + /// @return value Liquidity value or underlying token amount minted when investing + function _previewDepositAssetsWrite(uint[] memory amountsMax) + internal + virtual + returns (uint[] memory amountsConsumed, uint value) + { + return _previewDepositAssets(amountsMax); + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* INTERNAL LOGIC */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ diff --git a/src/strategies/libs/StrategyDeveloperLib.sol b/src/strategies/libs/StrategyDeveloperLib.sol index bf1767bc..0b620a8f 100644 --- a/src/strategies/libs/StrategyDeveloperLib.sol +++ b/src/strategies/libs/StrategyDeveloperLib.sol @@ -44,6 +44,9 @@ library StrategyDeveloperLib { if (CommonLib.eq(strategyId, StrategyIdLib.TRIDENT_PEARL_FARM)) { return 0x88888887C3ebD4a33E34a15Db4254C74C75E5D4A; } + if (CommonLib.eq(strategyId, StrategyIdLib.BEETS_STABLE_FARM)) { + return 0x88888887C3ebD4a33E34a15Db4254C74C75E5D4A; + } return address(0); } } diff --git a/src/strategies/libs/StrategyIdLib.sol b/src/strategies/libs/StrategyIdLib.sol index 4978a8b3..3298d9ae 100644 --- a/src/strategies/libs/StrategyIdLib.sol +++ b/src/strategies/libs/StrategyIdLib.sol @@ -16,4 +16,5 @@ library StrategyIdLib { string internal constant CURVE_CONVEX_FARM = "Curve Convex Farm"; string internal constant YEARN = "Yearn"; string internal constant TRIDENT_PEARL_FARM = "Trident Pearl Farm"; + string internal constant BEETS_STABLE_FARM = "Beets Stable Farm"; } diff --git a/src/strategies/libs/StrategyLib.sol b/src/strategies/libs/StrategyLib.sol index 0eb52f08..4a15460f 100644 --- a/src/strategies/libs/StrategyLib.sol +++ b/src/strategies/libs/StrategyLib.sol @@ -99,11 +99,15 @@ library StrategyLib { feeShareEcosystem: 0, amountEcosystem: 0 }); - // IPlatform _platform = IPlatform(platform); - // uint[] memory fees = new uint[](4); - // uint[] memory feeAmounts = new uint[](4); + (vars.feePlatform, vars.feeShareVaultManager, vars.feeShareStrategyLogic, vars.feeShareEcosystem) = vars.platform.getFees(); + try vars.platform.getCustomVaultFee(vault) returns (uint vaultCustomFee) { + if (vaultCustomFee != 0) { + vars.feePlatform = vaultCustomFee; + } + } catch {} + address vaultManagerReceiver = IVaultManager(vars.platform.vaultManager()).getRevenueReceiver(IVault(vault).tokenId()); //slither-disable-next-line unused-return diff --git a/test/adapters/Api3Adapter.Sonic.t.sol b/test/adapters/Api3Adapter.Sonic.t.sol new file mode 100644 index 00000000..9125dd9f --- /dev/null +++ b/test/adapters/Api3Adapter.Sonic.t.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "../base/chains/SonicSetup.sol"; + +contract Api3AdapterTest is SonicSetup { + Api3Adapter public adapter; + + constructor() { + _init(); + adapter = Api3Adapter(PriceReader(platform.priceReader()).adapters()[0]); + } + + function testApi3() public { + (uint usdcPrice,) = adapter.getPrice(SonicLib.TOKEN_USDC); + // console.log(usdcPrice); + assertGt(usdcPrice, 9e17); + assertLt(usdcPrice, 11e17); + + (uint zero,) = adapter.getPrice(address(10)); + assertEq(zero, 0); + + adapter.getAllPrices(); + adapter.assets(); + + // test adm methods + address[] memory fakeAssets = new address[](2); + fakeAssets[0] = address(0); + fakeAssets[1] = address(1); + vm.expectRevert(IControllable.IncorrectArrayLength.selector); + adapter.addPriceFeeds(new address[](1), fakeAssets); + + vm.expectRevert(IControllable.AlreadyExist.selector); + adapter.addPriceFeeds(new address[](2), fakeAssets); + + vm.expectRevert(IControllable.NotExist.selector); + adapter.removePriceFeeds(fakeAssets); + } +} diff --git a/test/adapters/BalancerComposableStableAdapter.Sonic.t.sol b/test/adapters/BalancerComposableStableAdapter.Sonic.t.sol new file mode 100644 index 00000000..57407e4d --- /dev/null +++ b/test/adapters/BalancerComposableStableAdapter.Sonic.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "../base/chains/SonicSetup.sol"; + +contract BalancerComposableStableAdapterTest is SonicSetup { + bytes32 public _hash; + IAmmAdapter public adapter; + + constructor() { + _init(); + _hash = keccak256(bytes(AmmAdapterIdLib.BALANCER_COMPOSABLE_STABLE)); + adapter = IAmmAdapter(platform.ammAdapter(_hash).proxy); + // console.logBytes32(keccak256(abi.encode(uint256(keccak256("erc7201:stability.BalancerComposableStableAdapter")) - 1)) & ~bytes32(uint256(0xff))); + } + + function testIBalancerAdapter() public { + IBalancerAdapter balancerAdapter = IBalancerAdapter(address(adapter)); + vm.expectRevert(IControllable.AlreadyExist.selector); + balancerAdapter.setupHelpers(address(1)); + + address pool = SonicLib.POOL_BEETHOVENX_wS_stS; + uint[] memory amounts = new uint[](2); + amounts[0] = 1e18; + amounts[1] = 100e18; + (uint liquidity, uint[] memory amountsConsumed) = balancerAdapter.getLiquidityForAmountsWrite(pool, amounts); + assertGt(liquidity, 1e10); + assertEq(amountsConsumed[0], amounts[0]); + assertEq(amountsConsumed[1], amounts[1]); + // console.log(liquidity); + // console.log(amountsConsumed[0]); + // console.log(amountsConsumed[1]); + } + + function testSwaps() public { + address pool = SonicLib.POOL_BEETHOVENX_wS_stS; + deal(SonicLib.TOKEN_wS, address(adapter), 1e16); + adapter.swap(pool, SonicLib.TOKEN_wS, SonicLib.TOKEN_stS, address(this), 10_000); + uint out = IERC20(SonicLib.TOKEN_stS).balanceOf(address(this)); + assertGt(out, 0); + // console.log(out); + deal(SonicLib.TOKEN_wS, address(adapter), 6e22); + vm.expectRevert(); + adapter.swap(pool, SonicLib.TOKEN_wS, SonicLib.TOKEN_stS, address(this), 10); + // out = IERC20(SonicLib.TOKEN_stS).balanceOf(address(this)); + // console.log(out); + } + + function testViewMethods() public view { + assertEq(keccak256(bytes(adapter.ammAdapterId())), _hash); + + address pool = SonicLib.POOL_BEETHOVENX_wS_stS; + uint price; + price = adapter.getPrice(pool, SonicLib.TOKEN_stS, SonicLib.TOKEN_wS, 1e10); + assertGt(price, 9e9); + assertLt(price, 11e9); + // console.log(price); + + address[] memory tokens = adapter.poolTokens(pool); + assertEq(tokens.length, 2); + assertEq(tokens[0], SonicLib.TOKEN_wS); + assertEq(tokens[1], SonicLib.TOKEN_stS); + + uint[] memory props = adapter.getProportions(pool); + assertGt(props[0], 1e16); + assertGt(props[1], 1e16); + // console.log(props[0]); + // console.log(props[1]); + + assertEq(adapter.supportsInterface(type(IAmmAdapter).interfaceId), true); + assertEq(adapter.supportsInterface(type(IBalancerAdapter).interfaceId), true); + assertEq(adapter.supportsInterface(type(IERC165).interfaceId), true); + } +} diff --git a/test/adapters/BalancerWeightedAdapter.Sonic.t.sol b/test/adapters/BalancerWeightedAdapter.Sonic.t.sol new file mode 100644 index 00000000..fb7e0b57 --- /dev/null +++ b/test/adapters/BalancerWeightedAdapter.Sonic.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "../base/chains/SonicSetup.sol"; + +contract BalancerWeightedAdapterTest is SonicSetup { + bytes32 public _hash; + IAmmAdapter public adapter; + + constructor() { + _init(); + _hash = keccak256(bytes(AmmAdapterIdLib.BALANCER_WEIGHTED)); + adapter = IAmmAdapter(platform.ammAdapter(_hash).proxy); + // console.logBytes32(keccak256(abi.encode(uint256(keccak256("erc7201:stability.BalancerWeightedAdapter")) - 1)) & ~bytes32(uint256(0xff))); + } + + function testIBalancerAdapter() public { + IBalancerAdapter balancerAdapter = IBalancerAdapter(address(adapter)); + vm.expectRevert(IControllable.AlreadyExist.selector); + balancerAdapter.setupHelpers(address(1)); + + address pool = SonicLib.POOL_BEETHOVENX_BEETS_stS; + uint[] memory amounts = new uint[](2); + amounts[0] = 1e18; + amounts[1] = 100e18; + (uint liquidity, uint[] memory amountsConsumed) = balancerAdapter.getLiquidityForAmountsWrite(pool, amounts); + assertGt(liquidity, 1e10); + assertEq(amountsConsumed[0], amounts[0]); + assertEq(amountsConsumed[1], amounts[1]); + // console.log(liquidity); + // console.log(amountsConsumed[0]); + // console.log(amountsConsumed[1]); + } + + function testSwaps() public { + address pool = SonicLib.POOL_BEETHOVENX_BEETS_stS; + deal(SonicLib.TOKEN_stS, address(adapter), 1e15); + adapter.swap(pool, SonicLib.TOKEN_stS, SonicLib.TOKEN_BEETS, address(this), 10_000); + uint out = IERC20(SonicLib.TOKEN_BEETS).balanceOf(address(this)); + assertGt(out, 0); + // console.log(out); + deal(SonicLib.TOKEN_stS, address(adapter), 8000e18); + vm.expectRevert(); + adapter.swap(pool, SonicLib.TOKEN_stS, SonicLib.TOKEN_BEETS, address(this), 10); + // out = IERC20(SonicLib.TOKEN_stS).balanceOf(address(this)); + // console.log(out); + } + + function testViewMethods() public view { + assertEq(keccak256(bytes(adapter.ammAdapterId())), _hash); + + address pool = SonicLib.POOL_BEETHOVENX_BEETS_stS; + address[] memory tokens = adapter.poolTokens(pool); + assertEq(tokens.length, 2); + assertEq(tokens[0], SonicLib.TOKEN_BEETS); + assertEq(tokens[1], SonicLib.TOKEN_stS); + + uint[] memory props = adapter.getProportions(pool); + assertEq(props[0], 8e17); + assertEq(props[1], 2e17); + // console.log(props[0]); + // console.log(props[1]); + + uint price; + price = adapter.getPrice(pool, SonicLib.TOKEN_BEETS, SonicLib.TOKEN_stS, 1e18); + assertGt(price, 1e8); + // console.log(price); + + assertEq(adapter.supportsInterface(type(IAmmAdapter).interfaceId), true); + assertEq(adapter.supportsInterface(type(IBalancerAdapter).interfaceId), true); + assertEq(adapter.supportsInterface(type(IERC165).interfaceId), true); + } +} diff --git a/test/base/UniversalTest.sol b/test/base/UniversalTest.sol index ebd9c406..3affc737 100644 --- a/test/base/UniversalTest.sol +++ b/test/base/UniversalTest.sol @@ -417,12 +417,12 @@ abstract contract UniversalTest is Test, ChainSetup, Utils { } } - _skip(duration3, strategies[i].farmId); - vm.roll(block.number + 6); - _preHardWork(); _preHardWork(strategies[i].farmId); + _skip(duration3, strategies[i].farmId); + vm.roll(block.number + 6); + // check not claimed revenue if available { (address[] memory __assets, uint[] memory amounts) = strategy.getRevenue(); @@ -709,7 +709,7 @@ abstract contract UniversalTest is Test, ChainSetup, Utils { uint balNow = IERC20(assets[j]).balanceOf(address(this)); vars.withdrawnUsdValue += balNow * price / 10 ** IERC20Metadata(assets[j]).decimals(); } - assertGe(vars.withdrawnUsdValue, vars.depositUsdValue - vars.depositUsdValue / 1000); + assertGe(vars.withdrawnUsdValue, vars.depositUsdValue - vars.depositUsdValue / 100, "E1"); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* COVERAGE */ diff --git a/test/base/chains/EthereumSetup.sol b/test/base/chains/EthereumSetup.sol index ef969a75..93ae0f2e 100644 --- a/test/base/chains/EthereumSetup.sol +++ b/test/base/chains/EthereumSetup.sol @@ -12,7 +12,7 @@ abstract contract EthereumSetup is ChainSetup, DeployCore { constructor() { vm.selectFork(vm.createFork(vm.envString("ETHEREUM_RPC_URL"))); - // vm.rollFork(55000000); + // vm.rollFork(55000000); } function testSetupStub() external {} @@ -26,11 +26,6 @@ abstract contract EthereumSetup is ChainSetup, DeployCore { } function _deal(address token, address to, uint amount) internal override { - if (token == EthereumLib.TOKEN_USDC) { - vm.prank(0x4B16c5dE96EB2117bBE5fd171E4d203624B014aa); - IERC20(token).transfer(to, amount); - } else { - deal(token, to, amount); - } + deal(token, to, amount); } } diff --git a/test/base/chains/SonicSetup.sol b/test/base/chains/SonicSetup.sol new file mode 100644 index 00000000..3d06d845 --- /dev/null +++ b/test/base/chains/SonicSetup.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "../../../chains/SonicLib.sol"; +import "../ChainSetup.sol"; +import "../../../src/core/Platform.sol"; +import "../../../src/core/Factory.sol"; +import {DeployCore} from "../../../script/base/DeployCore.sol"; + +abstract contract SonicSetup is ChainSetup, DeployCore { + bool public showDeployLog; + + constructor() { + vm.selectFork(vm.createFork(vm.envString("SONIC_RPC_URL"))); + // vm.rollFork(489000); // Dec-16-2024 06:18:01 PM +UTC + // vm.rollFork(850000); // Dec 20, 2024, 12:56 PM GMT+3 + // vm.rollFork(1168500); // Dec-22-2024 10:34:43 UTC + vm.rollFork(1462000); // Dec-24-2024 12:35:56 PM +UTC + } + + function testSetupStub() external {} + + function _init() internal override { + //region ----- DeployCore.sol ----- + platform = Platform(_deployCore(SonicLib.platformDeployParams())); + SonicLib.deployAndSetupInfrastructure(address(platform), showDeployLog); + factory = Factory(address(platform.factory())); + //endregion ----- DeployCore.sol ----- + } + + function _deal(address token, address to, uint amount) internal override { + deal(token, to, amount); + } +} diff --git a/test/core/Platform.Polygon.t.sol b/test/core/Platform.Polygon.t.sol index 34ed9d24..caaa8c2e 100644 --- a/test/core/Platform.Polygon.t.sol +++ b/test/core/Platform.Polygon.t.sol @@ -102,11 +102,6 @@ contract PlatformPolygonTest is PolygonSetup { defiEdgeFactory.setMinHeartbeat(PolygonLib.TOKEN_WMATIC, ETH, 86400 * 365); vm.stopPrank(); - // disable some strategies that cant work on this default forking block - vm.startPrank(platform.multisig()); - platform.addOperator(platform.multisig()); - vm.stopPrank(); - _disableStrategy(StrategyIdLib.COMPOUND_FARM); platform.setAllowedBBTokenVaults(platform.allowedBBTokens()[0], 1e4); diff --git a/test/core/Platform.t.sol b/test/core/Platform.t.sol index 3942a124..af92999a 100644 --- a/test/core/Platform.t.sol +++ b/test/core/Platform.t.sol @@ -299,7 +299,7 @@ contract PlatformTest is Test { vm.expectRevert(abi.encodeWithSelector(IPlatform.IncorrectFee.selector, _minFee, _maxFee)); platform.setFees(3_000, 30_000, 30_000, 0); vm.expectRevert(abi.encodeWithSelector(IPlatform.IncorrectFee.selector, _minFee, _maxFee)); - platform.setFees(13_000, 30_000, 30_000, 0); + platform.setFees(51_000, 19_000, 30_000, 0); _minFee = platform.MIN_FEE_SHARE_VAULT_MANAGER(); vm.expectRevert(abi.encodeWithSelector(IPlatform.IncorrectFee.selector, _minFee, 0)); @@ -313,6 +313,9 @@ contract PlatformTest is Test { vm.expectRevert(abi.encodeWithSelector(IPlatform.IncorrectFee.selector, 0, _maxFee)); platform.setFees(10_000, 60_000, 50_000, 0); + platform.setCustomVaultFee(address(1), 22_222); + assertEq(platform.getCustomVaultFee(address(1)), 22_222); + vm.stopPrank(); } diff --git a/test/core/PriceReader.t.sol b/test/core/PriceReader.t.sol index 0623d71a..5e630c24 100644 --- a/test/core/PriceReader.t.sol +++ b/test/core/PriceReader.t.sol @@ -144,13 +144,13 @@ contract PriceReaderTest is Test, MockSetup { (uint priceD, bool trustedD) = priceReader.getPrice(address(tokenD)); (uint priceE, bool trustedE) = priceReader.getPrice(address(tokenE)); (uint _zero, bool _false) = priceReader.getPrice(address(this)); - assertEq(priceA, 1e18); + assertEq(priceA, 1e18, "A0"); assertEq(trustedA, true); - assertEq(priceB, 2 * 1e18); + assertEq(priceB, 2 * 1e18, "A1"); assertEq(trustedB, true); - assertEq(priceD, 3 * 1e18); + assertEq(priceD, 3 * 1e18, "A2"); assertEq(trustedD, true); - assertEq(priceE, 3 * 2e12); + assertEq(priceE, 3 * 2e12, "A3"); assertEq(trustedE, false); assertEq(_zero, 0); assertEq(_false, false); diff --git a/test/core/Swapper.Sonic.t.sol b/test/core/Swapper.Sonic.t.sol new file mode 100644 index 00000000..b0072bd6 --- /dev/null +++ b/test/core/Swapper.Sonic.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import "../../chains/SonicLib.sol"; +import "../base/chains/SonicSetup.sol"; + +contract SwapperSonicTest is Test, SonicSetup { + ISwapper public swapper; + + function setUp() public { + _init(); + swapper = ISwapper(platform.swapper()); + _deal(SonicLib.TOKEN_wS, address(this), 1e18); + IERC20(SonicLib.TOKEN_wS).approve(address(swapper), type(uint).max); + IERC20(SonicLib.TOKEN_stS).approve(address(swapper), type(uint).max); + IERC20(SonicLib.TOKEN_BEETS).approve(address(swapper), type(uint).max); + IERC20(SonicLib.TOKEN_USDC).approve(address(swapper), type(uint).max); + } + + function testSwaps() public { + uint got; + swapper.swap(SonicLib.TOKEN_wS, SonicLib.TOKEN_stS, 1e13, 1_000); // 1% + got = IERC20(SonicLib.TOKEN_stS).balanceOf(address(this)); + swapper.swap(SonicLib.TOKEN_stS, SonicLib.TOKEN_BEETS, got, 1_000); // 1% + got = IERC20(SonicLib.TOKEN_BEETS).balanceOf(address(this)); + swapper.swap(SonicLib.TOKEN_BEETS, SonicLib.TOKEN_USDC, got, 1_000); // 1% + got = IERC20(SonicLib.TOKEN_USDC).balanceOf(address(this)); + + // console.log(got); + } +} diff --git a/test/strategies/BSF.Sonic.t.sol b/test/strategies/BSF.Sonic.t.sol new file mode 100644 index 00000000..7eab8095 --- /dev/null +++ b/test/strategies/BSF.Sonic.t.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {console} from "forge-std/Test.sol"; +import {SonicSetup} from "../base/chains/SonicSetup.sol"; +import "../../chains/SonicLib.sol"; +import "../base/UniversalTest.sol"; +import "../../src/integrations/balancer/IBalancerGauge.sol"; + +contract BeetsStableFarmStrategyTest is SonicSetup, UniversalTest { + function testBSF() public universalTest { + _addStrategy(0); + } + + /*function _preHardWork() internal override { + address gauge = IFarmingStrategy(currentStrategy).stakingPool(); + S_0 memory s = IBalancerGauge(gauge).reward_data(SonicLib.TOKEN_BEETS); + address distributor = s.distributor; + _deal(SonicLib.TOKEN_BEETS, distributor, 1000e18); + vm.startPrank(distributor); + IERC20(SonicLib.TOKEN_BEETS).approve(gauge, type(uint).max); + IBalancerGauge(gauge).deposit_reward_token(SonicLib.TOKEN_BEETS, 200e18); + vm.stopPrank(); + }*/ + + function _addStrategy(uint farmId) internal { + strategies.push( + Strategy({id: StrategyIdLib.BEETS_STABLE_FARM, pool: address(0), farmId: farmId, underlying: address(0)}) + ); + } +}